Cum a Început
Lucram la o problemă de calcul fiscal și am dat peste capcana clasică din JavaScript aproape imediat:
0.1 + 0.2 === 0.30000000000000004
În majoritatea aplicațiilor poți ignora asta. Dar când calculezi taxa pe o tranzacție de 142.857,14 $ și combini o cotă de stat California de 7,25% cu un suprataxă de județ Los Angeles de 2,5%, acea eroare mică se acumulează în cenți reali care nu se reconciliază. Am decis să construiesc JurisLogic ca o soluție corectă - un microserviciu headless de taxe și comisioane unde fiecare operațiune monetară trece printr-un obiect de valoare imutabil Money susținut de decimal.js cu ROUND_HALF_UP (rotunjire bancară).
Rezultatul este un serviciu NestJS care gestionează patru familii de jurisdicții - SUA, UE, Marea Britanie și Canada - și demonstrează o arhitectură hexagonală curată în care logica de domeniu nu are nicio dependență de framework.
Stratul de Domeniu: Logică Pură de Business
Nucleul JurisLogic se află în src/domain/, și sunt sincer mândru de cât de curat a ieșit. Nu există decoratori @Injectable(), nicio importare din NestJS, nicio problemă de infrastructură. Doar clase TypeScript pure care modelează regulile fiscale.
Obiecte de Valoare
Trei obiecte de valoare imutabile transportă toate primitivele domeniului:
Moneyîmpacheteazădecimal.jsși expune metodele.add(),.subtract(),.multiply(),.round()și metode de comparare. Fiecare operațiune returnează o nouă instanțăMoney- imutabilitatea înseamnă că nu trebuie să mă îngrijorez niciodată că un calcul îl corupe pe altul.TaxRateare atât o fabricăfromPercentage(), cât și unafromDecimal(), astfel că codul apelant se citește natural:TaxRate.fromPercentage(7.25, 'California State Sales Tax').JurisdictionCodecodifică cheia pe mai multe niveluri: țară → stat → județ → oraș. O tranzacție din Houston, Texas devineJurisdictionCode.us('TX', 'HARRIS', 'HOUSTON'). Fabrici statice precumJurisdictionCode.eu('DE')șiJurisdictionCode.uk()aplică regulile regionale la momentul construcției.
Pattern-ul Strategy (și De Ce A Funcționat)
Fiecare familie de jurisdicții are reguli fiscale complet diferite. Taxa de vânzări din SUA se cumulează (stat + județ + suprataxe de oraș). TVA-ul european este per țară, cu cote standard și reduse pentru alimente, cărți etc. TVA-ul din Marea Britanie are trei niveluri: standard 20%, redus 5% și cota zero pentru îmbrăcămintea pentru copii. Canada face distincție între provinciile cu HST și provinciile cu GST+PST.
Am modelat acest lucru cu pattern-ul Strategy: o singură interfață ITaxStrategy cu o metodă calculate(context) și patru implementări concrete:
USSalesTaxStrategy- iterează prin cotele de stat, județ și oraș, acumulându-le într-un array de detaliiEUVATStrategy- caută cota standard/redusă a țării, aplică cote reduse pentru categoriile de produse eligibile, cum ar fi alimenteleUKVATStrategy- verifică categoria produsului față de seturileUK_REDUCED_CATEGORIESșiUK_ZERO_RATED_CATEGORIESCAGSTStrategy- determină dacă provincia folosește HST armonizat sau GST+PST separat
Un TaxStrategyFactory rezolvă strategia corectă dintr-un JurisdictionCode. Adăugarea unei noi jurisdicții înseamnă scrierea unei singure clase de strategie noi și înregistrarea ei în constructorul fabricii - zero modificări la codul existent. Acesta este Principiul Deschis/Închis în practică, nu doar în teorie.
Pipeline-ul: Chain of Responsibility
Calculul fiscal nu este doar „înmulțire cu cota". Ai nevoie de verificări de scutire, gestionarea suprataxa, praguri fiscale minime și logging de audit - toate preocupări transversale care nu ar trebui să locuiască în interiorul strategiei.
Am construit un TaxRulePipeline care înlănțuie handler-e în ordine:
ExemptionHandler- verificăcontext.isExempt. Dacă este adevărat, scurtcircuitează cu un rezultat cu taxa zero.SurchargeHandler- adaugă suprataxe fixe sau procentuale la totalul curent (taxe de procesare, taxe de mediu).MinimumTaxHandler- aplică un prag fiscal minim dacă regulile de business o cer.
Pipeline-ul are un API fluent: new TaxRulePipeline().addHandler(exemption).addHandler(surcharge), iar metoda sa .execute() reduce prin toți handler-ii, lăsând fiecare să transforme sau să scurtcircuiteze rezultatul. Aceasta separă preocupările transversale de logica fiscală de bază în mod elegant.
Stratul de Aplicație: Use Case-urile ca Orchestratori
Stratul de aplicație conține trei use case-uri - CalculateTaxUseCase, CalculateCommissionUseCase și ProcessBatchTransactionUseCase - fiecare adnotat cu @Injectable() din NestJS. Nu conțin reguli de business; orchestrează:
- Construiesc obiecte de valoare din domeniu pornind de la DTO-urile de intrare
- Verifică cache-ul (via
ICachePort) pentru request-uri identice recente - Rezolvă strategia, construiesc pipeline-ul, rulează calculul
- Creează agregatul
Transaction, aplică rezultatul - Salvează în cache și scriu un log de audit (fire-and-forget)
Cheia de cache este deterministă: tax:{country}:{state}:{county}:{city}:{subtotal}:{currency}:{category}:{exempt}. Două request-uri identice în cadrul TTL-ului sar peste întregul calcul.
Comisioane: Trei Modele Într-Unul
Sistemul de comisioane suportă trei modele - flat, procentual și pe niveluri - toate gestionate de un singur use case cu un switch:
- Flat: sumă fixă per tranzacție
- Procentual: cotă × sumă
- Pe niveluri: tranșe progresive, similar impozitului pe venit. Nivelurile se sortează după
minAmount, iar eu parcurg fiecare tranșă scăzând suma aplicabilă până nu mai rămâne nimic.
Calculul pe niveluri a fost cel mai dificil de implementat corect. A trebuit să gestionez cazuri limită cum ar fi o tranzacție care se întinde pe trei niveluri, să mă asigur că Decimal.min(remaining, tierRange) alege applicableAmount-ul corect și să rotunjesc rezultatul per nivel, nu la final.
Ce Aș Schimba
Privind înapoi, aș adăuga event sourcing la entitatea Transaction. Momentan, agregatul ajunge la starea sa finală după .applyTax() și .markCalculated(). Cu event sourcing aș putea reprelua orice calcul - valoros pentru audit.
Aș muta și procesorul batch într-un serviciu worker dedicat, în loc să îl rulez în același proces cu BullMQ. Pentru exercițiul de învățare funcționează, dar la scară vrei ca procesarea job-urilor să fie izolată de API.
Proiectul este open-source: github.com/ionutn0301/juris-logic