mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
feat(faturas/extrato): ajuste de fatura, reembolso e ajuste de saldo da conta
- botão "Ajustar fatura" na página da fatura abre dialog com input do valor real e preview da diferença; action faz upsert/delete idempotente do lançamento de ajuste - opção "Reembolso" no dropdown de ações de despesas à vista cria receita espelhada no extrato ou fatura correta, vinculada ao lançamento original - botão "Ajustar saldo" no extrato da conta compara saldo real informado e gera lançamento de ajuste por (accountId, period) via upsert/delete idempotente - constantes INVOICE_ADJUSTMENT_NAME, ACCOUNT_BALANCE_ADJUSTMENT_NAME, REFUND_NOTE_PREFIX e buildRefundNote() centralizadas em shared/lib/accounts/constants.ts - extrato agora contabiliza transferências internas em Entradas e Saídas corretamente Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
||||
type UpdateInput,
|
||||
updateSchema,
|
||||
validateAllOwnership,
|
||||
validateCardLimit,
|
||||
} from "./core";
|
||||
|
||||
export async function createTransactionAction(
|
||||
@@ -132,6 +133,20 @@ export async function createTransactionAction(
|
||||
)} já estão pagas. Desfaça o pagamento antes de adicionar este lançamento.`,
|
||||
} as ActionResult<{ ids: string[] }>;
|
||||
}
|
||||
|
||||
if (data.transactionType === "Despesa") {
|
||||
const limitCheck = await validateCardLimit({
|
||||
userId: user.id,
|
||||
cardId: data.cardId,
|
||||
addAmount: Math.abs(data.amount),
|
||||
});
|
||||
if (!limitCheck.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: limitCheck.error,
|
||||
} as ActionResult<{ ids: string[] }>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const inserted = await db
|
||||
@@ -287,6 +302,22 @@ export async function updateTransactionAction(
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
data.paymentMethod === "Cartão de crédito" &&
|
||||
data.cardId &&
|
||||
data.transactionType === "Despesa"
|
||||
) {
|
||||
const limitCheck = await validateCardLimit({
|
||||
userId: user.id,
|
||||
cardId: data.cardId,
|
||||
addAmount: Math.abs(data.amount),
|
||||
excludeTransactionIds: [data.id],
|
||||
});
|
||||
if (!limitCheck.ok) {
|
||||
return { success: false, error: limitCheck.error };
|
||||
}
|
||||
}
|
||||
|
||||
await db
|
||||
.update(transactions)
|
||||
.set({
|
||||
@@ -582,7 +613,7 @@ export async function toggleTransactionSettlementAction(
|
||||
const data = toggleSettlementSchema.parse(input);
|
||||
|
||||
const existing = await db.query.transactions.findFirst({
|
||||
columns: { id: true, paymentMethod: true },
|
||||
columns: { id: true, paymentMethod: true, accountId: true },
|
||||
where: and(
|
||||
eq(transactions.id, data.id),
|
||||
eq(transactions.userId, user.id),
|
||||
@@ -601,18 +632,52 @@ export async function toggleTransactionSettlementAction(
|
||||
}
|
||||
|
||||
const isBoleto = existing.paymentMethod === "Boleto";
|
||||
const customPaymentDate =
|
||||
isBoleto && data.value && data.paymentDate
|
||||
? parseLocalDateString(data.paymentDate)
|
||||
: null;
|
||||
const boletoPaymentDate = isBoleto
|
||||
? data.value
|
||||
? getBusinessTodayDate()
|
||||
? (customPaymentDate ?? getBusinessTodayDate())
|
||||
: null
|
||||
: null;
|
||||
|
||||
const shouldUpdateAccount =
|
||||
isBoleto && data.value && data.paymentAccountId !== undefined;
|
||||
|
||||
if (shouldUpdateAccount && data.paymentAccountId) {
|
||||
const paymentAccount = await db.query.financialAccounts.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(
|
||||
eq(financialAccounts.id, data.paymentAccountId),
|
||||
eq(financialAccounts.userId, user.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!paymentAccount) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Conta de pagamento não encontrada.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const updatePayload: {
|
||||
isSettled: boolean;
|
||||
boletoPaymentDate: Date | null;
|
||||
accountId?: string | null;
|
||||
} = {
|
||||
isSettled: data.value,
|
||||
boletoPaymentDate,
|
||||
};
|
||||
|
||||
if (shouldUpdateAccount) {
|
||||
updatePayload.accountId = data.paymentAccountId ?? null;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(transactions)
|
||||
.set({
|
||||
isSettled: data.value,
|
||||
boletoPaymentDate,
|
||||
})
|
||||
.set(updatePayload)
|
||||
.where(
|
||||
and(eq(transactions.id, data.id), eq(transactions.userId, user.id)),
|
||||
);
|
||||
|
||||
@@ -64,6 +64,9 @@ export function TransactionDetailsDialog({
|
||||
: 0;
|
||||
|
||||
const isBoleto = transaction.paymentMethod === "Boleto";
|
||||
const shortTransactionId = `…${
|
||||
transaction.id.split("-").at(-1) ?? transaction.id
|
||||
}`;
|
||||
|
||||
const handleEdit = () => {
|
||||
onOpenChange(false);
|
||||
@@ -120,6 +123,12 @@ export function TransactionDetailsDialog({
|
||||
Detalhes
|
||||
</h3>
|
||||
<ul className="min-w-0 grid gap-2 rounded-lg border p-3">
|
||||
<DetailRow
|
||||
label="ID"
|
||||
value={shortTransactionId}
|
||||
valueClassName="font-mono"
|
||||
/>
|
||||
|
||||
<DetailRow
|
||||
label="Período"
|
||||
value={formatPeriod(transaction.period)}
|
||||
@@ -253,13 +262,16 @@ export function TransactionDetailsDialog({
|
||||
interface DetailRowProps {
|
||||
label: string;
|
||||
value: string;
|
||||
valueClassName?: string;
|
||||
}
|
||||
|
||||
function DetailRow({ label, value }: DetailRowProps) {
|
||||
function DetailRow({ label, value, valueClassName }: DetailRowProps) {
|
||||
return (
|
||||
<li className="min-w-0 flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span className="min-w-0 truncate">{value}</span>
|
||||
<span className={`min-w-0 truncate ${valueClassName ?? ""}`}>
|
||||
{value}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user