feat(banking C): credit cards + monthly statements
Final banking step. User-managed: each month, paste your bank statement totals into the form. We don't auto-compute interest — bank's number is authoritative. Schema: - credit_cards: bankName, cardName, last4, creditLimit, statementDay, dueDay, interestRate (monthly nominal %), bankAccountId (optional FK, payments expense from this account), archived, notes. - credit_card_statements: cardId, period (YYYY-MM), statementDate, dueDate, totalDebt, minimumPayment, paidAmount, status (pending/partial/paid/overdue), financeEntryId (link to last payment). - Indexes on (tenantId, archived) for cards and (tenantId, cardId) + (tenantId, status, dueDate) for statements. Server (lib/appwrite/credit-card-actions.ts): - create/update/archive(toggle)/delete for cards. Card delete cascades through statements + their finance_entries. - createStatementAction: computes status from dueDate + paidAmount. - payStatementAction: partial-payment friendly. Creates a single finance_entry expense for the pay amount, capped at remaining balance, bankAccountId carried from the card. Recomputes status: paid (full), partial (some + pre-due), overdue (past due with anything < total). - deleteStatementAction: removes linked finance_entry too. - All audit-logged. UI (/finance/cards): - 3 stat cards: aktif kart sayısı, bekleyen toplam, vadesi geçmiş ekstre. - Per-card panel: bank/name/last4, limit + statement/due day + monthly interest, current outstanding. Statement table inside card with status badges, Öde button (opens partial-payment dialog), delete button. - CardFormSheet for card CRUD, StatementFormSheet for statement creation with default period/dates derived from card's statementDay/dueDay. Sidebar Finans submenu now functional: Banka hesapları → /finance/banks, Krediler → /finance/loans, Kredi kartları → /finance/cards.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const creditCardSchema = z.object({
|
||||
bankName: z.string().trim().min(1, "Banka adı zorunlu.").max(100),
|
||||
cardName: z.string().trim().min(1, "Kart adı zorunlu.").max(100),
|
||||
last4: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(4)
|
||||
.optional()
|
||||
.transform((v) => (v ? v.replace(/\D/g, "").slice(-4) : undefined)),
|
||||
creditLimit: z
|
||||
.union([z.number(), z.string()])
|
||||
.optional()
|
||||
.transform((v) => {
|
||||
if (v === undefined || v === "") return 0;
|
||||
const n = typeof v === "string" ? Number(v.replace(",", ".")) : v;
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}),
|
||||
statementDay: z
|
||||
.union([z.number(), z.string()])
|
||||
.optional()
|
||||
.transform((v) => {
|
||||
if (v === undefined || v === "") return 1;
|
||||
const n = typeof v === "string" ? parseInt(v, 10) : v;
|
||||
return Number.isFinite(n) ? Math.min(28, Math.max(1, n)) : 1;
|
||||
}),
|
||||
dueDay: z
|
||||
.union([z.number(), z.string()])
|
||||
.optional()
|
||||
.transform((v) => {
|
||||
if (v === undefined || v === "") return 10;
|
||||
const n = typeof v === "string" ? parseInt(v, 10) : v;
|
||||
return Number.isFinite(n) ? Math.min(28, Math.max(1, n)) : 10;
|
||||
}),
|
||||
interestRate: z
|
||||
.union([z.number(), z.string()])
|
||||
.optional()
|
||||
.transform((v) => {
|
||||
if (v === undefined || v === "") return 4.25;
|
||||
const n = typeof v === "string" ? Number(v.replace(",", ".")) : v;
|
||||
return Number.isFinite(n) ? n : 4.25;
|
||||
}),
|
||||
bankAccountId: z.string().optional().transform((v) => (v ? v : undefined)),
|
||||
notes: z.string().trim().max(500).optional().transform((v) => (v ? v : undefined)),
|
||||
});
|
||||
|
||||
export type CreditCardInput = z.infer<typeof creditCardSchema>;
|
||||
|
||||
export const statementSchema = z.object({
|
||||
cardId: z.string().min(1, "Kart seçin."),
|
||||
period: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}$/, "Dönem YYYY-AA formatında olmalı.")
|
||||
.max(7),
|
||||
statementDate: z.string().min(1, "Hesap kesim tarihi zorunlu."),
|
||||
dueDate: z.string().min(1, "Son ödeme tarihi zorunlu."),
|
||||
totalDebt: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => (typeof v === "string" ? Number(v.replace(",", ".")) : v))
|
||||
.pipe(z.number().nonnegative("Negatif olamaz.")),
|
||||
minimumPayment: z
|
||||
.union([z.number(), z.string()])
|
||||
.optional()
|
||||
.transform((v) => {
|
||||
if (v === undefined || v === "") return 0;
|
||||
const n = typeof v === "string" ? Number(v.replace(",", ".")) : v;
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}),
|
||||
notes: z.string().trim().max(500).optional().transform((v) => (v ? v : undefined)),
|
||||
});
|
||||
|
||||
export type StatementInput = z.infer<typeof statementSchema>;
|
||||
Reference in New Issue
Block a user