Skip to content

adesima/Proiect-PSSC

Repository files navigation

E-Commerce System (Order, Billing & Shipping)

Echipa

  • Isac Lucas-Horatiu
  • Lemnaru Alin-Gabriel
  • Sima Adelin-Sebastian

Domeniul Ales

Sistem de Gestiune Comenzi Magazin Online (Retail / E-commerce)

Descriere

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).

Bounded Contexts Identificate

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.

Event Storming Results

  • 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
Loading
  • 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
Loading
  • 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
Loading

Implementare

Value Objects

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)

Entity States (Exemplu pentru Workflow-ul "Preluare Comandă")

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ă.

Operations

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

Workflow

  • 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.

Rulare

# 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

Lecții Învățate

Ce a funcționat bine cu AI

  • Generarea rapidă a structurilor de tip record (C# 9+) pentru stările imutabile.
  • Sugestii pentru implementare.
  • Idei pentru structurarea mesajelor JSON pentru comunicarea asincronă.

Limitări ale AI identificate

  • 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.

Prompturi Utile

"Generează o clasă C# record 'Money.cs'..."

Design Decisions

  • 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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages