Skip to content

milesbuckton/accso

Repository files navigation

Shipment Tracker — Accso Technical Assignment

A consulting-style response to: "Downstream systems disagree about shipment state because courier events arrive late, out of order, duplicated, or with conflicting data."

The submission is document-first. The executable slice is a thin, targeted demonstration of the data-integrity strategy — not a platform.

What's in here

Path Purpose
docs/strategy-memo.md The technical strategy memo (the main read)
docs/delivery-plan.md Phased delivery plan + risk register + success signals
docs/architecture.md Component + sequence diagrams (Mermaid)
docs/adr/0001-event-sourced-audit-with-projected-state.md ADR: audit log + materialised projection
docs/adr/0002-idempotent-ingestion-and-conflict-resolution.md ADR: dedup + conflict-resolution rules
docs/ai-process-note.md How AI was used, what I overrode
docs/stakeholder-note.md Half-page trade-off note for a non-technical sponsor
docs/demo-runbook.md Pre-demo dry-run script — six scenarios that exercise every reviewer-relevant property
scripts/ DB reset scripts: reset-soft.sql (DELETE), reset-hard.sql (DROP TABLE), reset-nuke.sql (DROP DATABASE). Run via sqlcmd -S "LAPTOP-DELL\SQLEXPRESS" -d ShipmentTracker -i scripts\reset-*.sql — see docs/demo-runbook.md for usage
src/ShipmentTracker.Api ASP.NET Core 10 Web API (controller-based) — the slice
tests/ShipmentTracker.Tests xUnit v3 tests (30 tests, all green)

Tech stack (kept deliberately small)

  • .NET 10 / ASP.NET Core 10 (MVC controllers) — required
  • EF Core 10 over SQL Server Express (LAPTOP-DELL\SQLEXPRESS). Read paths (the three GET /shipments/{id}/… endpoints and the projector's re-projection query) use AsNoTracking() to skip change-tracker hydration — the change tracker is only carrying its weight on the write path where SaveChangesAsync needs it.
  • Microsoft.AspNetCore.OpenApi for spec generation + Swagger UI viewer served at the site root (/)
  • Serilog for structured operational logging (console + rolling daily file under logs/). Distinct from the domain AuditEntry log — Serilog answers "what did the process do at 03:14?", AuditEntry answers "why did the system decide X about this shipment?"
  • Hangfire with SQL Server storage — for the 30-day raw-payload retention job introduced by the change request (RawEventRetentionJob, registered daily at 03:00 UTC). Dashboard at /hangfire.
  • xUnit v3 for tests (assertions use the built-in Assert.* API — no third-party assertion library)
  • Not used: RabbitMQ (overkill for the slice — see ADR 0002 for the evolution path), Web frontend (not asked for)

Running locally

Prerequisites

  • .NET 10 SDK (dotnet --version10.0.300)
  • SQL Server Express reachable at LAPTOP-DELL\SQLEXPRESS with Windows auth, or edit appsettings.jsonConnectionStrings:Tracker
  • The database ShipmentTracker is created automatically on first run (EnsureCreated).

Run the API

cd src/ShipmentTracker.Api
dotnet run

Open http://localhost:5049/ (or whatever Kestrel logs) for Swagger UI, and http://localhost:5049/hangfire for the Hangfire dashboard.

Try it

Send a webhook via Swagger or curl:

curl -X POST http://localhost:5049/webhooks/dhl `
  -H "Content-Type: application/json" `
  -d '{
    "eventId":"evt-123","partner":"dhl","shipmentId":"456",
    "status":"IN_TRANSIT","occurredAt":"2026-03-10T12:00:00Z",
    "receivedAt":"2026-03-10T12:00:05Z","location":"Amsterdam"
  }'

curl http://localhost:5049/shipments/456
curl http://localhost:5049/shipments/456/events
curl http://localhost:5049/shipments/456/audit

Send a batched Acme payload (the change-request scenario — deliberately out of order):

curl -X POST http://localhost:5049/webhooks/acme/batch `
  -H "Content-Type: application/json" `
  -d '{
    "batchId":"b-001",
    "events":[
      {"id":"a3","trackingRef":"200","state":"OUT_FOR_DELIVERY","at":"2026-03-10T15:00:00Z","city":"Munich"},
      {"id":"a1","trackingRef":"200","state":"LABEL","at":"2026-03-10T08:00:00Z"},
      {"id":"a2","trackingRef":"200","state":"IN_TRANSIT","at":"2026-03-10T10:00:00Z","city":"Berlin"}
    ]
  }'

curl http://localhost:5049/shipments/200

The Hangfire dashboard is at http://localhost:5049/hangfire — find the raw-event-retention recurring job there. You can trigger it manually for an end-to-end demo.

Send the same event again — observe "outcome":"DuplicateIgnored". Send an earlier-timestamped event — observe "outcome":"AcceptedNoStateChange" (state does not regress). Send DELIVERED then a later IN_TRANSIT — observe "outcome":"RejectedAfterTerminal".

Run tests

cd tests/ShipmentTracker.Tests
dotnet test

Tests use SQL Server LocalDB ((localdb)\MSSQLLocalDB) — one fresh database per test, dropped on process exit. Real FK enforcement, unique constraints, and RowVersion concurrency catch the same classes of bug your production SQL Server would. LocalDB ships with Visual Studio and the .NET SDK on Windows, so no separate install needed.

⚠ Do not pass --nologo to dotnet test here. xUnit v3 runs on Microsoft.Testing.Platform, which doesn't recognise the legacy VSTest flag — dotnet test forwards it to the test app and the run exits with code 5 having discovered zero tests. dotnet build --nologo is unaffected.

What the slice deliberately does NOT include

These were considered and skipped to honour the brief's "no bloat" guidance — discussed in the strategy memo:

  • Authentication / signature verification on the webhook (production must add HMAC per partner)
  • A real message broker (in-process Hangfire is sufficient for the slice; ADR 0002 covers when to introduce RabbitMQ/Kafka)
  • Hangfire dashboard authentication (currently open — required before prod, tracked in delivery plan Phase 2 exit criteria)
  • Containerisation, CI pipelines, observability stack (OpenTelemetry hooks noted in memo, not wired)
  • A Web frontend (brief does not call for one)
  • Multiple production environments / migrations bundle (uses EnsureCreated — production would use dotnet ef migrations)

Repo conventions

  • Three git commits are intentional:

    1. Initial strategy + slice (feat: initial strategy memo, delivery plan, and executable slice)
    2. Response to the mandatory change request (feat: 2nd courier, batch ingestion, retention)
    3. Post-review polish (OpenAPI generator switch, Serilog, explicit types in hot paths, AsNoTracking on read queries, FK constraint on the projection, test harness moved from EF InMemory to SQL Server LocalDB, reset scripts under scripts/, demo runbook + launch profiles, xUnit v3 cancellation-token propagation)

    The diff between commits 1 and 2 is itself an artefact — it shows how the strategy adapted to the change request. Commit 3 is the kind of self-review pass I'd expect to do after sleeping on a piece of work; the per-commit story is in docs/ai-process-note.md.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages