File size: 3,050 Bytes
ce8f04a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
 * 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 };
}