An operations platform that closes the day on its own

Every evening someone had to open a laptop, pull the takings, post them to the accounts and check the numbers. We built an always-on platform that does it unattended, ties out to the penny and gives each member of staff the reports their role needs.

Locality
East Midlands, UK
Disciplines
Product engineering, Systems integration
Stack
  • Django
  • Next.js PWA
  • PostgreSQL
  • Redis + RQ
  • Docker
  • Azure Pipelines

At a glance

  • 718

    backend tests on real PostgreSQL

  • 185

    frontend tests

  • 67

    tables replicated from a 450-table source

  • 17

    report marts

The challenge

The business's accounting and reporting ran by hand, with an engineer's session acting as the scheduler, the dashboard and the confirm button. If nobody opened that session, nothing happened.

The goal was an application that is the system of record: one that posts the day's takings to the accounts unattended, refuses to post anything that doesn't reconcile, and shows the owner and staff the reports that fit their role.

Architecture

Retail operations platform architectureArchitecture diagram with 9 components. Point-of-sale system to Sync worker. Sync worker to PostgreSQL. PostgreSQL to Django API. Django API to Staff and owner PWA. PostgreSQL to End-of-day poster (nightly). End-of-day poster to Accounting server. Accounting server to QuickBooks Online. End-of-day poster to Audit receipt.nightlyPoint-of-sale systemRead-onlySync workerIncremental, ~5 minPostgreSQLReplica + report martsDjango APIRole-scoped RBACStaff and owner PWAPasskey sign-inEnd-of-day posterTrading-hours gateAccounting serverSole OAuth authorityQuickBooks OnlineAudit receiptSalon printer

Swipe to explore the diagram

The replica feeds both the reports and the nightly poster. The poster reconciles before it posts, posts idempotently, and prints a receipt for the audit trail.

What we built

Data layer

  • A PostgreSQL replica of the point-of-sale system: 67 reconciled tables from a 450-table, 6,734-column source, with the rest catalogued for later
  • Six analytics sources (web, search, business profile, social and more) refreshed three times a day
  • 17 report marts as read-side views, so business rules live in versioned SQL and never as edits to replicated data

Unattended accounting

  • A nightly poster that builds a sales receipt and the drawer-to-bank transfers, reconciles to the penny and posts idempotently
  • A trading-hours gate, and an audit receipt printed to the shop printer
  • A tie-out against six years of trading history that must pass on every deploy

Reports and access

  • Daily sales, commission, discounts, promotions, staff performance and live bed status on a house chart system
  • Passkeys first, password and TOTP as fallback, with per-report RBAC (own figures, manager or owner)
  • A jobs panel over seven background runners, and backups with a tested restore

Delivery

  • Every merge deploys through CI: tests against real PostgreSQL and Redis, scans, migrate-first deploy, a health check that reports the commit, and automatic rollback
  • A daily security scan of every runtime image

Engineering decisions

Test on the real database

Tests run on PostgreSQL, not SQLite, and the job-queue mocks were deleted so every enqueue path runs for real. A posture suite checks CI, deploy and scanning stay consistent with each other.

Tie out by hand, then automate it

A tie-out of every derived runner against live data, with snapshot rollback, caught two bugs CI couldn't see. That tie-out is now a deploy gate.

Pin the clock

A timezone bug survived months of green builds because CI ran in UTC. The test environment's timezone is now pinned, with a summer-time regression test to keep it honest.

Separate the money queue

Cache and job queues live in separate Redis databases after we found that clearing the cache would have flushed the queue that posts the takings.

Outcome

The nightly poster runs on its own and the manual end-of-day routine is retired. Six days of takings were backfilled on the first run without a duplicate, and every change since has deployed through CI with rollback.