feat(in-kind): varlık türleri, fiyat tablosu, aynî borç/alacak, birikim planı ve ödeme akışı, 008 migration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 12:37:48 +03:00
parent 1062b2d96c
commit 9781cd9ed2
15 changed files with 1354 additions and 3 deletions
+118
View File
@@ -0,0 +1,118 @@
import { QUANTITY_SCALE } from "./quantity";
import { addMonths } from "./recurrence";
/**
* Varlık türleri (Bölüm 6.5–6.6): altın, ziynet, gümüş ve döviz. Miktarlar binde bir tamsayı
* (5 gr → 5000, 2 adet çeyrek → 2000). Fiyatlar birim başına kuruş.
*
* Fiyat terimleri kuyumcu/döviz bürosu tarafından: "alış" = kurumun sizden aldığı (düşük),
* "satış" = kurumun size sattığı (yüksek). Faz 0 kararı: varlıklar ve aynî alacaklar ALIŞ,
* aynî borçlar SATIŞ fiyatıyla değerlenir.
*/
export const ASSET_KEYS = [
"gram_gold",
"quarter_gold",
"half_gold",
"full_gold",
"republic_gold",
"bracelet_22k",
"silver_gram",
"usd",
"eur",
"gbp",
] as const;
export type AssetKey = (typeof ASSET_KEYS)[number];
export type AssetUnit = "gr" | "adet" | "birim";
export const ASSETS: Record<
AssetKey,
{ label: string; unit: AssetUnit; group: "gold" | "silver" | "fx" }
> = {
gram_gold: { label: "Gram altın (24 ayar)", unit: "gr", group: "gold" },
quarter_gold: { label: "Çeyrek altın", unit: "adet", group: "gold" },
half_gold: { label: "Yarım altın", unit: "adet", group: "gold" },
full_gold: { label: "Tam altın", unit: "adet", group: "gold" },
republic_gold: { label: "Cumhuriyet (Ata) altını", unit: "adet", group: "gold" },
bracelet_22k: { label: "22 ayar bilezik", unit: "gr", group: "gold" },
silver_gram: { label: "Gümüş (gram)", unit: "gr", group: "silver" },
usd: { label: "ABD doları", unit: "birim", group: "fx" },
eur: { label: "Euro", unit: "birim", group: "fx" },
gbp: { label: "İngiliz sterlini", unit: "birim", group: "fx" },
};
const FX_SYMBOL: Partial<Record<AssetKey, string>> = { usd: "USD", eur: "EUR", gbp: "GBP" };
/** "5 gr Gram altın (24 ayar)", "2 adet Çeyrek altın", "1.500 USD". */
export function formatAssetQuantity(key: AssetKey, quantity: number): string {
const n = (quantity / QUANTITY_SCALE).toLocaleString("tr-TR", { maximumFractionDigits: 3 });
const fx = FX_SYMBOL[key];
if (fx) return `${n} ${fx}`;
const a = ASSETS[key];
return `${n} ${a.unit} ${a.label}`;
}
/** Miktar × birim fiyat (kuruş, yuvarlanır). */
export function valueOf(quantity: number, unitPriceMinor: number): number {
return Math.round((quantity * unitPriceMinor) / QUANTITY_SCALE);
}
/** TL tutarın karşılığı olan miktar (binde bir, yuvarlanır). */
export function quantityForAmount(amountMinor: number, unitPriceMinor: number): number {
if (unitPriceMinor <= 0) return 0;
return Math.round((amountMinor * QUANTITY_SCALE) / unitPriceMinor);
}
export type InKindDirection = "debt" | "receivable";
/** Değerlemede kullanılacak fiyat: borç → satış, alacak → alış. */
export function valuationPrice(
direction: InKindDirection,
price: { buyMinor: number; sellMinor: number } | null,
): number | null {
if (!price) return null;
return direction === "debt" ? price.sellMinor : price.buyMinor;
}
export type InKindProgress = {
remainingQuantity: number;
/** Kalan miktarın güncel TL karşılığı (fiyat yoksa null). */
currentValueMinor: number | null;
/** Birikim planı: biriken − ödemelerde kullanılan. */
accumulatedMinor: number;
remainingNeedMinor: number | null;
/** Aylık birikimle kalan ihtiyaç kaç ayda kapanır; "YYYY-MM" (plan yoksa null). */
estimatedCloseMonth: string | null;
};
export function inKindProgress(input: {
quantity: number;
paidQuantity: number;
unitPriceMinor: number | null;
contributedMinor: number;
spentMinor: number;
monthlyPlanMinor: number | null;
today: string;
}): InKindProgress {
const remainingQuantity = Math.max(0, input.quantity - input.paidQuantity);
const currentValueMinor =
input.unitPriceMinor === null ? null : valueOf(remainingQuantity, input.unitPriceMinor);
const accumulatedMinor = Math.max(0, input.contributedMinor - input.spentMinor);
const remainingNeedMinor =
currentValueMinor === null ? null : Math.max(0, currentValueMinor - accumulatedMinor);
let estimatedCloseMonth: string | null = null;
if (remainingQuantity === 0) estimatedCloseMonth = null;
else if (remainingNeedMinor === 0) estimatedCloseMonth = input.today.slice(0, 7);
else if (remainingNeedMinor !== null && input.monthlyPlanMinor && input.monthlyPlanMinor > 0) {
const months = Math.ceil(remainingNeedMinor / input.monthlyPlanMinor);
estimatedCloseMonth = addMonths(`${input.today.slice(0, 7)}-01`, months).slice(0, 7);
}
return {
remainingQuantity,
currentValueMinor,
accumulatedMinor,
remainingNeedMinor,
estimatedCloseMonth,
};
}