feat(loyalty): pair POS terminal devices with one-time setup QR
Some checks failed
CI / ruff (push) Successful in 47s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Adds the backend half of the Android tablet rollout. Merchants can
pair tablets to specific stores from /merchants/loyalty/devices (or
admins can pair on behalf from the merchant detail page). Each
pairing issues a long-lived JWT shown ONCE in the response with a
server-rendered QR PNG containing {api_url, store_code, auth_token} —
the tablet scans it on first boot and persists the three fields.

The store API (/api/v1/store/loyalty/*) now accepts these device JWTs
alongside user JWTs. Revoking a device row immediately rejects its
token (401 TERMINAL_DEVICE_REVOKED). Tokens expire after 1 year;
re-pair to renew.

- Migration loyalty_010 + TerminalDevice model
- create_device_token / verify_device_token JWT helpers
- 5 endpoints x 2 portals (merchant + admin on-behalf)
- Bearer-auth wiring in app/api/deps.py
- Pages, shared list partial with one-time pairing-QR modal,
  Alpine.js factories
- Locale strings (en authoritative; fr/de/lb seeded with EN copy
  for translation)
- 6 integration tests covering pair, list, revoke, idempotency,
  cross-merchant rejection, store-API auth via device JWT

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 20:18:57 +02:00
parent c7ab5eb900
commit 6276e9e3ac
28 changed files with 1971 additions and 7 deletions

View File

@@ -83,7 +83,8 @@
"settings": "Einstellungen",
"wallet_debug": "Wallet Debug",
"staff_pins": "Mitarbeiter-PINs",
"transactions": "Transaktionen"
"transactions": "Transaktionen",
"terminal_devices": "Terminal Devices"
},
"permissions": {
"view_programs": "Programme anzeigen",
@@ -910,6 +911,51 @@
"pin_delete_error": "PIN konnte nicht gelöscht werden",
"pin_unlock_error": "PIN konnte nicht entsperrt werden"
},
"terminal_devices": {
"title": "Terminal Devices",
"subtitle": "Pair POS tablets to each store. The setup QR is shown only once at pairing time.",
"admin_subtitle": "Pair POS tablets on behalf of this merchant.",
"loading": "Loading devices...",
"error_loading": "Failed to load devices",
"no_devices": "No devices paired yet",
"show_revoked": "Include revoked",
"pair_device": "Pair Tablet",
"pair_hint": "A long-lived setup QR is generated. The tablet scans it once and connects from then on.",
"pairing_qr": "Tablet Pairing QR",
"qr_warning_title": "Save this now —",
"qr_warning_body": "this QR is shown only this time. If you close it before pairing, you will need to revoke and pair again.",
"qr_done": "I've paired the tablet",
"show_payload": "Show raw payload",
"label_field": "Device label",
"label_placeholder": "e.g. Counter 1",
"store_field": "Store",
"select_store": "Select store...",
"revoke": "Revoke",
"revoke_title": "Revoke device?",
"confirm_revoke": "Revoke device '{label}'? Its tablet will stop working immediately.",
"delete_title": "Delete device?",
"confirm_delete": "Permanently delete device '{label}'? This cannot be undone.",
"col_label": "Label",
"col_store": "Store",
"col_status": "Status",
"col_last_seen": "Last seen",
"col_expires": "Expires",
"col_actions": "Actions",
"status_active": "Active",
"status_revoked": "Revoked",
"status_expired": "Expired",
"toasts": {
"paired": "Tablet paired — show the QR to the device",
"revoked": "Device revoked",
"deleted": "Device deleted"
},
"errors": {
"store_required": "Please select a store",
"pair_failed": "Failed to pair device",
"revoke_failed": "Failed to revoke device",
"delete_failed": "Failed to delete device"
}
},
"errors": {
"card_not_found": "Karte nicht gefunden"
}