Designing a POS for Jordan's JoFotara e-invoicing: a reference architecture
Whitepaper · RAWA Engineering · rawaerp.com · open guide and runnable example: github.com/rawaerp/jordan-jofotara-pos-integration-guide
Problem
JoFotara requires every tax invoice a POS issues to be reported to the Income and Sales Tax Department. Treated as "POST an XML", the integration fails in exactly the place restaurants cannot tolerate: at the till, at peak. A restaurant rings a sale every 30–40 seconds at rush hour; a 15-second call to a government API visibly grows the queue. The engineering question is not how to build the XML — it is how to keep selling when the API is slow, down or strict.
Requirements
- The cashier never waits on JoFotara.
- No duplicate invoices, even across timeouts and retries.
- No lost invoices, even if the terminal or the service goes down mid-flight.
- Diagnosable rejections the operator can fix.
- Returns (credit notes) that reference the original invoice.
- Per-taxpayer isolation of credentials in a multi-tenant system.
Architecture
sale saved + receipt printed (never blocked)
│
├─► instant attempt (3 s budget, pooled keep-alive connection)
│ ├─ cleared → store QR + UUID → print QR
│ └─ timeout / error ─┐
└────────────────────────────┴─► background queue (retries, same UUID)
│
shared circuit breaker: 3 failures / 2 min → skip instant path for 60 s
- Client-generated UUID, stored with the sale, reused on every retry. A timeout does not reveal whether the server received request #1; the same UUID turns a retry into an idempotent re-submission instead of a second invoice.
- Monotonic invoice counter (ICV) allocated inside the same database transaction as the invoice.
- Circuit breaker, shared across terminals and tenants. If the national API is down it is down for everyone; discovering that individually, a timeout at a time, is the worst experience.
- Pooled sessions. A fresh TCP+TLS handshake per sale is a visible cost; reuse the connection.
- Rejection surfacing. The response carries a list of
{code, message}errors; show the first message and keep the invoice in a rejected state with one-tap resubmission after the fix. - Credit notes use type code 381, positive amounts, and a billing reference to the original's id, UUID and amount — so the original UUID must be stored at sale time.
- Secrets (Client ID / Secret Key) are per taxpayer, encrypted at rest, never logged.
Failure modes and how the design answers them
| Failure | Without the design | With it |
|---|---|---|
| JoFotara slow (10 s+) | Cashier stalls, queue grows | Sale completes; invoice clears from the queue |
| Timeout after the server accepted the request | Retry creates a duplicate | Same UUID: idempotent |
| Service down 20 minutes | Every cashier waits out every timeout | Breaker opens; queue drains on recovery |
| Terminal offline | Invoice never leaves the device | Queued locally; submitted in order on reconnect |
| Validation rejection | Bare "ERROR", support ticket | Specific message shown; resubmit after fixing |
Test checklist
Cash sale with tax · credit sale to a customer with a tax number · invoice-level discount · service charge · return as credit note · service unreachable (cashier not blocked, no duplicate after recovery) · rejection with readable reason · two terminals selling concurrently (counter strictly increasing). Runnable example and a longer checklist: github.com/rawaerp/jordan-jofotara-pos-integration-guide.
Result
RAWA ships this architecture with JoFotara included in every plan (200 / 400 / 600 JOD per year) and invoices sent from the cashier screen. Reference: https://rawaerp.com/guide/pos-with-jofotara-jordan
Common questions
How do you avoid duplicate JoFotara invoices when a request times out?
Generate the invoice UUID once on the client, store it with the sale and reuse the same UUID on every retry, so a retry is an idempotent re-submission instead of a second invoice.
Should the cashier wait for JoFotara?
No. Save and print the sale first, make one instant attempt with a short budget (3 seconds), and send anything that did not clear from a background queue.
What happens when JoFotara is down?
A shared circuit breaker stops the instant attempt for a cool-down window (for example 60 seconds after three failures in two minutes) while the queue keeps retrying, so cashiers keep selling.