Skip to content

feat: transaction lifecycle — states, events, and invariant guards (Phase 3.1/3.2)#50

Merged
jorge07 merged 1 commit intojorge07:masterfrom
josecarlospeer-cloud:feat/transaction-lifecycle
Feb 22, 2026
Merged

feat: transaction lifecycle — states, events, and invariant guards (Phase 3.1/3.2)#50
jorge07 merged 1 commit intojorge07:masterfrom
josecarlospeer-cloud:feat/transaction-lifecycle

Conversation

@josecarlospeer-cloud
Copy link
Contributor

Summary

Phase 3.1 + 3.2 from the evolution roadmap: full transaction lifecycle end-to-end.


Domain

TransactionStatus enumPENDING → CONFIRMED | FAILED; CONFIRMED → REFUNDED

Three new domain events (primitives only):

  • TransactionWasConfirmed(aggregateId)
  • TransactionFailed(aggregateId, reason)
  • TransactionWasRefunded(aggregateId)

Transaction aggregate updated:

  • status field, starts PENDING on creation
  • confirm() — guard throws InvalidStateException if not PENDING
  • fail(reason) — guard throws InvalidStateException if not PENDING
  • refund() — guard throws InvalidStateException if not CONFIRMED

InvalidStateException — new shared exception for invariant violations.
Source: Vernon (IDDD) ch. 7 — "each state transition must be guarded by invariants that prevent invalid moves."

ITransactionReadDTO — added status field
IRepository — added updateStatus(uuid, status)


Infrastructure

  • Transactions entity: status column (varchar, default PENDING)
  • PostgresRepository: save() includes status; updateStatus() via .update()
  • PostgresProjector: handles all four events; delegates status changes to readModel.updateStatus()
  • BillingAPI: added patch case to bindRouting() switch

Application layer

New command/handler pairs: Confirm, Fail, Refund

  • Load aggregate from event store
  • Call domain method (guard throws if invalid state)
  • Save → events published → projector updates read model
  • Throws NotFoundException if aggregate not found

TransactionModule: registers the three new commands; subscribes projector to all four events

HTTP: PATCH /transaction/:uuid/confirm|fail|refund


Tests

Test doubles cleaned up:

  • New InMemoryReadModelRepository: proper IRepository implementation backed by a Map
  • Replaces the old GenericInMemoryRepository + InMemoryReadModelRepository<any> duck-typing combo
  • InMemoryProjector now typed to IRepository and handles all four events
  • TestKernelFactory subscribes projector to all four events

Transaction.test.ts — 11 domain tests: create, all three lifecycle methods (happy path + guard violations)

Lifecycle.test.ts (new) — 7 integration tests: PENDING→CONFIRMED, PENDING→FAILED, CONFIRMED→REFUNDED, not-found, double-confirm guard, refund-before-confirm guard

41/41 tests pass.

…hase 3.1/3.2)

Phase 3.1 + 3.2 from the evolution roadmap.

### Domain

TransactionStatus enum (PENDING → CONFIRMED | FAILED; CONFIRMED → REFUNDED)

Three new domain events — primitives only, no domain objects:
  - TransactionWasConfirmed(aggregateId)
  - TransactionFailed(aggregateId, reason)
  - TransactionWasRefunded(aggregateId)

Transaction aggregate updated:
  - status field, starts PENDING on create
  - confirm(): guard throws InvalidStateException if not PENDING
  - fail(reason): guard throws InvalidStateException if not PENDING
  - refund(): guard throws InvalidStateException if not CONFIRMED
  - registerHandler for all four events; currentStatus getter

InvalidStateException: new shared exception for invariant violations.
  Source: Vernon (IDDD) ch. 7 — each state transition must be guarded.

ITransactionReadDTO: added status field
IRepository: added updateStatus(uuid, status)

### Infrastructure

Transactions entity: added status column (varchar, default PENDING)
PostgresRepository: save() includes status; new updateStatus() via .update()
PostgresProjector: handles all four events; onTransactionWas* delegates
  to readModel.updateStatus for the three transition events
BillingAPI: added patch case to bindRouting switch

### Application

New command/handler pairs: Confirm, Fail, Refund
  - Load aggregate from event store
  - Call the domain method (guard throws if invalid state)
  - Save back (events published to event bus, projector updates read model)
  - Throws NotFoundException if aggregate not found

TransactionModule: registers Confirm, Fail, Refund commands; subscribes
  projector to all four events

HTTP: PATCH /transaction/:uuid/confirm|fail|refund
  - confirm: 204 on success
  - fail: accepts optional { reason } body
  - refund: 204 on success

### Tests

Test doubles cleaned up:
  - InMemoryReadModelRepository: proper IRepository implementation (Map)
  - Replaced GenericInMemoryRepository + InMemoryReadModelRepository<any>
    combo with a typed, interface-compliant double
  - InMemoryProjector: now typed to IRepository, handles all four events
  - TestKernelFactory: subscribes projector to all four events

Transaction.test.ts: 11 domain tests covering create, guard invariants
  for confirm/fail/refund (happy path + guard violations)

Lifecycle.test.ts: 7 integration tests covering PENDING→CONFIRMED,
  PENDING→FAILED, CONFIRMED→REFUNDED, not-found, double-confirm guard,
  refund-before-confirm guard

41/41 tests pass.
@jorge07 jorge07 merged commit 33e0c19 into jorge07:master Feb 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants