Skip to content

refactor: rebuild the order engine and add a client panel - #1

Merged
Nain9Dev merged 5 commits into
mainfrom
refactor/order-engine-and-client-panel
Sep 8, 2026
Merged

Nain9Dev merged 5 commits into
mainfrom
refactor/order-engine-and-client-panel

Conversation

@Nain9Dev

@Nain9Dev Nain9Dev commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Rework of the order engine plus a client panel, so the repository demonstrates its claims
instead of asserting them.

Why

The engine had defects that only surface under conditions a demo rarely reaches:

  • Cancelling an order destroyed inventory. The transition existed in the domain but had no
    endpoint and no restitution, so reserved units simply vanished.
  • Money was stored as text. SQLite has no decimal type, so ORDER BY TotalAmount sorted
    lexicographically and SUM accumulated floating-point error. Both were live: the list sorts by
    amount and the dashboard aggregates revenue.
  • Every controller caught Exception and returned 400. A null reference was reported to the
    caller as a client error, swallowed, and never logged as a fault.
  • Nothing guarded a concurrent reservation. Two requests seeing the last unit both succeeded.
  • An unknown /api route returned 200 with the panel's HTML, so a client expecting JSON
    failed opaquely.

What changed

Area Change
Domain Complete state machine, typed exceptions with stable codes, basket operations reporting exact stock deltas, materialised totals
Application IUnitOfWork as the transactional boundary; repositories no longer commit
Infrastructure Money as integer cents, optimistic concurrency on stock, CQRS read side, idempotent seeding
Api RFC 7807 error contract, rate limiting, forwarded headers, 404 JSON for unknown API routes
Client Four-view panel served from the same origin: no framework, no bundler, no npm
Tests 40 tests: example-based, property-based (FsCheck) and integration over HTTP
CI Release build, vulnerability audit, tests, and a smoke test against the deployed image
Docs Charter, requirements, architecture and data model with Mermaid, six ADRs, traceability matrix, runbook

Verification

dotnet build NainOrder.slnx -c Release   →  0 warnings, 0 errors
dotnet test  NainOrder.slnx -c Release   →  40 passed, 0 failed
dotnet list  package --vulnerable        →  none
docker build && docker run               →  verified running, non-root, SQLite writable

The panel was exercised in a browser across all four views and the full lifecycle: catalogue → add
to order → change quantity → pay → process → ship, plus cancellation with stock restitution.

ConcurrencyTests.Concurrent_reservations_never_oversell_the_available_stock releases sixteen
simultaneous reservations against six units and asserts that granted units plus remaining stock
equal the initial stock exactly, that nothing fails for a technical reason, and that at least one
reservation succeeded so the assertions cannot pass vacuously.

Security

SQLitePCLRaw.lib.e_sqlite3 was resolved transitively at 2.1.11, which carries
GHSA-2m69-gcr7-jv3q (high). A patched 2.1.13
is now pinned explicitly, and CI fails on any future vulnerable dependency.

Breaking changes

  • Error responses are application/problem+json with a code field, replacing { "Error": "..." }.
  • ProductDto and OrderDto carry new fields.
  • Swagger moved from / to /swagger; the root now serves the client panel.

All are contained within the demo's own surface — there is no external consumer of this API.

Needs a decision

Left open on purpose rather than decided unilaterally:

  • Repeated product in an order (Q-002). AddItem rejects a product already in the basket. The
    usual shopping behaviour is to increase the quantity instead. Changing it alters business
    behaviour, so the existing rule was preserved and the panel works around it.
  • Reservation expiry (Q-003). Stock is reserved when a line is added, so an abandoned basket
    holds inventory indefinitely. Fine at this scope, not for production.
  • Five uncovered requirements (Q-001), declared as gaps in docs/50-traceability.md rather than
    mapped to tests that do not demonstrate them.
  • Documents are Draft and ADRs Proposed; promoting them is the owner's call.

…ional boundary

The engine could lose inventory, report business failures as technical ones and compute
money incorrectly. This reworks the four layers so those classes of defect become
structurally impossible rather than merely absent.

Domain
- Complete the state machine: Processing, Shipped and Cancelled, with the allowed
  transitions declared in one table the aggregate owns and publishes.
- Cancelling now reports every unit that must return to the catalogue, so the use case
  can restore it. Previously cancelling silently destroyed inventory.
- Basket operations: change a line quantity and remove a line, each reporting the exact
  stock delta so the adjustment is never approximated.
- Typed exceptions carrying a stable machine-readable code, replacing bare Exception.
- Paying an order with no lines is now rejected.
- Order totals are materialised on every basket mutation, so listings and metrics can
  aggregate them in the database without loading a single line.

Application
- IUnitOfWork owns the transactional boundary. Repositories no longer commit, so a use
  case touching two aggregates commits both or neither.
- Remove UpdateAsync from the repository contracts: it did nothing and invited callers
  to believe otherwise, which had already caused an insert to be issued as an update.
- Customer and dashboard use cases; paginated order listing with server-side filters.

Infrastructure
- Money is persisted as integer cents. SQLite has no decimal type, so EF Core stored it
  as text and ORDER BY and SUM operated on strings, returning wrong results.
- Optimistic concurrency token on product stock, so two simultaneous reservations of the
  last unit cannot both succeed.
- CQRS read side aggregating dashboard metrics in SQL over the cent columns.
- Idempotent seeding of a demonstration catalogue on start-up.
- Pin a patched SQLitePCLRaw: the transitively resolved version carried a known
  high-severity vulnerability.

Api
- One exception handler translating every failure to RFC 7807 with a stable code.
  Controllers no longer catch Exception and return 400, which was masking technical
  faults as client errors and keeping them out of the logs.
- An unknown route under /api or /swagger returns 404 as problem+json instead of falling
  through to the client application's HTML.
- Fixed-window rate limiting, response compression and forwarded headers.
- Swagger moves to /swagger, freeing the root for the client panel.

Also removes the empty scaffolding files, a stray debug entry point and the committed
SQLite database, which is now generated on start-up and ignored.

BREAKING CHANGE: error responses are now application/problem+json with a code field
instead of { "Error": "..." }; ProductDto and OrderDto carry new fields; Swagger moved
from / to /swagger.
Swagger documents the endpoints but does not demonstrate the product: it cannot show
that cancelling an order returns stock, or that the available transitions come from the
domain rather than from the client.

The panel lives in wwwroot and is served on the same origin as the API. Native ES
modules and CSS custom properties, no framework, no bundler, no npm and no CDN at
runtime, so the deployment stays a single dotnet publish.

Four views:
- Dashboard with confirmed and pending revenue, a revenue series, the status breakdown
  and the top products, all aggregated by the API in the database.
- Catalogue with product creation and editing, stock adjustment and adding to the order
  in progress.
- Orders with server-side filters and pagination, and a detail panel showing the
  lifecycle, the editable basket and the transitions the domain currently allows.
- Architecture with the layers, the state machine read from the API and a live console
  tracing every request the panel makes, with its real latency.

The panel presents and captures input only. Every derived figure comes from the API
response, so no business rule is duplicated in JavaScript.

Light and dark themes, responsive layout, command palette, keyboard navigation, AA
contrast in both themes and respect for prefers-reduced-motion.
Forty tests across three levels, so each claim the project makes is demonstrated rather
than asserted in prose.

- Example-based: the order state machine, covering every allowed transition and, more
  importantly, every transition it must refuse.
- Property-based with FsCheck: the arithmetic invariants are checked against hundreds of
  generated cases per run. Stock never goes negative whatever the sequence, a rejected
  removal leaves the inventory untouched, and an order total always equals the sum of
  its lines.
- Integration over HTTP: the real application with its migrations and middleware, driven
  through WebApplicationFactory against a per-run SQLite file. Full purchase flow, every
  error path, and delivery of both the panel and the OpenAPI document.

ConcurrencyTests releases sixteen simultaneous reservations against six units and
asserts that granted units plus remaining stock equal the initial stock exactly, that
nothing fails for a technical reason, and that at least one reservation succeeded so the
assertions cannot pass vacuously. That test is the reason the concurrency token exists.
Container
- Runs as a non-root user and writes the database to a directory it owns.
- Drops the redundant build stage; publish already builds.
- Copies the project files before the sources so the restore layer survives a source
  change.
- Adds a .dockerignore so build output and local databases stay out of the context.

CI
- Release build with TreatWarningsAsErrors, so a new warning stops the pipeline instead
  of accumulating as debt.
- Dependency audit that fails on any known vulnerability. It reads the JSON output
  rather than grepping the human-readable text, which depends on the runner's language.
- The full test suite, with results published as an artifact.
- A smoke test against the image that actually gets deployed: start-up, health, the
  panel, the OpenAPI document, a seeded catalogue, the 404 contract for unknown API
  routes, and that the process is not running as root.
The repository asks a reader to trust several strong claims about correctness. This adds
the material that lets them check instead.

- Charter with the objective and, explicitly, the non-goals: what is missing is missing
  on purpose.
- Requirements as REQ-### entries in EARS notation.
- Architecture and data model with Mermaid diagrams, including the failure path of a
  write request, which is what distinguishes this design from one where the stock check
  and the stock write are separate operations.
- Six ADRs recording the decisions that shape the code, each with the alternatives that
  were discarded and why: money as integer cents, optimistic concurrency, the unit of
  work, materialised totals, the error contract and the dependency-free panel.
- A traceability matrix mapping every requirement to the test that demonstrates it, and
  declaring the five requirements that are not yet covered rather than mapping them to a
  test that does not actually cover them.
- Runbook with configuration, known limits and recovery, stating plainly that trusting
  forwarded headers from any proxy is a demonstration trade-off and not a production
  posture.
- Open questions, including two business decisions an agent must not take alone.

AGENTS.md becomes the operational contract for any agent working here, and lists the
invariants that must never regress. Documents are left as Draft and the ADRs as
Proposed: promoting them is the owner's call.
@Nain9Dev
Nain9Dev merged commit a0a5177 into main Sep 8, 2026
2 checks passed
@Nain9Dev
Nain9Dev deleted the refactor/order-engine-and-client-panel branch September 8, 2026 14:23
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