The problem
An invoice number sequence must be gapless and non-duplicated to pass a fiscal audit. That is hard when:- The mobile app is in airplane mode or has a weak signal and still needs to issue a receipt.
- The same firm has a web user who could emit an invoice on the same series between the time the phone reserves a number and the time it actually syncs.
How it works
Lock a series
POST /invoice-series/{id}/lock binds the series to the calling mobile device. While locked:
- Only that device can call
POST /invoiceswithseries_idequal to this series. - Only that device can request reservations.
- Web / API-key callers that try to emit get
series_locked_to_device(403).
force: true from an admin.
Pre-allocate numbers
The mobile app keeps a small pool (10 by default) of reservations in local storage. It tops the pool up whenever online and usage drops below 3:{token, number} pairs. The app stores them in SQLite alongside each token’s expiry. When the user creates an invoice while offline, the app:
- Pops the next usable reservation and saves a draft with that number visible (e.g. “Factură #101”).
- Enqueues a
POST /invoicesmutation in its outbox withreservation_tokenandforce_chronology: false. - The outbox drains on the next network/foreground transition.
Emit an invoice from a draft
On success, the invoice is created with
number_idx equal to the reservation’s pre-allocated slot. The reservation flips to consumed and records the bill_id.
Discarded drafts
If the user deletes an offline draft before it syncs, the app callsPOST /invoice-series/reservations/{token}/release. The reservation flips to released — the number itself is still gone (documented gap), but the audit trail captures intent.
Gap handling
The series counter advances the moment a reservation is created, not when the invoice is actually emitted. That means expired / released reservations can become gaps in the sequence. Romanian fiscal regulations accept gaps when a firm can document why they occurred — keep the reservation TTL generous (default 30 days) and have the device refresh the pool often enough that expiry is rare in practice. If your jurisdiction does not accept gaps, disable the reservation feature entirely: leave series unlocked and require the mobile device to be online when emitting. The existing online-only path inPOST /invoices still works without any changes.