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.
Domain Model
Section titled “Domain Model”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
Aggregates
Section titled “Aggregates”Billing
Section titled “Billing”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
Domain Events
Section titled “Domain Events”ChargeAuthorized
Raised when a charge is authorized by the factory.
| Field | Type | Description |
|---|---|---|
| charge | ChargeId | |
| order | OrderId |
Lifecycle
Section titled “Lifecycle”stateDiagram-v2
[*] --> Authorized
Authorized --> Captured
Authorized --> Failed
Captured --> Refunded
Refunded --> [*]
Failed --> [*]
| From | To | Guard |
|---|---|---|
Authorized | Captured | |
Authorized | Failed | |
Captured | Refunded | |
Refunded | (terminal) | |
Failed | (terminal) |
Commands
Section titled “Commands”capture()
Section titled “capture()”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()
Section titled “refund()”Refund a captured charge.
Preconditions:
- only a captured charge can be refunded
Effects:
status -> Refunded
Factory Operations
Section titled “Factory Operations”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)
Commands
Section titled “Commands”record(amount: Decimal)
Section titled “record(amount: Decimal)”Post an amount to the ledger entry.
Effects:
balance -> amount
Domain Types
Section titled “Domain Types”Money — value object
Section titled “Money — value object”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.
| Field | Type | Description |
|---|---|---|
| amount | Decimal | |
| currency | String |
Business rules
- an amount cannot be negative
PaymentReceipt — value object
Section titled “PaymentReceipt — value object”| Field | Type | Description |
|---|---|---|
| reference | String | |
| amount | Decimal |
Business rules
- a receipt amount cannot be negative
PaymentMethod — enum
Section titled “PaymentMethod — enum”Values: Card, Cash, Voucher
ChargeStatus — enum
Section titled “ChargeStatus — enum”Values: Authorized, Captured, Refunded, Failed
Events
Section titled “Events”Domain Events
Section titled “Domain Events”ChargeCaptured
Section titled “ChargeCaptured”Recorded when a charge is captured. Triggers the ledger-posting policy.
| Field | Type | Description |
|---|---|---|
| charge | ChargeId | |
| capturedAmount | Decimal |
Services
Section titled “Services”PaymentService
Section titled “PaymentService”The application service interface (IPaymentService).
Use Cases
Section titled “Use Cases”-
AuthorizeCharge(order: OrderId, amount: Money, method: PaymentMethod): ChargeId -
CaptureCharge(charge: ChargeId)
Policies
Section titled “Policies”When a domain event occurs, trigger a reaction on another aggregate.
PostToLedger — when ChargeCaptured
Section titled “PostToLedger — when ChargeCaptured”Reaction: Books.record(amount: capturedAmount)
Coverage
Section titled “Coverage”| Kind | Covered | Total |
|---|---|---|
| aggregate | 2 | 2 |
| entity | 2 | 2 |
| enum | 2 | 2 |
| event | 2 | 2 |
| value | 2 | 2 |
All declared constructs are documented. ✅