sync efactura-generator -> 0.9-beta-14
Mirror sincronizat cu repo canonic /workspace/efactura-generator. CLAUDE.md: documentat workflow sync + exclude config.json din rsync deploy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
481
efactura-generator/js/validation/br-ro.js
Normal file
481
efactura-generator/js/validation/br-ro.js
Normal file
@@ -0,0 +1,481 @@
|
||||
/**
|
||||
* js/validation/br-ro.js — PR-BR (A2)
|
||||
* Top 30 reguli BR din CIUS-RO Schematron + EN 16931-1.
|
||||
* Selecție: severity fatal/error din Schematron + reguli care vizează
|
||||
* câmpuri editabile (CIF, date, totale, coduri TVA, articole factură).
|
||||
*
|
||||
* Fiecare regulă:
|
||||
* { code, severity ('fatal'|'error'|'warning'), message, fieldRef, check(invoiceData) }
|
||||
*
|
||||
* invoiceData = obiect snapshot din colectInvoiceDataForBR() în script.js.
|
||||
* Toate funcțiile sunt pure — fără acces DOM, fără efecte secundare.
|
||||
*/
|
||||
|
||||
import { validateCIF } from './cif.js';
|
||||
import { validateIBAN } from './iban.js';
|
||||
|
||||
// Coduri TVA valide per CIUS-RO
|
||||
const VALID_VAT_TYPES = ['S', 'AE', 'O', 'Z', 'E'];
|
||||
// Coduri tip factură valide per CIUS-RO
|
||||
const VALID_INVOICE_TYPES = ['380', '381', '384', '389'];
|
||||
// Coduri țară ISO 3166-1 alfa-2 (set parțial — UE + țări comune)
|
||||
const EU_COUNTRY_CODES = new Set([
|
||||
'AT','BE','BG','CY','CZ','DE','DK','EE','ES','FI','FR','GR','HR','HU',
|
||||
'IE','IT','LT','LU','LV','MT','NL','PL','PT','RO','SE','SI','SK',
|
||||
'AD','AL','BA','BY','CH','GB','GE','IS','LI','ME','MK','MD','MN','NO',
|
||||
'RS','TR','UA','US','CA','AU','JP','CN','KR','BR','IN','ZA','SG','AE',
|
||||
'XK','SM','VA','MC','GI','FO','GL','IM','JE','GG'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Parsează un număr din string ignorând formatare (punct sau virgulă drept separator mii).
|
||||
* Returnează NaN dacă nu e un număr valid.
|
||||
*/
|
||||
function parseNum(val) {
|
||||
if (val === null || val === undefined || val === '') return NaN;
|
||||
const s = String(val).trim().replace(/\s/g, '');
|
||||
// Format ro-RO are virgulă ca separator zecimal ("1.234,56" sau "1,5").
|
||||
// Doar când există virgulă tratăm punctele drept separator de mii.
|
||||
// Altfel: parse canonical decimal-dot (dataset.raw, XML) — "1.000" = 1, NU 1000.
|
||||
if (s.includes(',')) {
|
||||
return parseFloat(s.replace(/\./g, '').replace(',', '.'));
|
||||
}
|
||||
return parseFloat(s);
|
||||
}
|
||||
|
||||
/** Parsează o dată din format dd.mm.yyyy → Date object (sau null) */
|
||||
function parseRoDate(str) {
|
||||
if (!str || !/^\d{2}\.\d{2}\.\d{4}$/.test(str.trim())) return null;
|
||||
const [d, m, y] = str.trim().split('.').map(Number);
|
||||
const dt = new Date(y, m - 1, d);
|
||||
if (isNaN(dt.getTime())) return null;
|
||||
return dt;
|
||||
}
|
||||
|
||||
/** Compară două valori numerice cu toleranță ε */
|
||||
function approxEqual(a, b, eps = 0.02) {
|
||||
if (isNaN(a) || isNaN(b)) return false;
|
||||
return Math.abs(a - b) <= eps;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REGULILE BR — 30 reguli în ordinea: ID, date, furnizor, client,
|
||||
// articole, TVA, totaluri, CIUS-RO specifice.
|
||||
// ============================================================================
|
||||
|
||||
export const BR_RULES = [
|
||||
|
||||
// ── Identificare factură ─────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
code: 'BR-01',
|
||||
severity: 'fatal',
|
||||
message: 'Factura trebuie să aibă un număr de identificare (ID).',
|
||||
fieldRef: '[name="invoiceNumber"]',
|
||||
check: (d) => d.invoiceNumber !== '',
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-02',
|
||||
severity: 'fatal',
|
||||
message: 'Factura trebuie să aibă o dată de emitere.',
|
||||
fieldRef: '[name="issueDate"]',
|
||||
check: (d) => d.issueDate !== '' && parseRoDate(d.issueDate) !== null,
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-03',
|
||||
severity: 'error',
|
||||
message: 'Codul tipului de factură trebuie să fie 380, 381, 384 sau 389.',
|
||||
fieldRef: '[name="invoiceTypeCode"]',
|
||||
check: (d) => VALID_INVOICE_TYPES.includes(d.invoiceTypeCode),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-04',
|
||||
severity: 'fatal',
|
||||
message: 'Factura trebuie să specifice moneda (codul ISO 4217).',
|
||||
fieldRef: '[name="documentCurrencyCode"]',
|
||||
check: (d) => d.currencyCode !== '' && d.currencyCode.length === 3,
|
||||
},
|
||||
|
||||
// ── Date scadență ────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
code: 'BR-DT-01',
|
||||
severity: 'error',
|
||||
message: 'Data emiterii nu poate fi în viitor cu mai mult de 30 zile.',
|
||||
fieldRef: '[name="issueDate"]',
|
||||
check: (d) => {
|
||||
const issued = parseRoDate(d.issueDate);
|
||||
if (!issued) return true; // BR-02 handles missing date
|
||||
const limit = new Date();
|
||||
limit.setDate(limit.getDate() + 30);
|
||||
return issued <= limit;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-DT-02',
|
||||
severity: 'warning',
|
||||
message: 'Data scadenței (dueDate) nu trebuie să fie anterioară datei de emitere.',
|
||||
fieldRef: '[name="dueDate"]',
|
||||
check: (d) => {
|
||||
if (!d.dueDate) return true;
|
||||
const issued = parseRoDate(d.issueDate);
|
||||
const due = parseRoDate(d.dueDate);
|
||||
if (!issued || !due) return true;
|
||||
return due >= issued;
|
||||
},
|
||||
},
|
||||
|
||||
// ── Furnizor ─────────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
code: 'BR-06',
|
||||
severity: 'fatal',
|
||||
message: 'Furnizorul trebuie să aibă un nume (RegistrationName).',
|
||||
fieldRef: '[name="supplierName"]',
|
||||
check: (d) => d.supplierName !== '',
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-07',
|
||||
severity: 'fatal',
|
||||
message: 'Adresa furnizorului trebuie să includă orașul.',
|
||||
fieldRef: '[name="supplierCity"]',
|
||||
check: (d) => d.supplierCity !== '',
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-08',
|
||||
severity: 'fatal',
|
||||
message: 'Țara furnizorului trebuie specificată (cod ISO 3166-1).',
|
||||
fieldRef: '[name="supplierCountry"]',
|
||||
check: (d) => d.supplierCountry !== '',
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-RO-001',
|
||||
severity: 'error',
|
||||
message: 'CIF/CUI furnizor invalid: cifra de control nu se potrivește.',
|
||||
fieldRef: '[name="supplierVAT"]',
|
||||
check: (d) => {
|
||||
if (!d.supplierVAT) return true; // gol = alt BR verifică
|
||||
return validateCIF(d.supplierVAT).valid;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-RO-010',
|
||||
severity: 'fatal',
|
||||
message: 'Furnizorul trebuie să aibă un cod de identificare fiscală (CIF/VAT).',
|
||||
fieldRef: '[name="supplierVAT"]',
|
||||
check: (d) => d.supplierVAT !== '',
|
||||
},
|
||||
|
||||
// ── Client ───────────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
code: 'BR-07-C',
|
||||
severity: 'fatal',
|
||||
message: 'Clientul trebuie să aibă un nume (RegistrationName).',
|
||||
fieldRef: '[name="customerName"]',
|
||||
check: (d) => d.customerName !== '',
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-08-C',
|
||||
severity: 'fatal',
|
||||
message: 'Țara clientului trebuie specificată (cod ISO 3166-1).',
|
||||
fieldRef: '[name="customerCountry"]',
|
||||
check: (d) => d.customerCountry !== '',
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-RO-002',
|
||||
severity: 'error',
|
||||
message: 'CIF/CUI client invalid: cifra de control nu se potrivește.',
|
||||
fieldRef: '[name="customerVAT"]',
|
||||
check: (d) => {
|
||||
if (!d.customerVAT) return true;
|
||||
return validateCIF(d.customerVAT).valid;
|
||||
},
|
||||
},
|
||||
|
||||
// ── Articole factură ─────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
code: 'BR-21',
|
||||
severity: 'fatal',
|
||||
message: (d) => {
|
||||
const bad = d.lineItems.filter(li => !li.description);
|
||||
return bad.length === 1
|
||||
? `Linia ${bad[0].index + 1} trebuie să aibă o denumire (descriere).`
|
||||
: `${bad.length} linii fără denumire (liniile ${bad.map(l => l.index + 1).join(', ')}).`;
|
||||
},
|
||||
fieldRef: null, // dinamic — scroll la prima linie cu eroare
|
||||
fieldRefDynamic: (d) => {
|
||||
const bad = d.lineItems.find(li => !li.description);
|
||||
return bad ? `[name="description${bad.index}"]` : null;
|
||||
},
|
||||
check: (d) => d.lineItems.every(li => li.description !== ''),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-22',
|
||||
severity: 'fatal',
|
||||
message: (d) => {
|
||||
const bad = d.lineItems.filter(li => isNaN(parseNum(li.quantity)) || parseNum(li.quantity) === 0);
|
||||
return bad.length === 1
|
||||
? `Linia ${bad[0].index + 1} trebuie să aibă o cantitate validă (≠ 0).`
|
||||
: `${bad.length} linii cu cantitate lipsă sau zero.`;
|
||||
},
|
||||
fieldRefDynamic: (d) => {
|
||||
const bad = d.lineItems.find(li => isNaN(parseNum(li.quantity)) || parseNum(li.quantity) === 0);
|
||||
return bad ? `[name="quantity${bad.index}"]` : null;
|
||||
},
|
||||
check: (d) => d.lineItems.every(li => !isNaN(parseNum(li.quantity)) && parseNum(li.quantity) !== 0),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-23',
|
||||
severity: 'fatal',
|
||||
message: (d) => {
|
||||
const bad = d.lineItems.filter(li => isNaN(parseNum(li.unitPrice)));
|
||||
return `Linia ${bad[0]?.index + 1 || '?'}: prețul unitar trebuie specificat.`;
|
||||
},
|
||||
fieldRefDynamic: (d) => {
|
||||
const bad = d.lineItems.find(li => isNaN(parseNum(li.unitPrice)));
|
||||
return bad ? `[name="price${bad.index}"]` : null;
|
||||
},
|
||||
check: (d) => d.lineItems.every(li => !isNaN(parseNum(li.unitPrice))),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-24',
|
||||
severity: 'fatal',
|
||||
message: (d) => {
|
||||
const bad = d.lineItems.filter(li => !VALID_VAT_TYPES.includes(li.vatType));
|
||||
return `Linia ${bad[0]?.index + 1 || '?'}: codul categoriei TVA trebuie să fie S/AE/O/Z/E.`;
|
||||
},
|
||||
fieldRefDynamic: (d) => {
|
||||
const bad = d.lineItems.find(li => !VALID_VAT_TYPES.includes(li.vatType));
|
||||
return bad ? `[name="vatType${bad.index}"]` : null;
|
||||
},
|
||||
check: (d) => d.lineItems.every(li => VALID_VAT_TYPES.includes(li.vatType)),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-16',
|
||||
severity: 'error',
|
||||
message: (d) => {
|
||||
const bad = d.lineItems.find(li => {
|
||||
const qty = parseNum(li.quantity);
|
||||
const price = parseNum(li.unitPrice);
|
||||
const disc = parseNum(li.discount) || 0;
|
||||
const net = parseNum(li.lineTotal);
|
||||
if (isNaN(qty) || isNaN(price) || isNaN(net)) return false;
|
||||
return !approxEqual(qty * price - disc, net);
|
||||
});
|
||||
return bad
|
||||
? `Linia ${bad.index + 1}: total net ≠ cantitate × preț − discount.`
|
||||
: 'Total net linie inconsistent.';
|
||||
},
|
||||
fieldRefDynamic: (d) => {
|
||||
const bad = d.lineItems.find(li => {
|
||||
const qty = parseNum(li.quantity);
|
||||
const price = parseNum(li.unitPrice);
|
||||
const disc = parseNum(li.discount) || 0;
|
||||
const net = parseNum(li.lineTotal);
|
||||
if (isNaN(qty) || isNaN(price) || isNaN(net)) return false;
|
||||
return !approxEqual(qty * price - disc, net);
|
||||
});
|
||||
return bad ? `[data-line-total-index="${bad.index}"]` : null;
|
||||
},
|
||||
check: (d) => d.lineItems.every(li => {
|
||||
const qty = parseNum(li.quantity);
|
||||
const price = parseNum(li.unitPrice);
|
||||
const disc = parseNum(li.discount) || 0;
|
||||
const net = parseNum(li.lineTotal);
|
||||
if (isNaN(qty) || isNaN(price) || isNaN(net)) return true; // BR-22/23 handles
|
||||
return approxEqual(qty * price - disc, net);
|
||||
}),
|
||||
},
|
||||
|
||||
// ── Factură cu cel puțin un articol ────────────────────────────────────
|
||||
|
||||
{
|
||||
code: 'BR-16-L',
|
||||
severity: 'fatal',
|
||||
message: 'Factura trebuie să conțină cel puțin un articol (linie factură).',
|
||||
fieldRef: null,
|
||||
check: (d) => d.lineItems.length > 0,
|
||||
},
|
||||
|
||||
// ── TVA breakdown ────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
code: 'BR-31',
|
||||
severity: 'fatal',
|
||||
message: 'Defalcarea TVA (TaxTotal/TaxSubtotal) nu poate fi goală.',
|
||||
fieldRef: '#vatBreakdownRows',
|
||||
check: (d) => d.vatRows.length > 0,
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-32',
|
||||
severity: 'error',
|
||||
message: (d) => {
|
||||
const bad = d.vatRows.find(r => {
|
||||
const rt = parseNum(r.rate);
|
||||
return isNaN(rt) || rt < 0 || rt > 100;
|
||||
});
|
||||
return `Cota TVA ${bad?.rate ?? ''} este invalidă (trebuie 0–100%).`;
|
||||
},
|
||||
fieldRef: '.vat-rate',
|
||||
check: (d) => d.vatRows.every(r => {
|
||||
const rt = parseNum(r.rate);
|
||||
return !isNaN(rt) && rt >= 0 && rt <= 100;
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-45',
|
||||
severity: 'error',
|
||||
message: (d) => {
|
||||
const bad = d.vatRows.find(r => !VALID_VAT_TYPES.includes(r.type));
|
||||
return `Codul categoriei TVA "${bad?.type ?? ''}" este invalid. Valori acceptate: S, AE, O, Z, E.`;
|
||||
},
|
||||
fieldRef: '.vat-type',
|
||||
check: (d) => d.vatRows.every(r => VALID_VAT_TYPES.includes(r.type)),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-AE-01',
|
||||
severity: 'warning',
|
||||
message: 'Categoria AE (Taxare Inversă) trebuie să aibă cota TVA 0%.',
|
||||
fieldRef: '.vat-rate',
|
||||
check: (d) => d.vatRows
|
||||
.filter(r => r.type === 'AE')
|
||||
.every(r => parseNum(r.rate) === 0),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-O-01',
|
||||
severity: 'warning',
|
||||
message: 'Categoria O (Neplătitor TVA) trebuie să aibă cota TVA 0%.',
|
||||
fieldRef: '.vat-rate',
|
||||
check: (d) => d.vatRows
|
||||
.filter(r => r.type === 'O')
|
||||
.every(r => parseNum(r.rate) === 0),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-E-01',
|
||||
severity: 'warning',
|
||||
message: 'Categoria E (Neimpozabil) trebuie să aibă cota TVA 0%.',
|
||||
fieldRef: '.vat-rate',
|
||||
check: (d) => d.vatRows
|
||||
.filter(r => r.type === 'E')
|
||||
.every(r => parseNum(r.rate) === 0),
|
||||
},
|
||||
|
||||
// ── Consistență totaluri ─────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
code: 'BR-CO-15',
|
||||
severity: 'fatal',
|
||||
message: (d) => {
|
||||
const sumRows = d.vatRows.reduce((s, r) => s + (parseNum(r.amount) || 0), 0);
|
||||
const disp = parseNum(d.totalVat);
|
||||
const diff = Math.abs(sumRows - disp).toFixed(2);
|
||||
return `Total TVA afișat (${disp.toFixed(2)}) ≠ suma rândurilor TVA (${sumRows.toFixed(2)}). Diferență: ${diff} RON.`;
|
||||
},
|
||||
fieldRef: '#vat',
|
||||
check: (d) => {
|
||||
if (d.vatRows.length === 0) return true;
|
||||
const sumRows = d.vatRows.reduce((s, r) => s + (parseNum(r.amount) || 0), 0);
|
||||
const disp = parseNum(d.totalVat);
|
||||
return approxEqual(sumRows, disp);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-CO-16',
|
||||
severity: 'fatal',
|
||||
message: (d) => {
|
||||
const expected = parseNum(d.subtotal) - parseNum(d.allowances) + parseNum(d.charges) + parseNum(d.totalVat);
|
||||
const actual = parseNum(d.grandTotal);
|
||||
const diff = Math.abs(expected - actual).toFixed(2);
|
||||
return `Total factură (${actual.toFixed(2)}) ≠ subtotal − reduceri + adaosuri + TVA (${expected.toFixed(2)}). Diferență: ${diff} RON.`;
|
||||
},
|
||||
fieldRef: '#total',
|
||||
check: (d) => {
|
||||
const expected = parseNum(d.subtotal) - parseNum(d.allowances) + parseNum(d.charges) + parseNum(d.totalVat);
|
||||
const actual = parseNum(d.grandTotal);
|
||||
if (isNaN(expected) || isNaN(actual)) return true;
|
||||
return approxEqual(expected, actual);
|
||||
},
|
||||
},
|
||||
|
||||
// ── CIUS-RO specifice ────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
code: 'BR-RO-180',
|
||||
severity: 'error',
|
||||
message: 'CIUS-RO: codul tipului de factură trebuie să fie 380 (factură), 381 (credit note), 384 (corectată) sau 389 (autofactură).',
|
||||
fieldRef: '[name="invoiceTypeCode"]',
|
||||
check: (d) => d.invoiceTypeCode === '' || VALID_INVOICE_TYPES.includes(d.invoiceTypeCode),
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-RO-003',
|
||||
severity: 'warning',
|
||||
message: 'CIUS-RO: numărul facturii (ID) nu trebuie să fie gol sau să conțină doar spații.',
|
||||
fieldRef: '[name="invoiceNumber"]',
|
||||
check: (d) => d.invoiceNumber.trim() !== '',
|
||||
},
|
||||
|
||||
{
|
||||
code: 'BR-IBAN-01',
|
||||
severity: 'warning',
|
||||
message: (d) => {
|
||||
const badIdx = d.ibans.findIndex(ib => ib && !validateIBAN(ib).valid);
|
||||
return `IBAN #${badIdx + 1} invalid: verificați lungimea și cifrele de control.`;
|
||||
},
|
||||
fieldRef: null,
|
||||
fieldRefDynamic: (d) => {
|
||||
const badIdx = d.ibans.findIndex(ib => ib && !validateIBAN(ib).valid);
|
||||
return badIdx >= 0 ? `[name="paymentMeansIBAN${badIdx}"]` : null;
|
||||
},
|
||||
check: (d) => d.ibans.every(ib => !ib || validateIBAN(ib).valid),
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
/**
|
||||
* Rulează toate regulile pe invoiceData și returnează lista de violări.
|
||||
* @param {object} invoiceData — snapshot din collectInvoiceDataForBR()
|
||||
* @returns {{ code, severity, message, fieldRef }[]}
|
||||
*/
|
||||
export function runBRRules(invoiceData) {
|
||||
const violations = [];
|
||||
for (const rule of BR_RULES) {
|
||||
if (!rule.check(invoiceData)) {
|
||||
const msg = typeof rule.message === 'function'
|
||||
? rule.message(invoiceData)
|
||||
: rule.message;
|
||||
const fRef = rule.fieldRefDynamic
|
||||
? rule.fieldRefDynamic(invoiceData)
|
||||
: rule.fieldRef;
|
||||
violations.push({
|
||||
code: rule.code,
|
||||
severity: rule.severity,
|
||||
message: msg,
|
||||
fieldRef: fRef,
|
||||
});
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
71
efactura-generator/js/validation/cif.js
Normal file
71
efactura-generator/js/validation/cif.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* js/validation/cif.js — PR-VALID-IDS (A9)
|
||||
* Validare CIF/CUI românesc prin sumă ponderată (mod 11, mod 10).
|
||||
* Funcție pură, fără efecte secundare, fără dependențe externe.
|
||||
*/
|
||||
|
||||
// Greutățile pentru cifrele 1-9 (se aplică pe primele 9 cifre ale CIF-ului).
|
||||
const WEIGHTS = [7, 5, 3, 2, 1, 7, 5, 3, 2];
|
||||
|
||||
/**
|
||||
* Validează un CIF/CUI românesc.
|
||||
*
|
||||
* Algoritm:
|
||||
* 1. Elimină prefixul "RO" dacă există (case-insensitive).
|
||||
* 2. Elimină spații.
|
||||
* 3. Verifică că are între 2 și 10 cifre.
|
||||
* 4. Completează cu zerouri la stânga până la 10 cifre.
|
||||
* 5. Calculează suma ponderată pe primele 9 cifre cu WEIGHTS.
|
||||
* 6. (sumă * 10) % 11 % 10 trebuie să fie egal cu cifra de control (ultima).
|
||||
*
|
||||
* @param {string} value — valoarea brută din câmp (poate fi goală, poate conține "RO")
|
||||
* @returns {{ valid: boolean, message: string }}
|
||||
*/
|
||||
export function validateCIF(value) {
|
||||
if (!value || value.trim() === '') {
|
||||
return { valid: true, message: '' }; // câmp gol — valid (nu e required check)
|
||||
}
|
||||
|
||||
let normalized = value.trim().toUpperCase();
|
||||
|
||||
// Elimină prefixul RO
|
||||
if (normalized.startsWith('RO')) {
|
||||
normalized = normalized.slice(2).trim();
|
||||
}
|
||||
|
||||
// Elimină spații și cratime rămase
|
||||
normalized = normalized.replace(/[\s\-]/g, '');
|
||||
|
||||
// Trebuie să conțină doar cifre
|
||||
if (!/^\d+$/.test(normalized)) {
|
||||
return { valid: false, message: 'CIF invalid: conține caractere nepermise' };
|
||||
}
|
||||
|
||||
// Lungime: minim 2, maxim 10 cifre
|
||||
if (normalized.length < 2 || normalized.length > 10) {
|
||||
return { valid: false, message: 'CIF invalid: lungimea trebuie să fie între 2 și 10 cifre' };
|
||||
}
|
||||
|
||||
// Completează cu zerouri la stânga până la 10 cifre
|
||||
const padded = normalized.padStart(10, '0');
|
||||
|
||||
// Extrage primele 9 cifre (pentru ponderare) și cifra de control (ultima)
|
||||
const digits = padded.split('').map(Number);
|
||||
const checkDigit = digits[9];
|
||||
const controlDigits = digits.slice(0, 9);
|
||||
|
||||
// Calculează suma ponderată
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 9; i++) {
|
||||
sum += controlDigits[i] * WEIGHTS[i];
|
||||
}
|
||||
|
||||
// Cifra de control calculată
|
||||
const computed = (sum * 10) % 11 % 10;
|
||||
|
||||
if (computed !== checkDigit) {
|
||||
return { valid: false, message: 'CIF invalid: cifra de control nu se potrivește' };
|
||||
}
|
||||
|
||||
return { valid: true, message: '' };
|
||||
}
|
||||
63
efactura-generator/js/validation/iban.js
Normal file
63
efactura-generator/js/validation/iban.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* js/validation/iban.js — PR-VALID-IDS (A10)
|
||||
* Validare IBAN internațional prin algoritmul ISO 13616 (mod 97).
|
||||
* Funcție pură, fără efecte secundare, fără dependențe externe.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validează un IBAN (orice țară, inclusiv RO).
|
||||
*
|
||||
* Algoritm ISO 13616:
|
||||
* 1. Elimină spații și convertește la uppercase.
|
||||
* 2. Verifică lungimea minimă (4 caractere).
|
||||
* 3. Mută primele 4 caractere la sfârșitul șirului.
|
||||
* 4. Înlocuiește fiecare literă cu echivalentul numeric: A=10, B=11, ..., Z=35.
|
||||
* 5. Calculează numărul rezultat modulo 97 — trebuie să fie 1.
|
||||
*
|
||||
* Lungimi specifice per țară nu sunt forțate (validare structurală generică);
|
||||
* IBAN-ul RO are 24 caractere, verificat separat cu mesaj specific.
|
||||
*
|
||||
* @param {string} value — valoarea brută din câmp
|
||||
* @returns {{ valid: boolean, message: string }}
|
||||
*/
|
||||
export function validateIBAN(value) {
|
||||
if (!value || value.trim() === '') {
|
||||
return { valid: true, message: '' }; // câmp gol — valid (nu e required check)
|
||||
}
|
||||
|
||||
// Normalizare: elimină spații, uppercase
|
||||
const normalized = value.trim().toUpperCase().replace(/\s/g, '');
|
||||
|
||||
// Lungime minimă
|
||||
if (normalized.length < 4) {
|
||||
return { valid: false, message: 'IBAN invalid: lungime sau check digits' };
|
||||
}
|
||||
|
||||
// Verifică că IBAN-ul conține doar litere și cifre
|
||||
if (!/^[A-Z0-9]+$/.test(normalized)) {
|
||||
return { valid: false, message: 'IBAN invalid: caractere nepermise' };
|
||||
}
|
||||
|
||||
// IBAN RO trebuie să aibă exact 24 caractere
|
||||
if (normalized.startsWith('RO') && normalized.length !== 24) {
|
||||
return { valid: false, message: 'IBAN invalid: lungime sau check digits' };
|
||||
}
|
||||
|
||||
// Rearanjare: primele 4 caractere la final
|
||||
const rearranged = normalized.slice(4) + normalized.slice(0, 4);
|
||||
|
||||
// Înlocuiește literele cu cifre: A=10 ... Z=35
|
||||
const numericString = rearranged.replace(/[A-Z]/g, ch => String(ch.charCodeAt(0) - 55));
|
||||
|
||||
// Calculează mod 97 pe un număr mare (string chunking pentru a evita overflow)
|
||||
let remainder = 0;
|
||||
for (let i = 0; i < numericString.length; i++) {
|
||||
remainder = (remainder * 10 + parseInt(numericString[i], 10)) % 97;
|
||||
}
|
||||
|
||||
if (remainder !== 1) {
|
||||
return { valid: false, message: 'IBAN invalid: lungime sau check digits' };
|
||||
}
|
||||
|
||||
return { valid: true, message: '' };
|
||||
}
|
||||
Reference in New Issue
Block a user