- Isac Lucas-Horatiu
- Lemnaru Alin-Gabriel
- Sima Adelin-Sebastian
Sistem de Gestiune Comenzi Magazin Online (Retail / E-commerce)
Proiectul vizează implementarea unui sistem distribuit pentru gestionarea fluxului complet al unei comenzi într-un magazin online. Sistemul este împărțit în trei bounded context-uri distincte care comunică asincron prin mesaje (events).
Obiectivul principal este modelarea corectă a domeniului folosind principii DDD (Domain-Driven Design) și o abordare funcțională în C# (sistem de tipuri bogat, imutabilitate, pipeline-uri de operații).
Fiecare membru al echipei este responsabil de unul dintre următoarele contexte:
-
Ordering Context: Responsabil de preluarea comenzilor, validarea datelor de intrare (produs, cantitate, adresă livrare), verificarea disponibilității stocului și calculul valorii totale a comenzii. Output-ul său este evenimentul OrderPlacedEvent care declanșează procesul de facturare.
-
Invoicing (Billing) Context: Responsabil de primirea comenzilor deja validate (OrderPlacedEvent), calcularea sumelor financiare (TVA, total) pe baza liniilor de comandă și transformarea lor într‑o factură fiscală completă (PaidInvoice). De asemenea, se ocupă de generarea și publicarea evenimentului InvoicePaidEvent către celălalt context (Shipping), precum și de încărcarea facturilor în baza de date.
-
Shipping Context: Responsabil de generarea AWB-urilor, calculul costului de transport si finalizarea livrarii.
- Context Vanzari:
graph TD
%% Stiluri pentru noduri (Design modern)
classDef state fill:#e1f5fe,stroke:#01579b,stroke-width:2px,rx:10,ry:10;
classDef operation fill:#fff3e0,stroke:#e65100,stroke-width:2px,stroke-dasharray: 5 5;
classDef db fill:#f5f5f5,stroke:#616161,stroke-width:2px;
classDef bus fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:2px;
classDef external fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
%% Actorul Extern
Client([Client / API Request]) -->|POST /orders| Controller[Sales.Api\nOrderController]
subgraph SalesContext [Sales Bounded Context]
direction TB
%% Fluxul Principal
Controller -->|Mapează JSON în| State1(UnvalidatedOrder):::state
State1 --> Op1[[ValidateOrderOperation]]:::operation
Op1 --> Check{Valid?}
Check -- NU --> StateErr(InvalidOrder):::error
Check -- DA --> State2(ValidatedOrder):::state
State2 --> Op2[[CalculatePricesOperation]]:::operation
Op2 --> State3(CalculatedOrder):::state
State3 --> Op3[[PlaceOrderOperation]]:::operation
Op3 --> State4(PlacedOrder):::state
end
%% Infrastructura si Comunicarea Externa
Op3 -->|Salvează| SQL[(SQL Server\nDatabase)]:::db
%% AICI ESTE SCHIMBAREA CERUTA:
Op3 -->|Publică: OrderPlacedEvent| ASB{{Azure Service Bus\nTopic: orders-confirmed}}:::bus
ASB -.->|Trigger| Billing(Billing Context\nOrderPlacedListener):::external
%% Răspunsuri API
State4 -.->|200 OK| Client
StateErr -.->|400 Bad Request| Client
- Context Facturare:
flowchart TD
classDef command fill:#2D88EF,stroke:#000,stroke-width:1px,color:white
classDef aggregate fill:#F5E942,stroke:#000,stroke-width:1px
classDef event fill:#FF9F4B,stroke:#000,stroke-width:1px
classDef process fill:#E6E6FA,stroke:#000,stroke-width:1px
classDef readmodel fill:#77DD77,stroke:#000,stroke-width:1px
classDef policy fill:#DDA0DD,stroke:#000,stroke-width:1px,color:white
classDef actorStyle fill:#FFFFFF,stroke:#000,stroke-width:1px
Order((Order))
CmdGenInv["Command: GenerateInvoiceDraftCommand"]
ProcGen["Process: GenerateInvoiceDraftOperation"]
AggInvUnval["Invoice State: UnvalidatedInvoice"]
ProcInvValid["Process: ValidateInvoiceOperation"]
AggInvVal["Invoice State: ValidatedInvoice"]
ProcCalcInv["Process: CalculateInvoiceTotalsOperation"]
AggInvCalc["Invoice State: CalculatedInvoice"]
EvtPay["Event: PaymentConfirmedEvent"]
ProcMarkPaid["Process: MarkInvoiceAsPaidOperation"]
AggInvPaid["Invoice State: PaidInvoice"]
EvtInvPaid["Event: InvoicePaidEvent"]
PolSalesToBill["Policy: OrderPlaced → Start BillingWorkflow"]
EvtPlaced["Event: OrderPlacedEvent"]
PolBillToShip["Policy: InvoicePaidEvent → Trigger Shipping Context"]
Order --> EvtPlaced
EvtPlaced --> PolSalesToBill
PolSalesToBill --> CmdGenInv
CmdGenInv --> ProcGen
ProcGen --> AggInvUnval
AggInvUnval --> ProcInvValid
ProcInvValid --> AggInvVal
AggInvVal --> ProcCalcInv
ProcCalcInv --> AggInvCalc
AggInvCalc --> ProcMarkPaid
EvtPay --> ProcMarkPaid
ProcMarkPaid --> AggInvPaid
AggInvPaid --> EvtInvPaid
EvtInvPaid --> PolBillToShip
class CmdGenInv command
class AggInvUnval,AggInvVal,AggInvCalc,AggInvPaid aggregate
class EvtPlaced,EvtPay,EvtInvPaid event
class ProcGen,ProcInvValid,ProcCalcInv,ProcMarkPaid process
class PolSalesToBill,PolBillToShip policy
class Order actorStyle
- Context Livrare:
flowchart TD
classDef command fill:#2D88EF,stroke:#000,stroke-width:1px,color:white
classDef aggregate fill:#F5E942,stroke:#000,stroke-width:1px
classDef event fill:#FF9F4B,stroke:#000,stroke-width:1px
classDef process fill:#E6E6FA,stroke:#000,stroke-width:1px
classDef readmodel fill:#77DD77,stroke:#000,stroke-width:1px
classDef policy fill:#DDA0DD,stroke:#000,stroke-width:1px,color:white
classDef actorStyle fill:#FFFFFF,stroke:#000,stroke-width:1px
%% Actori si Triggeri Externi
BillingContext((Billing Context))
%% Input Event
EvtInvPaid["Event: InvoicePaidEvent"]
%% Listener logic (Policy)
PolTrigger["Policy: InvoicePaidListener<br/>(Trigger Workflow)"]
%% The Command
CmdProcShip["Command: ProcessShipmentCommand"]
%% Step 1: Entry / Mapping
ProcEntry["Process: ProcessShipmentOperation<br/>(Map to Domain)"]
StateUnval["State: UnvalidatedShipment"]
%% Step 2: Validation
ProcValid["Process: ValidateShipmentOperation<br/>(Check Address & Items)"]
StateVal["State: ValidatedShipment"]
%% Step 3: Calculation
ProcCalc["Process: CalculateShippingCostOperation<br/>(Apply Pricing Rules)"]
StateCalc["State: CalculatedShipment"]
%% Step 4: Manifestation (Final Step)
ProcManif["Process: ManifestShipmentOperation<br/>(Generate AWB)"]
StateManif["State: ManifestedShipment"]
%% Output Event
EvtManif["Event: ShipmentManifestedEvent"]
%% Relatii
BillingContext --> EvtInvPaid
EvtInvPaid --> PolTrigger
PolTrigger --> CmdProcShip
CmdProcShip --> ProcEntry
ProcEntry --> StateUnval
StateUnval --> ProcValid
ProcValid --> StateVal
StateVal --> ProcCalc
ProcCalc --> StateCalc
StateCalc --> ProcManif
ProcManif --> StateManif
StateManif --> EvtManif
%% Aplicare stiluri
class CmdProcShip command
class StateUnval,StateVal,StateCalc,StateManif aggregate
class EvtInvPaid,EvtManif event
class ProcEntry,ProcValid,ProcCalc,ProcManif process
class PolTrigger policy
class BillingContext actorStyle
Utilizăm Value Objects pentru a preveni "Primitive Obsession" și a garanta validitatea datelor la nivelul cel mai jos.
-
Context Vanzari:
- ProductCode: Format strict validat prin Regex (^PROD-[0-9]{4}$ - ex PROD-0001)
- Quantity: Număr întreg strict pozitiv (>0) limitat la un maxim per comanda.
- Money: Valoare zecimală + Monedă, nu permite valori negative.
- Address: Structură imutabilă ce grupează datele de livrare (Oraș, Județ, Stradă), validată la creare.
-
Context Facturare:
- BillingAddress: Obiect imutabil care grupează toate câmpurile de adresă de facturare (județ, oraș, stradă, cod poștal).
- Money/Price: Valoare zecimală + Monedă, nu permite valori negative.
- TaxRate: procent TVA (ex: 19%).
-
Context Livrare:
- AwbCode: Format specific curierului.
- Moneu: Valoare zecimală + Monedă, nu permite valori negative.
- ShippingAddress: Structură complexă (județ, stradă, oraș, cod poștal)
Stările sunt modelate ca tipuri distincte (clase/record-uri) pentru a forța verificarea lor la compilare.
-
Context Vanzari:
- UnvalidatedOrder: Comanda brută primită de la client (poate avea stoc lipsă, adresă invalidă).
- ValidatedOrder: Datele au fost convertite în Value Objects și validate (ex: stocul există, cantitățile sunt corecte).
- CalculatedOrder: S-au aplicat prețurile unitare din baza de date și s-a calculat totalul comenzii.
- PlacedOrder: Starea finală în acest context. Comanda este salvată și confirmată, fiind emis un eveniment pentru a notifica Billing Context.
- InvalidOrder: Stare rezultată în cazul eșuării oricărei validări anterioare, conținând lista motivelor de eroare (fără a arunca excepții în flow).
-
Context Facturare:
- UnvalidatedInvoice: Reprezintă proiectul brut de factură obținut direct din comandă; poate conține date lipsă sau invalide (adresă, prețuri, cantități) și nu este încă pregătit pentru emitere.
- ValidatedInvoice: Stare în care toate datele au fost verificate și convertite în Value Objects (BillingAddress, Money, TaxRate); factura este coerentă din punct de vedere fiscal și poate intra în etapa de calcul.
- CalculatedInvoice: Factură pentru care s-au calculat corect subtotalul, TVA și totalul final pe baza liniilor de comandă și a cotei de TVA; este pregătită pentru a fi emisă clientului sau trimisă spre plată.
- PaidInvoice: Starea finală în contextul de facturare, în care plata a fost confirmată pentru factura respectivă; la acest punct se poate publica evenimentul InvoicePaidEvent și se pot declanșa fluxuri din alte contexte (ex. livrare).
-
Context Livrare:
- UnvalidatedShipment: Cererea de livrare brută, venită după plata facturii.
- ValidatedShipment: Adresa de livrare e validă,
- CalculatedShipment: Costul de transport calculat.
- ManifestedShipment: AWB-ul a fost generat si livrarea finalizată.
Operațiile sunt funcții pure (pe cât posibil) care transformă o stare în alta.
-
Context Vanzari:
- ValidateOrderOperation: UnvalidatedOrder → IOrder (Verifică formatul codurilor de produs și disponibilitatea acestora în baza de date prin IProductRepository). Returnează ValidatedOrder sau InvalidOrder.
- CalculatePricesOperation: ValidatedOrder → IOrder (Preia prețurile actuale din DB și calculează totalul per linie și totalul general). Returnează CalculatedOrder sau InvalidOrder.
- PlaceOrderOperation: CalculatedOrder → IOrder (Persistă comanda în SQL Server folosind IOrderRepository, decrementează stocul și publică mesajul asincron OrderConfirmedMessage). Returnează PlacedOrder.
-
Context Facturare:
- GenerateInvoiceDraftOperation: GenerateInvoiceDraftCommand → UnvalidatedInvoice (Primește datele venite din contextul de vânzări: OrderId, CustomerId, BillingAddress, Lines, Amount, PlacedDate și le proiectează într-un obiect de domeniu UnvalidatedInvoice, fără să facă încă validări complexe; practic traduce DTO-ul extern în model intern de facturare.)
- ValidateInvoiceOperation: UnvalidatedInvoice → ValidatedInvoice (Verifică integritatea datelor de facturare: validează BillingAddress, convertește prețurile și cantitățile în Money și alte value object‑uri, și se asigură că structura facturii respectă regulile domeniului; rezultatul este un ValidatedInvoice gata pentru calculul sumelor.)
- CalculateInvoiceTotalsOperation: ValidatedInvoice → CalculatedInvoice (Parcurge liniile facturii aplică TaxRate pentru a obține TVA și construiește totalul de plată ca Money; rezultatul este CalculatedInvoice, care conține toate valorile financiare necesare emiterii facturii.)
- MarkInvoiceAsPaidOperation: CalculatedInvoice + PaymentConfirmedEvent → PaidInvoice (Combină factura calculată cu evenimentul de plată confirmată: sumă plătită, momentul plății; și produce PaidInvoice, starea finală în care factura este marcată ca plătită și din care se generează evenimentul InvoicePaidEvent ce va fi trimis către contextul Livrare.
-
Context Livrare:
- ProcessShipmentOperation: command -> UnvalidatedShipment
- ValidateShipmentOperation: UnvalidatedShipment -> ValidatedShipment
- CalculateShippingCostOperation: ValidatedShipment -> CalculatedShipment
- ManifestShipmentOperation: CalculatedShipment -> ManifestedShipment
-
PlaceOrderWorkflow: Acest workflow orchestrează procesul de cumpărare (Pipeline):
- Primește starea inițială UnvalidatedOrder (convertită din PlaceOrderRequest în Controller).
- Execută ValidateOrderOperation (returnează ValidatedOrder sau InvalidOrder).
- Dacă rezultatul e valid, execută CalculatePricesOperation (returnează CalculatedOrder).
- Execută PlaceOrderOperation (salvează comanda în DB și returnează PlacedOrder).
- Ca efect al ultimei operații, publică mesajul de integrare OrderConfirmedMessage pe topicul orders-confirmed din Azure Service Bus.
-
BillingWorkflow:
- Primește un GenerateInvoiceDraftCommand construit din OrderPlacedEvent, împreună cu PaymentConfirmedEvent.
- Execută GenerateInvoiceDraftOperation pentru a crea UnvalidatedInvoice, adică draftul brut de factură.
- Rulează ValidateInvoiceOperation, care transformă UnvalidatedInvoice în ValidatedInvoice după ce verifică adresa de facturare, sumele și regulile fiscale.
- Rulează CalculateInvoiceTotalsOperation, care produce CalculatedInvoice calculând subtotalul, TVA și totalul de plată.
- Rulează MarkInvoiceAsPaidOperation, care combină CalculatedInvoice cu confirmarea de plată și generează PaidInvoice. Din această stare finală se construiește și se publică evenimentul InvoicePaidEvent, folosit de contextul Livrare.
-
ShippingWokflow
- Primește evenimentul InvoicePaidEvent.
- Creează UnvalidatedShipment.
- Validează adresa
- Calculează costul transportului și generează AWB.
- Publică evenimentul ManifestedShipment.
# 1. Compile solution
dotnet build
# 2. Run Sales Context (Terminal 1)
dotnet run --project Sales.Api
# 3. Run Billing Context (Terminal 2)
dotnet run --project Billing.Api
# 4. Run Shipping Context (Terminal 3)
dotnet run --project Shipping.Api
- Generarea rapidă a structurilor de tip record (C# 9+) pentru stările imutabile.
- Sugestii pentru implementare.
- Idei pentru structurarea mesajelor JSON pentru comunicarea asincronă.
- Dificultate în a înțelege nuanțele specifice ale comunicării asincrone între cele 3 contexte (uneori sugera apeluri directe HTTP în loc de mesagerie).
- Codul generat uneori ignora/uita celelalte script-uri deja existente.
"Generează o clasă C# record 'Money.cs'..."
- Am ales să folosim Azure Service Bus pentru comunicarea asincronă între contexte.
- Logica de domeniu este pură, fără dependențe de baza de date.