Skip to main content
Software Mobile Offline

A sale that survives a dropped network

The sale happens at the counter, not on the server. If the network drops after the customer has paid, the receipt must still exist, and the server must accept that sale once, when the connection returns.

LesterTechLabs Engineering Estimated 3 min read
A small shop counter at dusk with a phone, a notebook, and a receipt spike

The sale happens at the counter, not on the server. If the network drops after the customer has paid, the receipt must still exist, and the server must accept that sale once, when the connection returns.

The phone keeps the document

Write the sale on the device first, with an id the device generated, the lines, the amounts, and the time on the device. The seller can hand over the goods. A spinner that waits for the server is a queue of angry customers the moment the tower is busy.

Show the sale as saved on this phone, and separately as received by the office. Those are different states. Staff should not have to guess whether the figure in today’s total includes the offline receipts.

The server treats the device id as an idempotency key

When the phone syncs, it sends the same id it stored. The server inserts the sale if that id is new and returns the existing sale if it is not. A flaky upload will otherwise create two sales and two stock movements for one bag of cement.

Conflicts are real. The same last unit can be sold on two phones while both are offline. The server should accept both documents, flag the negative stock, and put the pair in a queue a person resolves. Silently dropping the second sale deletes money the customer already paid.

Time and money need types that survive the trip

Send amounts as decimal strings, not floating point. Send the device time and the timezone. The server records when it received the sync as well. A report that uses only the sync time will shove Saturday’s sales into Monday’s books every time the phone was in a drawer.

Catalogues go the other way. The phone needs the prices it is allowed to sell, and a moment when that list was fetched. If the office changes a price, sales made on the old list should still post at the old price, and the next fetch should carry the new one. Do not rewrite a completed offline sale.

Accept the upload once

The client id is unique. The stock movement uses that same id, so a second delivery of the JSON does not move stock again.

Python · sales/sync.py

from django.db import transaction

def accept_offline_sale(payload: dict):
    with transaction.atomic():
        sale, created = Sale.objects.get_or_create(
            client_id=payload["client_id"],
            defaults={
                "branch_id": payload["branch_id"],
                "total": payload["total"],
                "sold_at": payload["sold_at"],
            },
        )
        if created:
            for line in payload["lines"]:
                sell(sale.branch_id, line["sku_id"], line["qty"], sale.id)
        return sale, created

The phone is the till

The sale is written on the device before any request is attempted. The id was born on the device. The seller hands over the goods. A spinner that waits for the server is a queue of customers the moment the tower is busy. The screen shows two states: saved here, and received by the office. Staff should not guess whether today's total includes the offline receipts.

The server accepts that id once. A flaky upload will otherwise create two sales and two stock movements. The stock movement uses the same id. Tests send the payload twice and expect one sale. Conflicts are real: two phones can sell the last unit while both are offline. Accept both documents, flag the negative, and let a person resolve it. Dropping the second sale deletes money the customer already paid.

Amounts travel as decimal strings. The device time and the timezone travel with the sale, and the server also stores when the sync arrived. A report that uses only the sync time will file Saturday's sales on Monday. Prices on the phone are the list it fetched, and a completed sale keeps that price when the office changes the list later.

Pilot with one shop and a switch that can drop the network. Sell, kill the signal, sell again, restore the signal, and reconcile. If the office total matches the paper spike, the sync is real. If it does not, you are not ready for a second shop.

On the next pilot in a shop

  1. Save the sale on the device before you try the network.
  2. Sync with a device-generated id the server will accept only once.
  3. Flag two offline sales of the last unit instead of dropping one.

On the site

Software

Have a system to build?

We design and ship custom software with a scope you can track, from the first request to handover.