feat(lancamentos): amplia divisao e resumo do modal

This commit is contained in:
Felipe Coutinho
2026-05-28 10:59:13 -03:00
parent ef2c8c50e8
commit 311369f81b
10 changed files with 981 additions and 290 deletions

View File

@@ -155,14 +155,20 @@ export async function validateAllOwnership(
fields: {
payerId?: string | null;
secondaryPayerId?: string | null;
splitPayerIds?: Array<string | null | undefined>;
categoryId?: string | null;
accountId?: string | null;
cardId?: string | null;
},
): Promise<string | null> {
const payerIds = [
fields.payerId,
fields.secondaryPayerId,
...(fields.splitPayerIds ?? []),
];
const [ownedPayerIds, ownedCategoryIds, ownedAccountIds, ownedCardIds] =
await Promise.all([
fetchOwnedPayerIds(userId, [fields.payerId, fields.secondaryPayerId]),
fetchOwnedPayerIds(userId, payerIds),
fetchOwnedCategoryIds(userId, [fields.categoryId]),
fetchOwnedAccountIds(userId, [fields.accountId]),
fetchOwnedCardIds(userId, [fields.cardId]),
@@ -171,6 +177,7 @@ export async function validateAllOwnership(
const checks = [
!fields.payerId || ownedPayerIds.has(fields.payerId),
!fields.secondaryPayerId || ownedPayerIds.has(fields.secondaryPayerId),
(fields.splitPayerIds ?? []).every((id) => !id || ownedPayerIds.has(id)),
!fields.categoryId || ownedCategoryIds.has(fields.categoryId),
!fields.accountId || ownedAccountIds.has(fields.accountId),
!fields.cardId || ownedCardIds.has(fields.cardId),
@@ -178,7 +185,8 @@ export async function validateAllOwnership(
const errors = [
"Pessoa não encontrada ou sem permissão.",
"Pessoa secundário não encontrado ou sem permissão.",
"Pessoa secundária não encontrada ou sem permissão.",
"Uma das pessoas selecionadas não foi encontrada ou está sem permissão.",
"Categoria não encontrada.",
"Conta não encontrada.",
"Cartão não encontrado.",
@@ -322,6 +330,14 @@ const baseFields = z.object({
}),
payerId: uuidSchema("Payer").nullable().optional(),
secondaryPayerId: uuidSchema("Payer secundário").optional(),
splitShares: z
.array(
z.object({
payerId: uuidSchema("Pessoa"),
amount: z.coerce.number().min(0.01, "Informe um valor maior que zero."),
}),
)
.optional(),
isSplit: z.boolean().optional().default(false),
primarySplitAmount: z.coerce.number().min(0).optional(),
secondarySplitAmount: z.coerce.number().min(0).optional(),
@@ -434,6 +450,8 @@ const refineLancamento = (
}
if (data.isSplit) {
const shares = resolveSplitShares(data);
if (!data.payerId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@@ -442,30 +460,38 @@ const refineLancamento = (
});
}
if (!data.secondaryPayerId) {
if (shares.length < 2) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["secondaryPayerId"],
message: "Selecione a pessoa secundário para dividir o lançamento.",
});
} else if (data.payerId && data.secondaryPayerId === data.payerId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["secondaryPayerId"],
message: "Escolha uma pessoa diferente para dividir o lançamento.",
path: ["splitShares"],
message: "Selecione pelo menos uma pessoa para dividir o lançamento.",
});
}
if (
data.primarySplitAmount !== undefined &&
data.secondarySplitAmount !== undefined
) {
const sum = data.primarySplitAmount + data.secondarySplitAmount;
const uniquePayerIds = new Set(shares.map((share) => share.payerId));
if (uniquePayerIds.size !== shares.length) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["splitShares"],
message: "Escolha pessoas diferentes para dividir o lançamento.",
});
}
if (shares.some((share) => share.amount <= 0)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["splitShares"],
message: "Informe um valor maior que zero para cada pessoa.",
});
}
if (shares.length > 0) {
const sum = shares.reduce((total, share) => total + share.amount, 0);
const total = Math.abs(data.amount);
if (Math.abs(sum - total) > 0.01) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["primarySplitAmount"],
path: ["splitShares"],
message: "A soma das divisões deve ser igual ao valor total.",
});
}
@@ -561,11 +587,41 @@ type Share = {
amountCents: number;
};
type SplitShareInput = {
payerId: string;
amount: number;
};
export const resolveSplitShares = (data: {
payerId?: string | null;
secondaryPayerId?: string | null;
splitShares?: SplitShareInput[];
primarySplitAmount?: number;
secondarySplitAmount?: number;
}): SplitShareInput[] => {
if (data.splitShares && data.splitShares.length > 0) {
return data.splitShares;
}
if (!data.payerId || !data.secondaryPayerId) {
return [];
}
return [
{ payerId: data.payerId, amount: data.primarySplitAmount ?? 0 },
{
payerId: data.secondaryPayerId,
amount: data.secondarySplitAmount ?? 0,
},
];
};
export const buildShares = ({
totalCents,
payerId,
isSplit,
secondaryPayerId,
splitShares,
primarySplitAmountCents,
secondarySplitAmountCents,
}: {
@@ -573,10 +629,18 @@ export const buildShares = ({
payerId: string | null;
isSplit: boolean;
secondaryPayerId?: string;
splitShares?: SplitShareInput[];
primarySplitAmountCents?: number;
secondarySplitAmountCents?: number;
}): Share[] => {
if (isSplit) {
if (splitShares && splitShares.length > 0) {
return splitShares.map((share) => ({
payerId: share.payerId,
amountCents: Math.round(share.amount * 100),
}));
}
if (!payerId || !secondaryPayerId) {
throw new Error("Configuração de divisão inválida para o lançamento.");
}

View File

@@ -56,6 +56,7 @@ export async function createTransactionAction(
const ownershipError = await validateAllOwnership(user.id, {
payerId: data.payerId,
secondaryPayerId: data.secondaryPayerId,
splitPayerIds: data.splitShares?.map((share) => share.payerId),
categoryId: data.categoryId,
accountId: data.accountId,
cardId: data.cardId,
@@ -84,6 +85,7 @@ export async function createTransactionAction(
payerId: data.payerId ?? null,
isSplit: data.isSplit ?? false,
secondaryPayerId: data.secondaryPayerId,
splitShares: data.splitShares,
primarySplitAmountCents: data.primarySplitAmount
? Math.round(data.primarySplitAmount * 100)
: undefined,
@@ -207,6 +209,7 @@ export async function updateTransactionAction(
const ownershipError = await validateAllOwnership(user.id, {
payerId: data.payerId,
secondaryPayerId: data.secondaryPayerId,
splitPayerIds: data.splitShares?.map((share) => share.payerId),
categoryId: data.categoryId,
accountId: data.accountId,
cardId: data.cardId,
@@ -477,6 +480,7 @@ export async function updateTransactionSplitPairAction(
const ownershipError = await validateAllOwnership(user.id, {
payerId: data.payerId,
splitPayerIds: data.splitShares?.map((share) => share.payerId),
categoryId: data.categoryId,
accountId: data.accountId,
cardId: data.cardId,