9781cd9ed2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
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,
|
||
};
|
||
}
|