/** * Pure helpers for EligibilitySpineCard (display + NIP normalize). * No React / network — unit-testable with node --test. */ export const DE_MINIMIS_LIMIT_EUR = 300_000; export const MSP_LABELS: Record = { mikro: 'Mikroprzedsiębiorstwo', mala: 'Małe przedsiębiorstwo', srednia: 'Średnie przedsiębiorstwo', duza: 'Duże przedsiębiorstwo', unknown: 'Nieustalony', }; export const SOURCE_LABELS: Record = { sudop: 'SUDOP', manual: 'Ręczne zaświadczenie', manual_override: 'Override użytkownika', profile_field: 'Profil firmy', unknown: 'Brak źródła', sudop_error: 'Błąd SUDOP', sudop_unconfigured: 'SUDOP nie skonfigurowany', declared: 'Deklaracja', neo4j: 'Analiza powiązań', krs: 'KRS', analyzer: 'Analizator', }; /** Strip non-digits; Polish NIP is 10 digits. */ export function normalizeNip(raw: string): string { return String(raw || '').replace(/\D/g, ''); } /** True when NIP has exactly 10 digits (PL). */ export function isValidNip(raw: string): boolean { return normalizeNip(raw).length === 10; } export function formatEur(value: number | null | undefined): string { if (value === null || value === undefined || Number.isNaN(Number(value))) { return '—'; } return new Intl.NumberFormat('pl-PL', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0, }).format(Number(value)); } export function mspLabel(status: string | null | undefined): string { const key = String(status || 'unknown').toLowerCase(); return MSP_LABELS[key] || status || MSP_LABELS.unknown; } export function sourceLabel(source: string | null | undefined): string { if (!source) return '—'; return SOURCE_LABELS[source] || source; } /** * De minimis usage slice for the progress bar and totals. * Falls back to 300k limit and derives remaining when API omits it. */ export function deMinimisUsage(dm: { used_eur?: number | null; limit_eur?: number | null; remaining_eur?: number | null; exhausted?: boolean; } | null | undefined): { used: number | null; limit: number; remaining: number | null; exhausted: boolean; usagePct: number; } { const limit = dm?.limit_eur != null && !Number.isNaN(Number(dm.limit_eur)) ? Number(dm.limit_eur) : DE_MINIMIS_LIMIT_EUR; const used = dm?.used_eur === null || dm?.used_eur === undefined || Number.isNaN(Number(dm.used_eur)) ? null : Number(dm.used_eur); let remaining: number | null = dm?.remaining_eur === null || dm?.remaining_eur === undefined || Number.isNaN(Number(dm.remaining_eur)) ? null : Number(dm.remaining_eur); if (remaining === null && used !== null) { remaining = Math.max(0, limit - used); } const exhausted = Boolean(dm?.exhausted) || (remaining !== null && remaining <= 0) || (used !== null && used >= limit); const usagePct = used !== null && limit > 0 ? Math.min(100, Math.max(0, (used / limit) * 100)) : 0; return { used, limit, remaining, exhausted, usagePct }; }