grantforge-api / frontend-react /src /components /company /eligibilitySpineModel.ts
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
3.05 kB
/**
* 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<string, string> = {
mikro: 'Mikroprzedsiębiorstwo',
mala: 'Małe przedsiębiorstwo',
srednia: 'Średnie przedsiębiorstwo',
duza: 'Duże przedsiębiorstwo',
unknown: 'Nieustalony',
};
export const SOURCE_LABELS: Record<string, string> = {
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 };
}