feat(kakeibo): dört harcama grubu, banka maliyeti ve geri alınabilir migration

- shared/kakeibo: grup devralma, banka maliyeti, kayıt bazında grup, grup özeti (saf, testli)
- kategori ve işlem/kalem modellerine kakeiboGroup ve bankCost; form şemaları
- migration 016 varsayılanları doldurur; npm run migrate -- --down <ad> ile geri alma
- Kakeibo rapor servisi: tutarlar bütçeyle aynı kaynaktan, grup değiştirmeler taşınır

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 04:51:35 +03:00
parent 18c892bdcd
commit afc6da1606
17 changed files with 831 additions and 13 deletions
+8
View File
@@ -1,6 +1,7 @@
import { z } from "zod";
import { tr } from "../../i18n/tr";
import { ICON_KEYS, SWATCHES } from "../appearance";
import { KAKEIBO_GROUPS } from "../kakeibo";
export const CATEGORY_KINDS = ["expense", "income"] as const;
export type CategoryKind = (typeof CATEGORY_KINDS)[number];
@@ -14,6 +15,13 @@ export const categoryFormSchema = z.object({
.transform((s) => (s === "" ? null : s)),
icon: z.enum(ICON_KEYS),
color: z.enum(SWATCHES),
/** Kakeibo grubu ("" = üst kategoriden devral); yalnızca gider. */
kakeiboGroup: z
.union([z.enum(KAKEIBO_GROUPS), z.literal("")])
.default("")
.transform((x) => (x === "" ? null : x)),
/** Banka maliyeti (KMH faizi, ücret): Kakeibo harcamasına girmez. */
bankCost: z.boolean().default(false),
});
export type CategoryFormInput = z.input<typeof categoryFormSchema>;
+9
View File
@@ -3,6 +3,7 @@ import { tr } from "../../i18n/tr";
import { parseMoney } from "../money";
import { MAX_ATTACHMENTS_PER_RECORD } from "../attachments";
import { MAX_INSTALLMENTS } from "../card-periods";
import { KAKEIBO_GROUPS } from "../kakeibo";
import { MAX_SPLITS, normalizeTags, TRANSACTION_TYPES } from "../transactions";
const v = tr.validation;
@@ -28,10 +29,17 @@ const positiveMoney = z
return minor;
});
/** Kakeibo grubu bu kayıt için değiştirildiyse; "" / yok = kategoriden (Faz 16). */
const kakeiboOverride = z
.union([z.enum(KAKEIBO_GROUPS), z.literal(""), z.null()])
.transform((x) => (x ? x : null))
.optional();
const splitSchema = z.object({
categoryId: objectId.refine(Boolean, t.selectCategory),
amount: positiveMoney,
note: z.string().trim().max(120).default(""),
kakeiboGroup: kakeiboOverride,
});
/**
@@ -48,6 +56,7 @@ export const transactionFormSchema = z
toAccountId: optionalId,
categoryId: optionalId,
splits: z.array(splitSchema).max(MAX_SPLITS).default([]),
kakeiboGroup: kakeiboOverride,
payee: z.string().trim().max(80).default(""),
description: z.string().trim().max(500).default(""),
tags: z