import type { VatRate } from '@prisma/client'; const VAT_PERCENT: Record = { none: 0, vat_0: 0, vat_5: 5, vat_7: 7, vat_10: 10, vat_20: 20, }; export type LineCalc = { qtyMilli: bigint; priceCents: bigint; vat: VatRate; }; /** * Сумма по строке с НДС-логикой: * - В РФ цена в счёте ВКЛЮЧАЕТ НДС. Сумма = price * qty / 1000. * - НДС-часть выделяется из суммы: vatPart = sum * pct / (100 + pct). * - Для VatRate=none — налога нет, vatPart=0. */ export function computeLine(line: LineCalc): { sumCents: bigint; vatCents: bigint } { const sumCents = (line.priceCents * line.qtyMilli) / 1000n; const pct = VAT_PERCENT[line.vat]; if (pct === 0) { return { sumCents, vatCents: 0n }; } // целочисленное округление до копейки: round(sum * pct / (100 + pct)) const num = sumCents * BigInt(pct); const den = BigInt(100 + pct); const vatCents = (num + den / 2n) / den; return { sumCents, vatCents }; } export function totals(lines: LineCalc[]): { totalCents: bigint; vatCents: bigint } { let total = 0n; let vat = 0n; for (const l of lines) { const r = computeLine(l); total += r.sumCents; vat += r.vatCents; } return { totalCents: total, vatCents: vat }; } export function formatRub(cents: bigint | number): string { const c = typeof cents === 'bigint' ? Number(cents) : cents; return (c / 100).toLocaleString('ru-RU', { style: 'currency', currency: 'RUB', minimumFractionDigits: 2, }); } const ONES = ['', 'один', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']; const ONES_F = ['', 'одна', 'две', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']; const TEENS = [ 'десять', 'одиннадцать', 'двенадцать', 'тринадцать', 'четырнадцать', 'пятнадцать', 'шестнадцать', 'семнадцать', 'восемнадцать', 'девятнадцать', ]; const TENS = ['', '', 'двадцать', 'тридцать', 'сорок', 'пятьдесят', 'шестьдесят', 'семьдесят', 'восемьдесят', 'девяносто']; const HUNDREDS = ['', 'сто', 'двести', 'триста', 'четыреста', 'пятьсот', 'шестьсот', 'семьсот', 'восемьсот', 'девятьсот']; function group(n: number, feminine: boolean): string { const ones = feminine ? ONES_F : ONES; const parts: string[] = []; const h = Math.floor(n / 100); const t = Math.floor((n % 100) / 10); const u = n % 10; if (h > 0) parts.push(HUNDREDS[h]!); if (t === 1) { parts.push(TEENS[u]!); } else { if (t > 0) parts.push(TENS[t]!); if (u > 0) parts.push(ones[u]!); } return parts.join(' '); } function pluralRu(n: number, forms: [string, string, string]): string { const mod10 = n % 10; const mod100 = n % 100; if (mod10 === 1 && mod100 !== 11) return forms[0]; if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return forms[1]; return forms[2]; } /** Сумма прописью на русском. cents — копейки. */ export function rubInWords(cents: bigint | number): string { const total = typeof cents === 'bigint' ? Number(cents) : cents; const rub = Math.floor(Math.abs(total) / 100); const kop = Math.abs(total) % 100; const billions = Math.floor(rub / 1_000_000_000); const millions = Math.floor((rub % 1_000_000_000) / 1_000_000); const thousands = Math.floor((rub % 1_000_000) / 1_000); const ones = rub % 1_000; const parts: string[] = []; if (billions > 0) parts.push(`${group(billions, false)} ${pluralRu(billions, ['миллиард', 'миллиарда', 'миллиардов'])}`); if (millions > 0) parts.push(`${group(millions, false)} ${pluralRu(millions, ['миллион', 'миллиона', 'миллионов'])}`); if (thousands > 0) parts.push(`${group(thousands, true)} ${pluralRu(thousands, ['тысяча', 'тысячи', 'тысяч'])}`); if (ones > 0 || (billions === 0 && millions === 0 && thousands === 0)) { parts.push(group(ones || 0, false)); } const rubText = parts.join(' ').replace(/\s+/g, ' ').trim() || 'ноль'; const kopText = String(kop).padStart(2, '0'); const sign = total < 0 ? 'минус ' : ''; return `${sign}${rubText} ${pluralRu(rub, ['рубль', 'рубля', 'рублей'])} ${kopText} ${pluralRu(kop, ['копейка', 'копейки', 'копеек'])}`; }