mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
refactor: faxina arquitetural — código morto, identificadores em inglês e estrutura padronizada
Refatoração estrutural sem mudanças funcionais. Saldo líquido: −428 linhas. Removido: - 14 funções/constantes mortas verificadas via grep no repo todo: validateCategoriaOwnership, getInstallmentAnticipationsAction, getAnticipationDetailsAction, formatDecimalForDb, currencyFormatterNoCents, optionalDecimalSchema, formatMonthLabel, getGoalProgressStatusColorClass, MONTH_PERIOD_PARAM, calculateRemainingInstallments, e 5 funções fetch* não usadas em inbox/queries.ts. - 1 tipo morto (ImportRow) + 2 órfãos consequentes (InstallmentAnticipationWithRelations, GoalProgressStatus convertido em interno). - ~30 export keywords desnecessários (símbolos usados apenas no próprio arquivo). - Re-exports mortos em barrels: EstablishmentLogoPicker, CategoryReportSkeleton, WidgetSkeleton, toNameKey. - Arquivo features/reports/types.ts (barrel inteiro era órfão). Padronizado (PT-BR→EN em identificadores expostos): - 4 constantes globais (LANCAMENTOS_* → TRANSACTIONS_*). - 12 tipos/interfaces (Lancamento*/Pagador*/Estabelecimento* → equivalentes EN). - 13 funções/components exportados (fetchPagador*, EstabelecimentoInput, PagadorInfoCard, etc.). - 5 props cross-file (preLancamentosCount → inboxPendingCount, pagadorAvatarUrl → payerAvatarUrl, etc.). - Mantidas em PT-BR conforme exceção do CLAUDE.md: variáveis locais (pagador, categoria, lancamento), accessor key pagadorName (persistida em preferências), strings de UI. Reorganizado: - transactions/: 14 helpers soltos na raiz movidos para lib/; barrel actions.ts reduzido de 76 linhas de wrappers para 14 linhas de re-exports puros; anticipation-actions.ts movido para actions/anticipation.ts. - dashboard/: 8 helpers soltos consolidados em dashboard/lib/. - reports/: 5 query files na raiz consolidados em reports/lib/. - payers/: detail-actions.ts (21KB) e detail-queries.ts movidos para payers/lib/. - shared/components/: 9 dos 16 componentes soltos agrupados em brand/, widgets/, feedback/. - shared/lib/fetch-json.ts movido para shared/utils/fetch-json.ts. Validação: pnpm exec tsc --noEmit (0 erros), biome check (0 issues), knip (sem unused). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
358
src/features/transactions/lib/form-helpers.ts
Normal file
358
src/features/transactions/lib/form-helpers.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import type { TransactionItem } from "@/features/transactions/components/types";
|
||||
import { getTodayDateString } from "@/shared/utils/date";
|
||||
import { derivePeriodFromDate, getNextPeriod } from "@/shared/utils/period";
|
||||
import {
|
||||
PAYMENT_METHODS,
|
||||
TRANSACTION_CONDITIONS,
|
||||
TRANSACTION_TYPES,
|
||||
} from "./constants";
|
||||
|
||||
/**
|
||||
* Derives the fatura period for a credit card purchase based on closing day
|
||||
* and due day. The period represents the month the fatura is due (vencimento).
|
||||
*
|
||||
* Steps:
|
||||
* 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).
|
||||
* 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),
|
||||
* so we add another +1 month.
|
||||
*
|
||||
* @example
|
||||
* // Card closes day 22, due day 1 (dueDay < closingDay → +1 extra)
|
||||
* deriveCreditCardPeriod("2026-02-25", "22", "1") // "2026-04" (missed Feb closing → Mar cycle → due Apr)
|
||||
* deriveCreditCardPeriod("2026-02-15", "22", "1") // "2026-03" (in Feb cycle → due Mar)
|
||||
*
|
||||
* // 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-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)
|
||||
*/
|
||||
export function deriveCreditCardPeriod(
|
||||
purchaseDate: string,
|
||||
closingDay: string | null | undefined,
|
||||
dueDay?: string | null | undefined,
|
||||
): string {
|
||||
const basePeriod = derivePeriodFromDate(purchaseDate);
|
||||
if (!closingDay) return basePeriod;
|
||||
|
||||
const closingDayNum = Number.parseInt(closingDay, 10);
|
||||
if (Number.isNaN(closingDayNum)) return basePeriod;
|
||||
|
||||
const dayPart = purchaseDate.split("-")[2];
|
||||
const purchaseDayNum = Number.parseInt(dayPart ?? "1", 10);
|
||||
|
||||
// Start with the purchase month as the billing cycle
|
||||
let period = basePeriod;
|
||||
|
||||
// If purchase is on/after closing day, it enters the next billing cycle
|
||||
if (purchaseDayNum >= closingDayNum) {
|
||||
period = getNextPeriod(period);
|
||||
}
|
||||
|
||||
// If due day < closing day, the due date falls in the month after closing
|
||||
// (e.g., closes 22nd, due 1st → closing in March means due in April)
|
||||
const dueDayNum = Number.parseInt(dueDay ?? "", 10);
|
||||
if (!Number.isNaN(dueDayNum) && dueDayNum < closingDayNum) {
|
||||
period = getNextPeriod(period);
|
||||
}
|
||||
|
||||
return period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form state type for lancamento dialog
|
||||
*/
|
||||
export type TransactionFormState = {
|
||||
purchaseDate: string;
|
||||
period: string;
|
||||
name: string;
|
||||
transactionType: string;
|
||||
amount: string;
|
||||
condition: string;
|
||||
paymentMethod: string;
|
||||
payerId: string | undefined;
|
||||
secondaryPayerId: string | undefined;
|
||||
isSplit: boolean;
|
||||
primarySplitAmount: string;
|
||||
secondarySplitAmount: string;
|
||||
accountId: string | undefined;
|
||||
cardId: string | undefined;
|
||||
categoryId: string | undefined;
|
||||
installmentCount: string;
|
||||
recurrenceCount: string;
|
||||
dueDate: string;
|
||||
boletoPaymentDate: string;
|
||||
note: string;
|
||||
isSettled: boolean | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initial state overrides for lancamento form
|
||||
*/
|
||||
type TransactionFormOverrides = {
|
||||
defaultCardId?: string | null;
|
||||
defaultPaymentMethod?: string | null;
|
||||
defaultPurchaseDate?: string | null;
|
||||
defaultName?: string | null;
|
||||
defaultAmount?: string | null;
|
||||
defaultTransactionType?: "Despesa" | "Receita";
|
||||
isImporting?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds initial form state from lancamento data and defaults
|
||||
*/
|
||||
export function buildTransactionInitialState(
|
||||
transaction?: TransactionItem,
|
||||
defaultPayerId?: string | null,
|
||||
preferredPeriod?: string,
|
||||
overrides?: TransactionFormOverrides,
|
||||
): TransactionFormState {
|
||||
const purchaseDate = transaction?.purchaseDate
|
||||
? transaction.purchaseDate.slice(0, 10)
|
||||
: (overrides?.defaultPurchaseDate ?? getTodayDateString());
|
||||
|
||||
const paymentMethod =
|
||||
transaction?.paymentMethod ??
|
||||
overrides?.defaultPaymentMethod ??
|
||||
PAYMENT_METHODS[0];
|
||||
|
||||
const derivedPeriod = derivePeriodFromDate(purchaseDate);
|
||||
const fallbackPeriod =
|
||||
preferredPeriod && /^\d{4}-\d{2}$/.test(preferredPeriod)
|
||||
? preferredPeriod
|
||||
: derivedPeriod;
|
||||
|
||||
// Quando importando, usar valores padrão do usuário logado ao invés dos valores do lançamento original
|
||||
const isImporting = overrides?.isImporting ?? false;
|
||||
const fallbackPayerId = isImporting
|
||||
? (defaultPayerId ?? null)
|
||||
: (transaction?.payerId ?? defaultPayerId ?? null);
|
||||
|
||||
const boletoPaymentDate =
|
||||
transaction?.boletoPaymentDate ??
|
||||
(paymentMethod === "Boleto" && (transaction?.isSettled ?? false)
|
||||
? getTodayDateString()
|
||||
: "");
|
||||
|
||||
// Calcular o valor correto para importação de parcelados
|
||||
let amountValue = overrides?.defaultAmount ?? "";
|
||||
if (!amountValue && typeof transaction?.amount === "number") {
|
||||
let baseAmount = Math.abs(transaction.amount);
|
||||
|
||||
// Se está importando e é parcelado, usar o valor total (parcela * quantidade)
|
||||
if (
|
||||
isImporting &&
|
||||
transaction.condition === "Parcelado" &&
|
||||
transaction.installmentCount
|
||||
) {
|
||||
baseAmount = baseAmount * transaction.installmentCount;
|
||||
}
|
||||
|
||||
amountValue = (Math.round(baseAmount * 100) / 100).toFixed(2);
|
||||
}
|
||||
|
||||
return {
|
||||
purchaseDate,
|
||||
period:
|
||||
transaction?.period && /^\d{4}-\d{2}$/.test(transaction.period)
|
||||
? transaction.period
|
||||
: fallbackPeriod,
|
||||
name: transaction?.name ?? overrides?.defaultName ?? "",
|
||||
transactionType:
|
||||
transaction?.transactionType ??
|
||||
overrides?.defaultTransactionType ??
|
||||
TRANSACTION_TYPES[0],
|
||||
amount: amountValue,
|
||||
condition: transaction?.condition ?? TRANSACTION_CONDITIONS[0],
|
||||
paymentMethod,
|
||||
payerId: fallbackPayerId ?? undefined,
|
||||
secondaryPayerId: undefined,
|
||||
isSplit: false,
|
||||
|
||||
primarySplitAmount: "",
|
||||
secondarySplitAmount: "",
|
||||
accountId:
|
||||
paymentMethod === "Cartão de crédito"
|
||||
? undefined
|
||||
: isImporting
|
||||
? undefined
|
||||
: (transaction?.accountId ?? undefined),
|
||||
cardId:
|
||||
paymentMethod === "Cartão de crédito"
|
||||
? isImporting
|
||||
? (overrides?.defaultCardId ?? undefined)
|
||||
: (transaction?.cardId ?? overrides?.defaultCardId ?? undefined)
|
||||
: undefined,
|
||||
categoryId: isImporting
|
||||
? undefined
|
||||
: (transaction?.categoryId ?? undefined),
|
||||
installmentCount: transaction?.installmentCount
|
||||
? String(transaction.installmentCount)
|
||||
: "",
|
||||
recurrenceCount: transaction?.recurrenceCount
|
||||
? String(transaction.recurrenceCount)
|
||||
: "",
|
||||
dueDate: transaction?.dueDate ?? "",
|
||||
boletoPaymentDate,
|
||||
note: transaction?.note ?? "",
|
||||
isSettled:
|
||||
paymentMethod === "Cartão de crédito"
|
||||
? null
|
||||
: (transaction?.isSettled ?? true),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies field dependencies when form state changes
|
||||
* This function encapsulates the business logic for field interdependencies
|
||||
*/
|
||||
export function applyFieldDependencies(
|
||||
key: keyof TransactionFormState,
|
||||
value: TransactionFormState[keyof TransactionFormState],
|
||||
currentState: TransactionFormState,
|
||||
cardInfo?: { closingDay: string | null; dueDay: string | null } | null,
|
||||
): Partial<TransactionFormState> {
|
||||
const updates: Partial<TransactionFormState> = {};
|
||||
|
||||
// Auto-derive period from purchaseDate
|
||||
if (key === "purchaseDate" && typeof value === "string" && value) {
|
||||
const method = currentState.paymentMethod;
|
||||
if (method === "Cartão de crédito") {
|
||||
updates.period = deriveCreditCardPeriod(
|
||||
value,
|
||||
cardInfo?.closingDay,
|
||||
cardInfo?.dueDay,
|
||||
);
|
||||
} else if (method !== "Boleto") {
|
||||
updates.period = derivePeriodFromDate(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-derive period from dueDate when payment method is boleto
|
||||
if (key === "dueDate" && typeof value === "string" && value) {
|
||||
if (currentState.paymentMethod === "Boleto") {
|
||||
updates.period = derivePeriodFromDate(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-derive period when cardId changes (credit card selected)
|
||||
if (key === "cardId" && currentState.paymentMethod === "Cartão de crédito") {
|
||||
if (typeof value === "string" && value && currentState.purchaseDate) {
|
||||
updates.period = deriveCreditCardPeriod(
|
||||
currentState.purchaseDate,
|
||||
cardInfo?.closingDay,
|
||||
cardInfo?.dueDay,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// When condition changes, clear irrelevant fields
|
||||
if (key === "condition" && typeof value === "string") {
|
||||
if (value !== "Parcelado") {
|
||||
updates.installmentCount = "";
|
||||
}
|
||||
if (value !== "Recorrente") {
|
||||
updates.recurrenceCount = "";
|
||||
}
|
||||
}
|
||||
|
||||
// When payment method changes, adjust related fields
|
||||
if (key === "paymentMethod" && typeof value === "string") {
|
||||
if (value === "Cartão de crédito") {
|
||||
updates.accountId = undefined;
|
||||
updates.isSettled = null;
|
||||
} else {
|
||||
updates.cardId = undefined;
|
||||
updates.isSettled = currentState.isSettled ?? true;
|
||||
}
|
||||
|
||||
// Re-derive period based on new payment method
|
||||
if (value === "Cartão de crédito") {
|
||||
if (
|
||||
currentState.purchaseDate &&
|
||||
currentState.cardId &&
|
||||
cardInfo?.closingDay
|
||||
) {
|
||||
updates.period = deriveCreditCardPeriod(
|
||||
currentState.purchaseDate,
|
||||
cardInfo.closingDay,
|
||||
cardInfo.dueDay,
|
||||
);
|
||||
} else if (currentState.purchaseDate) {
|
||||
updates.period = derivePeriodFromDate(currentState.purchaseDate);
|
||||
}
|
||||
} else if (value === "Boleto" && currentState.dueDate) {
|
||||
updates.period = derivePeriodFromDate(currentState.dueDate);
|
||||
} else if (currentState.purchaseDate) {
|
||||
updates.period = derivePeriodFromDate(currentState.purchaseDate);
|
||||
}
|
||||
|
||||
// Clear boleto-specific fields if not boleto
|
||||
if (value !== "Boleto") {
|
||||
updates.dueDate = "";
|
||||
updates.boletoPaymentDate = "";
|
||||
} else if (
|
||||
currentState.isSettled ||
|
||||
(updates.isSettled !== null && updates.isSettled !== undefined)
|
||||
) {
|
||||
// Set today's date for boleto payment if settled
|
||||
const settled = updates.isSettled ?? currentState.isSettled;
|
||||
if (settled) {
|
||||
updates.boletoPaymentDate =
|
||||
currentState.boletoPaymentDate || getTodayDateString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When split is disabled, clear secondary pagador and split fields
|
||||
if (key === "isSplit" && value === false) {
|
||||
updates.secondaryPayerId = undefined;
|
||||
updates.primarySplitAmount = "";
|
||||
updates.secondarySplitAmount = "";
|
||||
}
|
||||
|
||||
// When split is enabled and amount exists, calculate initial split amounts
|
||||
if (key === "isSplit" && value === true) {
|
||||
const totalAmount = Number.parseFloat(currentState.amount) || 0;
|
||||
if (totalAmount > 0) {
|
||||
const half = (totalAmount / 2).toFixed(2);
|
||||
updates.primarySplitAmount = half;
|
||||
updates.secondarySplitAmount = half;
|
||||
}
|
||||
}
|
||||
|
||||
// When amount changes and split is enabled, recalculate split amounts
|
||||
if (key === "amount" && typeof value === "string" && currentState.isSplit) {
|
||||
const totalAmount = Number.parseFloat(value) || 0;
|
||||
if (totalAmount > 0) {
|
||||
const half = (totalAmount / 2).toFixed(2);
|
||||
updates.primarySplitAmount = half;
|
||||
updates.secondarySplitAmount = half;
|
||||
} else {
|
||||
updates.primarySplitAmount = "";
|
||||
updates.secondarySplitAmount = "";
|
||||
}
|
||||
}
|
||||
|
||||
// When primary pagador changes, clear secondary if it matches
|
||||
if (key === "payerId" && typeof value === "string") {
|
||||
const secondaryValue = currentState.secondaryPayerId;
|
||||
if (secondaryValue && secondaryValue === value) {
|
||||
updates.secondaryPayerId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// When isSettled changes and payment method is Boleto
|
||||
if (key === "isSettled" && currentState.paymentMethod === "Boleto") {
|
||||
if (value === true) {
|
||||
updates.boletoPaymentDate =
|
||||
currentState.boletoPaymentDate || getTodayDateString();
|
||||
} else if (value === false) {
|
||||
updates.boletoPaymentDate = "";
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
Reference in New Issue
Block a user