feat(assets): varlık lotları, ortalama maliyet ve kâr/zarar servisi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { z } from "zod";
|
||||
import { tr } from "../../i18n/tr";
|
||||
import { ASSET_KEYS } from "../assets";
|
||||
import { parseMoney } from "../money";
|
||||
import { MOVEMENT_KINDS } from "../portfolio";
|
||||
import { parseQuantity } from "../quantity";
|
||||
|
||||
const v = tr.validation;
|
||||
const a = tr.assets.validation;
|
||||
const ymd = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, v.date);
|
||||
const objectId = z.string().regex(/^[a-f0-9]{24}$/i, tr.transactions.validation.selectAccount);
|
||||
const emptyToNull = (x: unknown) => (x === "" || x === undefined ? null : x);
|
||||
|
||||
/** Boş → null; dolu → pozitif kuruş. */
|
||||
const optionalMoney = z.preprocess(
|
||||
emptyToNull,
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((s, ctx) => {
|
||||
const minor = parseMoney(s);
|
||||
if (minor === null || minor <= 0) {
|
||||
ctx.addIssue({ code: "custom", message: v.money });
|
||||
return z.NEVER;
|
||||
}
|
||||
return minor;
|
||||
})
|
||||
.nullable(),
|
||||
);
|
||||
|
||||
/** Varlık (birikim kalemi) formu. */
|
||||
export const assetFormSchema = z.object({
|
||||
assetKey: z.enum(ASSET_KEYS),
|
||||
name: z.string().trim().min(1, tr.common.required).max(80, v.nameMax),
|
||||
note: z.string().trim().max(500).default(""),
|
||||
isPrivate: z.boolean().default(false),
|
||||
});
|
||||
export type AssetFormInput = z.input<typeof assetFormSchema>;
|
||||
export type AssetForm = z.output<typeof assetFormSchema>;
|
||||
|
||||
/**
|
||||
* Varlık hareketi. Tutar ve birim fiyat isteğe bağlı: tutar varsa birim fiyat ondan, yalnızca
|
||||
* birim fiyat varsa tutar ondan hesaplanır; ikisi de boşsa fiyat tablosu kullanılır
|
||||
* (alım → satış fiyatı, satış ve gelen hediye → alış fiyatı). Hesap yalnızca alım/satışta.
|
||||
*/
|
||||
export const movementFormSchema = z
|
||||
.object({
|
||||
kind: z.enum(MOVEMENT_KINDS),
|
||||
date: ymd,
|
||||
quantity: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((s, ctx) => {
|
||||
const q = parseQuantity(s);
|
||||
if (q === null || q <= 0) {
|
||||
ctx.addIssue({ code: "custom", message: tr.inKind.validation.quantity });
|
||||
return z.NEVER;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
unitPrice: optionalMoney,
|
||||
amount: optionalMoney,
|
||||
accountId: z.preprocess(emptyToNull, objectId.nullable()),
|
||||
storage: z.string().trim().max(120).default(""),
|
||||
note: z.string().trim().max(300).default(""),
|
||||
})
|
||||
.superRefine((x, ctx) => {
|
||||
if ((x.kind === "gift_in" || x.kind === "gift_out") && x.accountId)
|
||||
ctx.addIssue({ code: "custom", path: ["accountId"], message: a.noAccountForGift });
|
||||
})
|
||||
.transform((x) =>
|
||||
x.kind === "gift_out" ? { ...x, unitPrice: null, amount: null, accountId: null } : x,
|
||||
);
|
||||
export type MovementFormInput = z.input<typeof movementFormSchema>;
|
||||
export type MovementForm = z.output<typeof movementFormSchema>;
|
||||
Reference in New Issue
Block a user