mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-07-09 11:26:00 +00:00
feat(lancamentos): aprimora parcelamentos e protecoes
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
TRANSACTION_CONDITIONS,
|
||||
TRANSACTION_TYPES,
|
||||
} from "@/features/transactions/lib/constants";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
||||
import { handleActionError } from "@/shared/lib/actions/helpers";
|
||||
import { getUser } from "@/shared/lib/auth/server";
|
||||
import { db } from "@/shared/lib/db";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
fetchOwnedPayerIds,
|
||||
formatPaidInvoicePeriods,
|
||||
getPaidInvoicePeriods,
|
||||
isInitialBalanceTransaction,
|
||||
type MassAddInput,
|
||||
massAddSchema,
|
||||
resolvePeriod,
|
||||
@@ -47,6 +49,19 @@ const getPeriodOffset = (basePeriod: string, targetPeriod: string) => {
|
||||
return (target.year - base.year) * 12 + (target.month - base.month);
|
||||
};
|
||||
|
||||
type ProtectedTransactionCandidate = {
|
||||
note: string | null;
|
||||
transactionType: string | null;
|
||||
condition: string | null;
|
||||
paymentMethod: string | null;
|
||||
};
|
||||
|
||||
const isProtectedTransaction = (
|
||||
record: ProtectedTransactionCandidate,
|
||||
): boolean =>
|
||||
Boolean(record.note?.startsWith(ACCOUNT_AUTO_INVOICE_NOTE_PREFIX)) ||
|
||||
isInitialBalanceTransaction(record);
|
||||
|
||||
export async function deleteTransactionBulkAction(
|
||||
input: DeleteBulkInput,
|
||||
): Promise<ActionResult> {
|
||||
@@ -61,6 +76,9 @@ export async function deleteTransactionBulkAction(
|
||||
seriesId: true,
|
||||
period: true,
|
||||
condition: true,
|
||||
transactionType: true,
|
||||
paymentMethod: true,
|
||||
note: true,
|
||||
},
|
||||
where: and(
|
||||
eq(transactions.id, data.id),
|
||||
@@ -79,6 +97,13 @@ export async function deleteTransactionBulkAction(
|
||||
};
|
||||
}
|
||||
|
||||
if (isProtectedTransaction(existing)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos protegidos não podem ser removidos em massa.",
|
||||
};
|
||||
}
|
||||
|
||||
let scopeFilter: ReturnType<typeof and>;
|
||||
let successMessage: string;
|
||||
|
||||
@@ -171,6 +196,7 @@ export async function updateTransactionBulkAction(
|
||||
purchaseDate: true,
|
||||
payerId: true,
|
||||
cardId: true,
|
||||
note: true,
|
||||
},
|
||||
where: and(
|
||||
eq(transactions.id, data.id),
|
||||
@@ -189,6 +215,13 @@ export async function updateTransactionBulkAction(
|
||||
};
|
||||
}
|
||||
|
||||
if (isProtectedTransaction(existing)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos protegidos não podem ser atualizados em massa.",
|
||||
};
|
||||
}
|
||||
|
||||
const baseUpdatePayload: Record<string, unknown> = {
|
||||
name: data.name,
|
||||
categoryId: data.categoryId ?? null,
|
||||
@@ -753,6 +786,13 @@ export async function deleteMultipleTransactionsAction(
|
||||
return { success: false, error: "Nenhum lançamento encontrado." };
|
||||
}
|
||||
|
||||
if (existing.some(isProtectedTransaction)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos protegidos não podem ser removidos em massa.",
|
||||
};
|
||||
}
|
||||
|
||||
const linkedAttachments = await db
|
||||
.select({ id: attachments.id, fileKey: attachments.fileKey })
|
||||
.from(transactionAttachments)
|
||||
|
||||
@@ -335,6 +335,12 @@ const baseFields = z.object({
|
||||
.min(1, "Selecione uma quantidade válida.")
|
||||
.max(60, "Selecione uma quantidade válida.")
|
||||
.optional(),
|
||||
startInstallment: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1, "Selecione uma parcela válida.")
|
||||
.max(60, "Selecione uma parcela válida.")
|
||||
.optional(),
|
||||
recurrenceCount: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
@@ -415,6 +421,15 @@ const refineLancamento = (
|
||||
path: ["installmentCount"],
|
||||
message: "Selecione pelo menos duas parcelas.",
|
||||
});
|
||||
} else if (
|
||||
data.startInstallment &&
|
||||
data.startInstallment > data.installmentCount
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["startInstallment"],
|
||||
message: "A parcela inicial não pode ser maior que o total.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,24 +666,27 @@ export const buildTransactionRecords = ({
|
||||
|
||||
if (data.condition === "Parcelado") {
|
||||
const installmentTotal = data.installmentCount ?? 0;
|
||||
const startInstallment = data.startInstallment ?? 1;
|
||||
const amountsByShare = shares.map((share) =>
|
||||
splitAmount(share.amountCents, installmentTotal),
|
||||
);
|
||||
|
||||
for (
|
||||
let installment = 0;
|
||||
installment < installmentTotal;
|
||||
installment += 1
|
||||
let index = 0;
|
||||
index <= installmentTotal - startInstallment;
|
||||
index += 1
|
||||
) {
|
||||
const installmentPeriod = addMonthsToPeriod(period, installment);
|
||||
const currentInstallment = startInstallment + index;
|
||||
const installmentPeriod = addMonthsToPeriod(period, index);
|
||||
const installmentDueDate = dueDate
|
||||
? addMonthsToDate(dueDate, installment)
|
||||
? addMonthsToDate(dueDate, index)
|
||||
: null;
|
||||
const splitGroupId = cycleSplitGroupId();
|
||||
|
||||
shares.forEach((share, shareIndex) => {
|
||||
const amountCents = amountsByShare[shareIndex]?.[installment] ?? 0;
|
||||
const settled = resolveSettledValue(installment);
|
||||
const amountCents =
|
||||
amountsByShare[shareIndex]?.[currentInstallment - 1] ?? 0;
|
||||
const settled = resolveSettledValue(index);
|
||||
records.push({
|
||||
...basePayload,
|
||||
amount: centsToDecimalString(amountCents * amountSign),
|
||||
@@ -677,7 +695,7 @@ export const buildTransactionRecords = ({
|
||||
period: installmentPeriod,
|
||||
isSettled: settled,
|
||||
installmentCount: installmentTotal,
|
||||
currentInstallment: installment + 1,
|
||||
currentInstallment,
|
||||
recurrenceCount: null,
|
||||
dueDate: installmentDueDate,
|
||||
splitGroupId,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
transactionAttachments,
|
||||
transactions,
|
||||
} from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
||||
import { handleActionError } from "@/shared/lib/actions/helpers";
|
||||
import { getUser } from "@/shared/lib/auth/server";
|
||||
import { db } from "@/shared/lib/db";
|
||||
@@ -230,13 +231,6 @@ export async function updateTransactionAction(
|
||||
eq(transactions.id, data.id),
|
||||
eq(transactions.userId, user.id),
|
||||
),
|
||||
with: {
|
||||
category: {
|
||||
columns: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as
|
||||
| {
|
||||
id: string;
|
||||
@@ -248,7 +242,6 @@ export async function updateTransactionAction(
|
||||
accountId: string | null;
|
||||
cardId: string | null;
|
||||
categoryId: string | null;
|
||||
category: { name: string } | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@@ -256,14 +249,17 @@ export async function updateTransactionAction(
|
||||
return { success: false, error: "Lançamento não encontrado." };
|
||||
}
|
||||
|
||||
const categoriasProtegidasEdicao = ["Saldo inicial", "Pagamentos"];
|
||||
if (
|
||||
existing.category?.name &&
|
||||
categoriasProtegidasEdicao.includes(existing.category.name)
|
||||
) {
|
||||
if (existing.note?.startsWith(ACCOUNT_AUTO_INVOICE_NOTE_PREFIX)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Lançamentos com a categoria '${existing.category.name}' não podem ser editados.`,
|
||||
error: "Pagamentos automáticos de fatura não podem ser editados.",
|
||||
};
|
||||
}
|
||||
|
||||
if (isInitialBalanceTransaction(existing)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos de saldo inicial não podem ser editados.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -391,13 +387,6 @@ export async function deleteTransactionAction(
|
||||
eq(transactions.id, data.id),
|
||||
eq(transactions.userId, user.id),
|
||||
),
|
||||
with: {
|
||||
category: {
|
||||
columns: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as
|
||||
| {
|
||||
id: string;
|
||||
@@ -411,7 +400,6 @@ export async function deleteTransactionAction(
|
||||
period: string;
|
||||
note: string | null;
|
||||
categoryId: string | null;
|
||||
category: { name: string } | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@@ -419,14 +407,17 @@ export async function deleteTransactionAction(
|
||||
return { success: false, error: "Lançamento não encontrado." };
|
||||
}
|
||||
|
||||
const categoriasProtegidasRemocao = ["Saldo inicial", "Pagamentos"];
|
||||
if (
|
||||
existing.category?.name &&
|
||||
categoriasProtegidasRemocao.includes(existing.category.name)
|
||||
) {
|
||||
if (existing.note?.startsWith(ACCOUNT_AUTO_INVOICE_NOTE_PREFIX)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Lançamentos com a categoria '${existing.category.name}' não podem ser removidos.`,
|
||||
error: "Pagamentos automáticos de fatura não podem ser removidos.",
|
||||
};
|
||||
}
|
||||
|
||||
if (isInitialBalanceTransaction(existing)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos de saldo inicial não podem ser removidos.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user