Supplier returns · Grocery retail · .NET + PANTHEON ERP

The deadline that costs money isn't the expiry date.

A grocery retailer was writing off fresh food not because it expired, but because nobody told the supplier in time. We built the system that turns an expiry date into a report-by deadline — and chases it.

  • IndustryGrocery retail
  • Project typeBuild · Product Development
  • Team6 people
  • Duration~4 months
The Client

A regional grocery chain with fresh food on every shelf.

Several stores in the region. A big part of every shelf is fresh — dairy, produce, meat, anything with a short shelf life. On a good day, most of it moves. What doesn't is where the problem starts.

  • Multiple stores, hundreds to low-thousands of live batches at any moment
  • Several producers, each with its own return terms
  • Near-expiry stock tracked in spreadsheets
  • Return deadlines chased in Viber/WhatsApp groups
Supplier returns dashboard — traffic-light view of batches to be reported today, with the derived report-by rule on screen. Grocery retail .NET application.
The production dashboard, in a store manager's hands — today's report-by list, at a glance.
The Challenge

The deadline that costs money is a derived date.

Food that won't sell before it expires can be returned to the supplier — but only inside a contractual window. Miss it, and the store eats the loss.

That window is a derived date: expiry minus the supplier's lead time, different for every producer, on every batch. On a spreadsheet, it's invisible until it has already passed — and by then the goods are a write-off, not a return.

  • The ERP (Datalab PANTHEON) stays the system of record — the tool integrates with it read-only, with write-back deliberately deferred.
  • Return terms differ per producer and change as contracts are renegotiated — they can't be baked into code.
  • Access is scoped per store: staff see only their own store's batches; management sees the whole chain.
  • Daily users are store and procurement managers on tablets and phones — the workflow has to read like a traffic light, not a report.
The Solution

How the supplier returns deadline engine works.

Three mechanisms doing the work: one canonical rule that turns expiry into a report-by date, an engine that pushes people at the right time, and a read-only boundary that keeps risk out of the ERP.

The derived deadline on a timeline — report-by = expiry − producer lead time. Two producers share an expiry date but land on different report-by dates.

Two producers, the same carton expiry — different report-by dates. That derived, per-producer date is the whole game.

01

One canonical rule

report-by = expiry − lead time, computed per batch and driven through a single state machine.

02

An engine that pushes

Scheduled jobs run the ERP sync and send a 07:00 digest plus in-app alerts — idempotent and per-batch deduplicated.

03

Read-only boundary

Stock is pulled read-only from the ERP. Producer terms live as data — a new contract is a config edit, not a deploy.

Production hardening

The compute is tested where it hurts

The one rule that costs money — report-by = expiry − producer lead time — is covered by unit and property tests across the awkward edges: batches with the window already passed, missing lead times, month and time-zone boundaries. Idempotency and per-batch/day dedup are verified against fixed fixtures, so a replay provably can't double-send or skip a deadline.

The run is observable — built to fail loudly, not silently

Health checks and Application Insights watch every scheduled run. A failed or missed job, a stalled ERP sync, or a dependency outage raises an alert to us before the 07:00 window closes — so the silent failure a low-code stack can't see is exactly the one we get paged on. Because the recompute is idempotent, recovery is usually a safe re-run rather than a scramble.

Change without redeploys, plus a stabilisation tail

Producer terms live as data, so the most common business change — a renegotiated lead time or settlement type — is a config edit, not a release. After v1.0 we ran a stabilisation phase and stayed on for monitoring and support, tightening alerts and edge-case handling as real volume moved through.

Returns lifecycle screen — the audit trail per batch.
Returns lifecycleReminder → review → report — every step recorded against the batch instead of scattered across chat threads.
Integrations screen — read-only PANTHEON sync and notification channels.
ERP boundaryStock and batches pulled read-only from PANTHEON. The dashed line is a deliberate architectural choice.
Producers screen — return rules as configuration.
Rules as dataEach producer's lead time and settlement type live as config — a renegotiated contract is not a deploy.
Deferred by design

Scope decisions we made on purpose, not by omission.

  • Write-back into the ERP

    The tool records and issues the reporting, but posting the final credit note or return back into PANTHEON stays with the ERP's own process for now. A conscious choice not to hold write credentials to the system of record until it's clearly warranted — and to stop the tool from becoming a second source of truth for stock. Automated write-back is the planned next phase.

  • A native mobile app

    The workflow runs as a responsive web app on the tablets and phones already in the store, rather than a separate native app to build and maintain. When the store already has the device, the browser is the shortest path to production.

  • Direct supplier-side integration

    Producers are notified by e-mail because that's what they act on today. A direct API/EDI handshake with suppliers is a later option, not a v1 requirement — the deadline engine is the same either way; the channel is the incremental change.

How we built it

Six people, one lead, a stack you already know.

The same team that scoped the model in Discovery shipped it into production — and still supports it. The stack is right-sized for a production line-of-business system: mainstream tools, no lock-in, no surprises.

Team

  • Engineering leadFull-timeArchitecture, delivery, code review
  • Backend engineerFull-time.NET APIs, PANTHEON sync, background jobs
  • Frontend engineerFull-timeReact app, dashboards, forms
  • QA engineerFull-timeTest plans, regression, release sign-off
  • DesignPart-timeProduct surfaces, prototype-to-production fidelity
  • Product & DevOpsPart-timeDiscovery, releases, Azure infrastructure

Stack

Backend
.NET 8ASP.NET Core Web APIEF CorePostgreSQLHangfireSendGridEntra ID (SSO)
Frontend
ReactTypeScriptViteTanStack QueryTailwind + shadcn/uiRechartsreact-hook-form + zod
Infrastructure
Azure Container AppsPostgreSQL Flexible ServerBlob StorageApplication InsightsAzure DevOps CI/CDDocker
Integration
Datalab PANTHEON (read-only, REST API)
The Outcome

What changed on the floor.

The deadline stopped being invisible — everything below follows from that.

PrototypeDays
To production~4 months
ERP write surfaceZero
On-time reportingDefault, not exception

The deadline that mattered became visible — each store sees what must be reported today, instead of reconstructing it from a spreadsheet after the loss.

Supplier reporting moved off Viber/WhatsApp into one workflow with a timestamped, append-only audit trail — down to the e-mail provider's delivery confirmation, not just that a notice was sent.

Management got a cross-store view of where risk and losses concentrate, by store and by product.

What the client says the team used to miss routinely became the default — reporting to producers on time went from the exception to the norm.

It all used to hang in Viber groups and Excel, and something always slipped through. Now in the morning I just see what has to be reported today — I don't have to keep it in my head anymore.

Procurement lead, the client
Cross-store analytics — saved vs. lost, on-time reporting, ranking by store.
Cross-store analytics — saved vs. lost, on-time reporting, ranking by store and by product.
Who this fits

This is you if…

  1. You've got a deadline that's computed, not scheduled.

    It's derived per counterparty — an SLA window, a contractual notice, a warranty clock — and it moves whenever the underlying contract does. Different for every supplier, on every batch, on every ticket.

  2. The deadline lives outside your system of record.

    In a spreadsheet with formulas two people fully understand, or in a chat group where somebody @-mentions when something's due. Every slip starts the same way — nobody looked in time.

  3. When one slips, both the loss and the audit trail hurt.

    A written-off shipment, a missed credit note, an SLA penalty. And when the counterparty pushes back on a disputed one, you're reconstructing from an inbox instead of producing a timestamped record.

If any of that maps to your operation — that's the shape of deadline this system was built to catch. And the audit trail it was built to keep.

Start here

Have a deadline like this hiding in a spreadsheet?

A derived, per-counterparty date that quietly costs money whenever it slips — fresh food nearing its expiry, warranty windows, SLA clocks, contractual notice periods. It's exactly the kind of problem we take end to end: a Discovery prototype to lock the model, then a production system that owns the outcome.