feat(accounts): hesaplar (KMH, kredi kartı, yemek kartı, yalnızca ben)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/** Hesap türleri ve yemek kartı sağlayıcıları (Bölüm 6.1). */
|
||||
export const ACCOUNT_TYPES = ["cash", "bank", "savings", "credit_card", "meal_card"] as const;
|
||||
export type AccountType = (typeof ACCOUNT_TYPES)[number];
|
||||
|
||||
export const MEAL_CARD_PROVIDERS = [
|
||||
"pluxee",
|
||||
"multinet",
|
||||
"edenred",
|
||||
"setcard",
|
||||
"metropol",
|
||||
"other",
|
||||
] as const;
|
||||
export type MealCardProvider = (typeof MEAL_CARD_PROVIDERS)[number];
|
||||
|
||||
/** Kredi kartı son ödeme kuralı: kesimden N gün sonra | ayın sabit günü. */
|
||||
export const DUE_RULE_KINDS = ["daysAfter", "fixedDay"] as const;
|
||||
export type DueRuleKind = (typeof DUE_RULE_KINDS)[number];
|
||||
|
||||
/** Borç (negatif bakiye) taşıyan hesap türleri. */
|
||||
export const isLiabilityType = (type: AccountType) => type === "credit_card";
|
||||
@@ -0,0 +1,89 @@
|
||||
import { z } from "zod";
|
||||
import { tr } from "../../i18n/tr";
|
||||
import { ACCOUNT_TYPES, DUE_RULE_KINDS, MEAL_CARD_PROVIDERS } from "../accounts";
|
||||
import { ICON_KEYS, SWATCHES } from "../appearance";
|
||||
import { parseMoney } from "../money";
|
||||
|
||||
const v = tr.validation;
|
||||
|
||||
/** Türkçe para girişi ("1.250,50") → kuruş. Boş → 0 (opsiyonel alanlarda). */
|
||||
export const moneyInputSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((s, ctx) => {
|
||||
if (s === "") return 0;
|
||||
const minor = parseMoney(s);
|
||||
if (minor === null) {
|
||||
ctx.addIssue({ code: "custom", message: v.money });
|
||||
return z.NEVER;
|
||||
}
|
||||
return minor;
|
||||
});
|
||||
|
||||
/** Pozitif (≥ 0) para girişi. */
|
||||
export const nonNegativeMoneySchema = moneyInputSchema.refine((n) => n >= 0, v.moneyNonNegative);
|
||||
|
||||
const day = (max: number) => z.coerce.number().int().min(1).max(max);
|
||||
const objectIdOrEmpty = z
|
||||
.string()
|
||||
.regex(/^([a-f0-9]{24})?$/i)
|
||||
.transform((s) => (s === "" ? null : s));
|
||||
|
||||
/** Yüzde girişi ("4,25") → baz puan (425). Boş → null. */
|
||||
const percentToBp = z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((s, ctx) => {
|
||||
if (s === "") return null;
|
||||
const minor = parseMoney(s); // aynı ondalık kuralları: 2 hane
|
||||
if (minor === null || minor < 0 || minor > 100_00) {
|
||||
ctx.addIssue({ code: "custom", message: v.percent });
|
||||
return z.NEVER;
|
||||
}
|
||||
return minor;
|
||||
});
|
||||
|
||||
export const accountFormSchema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1, tr.common.required).max(80, v.nameMax),
|
||||
type: z.enum(ACCOUNT_TYPES),
|
||||
institution: z.string().trim().max(80, v.nameMax),
|
||||
mealCardProvider: z.enum(MEAL_CARD_PROVIDERS).nullable(),
|
||||
/** Kart için "açılış borcu" pozitif girilir; sunucu negatife çevirir. */
|
||||
openingBalance: moneyInputSchema,
|
||||
/** yyyy-MM-dd (İstanbul günü) */
|
||||
openingDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, v.date),
|
||||
color: z.enum(SWATCHES),
|
||||
icon: z.enum(ICON_KEYS),
|
||||
ownerUserId: objectIdOrEmpty,
|
||||
isPrivate: z.boolean(),
|
||||
overdraftEnabled: z.boolean(),
|
||||
overdraftLimit: nonNegativeMoneySchema,
|
||||
overdraftMonthlyRate: percentToBp,
|
||||
overdraftStatementDay: z
|
||||
.union([z.literal(""), day(31)])
|
||||
.transform((d) => (d === "" ? null : d)),
|
||||
cardLimit: nonNegativeMoneySchema,
|
||||
cardStatementDay: day(31),
|
||||
cardDueKind: z.enum(DUE_RULE_KINDS),
|
||||
cardDueValue: day(31),
|
||||
cardPaymentAccountId: objectIdOrEmpty,
|
||||
})
|
||||
.superRefine((a, ctx) => {
|
||||
if (a.type === "meal_card" && !a.mealCardProvider) {
|
||||
ctx.addIssue({ code: "custom", path: ["mealCardProvider"], message: v.selectProvider });
|
||||
}
|
||||
if (a.isPrivate && !a.ownerUserId) {
|
||||
ctx.addIssue({ code: "custom", path: ["ownerUserId"], message: v.privateNeedsOwner });
|
||||
}
|
||||
if (a.type === "credit_card" && a.cardDueKind === "daysAfter" && a.cardDueValue > 30) {
|
||||
ctx.addIssue({ code: "custom", path: ["cardDueValue"], message: v.dueDays });
|
||||
}
|
||||
// Yalnızca banka hesabı (KMH ile) negatif açılabilir; kart borcu pozitif girilir.
|
||||
if (a.openingBalance < 0 && a.type !== "bank") {
|
||||
ctx.addIssue({ code: "custom", path: ["openingBalance"], message: v.moneyNonNegative });
|
||||
}
|
||||
});
|
||||
|
||||
export type AccountFormInput = z.input<typeof accountFormSchema>;
|
||||
export type AccountForm = z.output<typeof accountFormSchema>;
|
||||
Reference in New Issue
Block a user