fix: corrige antecipacao e fechamento de compras no cartao

This commit is contained in:
Felipe Coutinho
2026-03-06 13:58:28 +00:00
parent 137c7b305d
commit 2f781a8dca
7 changed files with 48 additions and 41 deletions

View File

@@ -17,10 +17,10 @@ import {
generateAnticipationNote, generateAnticipationNote,
} from "@/lib/installments/anticipation-helpers"; } from "@/lib/installments/anticipation-helpers";
import type { import type {
InstallmentAnticipationWithRelations,
CancelAnticipationInput, CancelAnticipationInput,
CreateAnticipationInput, CreateAnticipationInput,
EligibleInstallment, EligibleInstallment,
InstallmentAnticipationWithRelations,
} from "@/lib/installments/anticipation-types"; } from "@/lib/installments/anticipation-types";
import { uuidSchema } from "@/lib/schemas/common"; import { uuidSchema } from "@/lib/schemas/common";
import { formatDecimalForDbRequired } from "@/lib/utils/currency"; import { formatDecimalForDbRequired } from "@/lib/utils/currency";
@@ -94,7 +94,7 @@ export async function getEligibleInstallmentsAction(
}, },
}); });
const eligibleInstallments: EligibleInstallment[] = rows.map((row) => ({ const eligibleInstallments: EligibleInstallment[] = rows.map((row: any) => ({
id: row.id, id: row.id,
name: row.name, name: row.name,
amount: row.amount, amount: row.amount,
@@ -110,10 +110,11 @@ export async function getEligibleInstallmentsAction(
return { return {
success: true, success: true,
message: "Parcelas elegíveis carregadas.",
data: eligibleInstallments, data: eligibleInstallments,
}; };
} catch (error) { } catch (error) {
return handleActionError(error); return handleActionError(error) as ActionResult<EligibleInstallment[]>;
} }
} }
@@ -154,7 +155,7 @@ export async function createInstallmentAnticipationAction(
// 2. Calcular valor total // 2. Calcular valor total
const totalAmountCents = installments.reduce( const totalAmountCents = installments.reduce(
(sum, inst) => sum + Number(inst.amount) * 100, (sum: number, inst: any) => sum + Number(inst.amount) * 100,
0, 0,
); );
const totalAmount = totalAmountCents / 100; const totalAmount = totalAmountCents / 100;
@@ -181,7 +182,7 @@ export async function createInstallmentAnticipationAction(
const firstInstallment = installments[0]; const firstInstallment = installments[0];
// 4. Criar lançamento e antecipação em transação // 4. Criar lançamento e antecipação em transação
await db.transaction(async (tx) => { await db.transaction(async (tx: any) => {
// 4.1. Criar o lançamento de antecipação (com desconto aplicado) // 4.1. Criar o lançamento de antecipação (com desconto aplicado)
const [newLancamento] = await tx const [newLancamento] = await tx
.insert(lancamentos) .insert(lancamentos)
@@ -205,7 +206,7 @@ export async function createInstallmentAnticipationAction(
note: note:
data.note || data.note ||
generateAnticipationNote( generateAnticipationNote(
installments.map((inst) => ({ installments.map((inst: any) => ({
id: inst.id, id: inst.id,
name: inst.name, name: inst.name,
amount: inst.amount, amount: inst.amount,
@@ -329,10 +330,13 @@ export async function getInstallmentAnticipationsAction(
return { return {
success: true, success: true,
message: "Antecipações carregadas.",
data: anticipations, data: anticipations,
}; };
} catch (error) { } catch (error) {
return handleActionError(error); return handleActionError(
error,
) as ActionResult<InstallmentAnticipationWithRelations[]>;
} }
} }
@@ -347,7 +351,7 @@ export async function cancelInstallmentAnticipationAction(
const user = await getUser(); const user = await getUser();
const data = cancelAnticipationSchema.parse(input); const data = cancelAnticipationSchema.parse(input);
await db.transaction(async (tx) => { await db.transaction(async (tx: any) => {
// 1. Buscar antecipação usando query builder // 1. Buscar antecipação usando query builder
const anticipationRows = await tx const anticipationRows = await tx
.select({ .select({
@@ -469,9 +473,12 @@ export async function getAnticipationDetailsAction(
return { return {
success: true, success: true,
message: "Detalhes da antecipação carregados.",
data: anticipation, data: anticipation,
}; };
} catch (error) { } catch (error) {
return handleActionError(error); return handleActionError(
error,
) as ActionResult<InstallmentAnticipationWithRelations>;
} }
} }

View File

@@ -60,7 +60,7 @@ interface AnticipateInstallmentsDialogProps {
type AnticipationFormValues = { type AnticipationFormValues = {
anticipationPeriod: string; anticipationPeriod: string;
discount: number; discount: string;
pagadorId: string; pagadorId: string;
categoriaId: string; categoriaId: string;
note: string; note: string;
@@ -95,7 +95,7 @@ export function AnticipateInstallmentsDialog({
const { formState, updateField, setFormState } = const { formState, updateField, setFormState } =
useFormState<AnticipationFormValues>({ useFormState<AnticipationFormValues>({
anticipationPeriod: defaultPeriod, anticipationPeriod: defaultPeriod,
discount: 0, discount: "0",
pagadorId: "", pagadorId: "",
categoriaId: "", categoriaId: "",
note: "", note: "",
@@ -110,23 +110,25 @@ export function AnticipateInstallmentsDialog({
getEligibleInstallmentsAction(seriesId) getEligibleInstallmentsAction(seriesId)
.then((result) => { .then((result) => {
if (result.success && result.data) { if (!result.success) {
setEligibleInstallments(result.data);
// Pré-preencher pagador e categoria da primeira parcela
if (result.data.length > 0) {
const first = result.data[0];
setFormState({
anticipationPeriod: defaultPeriod,
discount: 0,
pagadorId: first.pagadorId ?? "",
categoriaId: first.categoriaId ?? "",
note: "",
});
}
} else {
toast.error(result.error || "Erro ao carregar parcelas"); toast.error(result.error || "Erro ao carregar parcelas");
setEligibleInstallments([]); setEligibleInstallments([]);
return;
}
const installments = result.data ?? [];
setEligibleInstallments(installments);
// Pré-preencher pagador e categoria da primeira parcela
if (installments.length > 0) {
const first = installments[0];
setFormState({
anticipationPeriod: defaultPeriod,
discount: "0",
pagadorId: first.pagadorId ?? "",
categoriaId: first.categoriaId ?? "",
note: "",
});
} }
}) })
.catch((error) => { .catch((error) => {
@@ -268,9 +270,7 @@ export function AnticipateInstallmentsDialog({
<CurrencyInput <CurrencyInput
id="anticipation-discount" id="anticipation-discount"
value={formState.discount} value={formState.discount}
onValueChange={(value) => onValueChange={(value) => updateField("discount", value)}
updateField("discount", value ?? 0)
}
placeholder="R$ 0,00" placeholder="R$ 0,00"
disabled={isPending} disabled={isPending}
/> />

View File

@@ -59,14 +59,15 @@ export function AnticipationHistoryDialog({
try { try {
const result = await getInstallmentAnticipationsAction(seriesId); const result = await getInstallmentAnticipationsAction(seriesId);
if (result.success && result.data) { if (!result.success) {
setAnticipations(result.data);
} else {
toast.error( toast.error(
result.error || "Erro ao carregar histórico de antecipações", result.error || "Erro ao carregar histórico de antecipações",
); );
setAnticipations([]); setAnticipations([]);
return;
} }
setAnticipations(result.data ?? []);
} catch (error) { } catch (error) {
console.error("Erro ao buscar antecipações:", error); console.error("Erro ao buscar antecipações:", error);
toast.error("Erro ao carregar histórico de antecipações"); toast.error("Erro ao carregar histórico de antecipações");

View File

@@ -1,9 +1,9 @@
import { RiArrowDownFill, RiCheckLine } from "@remixicon/react"; import { RiArrowDownFill, RiCheckLine } from "@remixicon/react";
import { import {
calculateLastInstallmentDate, calculateLastInstallmentDate,
formatCurrentInstallment,
formatLastInstallmentDate,
formatPurchaseDate, formatPurchaseDate,
formatLastInstallmentDate,
formatCurrentInstallment,
} from "@/lib/installments/utils"; } from "@/lib/installments/utils";
type InstallmentTimelineProps = { type InstallmentTimelineProps = {

View File

@@ -38,6 +38,7 @@ export type CreateAnticipationInput = {
seriesId: string; seriesId: string;
installmentIds: string[]; installmentIds: string[];
anticipationPeriod: string; anticipationPeriod: string;
discount?: number;
pagadorId?: string; pagadorId?: string;
categoriaId?: string; categoriaId?: string;
note?: string; note?: string;

View File

@@ -71,9 +71,6 @@ export function formatPurchaseDate(date: Date): string {
* Formata o texto da parcela atual * Formata o texto da parcela atual
* Exemplo: "1 de 6" * Exemplo: "1 de 6"
*/ */
export function formatCurrentInstallment( export function formatCurrentInstallment(current: number, total: number): string {
current: number,
total: number,
): string {
return `${current} de ${total}`; return `${current} de ${total}`;
} }

View File

@@ -12,7 +12,7 @@ import {
* and due day. The period represents the month the fatura is due (vencimento). * and due day. The period represents the month the fatura is due (vencimento).
* *
* Steps: * Steps:
* 1. If purchase day > closing day → the purchase missed this month's closing, * 1. If purchase day >= closing day → the purchase missed this month's closing,
* so it enters the NEXT month's billing cycle (+1 month from purchase). * so it enters the NEXT month's billing cycle (+1 month from purchase).
* 2. Then, if dueDay < closingDay, the due date falls in the month AFTER the * 2. Then, if dueDay < closingDay, the due date falls in the month AFTER the
* closing month (e.g., closes 22nd, due 1st → closes Mar/22, due Apr/1), * closing month (e.g., closes 22nd, due 1st → closes Mar/22, due Apr/1),
@@ -25,6 +25,7 @@ import {
* *
* // Card closes day 5, due day 15 (dueDay >= closingDay → no extra) * // Card closes day 5, due day 15 (dueDay >= closingDay → no extra)
* deriveCreditCardPeriod("2026-02-10", "5", "15") // "2026-03" (missed Feb closing → Mar cycle → due Mar) * deriveCreditCardPeriod("2026-02-10", "5", "15") // "2026-03" (missed Feb closing → Mar cycle → due Mar)
* deriveCreditCardPeriod("2026-02-05", "5", "15") // "2026-03" (closing day itself already goes to next cycle)
* deriveCreditCardPeriod("2026-02-03", "5", "15") // "2026-02" (in Feb cycle → due Feb) * deriveCreditCardPeriod("2026-02-03", "5", "15") // "2026-02" (in Feb cycle → due Feb)
*/ */
export function deriveCreditCardPeriod( export function deriveCreditCardPeriod(
@@ -44,8 +45,8 @@ export function deriveCreditCardPeriod(
// Start with the purchase month as the billing cycle // Start with the purchase month as the billing cycle
let period = basePeriod; let period = basePeriod;
// If purchase is after closing day, it enters the next billing cycle // If purchase is on/after closing day, it enters the next billing cycle
if (purchaseDayNum > closingDayNum) { if (purchaseDayNum >= closingDayNum) {
period = getNextPeriod(period); period = getNextPeriod(period);
} }