Skip to content

Payment — version 1

Payment bounded context — charging the customer for an order and keeping the books. It is downstream of a third-party card gateway whose model we do NOT control, so it shields itself with an anti-corruption layer: the raw gateway result is translated into our own PaymentReceipt (the acl block lives in context-map.koi). Two aggregates — Billing and Ledger — exercise a multi- aggregate context with a cross-aggregate policy. Naming note: the root entity is Charge, deliberately NOT Payment. A C# type must not share its enclosing namespace’s name, and this context emits into a Payment namespace; an entity called Payment would collide with it, exactly as the demo avoided by pairing a Payments context with a Payment entity.

classDiagram
    class PaymentMethod {
        <<enumeration>>
        Card
        Cash
        Voucher
    }
    class ChargeStatus {
        <<enumeration>>
        Authorized
        Captured
        Refunded
        Failed
    }
    class Money {
        <<value object>>
        +Decimal amount
        +String currency
    }
    class PaymentReceipt {
        <<value object>>
        +String reference
        +Decimal amount
    }
    class ChargeCaptured {
        <<event>>
        +ChargeId charge
        +Decimal capturedAmount
    }
    class ChargeAuthorized {
        <<event>>
        +ChargeId charge
        +OrderId order
    }
    class Charge {
        <<aggregate root>>
        +ChargeId id
        +OrderId order
        +Money amount
        +PaymentMethod method
        +ChargeStatus status
        +Bool /isSettled
        +authorize(OrderId order, Money amount, PaymentMethod method) Charge
        +capture()
        +refund()
    }
    class LedgerEntry {
        <<aggregate root>>
        +LedgerEntryId id
        +ChargeId charge
        +Decimal balance
        +record(Decimal amount)
    }
    ChargeCaptured --> Charge
    ChargeAuthorized --> Charge
    Charge *-- Money
    Charge *-- PaymentMethod
    Charge *-- ChargeStatus
    LedgerEntry --> Charge

The charge aggregate — one charge against an order.

Root entity: Charge (identified by ChargeId)

Repository finders:

  • byOrder(order: OrderId): List<Charge>

Repository operations: getById, add, update

ChargeAuthorized

Raised when a charge is authorized by the factory.

FieldTypeDescription
chargeChargeId
orderOrderId
stateDiagram-v2
    [*] --> Authorized
    Authorized --> Captured
    Authorized --> Failed
    Captured --> Refunded
    Refunded --> [*]
    Failed --> [*]
FromToGuard
AuthorizedCaptured
AuthorizedFailed
CapturedRefunded
Refunded(terminal)
Failed(terminal)

Capture an authorized charge. The ledger-posting policy reacts.

Preconditions:

  • only an authorized charge can be captured

Effects:

  • status -> Captured

Events:

  • ChargeCaptured(charge: id, capturedAmount: amount.amount)

Refund a captured charge.

Preconditions:

  • only a captured charge can be refunded

Effects:

  • status -> Refunded
authorize(order: OrderId, amount: Money, method: PaymentMethod)
Section titled “authorize(order: OrderId, amount: Money, method: PaymentMethod)”

Authorize a charge for an order.

Events:

  • ChargeAuthorized(charge: id, order: order)

A second aggregate — the revenue ledger. Two aggregates in one context means the generated IUnitOfWork exposes both repositories.

Root entity: LedgerEntry (identified by LedgerEntryId)

Post an amount to the ledger entry.

Effects:

  • balance -> amount

A monetary amount. currency is intentionally a primitive String, not the shared-kernel Currency enum: Payment is downstream of the shared kernel and does not import Currency (see context-map.koi), so it keeps money’s currency loose to match whatever the gateway hands back.

FieldTypeDescription
amountDecimal
currencyString

Business rules

  • an amount cannot be negative
FieldTypeDescription
referenceString
amountDecimal

Business rules

  • a receipt amount cannot be negative

Values: Card, Cash, Voucher

Values: Authorized, Captured, Refunded, Failed

Recorded when a charge is captured. Triggers the ledger-posting policy.

FieldTypeDescription
chargeChargeId
capturedAmountDecimal

The application service interface (IPaymentService).

  • AuthorizeCharge(order: OrderId, amount: Money, method: PaymentMethod): ChargeId

  • CaptureCharge(charge: ChargeId)

When a domain event occurs, trigger a reaction on another aggregate.

Reaction: Books.record(amount: capturedAmount)

KindCoveredTotal
aggregate22
entity22
enum22
event22
value22

All declared constructs are documented.