fix: corrige antecipacao e fechamento de compras no cartao
This commit is contained in:
@@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user