feat(loyalty): pair POS terminal devices with one-time setup QR
Some checks failed
Some checks failed
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:
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user