Skip to content

[Domain] Order timeline and state-change audit - #65

Merged
vedanshujain merged 1 commit into
mainfrom
feat/order-timeline-audit
Jul 22, 2026
Merged

vedanshujain merged 1 commit into
mainfrom
feat/order-timeline-audit

Conversation

@vedanshujain

Copy link
Copy Markdown
Contributor

What

The order timeline / audit vertical slice (admin-UX Increment 1, slice 5 — the last of the order-detail build). The admin order detail page now shows a chronological timeline of everything that happened to an order — state transitions (with who/when), fulfillment recorded, cancellation with reason, reconciliation resolved, and notes — and every state change is durably audited going forward.

Design

Choke-point audit capture. After #63/#64, ALL order state flips (transition/markPaid/markFailed/expire/recordFulfillment/cancelOrder) route through ONE shared primitive — #flipAndEnqueue (a guarded WHERE state=:fromState UPDATE + the outbox INSERT in one transaction). This slice extends THAT primitive (and the in-memory fake's equivalent) to also INSERT one append-only order_events row IN the same transaction. So every state change self-audits with no behavior change to the flips themselves, and — because the event is written only after the guarded flip matched a row — a replayed or lost-race flip is a 0-row miss that records NO event (audit never double-counts a replay; falls straight out of the choke-point design, and is tested under a Postgres race).

Merge vs. write, per artifact. order_events is kept lean — it is ONLY the state-change spine (the history nothing else recorded before). Everything that already carries its own durable timestamp is merged at read time, never double-written:

Artifact Decision Source
State transitions Written to order_events the guarded flip
Order created Merged order.createdAt
Notes Merged order_notes
Fulfillment recorded Merged order.fulfillment
Cancellation w/ reason Merged order.cancellation
Reconciliation resolved Merged order.reconciliationResolution

The fulfillment/cancel flips also stamp their recorder/canceller as the state-change event's actor (the who this domain knows); bare transitions have no modeled actor (null).

Graceful degradation. Orders whose transitions predate this migration have no order_events rows. The timeline read-model degrades: their creation moment, notes, and any recorded fulfillment/cancellation/resolution still populate the view, and a stateChangesAudited flag (false) lets the surface say the state-change history is partial. Events are recorded from this release onward.

Layers

  • [Domain]OrderEvent/OrderEventKind types + OrderStore.listEventsForOrder; pure getOrderTimeline use-case (merge + stable same-timestamp tie-break: at ASC → kind rank → insertion order).
  • [Adapters] — forward-only migration 0014_order_events (append-only, portable text DDL, (order_id, at, id) index); the event INSERT rides #flipAndEnqueue's transaction; fake mirrors it.
  • [Service]GET /admin/orders/:id/timeline (read-only, internal-token guarded; structured entries on the wire, no money, no PII beyond the existing detail).
  • [Plugin] — read-only Timeline section (when/what/who/detail table), honest partial-history caption, independent degradation; sandbox-clean (Block Kit only).

Verification

  • Full suite green against Postgres (127.0.0.1:55432): 138 files / 1405 tests, incl. the new orderTimelineContract on fake + sqlite + pg.
  • pg races: exactly-one audit event under N concurrent markPaid (new); extended the fulfillment concurrent-record race to assert exactly one processing → shipped event.
  • Service: live-server HTTP contract for the timeline endpoint (driven through the real transition/fulfillment/notes endpoints), incl. degradation + guards.
  • Plugin: verified under the real workerd-on-Node sandbox (new Timeline-section + partial-history-caption cases).
  • pnpm lint / pnpm typecheck / pnpm format:check clean; changeset added (minor: domain/store-postgres/service/plugin).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XhEjVemVDUk62ohJ5nkFYx

Admin order detail now shows a chronological timeline (state transitions with
who/when, fulfillment, cancellation with reason, reconciliation resolved, notes),
and every state change is durably audited going forward (admin-UX Increment 1,
timeline slice — the last slice of the order-detail build).

- Domain: OrderEvent + OrderStore.listEventsForOrder port read; getOrderTimeline
  use-case merging the audited state-change spine with the order's derived
  artifacts (created/notes/fulfillment/cancellation/reconciliation) into one
  chronological view (stable same-timestamp tie-break).
- Adapters: migration 0014_order_events (append-only, portable DDL); the event
  INSERT rides the shared #flipAndEnqueue transaction so it is atomic with the
  guarded flip and records nothing on a replay/lost race. Fake mirrors it.
- Service: GET /admin/orders/:id/timeline (read-only, internal-token guarded).
- Plugin: read-only Timeline section on the order detail (sandbox-clean).

Verification: full suite green against Postgres (1405 tests) incl. exactly-one
audit event under a concurrent-flip race; plugin verified in the workerd sandbox.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhEjVemVDUk62ohJ5nkFYx
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@vedanshujain

Copy link
Copy Markdown
Contributor Author

VERDICT: APPROVE — no blocking issues (posted as comment; self-authored PR blocks a formal review).

  1. (Correctness, non-blocking) Verified the guarded UPDATE returns false on a 0-row miss before the order_events INSERT runs (kysely-order-store.ts #flipAndEnqueue), and the PG concurrent-markPaid race test asserts exactly one event — replay/lost-race genuinely writes zero events.
  2. (Safety, non-blocking) The event INSERT shares the flip's transaction, so a write failure there would also roll back the state flip. Intentional atomicity (audit and flip live or die together), not a bug — but it makes the audit write a hard dependency of every transition.
  3. (Correctness, non-blocking) stateChangesAudited=false covers both "predates the audit log" and "simply hasn't transitioned yet." The plugin caption implies missing history in both cases, which is misleading for a fresh pending order.
  4. (Maintainability, non-blocking) Cancellation/fulfillment render as two rows (generic state_change + rich derived artifact) for one event — intentional per the merge-vs-write table, ordered via KIND_RANK, but worth a UI sanity check for redundancy.
  5. (Correctness) Tie-break sort runs in JS after fetch (at ASC → kind rank → insertion order), so sqlite/pg text-collation differences don't matter.
  6. (Safety) listEventsForOrder is unbounded but scoped per-order and naturally small — fine.
  7. (Security) New GET gated by requireInternalToken identically to sibling admin routes; no new PII/money exposure.
  8. (Best practice) Migration 0014 follows the established forward-only, portable-DDL, no-FK pattern from 0010/0012/0013.

@vedanshujain

Copy link
Copy Markdown
Contributor Author

Verdict: Approve (posting as comment — GitHub rejects self-authored review)

Verified independently in the worktree against Postgres (127.0.0.1:55432): full suite green (138 files / 1405 tests, matches PR claim), orderTimelineContract passes identically on fake/sqlite/pg (31 tests), the new concurrent-markPaid race (N=12, exactly one event) and extended fulfillment race (N=8, exactly one processing→shipped event) both pass, the live-server admin timeline HTTP contract passes, plugin workerd-sandbox tests pass, and pnpm lint (incl. domain-purity dep-cruiser check) is clean.

Findings:

  1. (Correctness, non-blocking) In both the in-memory fake and the Kysely #flipAndEnqueue, the guard check (0-row UPDATE / state !== from) runs before the event append/INSERT, so a replayed or lost-race flip genuinely writes zero events — confirmed by the pg race tests, not just asserted in prose.
  2. (Correctness, non-blocking) stateChangesAudited is events.length > 0; a brand-new pending order with zero transitions also reports false and would surface the "state changes predate the audit log" caption even though nothing is actually missing — a cosmetic copy edge case, not a data-integrity issue.
  3. (Domain purity, verified) order-timeline.ts imports only port/model types, no IO — matches the ports-and-adapters rule.
  4. (Security/PII, verified) actor/author on events are pre-existing fields (recordedBy/cancelledBy/note author) already shown on the order detail; no new PII surfaced, and the endpoint is internal-token guarded (401 test present).
  5. (Maintainability, verified) All six flip kinds (transition/markPaid/markFailed/expire/recordFulfillment/cancelOrder) route through the one choke point in both adapters — no behavior change.
  6. (Scope, verified) No drive-by refactors; touched files are exactly what the timeline slice requires plus two mechanical interface-conformance updates (settle-order.test.ts, order-flow.dialects.test.ts).

No blocking issues found.

@vedanshujain
vedanshujain merged commit 75e147c into main Jul 22, 2026
2 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.

1 participant