refactor: rebuild the order engine and add a client panel - #1
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
endpoint and no restitution, so reserved units simply vanished.
decimaltype, soORDER BY TotalAmountsortedlexicographically and
SUMaccumulated floating-point error. Both were live: the list sorts byamount and the dashboard aggregates revenue.
Exceptionand returned400. A null reference was reported to thecaller as a client error, swallowed, and never logged as a fault.
/apiroute returned200with the panel's HTML, so a client expecting JSONfailed opaquely.
What changed
IUnitOfWorkas the transactional boundary; repositories no longer commit404JSON for unknown API routesVerification
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_stockreleases sixteensimultaneous 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_sqlite3was resolved transitively at2.1.11, which carriesGHSA-2m69-gcr7-jv3q (high). A patched
2.1.13is now pinned explicitly, and CI fails on any future vulnerable dependency.
Breaking changes
application/problem+jsonwith acodefield, replacing{ "Error": "..." }.ProductDtoandOrderDtocarry new fields./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:
AddItemrejects a product already in the basket. Theusual 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.
holds inventory indefinitely. Fine at this scope, not for production.
docs/50-traceability.mdrather thanmapped to tests that do not demonstrate them.
Draftand ADRsProposed; promoting them is the owner's call.