Commit Graph

1202 Commits

Author SHA1 Message Date
c158d920d2 chore(android-terminal): dev cleartext + readable HTTP error messages
Some checks failed
CI / ruff (push) Successful in 16s
CI / pytest (push) Failing after 2h32m6s
CI / validate (push) Successful in 31s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
Two small dev-quality fixes shaken out by manual testing:

- Add a debug-only network_security_config.xml that whitelists 10.0.2.2,
  localhost and 127.0.0.1 for cleartext HTTP. Without this, the dev
  emulator can't reach the Python dev server because targetSdk 35
  forbids cleartext HTTP by default. Lives under app/src/debug/ so it
  ships only in debug APKs — release builds keep the platform default
  (no cleartext at all).

- TerminalViewModel.runAction now extracts the JSON {message: ...} field
  from HttpException response bodies instead of just showing "HTTP 400".
  Cashiers (and developers) now see "Staff PIN is required for this
  operation" / "Daily stamp limit of N reached" inline in the failed
  action sheet, surfacing the same business-error text the server
  already returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:17:44 +02:00
c1bb225228 fix(loyalty): paired-device JWT bypasses program.require_staff_pin
When the request principal is a paired POS terminal device
(current_user.terminal_device_id is not None), the staff PIN is
considered already-verified — the cashier bcrypt-verified locally on
the tablet's lock screen against the cached hashes from
/pins/for-device. Web-terminal user JWTs still require the per-action
PIN as before; the strict fraud-prevention path is unchanged.

Threat-model note: the device JWT is itself proof of authentication.
The merchant owner paired the device, the cashier verified locally,
and the JWT is revocable from /merchants/loyalty/devices. The 2-min
idle auto-lock + acting_terminal_device_id audit column give us the
attribution we'd otherwise get from a per-action PIN.

Applied to: stamp_service.add_stamp / redeem_stamps / void_stamps;
points_service.earn_points / redeem_points / void_points. adjust_points
was already permissive on missing PIN. New tests in TestDevicePinBypass
lock both the bypass behavior and the still-strict web-terminal path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:16:33 +02:00
d3f1c33b37 feat(android-terminal): Phase D.5 — auto-lock idle timer
Some checks failed
CI / ruff (push) Successful in 19s
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
Wraps the terminal screen in an IdleTracker that observes pointer-down
events and fires onLockScreen after 2 min of silence (per spec). Each
tap restarts the timer via a LaunchedEffect re-launch.

awaitFirstDown(requireUnconsumed = false) lets us observe touches
without intercepting them, so children (text fields, buttons, lists,
keypad) keep working normally. Body extracted into a private
TerminalContent so the wrapper stays tidy.

Caveat: Compose AlertDialogs render in a separate window, so the timer
keeps ticking through an open dialog. This is correct behavior — a
cashier who walks away mid-dialog should be locked out — and the
dialog disposes when the NavHost pops the terminal route on lock.
Documented in IdleTracker.kt.

Verified by ./gradlew assembleDebug — clean build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:31:16 +02:00
01a12dcef4 feat(android-terminal): Phase D.4 — enrollment dialog + QR scanner overlay
Both left-pane buttons now functional. Cashier can find a customer by
scanning their loyalty QR or enroll a brand-new customer from the
tablet — closing the last gap in the everyday POS flow.

- TerminalViewModel: scannerOpen / enrollDialogOpen / enrolling /
  enrollError state. submitEnroll posts to /cards/enroll, then re-fetches
  the lookup shape so the customer pane renders fully (rewards,
  cooldown). On lookup-after-enroll failure (rare) the new card_number
  is pre-filled in the search field as a fallback.
- EnrollDialog.kt (new): AlertDialog with name + email (required), phone
  + birthday (optional; birthday is plain YYYY-MM-DD text — date picker
  is a polish task). Inline error surface for backend rejections.
- QrScannerOverlay.kt (new): fullscreen overlay reusing QrScannerView
  from Phase B. Cancel button top-right. Decoded value is treated as a
  card_number and feeds the lookup flow.
- TerminalScreen: scan/enroll buttons are no longer disabled; the two
  new composables render conditionally on top of the main layout.

The tablet now supports every everyday flow: lookup, scan, enroll,
stamp, earn points, redeem stamps, redeem reward, recent feed.

Verified by ./gradlew assembleDebug — clean build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:28:44 +02:00
d345d65fd4 feat(android-terminal): Phase D.3 — recent transactions list
Left pane below the search/scan/enroll buttons now shows the last ~10
transactions for the merchant, refreshed on init and after every
successful action sheet submission.

- ApiModels: TransactionListResponse + TransactionItem mirroring the
  store-API shape (deltas, customer/staff names, purchase amount,
  reward description, ISO transaction_at).
- LoyaltyApi.listRecentTransactions(skip, limit) → GET /transactions.
- TerminalViewModel.refreshRecentTransactions called on init and from
  runAction's success path so balances + feed stay in lockstep.
- TerminalScreen: RecentTransactionsList composable. Each row renders
  the type (translated by transactionLabel), customer name when known,
  signed delta (+50 pts / +1 ★ / -100 pts), and HH:mm timestamp
  localized to the device timezone via java.time (Android 26+).

Verified by ./gradlew assembleDebug — clean build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:25:10 +02:00
02652ee8c6 feat(android-terminal): Phase D.2 — action sheets (stamp/earn/redeem)
The four right-pane buttons now work end-to-end (online happy path).
Each opens an AlertDialog wired to a ViewModel action; on success the
sheet closes and the customer card refreshes from the server.

- TerminalViewModel: + categories cache load (via CategoryRepository),
  + activeAction / actionInProgress / actionResult on the state. Action
  methods (submitStamp, submitEarnPoints, submitRedeemStamps,
  submitRedeemReward) all funnel through a runAction helper that toggles
  in-progress, calls the LoyaltyApi, refreshes the customer on success,
  surfaces failures inline.
- ActionSheets.kt (new): one ActionSheet entrypoint that dispatches by
  ActionKind. Category multi-select via FlowRow + FilterChip pills.
  EarnPoints accepts "12.50" / "12,50" → cents. Reward picker shows
  available_rewards from the lookup response.
- TerminalScreen: action buttons enabled per state — stamp/earn stay
  active offline (Phase E will queue them), redeem is hard-disabled when
  offline per the plan ("redemption requires an internet connection").

Categories list comes from the cache filled at pairing; fresh refresh
runs in the background so newly-added categories show up next launch
without a manual sync.

Verified by ./gradlew assembleDebug — clean build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:13:31 +02:00
47565419e2 feat(android-terminal): Phase D.1 — terminal lookup + customer card
The terminal screen is now functional for card lookup and customer
display. Phase D.2 will fill in the action sheets (stamp / earn /
redeem) on top of this.

- TerminalViewModel: state machine with program (from cache), customer,
  search/error/online state. onSearchSubmit hits /cards/lookup;
  refreshCurrentCustomer re-fetches after actions land.
- TerminalScreen rewrite: top bar with staff name + online pill + Lock;
  left pane with search field + buttons; right pane shows the empty
  state or a customer panel (name/email/card number, points + stamps
  card, four placeholder action buttons greyed out for D.2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:27:06 +02:00
3bf23c1b23 feat(android-terminal): Phase C — PIN screen with offline bcrypt verify
Some checks failed
CI / ruff (push) Successful in 15s
CI / pytest (push) Failing after 2h27m32s
CI / validate (push) Successful in 33s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
Two-pane landscape: scrollable staff list on the left, PIN dots + numeric
keypad on the right. Footer shows online/offline + pending-sync count.

Going with cached-hashes for offline-capable PIN verify (decision logged
in chat). The threat model already accepts the device — a stolen tablet
holds a 1-year store-scoped JWT, so leaking 4-digit bcrypt hashes is
incremental. Hashes only ever leave the server when the requester is a
paired POS tablet, gated by the new endpoint refusing user JWTs.

Backend:
- GET /api/v1/store/loyalty/pins/for-device — returns PINs WITH pin_hash
  for terminal-device JWTs only; user JWTs receive 403.
- PinForDeviceResponse / PinForDeviceListResponse schemas.
- 2 integration tests in TestPinsForDevice (10/10 pass total).

Android:
- PinForDeviceItem / PinForDeviceListResponse Moshi models.
- LoyaltyApi.listPinsForDevice().
- StaffPinRepository.verifyPin(plain) — at.favre.lib bcrypt verify
  against cached hashes; filters active + unlocked rows in one pass.
- PendingTransactionDao.getPendingCount() switched to Flow<Int> so the
  badge auto-updates when transactions sync.
- PinViewModel state machine — loads pins on init, accumulates digits,
  bcrypt-verifies on length >= 4, fires verified/errorMessage. Combines
  pending-sync count + online state into the same StateFlow.
- PinScreen rewrite: avatar-circle staff list, 6-dot PIN display,
  spinner during verify, error label on wrong PIN, status footer.

Open follow-up (intentional, post-launch): tablet doesn't yet report
failed attempts back to the server's lockout counter. Path is clear —
small POST /pins/{id}/record-failed-attempt endpoint plus a call from
attemptVerify's failure branch.

Verified by ./gradlew assembleDebug — clean build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:58:18 +02:00
a0e3461c48 feat(android-terminal): Phase B — setup screen with QR scanner
Some checks failed
CI / ruff (push) Successful in 16s
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
End-to-end pairing flow:

1. SetupScreen renders a CameraX preview on the left, a manual-entry
   form on the right (dev fallback). Camera permission is requested
   in-place — no accompanist dep.
2. QrScannerView uses ML Kit's barcode scanner (QR format only),
   single-shot fires the decoded JSON to the ViewModel and stops
   analysing.
3. SetupViewModel.pairFromQr decodes via Moshi, persists the pairing
   in DataStore, then verifies by hitting /api/v1/store/loyalty/program
   through the AuthInterceptor (which now sees the new url + token).
   On 200 it warms the staff PIN and category caches and emits Success;
   on failure it rolls back via DeviceConfigRepository.resetDevice() so
   the user is back at a clean Setup with an error.
4. The NavHost watches is_device_set_up and forwards to PIN once Success
   fires. The DataStore key was aligned to "is_device_set_up" so this
   reactive switch keeps working.

Backend: the QR payload generated by POST /merchants/loyalty/devices
now includes store_id and store_name in addition to api_url, store_code
and auth_token, so the tablet doesn't have to resolve them later via a
separate call. Old QRs (which only had three fields) won't decode — the
merchant has to revoke and re-pair, which is the same flow they'd run
anyway after losing a tablet.

Files:
- ui/scanner/QrScannerView.kt    (new) — CameraX + ML Kit composable
- ui/setup/SetupViewModel.kt     (rewrite) — pair flow + state machine
- ui/setup/SetupScreen.kt        (rewrite) — two-pane layout, status overlay
- data/model/ApiModels.kt        — SetupPayload model
- data/repository/DeviceConfigRepository.kt — IS_SET_UP key alignment
- app/modules/loyalty/services/terminal_device_service.py — richer QR payload

Verified by ./gradlew assembleDebug — clean build, all warnings address
in this commit (LocalLifecycleOwner moved to lifecycle.compose, OptIn on
ExperimentalGetImage removed since it's no longer @RequiresOptIn).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:32:25 +02:00
3531ab8405 feat(android-terminal): Phase A — core infrastructure
Foundation work for the Android tablet POS app. Adds the singletons that
every screen needs (config persistence, network awareness, auth) without
touching the existing screen stubs yet — Phases B–F will build on these.

- gradle: bcrypt 0.10.2 pinned (used in Phase C for offline PIN verify)
- LoyaltyApi: + GET /api/v1/store/loyalty/categories endpoint
- ApiModels: + category_ids on StampRequest / PointsEarnRequest,
  + CategoryItem / CategoryListResponse
- DeviceConfigRepository: DataStore wrapper for the paired-tablet state
  (api_url, auth_token, store_id/code/name, cached program/pins/categories
  JSON, per-seller language, resetDevice())
- AuthInterceptor: rewrites every request's host to the paired api_url
  and adds Bearer auth — Retrofit keeps a placeholder baseUrl since the
  real URL only exists post-pair
- NetworkMonitor: Flow<Boolean> isOnline from ConnectivityManager
- StaffPinRepository / CategoryRepository: cache-or-refresh pattern,
  Moshi-serialized to DataStore
- AppModule: wires AuthInterceptor before the logging interceptor
- strings.xml: ~50 strings × 4 locales (en authoritative; fr/de/lb
  translated, mirroring the loyalty backend's i18n approach)

Verified by ./gradlew assembleDebug — clean build, only pre-existing
warnings (Theme statusBarColor deprecation, Moshi-kapt deprecation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:11:15 +02:00
90b5b3d135 fix(loyalty): drop unregistered device-tablet icon, guard QR template against null
Some checks failed
CI / ruff (push) Successful in 14s
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
- The icon registry has no 'device-tablet' (closest available is 'phone',
  which is already used for similar device-mobile contexts). Replace
  uses I added in the merchant menu item and the empty state.

- The pairing-QR modal uses x-show on the QR/payload blocks, but x-show
  only toggles display while Alpine still evaluates child expressions.
  pairingResult is null on first render, so the template threw "can't
  access property 'qr_png_base64'/'setup_payload', pairingResult is
  null" until pairing actually fired. Wrap the block in
  <template x-if="pairingResult"> so the bindings only mount when the
  data exists.

(There is a third 'device-tablet' reference in store/analytics.html
that predates this work — leaving it for a separate cleanup since
it's not on the Android-rollout path.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:31:12 +02:00
cb8e6a0ec3 fix(loyalty): accept store_id in body for merchant PIN create
Some checks failed
CI / ruff (push) Successful in 15s
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
The merchant /pins POST was reading store_id as a query parameter, but
the shared loyalty pins JS factory sends the form (including store_id)
as a JSON body — matching the store-side endpoint, which gets store_id
from the JWT and ignores any body field. Result: a 422 "Field
required" on every PIN create from /merchants/loyalty/pins.

Add PinCreateForMerchant (PinCreate + store_id) and switch the
endpoint to it. Validation that the store belongs to the merchant is
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:25:01 +02:00
573b0ef483 fix(loyalty): wire merchant staff PIN form to team-member autocomplete
Some checks failed
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 / ruff (push) Successful in 16s
CI / pytest (push) Has been cancelled
The shared loyalty pins list factory has an autocomplete-from-team
flow gated behind config.staffApiPrefix (loaded once into a list, then
filtered client-side). The merchant entry in static/merchant/js/loyalty-pins.js
never set staffApiPrefix, so the loadStaffMembers branch never ran and
the "Staff member name" field on /merchants/loyalty/pins fell back to
free text instead of suggesting actual team members.

Wire the merchant config to /merchants/account, and add a flat
GET /merchants/account/team/members alias next to the existing
/merchants/account/team that returns just {members: [...]} — matching
the shape the shared autocomplete factory already expects at
${prefix}/team/members.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:20:07 +02:00
eaffc764ec i18n(loyalty): translate terminal_devices block to FR/DE/LB
Some checks failed
CI / ruff (push) Successful in 15s
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
Replaces the EN placeholders that were seeded with the feature with
proper FR, DE and LB translations. Same scope: terminal_devices.* and
the menu.terminal_devices label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:05:58 +02:00
d99633345f feat(loyalty): attribute transactions to the acting POS tablet
Adds acting_terminal_device_id to loyalty_transactions so the audit
log can distinguish between operations performed via the web terminal
(human user JWT) and operations performed via a paired tablet (device
JWT). The principal-of-record stays the pairing user — existing
reports keep working — and this column adds "which tablet did it"
alongside.

Threaded through every store-API endpoint that creates a transaction
(stamp add/redeem/void, points earn/redeem/void/adjust, enrollment +
welcome bonus, card deactivate/reactivate). The route reads
current_user.terminal_device_id, which the bearer-auth dep populates
when a device JWT is presented. User-token requests leave the column
NULL, as covered by the new test.

Bulk admin operations (GDPR anonymization, bulk deactivate) and Celery
tasks (point expiration) are not threaded — they always come from a
human admin or the scheduler, never a tablet.

- Migration loyalty_011 + LoyaltyTransaction.acting_terminal_device_id
- 9 service signatures gain the optional kwarg
- 8 store-API routes pass it through
- Integration tests: device JWT populates the column, user JWT leaves
  it NULL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:04:56 +02:00
c267452dc6 fix(loyalty): align /locations endpoint shape with template bindings
The shared loyalty list partials (pins, cards, transactions, devices,
admin merchant detail) bind store filter dropdowns to
loc.store_id/loc.store_name, but the /merchants/loyalty/locations and
/admin/loyalty/merchants/{id}/locations endpoints were returning
{id, name, code}. Result: every store-filter dropdown was silently
empty across the loyalty module.

Switch both endpoints to {store_id, store_name, store_code}, matching
the shape used everywhere else (analytics, location stats). Storefront
locations come from a different code path and are unaffected.

Drop the temporary normalizer in the devices Alpine factory now that
the endpoint speaks the right shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:03:33 +02:00
cdacc8bc0d fix(loyalty): make device tokens authenticate via require_module_access too
Some checks failed
CI / ruff (push) Successful in 15s
CI / docs (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
Two follow-ups from the live smoke test:

1. The store router has two auth gates: its own get_current_store_api
   (already taught about device tokens) and router-level
   require_module_access("loyalty", FrontendType.STORE), which goes
   through get_current_store_from_cookie_or_header. That cookie-or-header
   variant didn't know about device tokens, so live curl with a paired
   device JWT was rejected with 401 "Authentication required". Tests
   passed only because dependency overrides bypass the module-access dep.
   Add the same _try_authenticate_terminal_device branch there.

2. Normalize the /merchants/loyalty/locations response in the devices
   Alpine factory: the endpoint returns {id, name, code} but the
   templates bind to loc.store_id/loc.store_name. Map both shapes so
   the pair-tablet store dropdown populates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:39:58 +02:00
6276e9e3ac 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>
2026-05-05 20:18:57 +02:00
c7ab5eb900 fix(makefile): use \alembic upgrade heads\ for multi-branch migrations
The project has multiple migration branch heads (one per module:
softdelete, cms, customers, loyalty, ...) so \`migrate-up\` and
\`db-reset\` need \`heads\` (plural). The \`head\` (singular) form
hit "Multiple head revisions are present" once the first module
branch was created.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:17:44 +02:00
b27d4ba6ff docs: add Android terminal implementation plan
Some checks failed
CI / ruff (push) Successful in 15s
CI / pytest (push) Failing after 2h21m3s
CI / validate (push) Successful in 29s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
Full implementation plan for the RewardFlow Terminal Android app:
4 screens (Setup, PIN, Terminal, Enrollment), 6 phases (~5 days),
ASCII wireframes, data layer design, offline queue strategy,
multi-language support, and API model changes needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 22:52:00 +02:00
6da48f88c1 fix(dev_tools): display JSON columns with brackets in SQL query tool
Some checks failed
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / ruff (push) Successful in 14s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
JSON arrays like [11, 4, 10] were rendered as "11,4,10" because
String() on a JS array drops brackets. Now uses JSON.stringify()
for object/array values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 22:43:04 +02:00
516141b41d fix(loyalty): add Category column to card detail transaction history
Some checks failed
CI / ruff (push) Successful in 18s
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
Added missing category column between Points and Location. Shows
translated category names (comma-separated for multi-select), or
"-" for transactions without categories.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 22:35:47 +02:00
4f70290af5 fix(loyalty): translate transaction types + notes on card detail page
Some checks failed
CI / ruff (push) Successful in 17s
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
Card detail transaction history now shows translated transaction type
labels and system-generated notes. Uses server-rendered labels object
(same pattern as terminal) to avoid async i18n flicker.

- Transaction types: server-rendered txLabels lookup (all 11 types)
- Notes: txNotes lookup maps English DB strings to translated text
  (e.g., "Welcome bonus on enrollment" → "Bonus de bienvenue...")
- Added welcome_bonus_note key to all 4 locales

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 22:23:02 +02:00
3fa159ff2a fix(loyalty): sanitize earn amount input on every keystroke
Some checks failed
CI / ruff (push) Successful in 15s
CI / pytest (push) Failing after 2h19m25s
CI / validate (push) Successful in 29s
CI / dependency-scanning (push) Successful in 33s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
Switch from type=number (leaks special chars via compose keys) to
type=text with @input sanitizer that strips non-digit/non-dot
characters and prevents multiple dots. Handles accented characters,
paste, and compose key sequences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 17:58:42 +02:00
143248ff0f fix(loyalty): restrict earn points input to digits and decimal only
Some checks failed
CI / ruff (push) Successful in 16s
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
Add keypress filter to block non-numeric characters (e, +, -) and
inputmode="decimal" for mobile keyboard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 16:11:10 +02:00
56c94ac2f4 fix(loyalty): use confirm modal for category deletion
Some checks failed
CI / ruff (push) Successful in 17s
CI / pytest (push) Failing after 2h21m18s
CI / validate (push) Successful in 29s
CI / dependency-scanning (push) Successful in 33s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
Replace browser confirm() dialog with the shared confirm_modal
macro for category deletion. Matches the existing program delete
pattern. Shows warning about impact on existing transactions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 14:40:29 +02:00
255ac6525e fix(loyalty): translate category names in transaction history
Some checks failed
CI / ruff (push) Successful in 15s
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
Category names in transaction tables now resolve to the current
page language instead of always showing English. Updated:
- category_service.validate_category_for_store: accepts lang param,
  uses get_translated_name()
- Store transactions list route: passes request.state.language
- Card detail transactions route: passes request.state.language
- card_service.get_customer_transactions_with_store_names: accepts
  lang param for storefront route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 14:17:43 +02:00
10e37e749b fix(loyalty): show translated category names on terminal
Some checks failed
CI / ruff (push) Successful in 14s
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 started running
Category pills in the PIN modal now display the translated name
based on the page's current_language, falling back to the default
name (English) if no translation exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 13:15:47 +02:00
f23990a4d9 fix(loyalty): add red star (*) to mandatory category fields
Some checks failed
CI / ruff (push) Successful in 17s
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 12:47:12 +02:00
62b83b46a4 feat(loyalty): category view mode + mandatory translations
Some checks failed
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / ruff (push) Successful in 14s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
- Add eye icon to expand read-only view of all translations
- View panel shows EN/FR/DE/LB values with "Edit" button to switch
  to edit mode
- All 4 language fields (EN/FR/DE/LB) now mandatory — Save button
  disabled until all are filled (both add and edit forms)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 12:34:37 +02:00
f8b2429533 fix(loyalty): rename category label "Name (default)" to "English (EN)"
Some checks failed
CI / validate (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / ruff (push) Successful in 18s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 12:28:00 +02:00
3883927be0 fix(loyalty): disable confirm button until category selected
Some checks failed
CI / ruff (push) Successful in 17s
CI / pytest (push) Failing after 2h35m37s
CI / deploy (push) Has been skipped
CI / validate (push) Successful in 41s
CI / dependency-scanning (push) Successful in 44s
CI / docs (push) Has been skipped
PIN modal confirm button stays disabled when categories exist and
the action is stamp/earn but no category is selected yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 22:33:26 +02:00
39e02f0d9b fix(loyalty): terminal icons, server-side i18n, category in transactions
Some checks failed
CI / ruff (push) Successful in 22s
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
- Fix icons: plus-circle → plus, backspace → arrow-left
- Convert terminal $t() calls to server-side _() for card_label,
  stamps_until_reward, reward_label, not_enough_stamps
- Inject transaction type labels as server-rendered window._txLabels
  object (eliminates all async i18n warnings on terminal page)
- Resolve category_names in store transactions list endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 22:20:52 +02:00
29593f4c61 feat(loyalty): multi-select categories on transactions
Some checks failed
CI / ruff (push) Successful in 24s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / pytest (push) Has been cancelled
Switch from single category_id to category_ids JSON array on
transactions. Sellers can now select multiple categories (e.g.,
Men + Accessories) when entering stamp/points transactions.

- Migration loyalty_009: drop category_id FK, add category_ids JSON
- Schemas: category_id → category_ids (list[int] | None)
- Services: stamp_service + points_service accept category_ids
- Terminal UI: pills are now multi-select (toggle on/off)
- Transaction response: category_names (list[str]) resolved from IDs
- Recent transactions table: new Category column showing comma-
  separated names

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 21:36:49 +02:00
220f7e3a08 fix(loyalty): replace $t() with server-side _() in program-view
Some checks failed
CI / ruff (push) Successful in 16s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / pytest (push) Has been cancelled
Convert 6 client-side $t() calls to server-rendered _() in the
shared program-view template to eliminate i18n timing warnings.
Uses .replace() for dynamic parameters (count, days).

Fixes warnings: loyalty.common.active, inactive, none, never,
loyalty.shared.program_view.x_points, x_days_inactivity, x_minutes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 19:58:03 +02:00
258aa6a34b fix(loyalty): missing i18n keys, wrong icon names in admin
Some checks failed
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
CI / ruff (push) Successful in 22s
CI / validate (push) Has been cancelled
- Add missing common keys: add, activate, copy, deactivate
- Fix icon: building-office → office-building (2 templates)
- Fix icon: pause → ban (pause not in icon set, ban used for deactivate)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 19:52:39 +02:00
51bcc9f874 feat(loyalty): inline edit for transaction categories in admin
Some checks failed
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
CI / ruff (push) Successful in 21s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
Category list now has a pencil edit button that expands inline with
name + FR/DE/LB translation fields. Save updates via PATCH API.
View mode shows translations summary next to the name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 19:27:55 +02:00
eafa086c73 feat(loyalty): translatable categories + mandatory on earn points
Some checks failed
CI / pytest (push) Failing after 2h47m45s
CI / validate (push) Successful in 39s
CI / dependency-scanning (push) Successful in 47s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 21s
- Add name_translations JSON column to StoreTransactionCategory
  (migration loyalty_008). Stores {"en": "Men", "fr": "Hommes", ...}.
  Model has get_translated_name(lang) helper.
- Admin CRUD form now has FR/DE/LB translation inputs alongside the
  default name.
- Points earn: category_id is now mandatory when the store has
  active categories configured. Returns CATEGORY_REQUIRED error.
- Stamps: category remains optional (quick tap workflow).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 14:12:55 +02:00
ab2daf99bd feat(loyalty): transaction categories — admin UI + web terminal
Some checks failed
CI / ruff (push) Successful in 27s
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 started running
Admin merchant detail page:
- New "Transaction Categories" section with store selector
- Inline add form, activate/deactivate toggle, delete button
- Categories CRUD via /admin/loyalty/stores/{id}/categories API

Web terminal:
- Loads categories on init via /store/loyalty/categories
- Category pill selector shown in PIN modal before stamp/earn actions
- Selected category_id passed to stamp and points API calls
- Categories are optional (selector hidden when none configured)

4 new i18n keys (EN).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 12:28:55 +02:00
1cf9fea40a feat(loyalty): transaction categories (what was sold)
Some checks failed
CI / ruff (push) Successful in 20s
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
Merchants can configure per-store product categories (e.g., Men,
Women, Accessories, Kids) that sellers select when entering loyalty
transactions. Enables per-category sales analytics.

Backend:
- New model: StoreTransactionCategory (store-scoped, max 10 per store)
- Migration loyalty_007: creates table + adds category_id FK on
  loyalty_transactions
- New category_service.py with CRUD + validation
- New schemas/category.py (Create, Update, Response, ListResponse)
- Admin CRUD: GET/POST/PATCH/DELETE /admin/loyalty/stores/{id}/categories
- Store CRUD: GET/POST/PATCH/DELETE /store/loyalty/categories
- Stamp/Points request schemas accept optional category_id
- Stamp/Points services pass category_id to transaction creation
- TransactionResponse includes category_id + category_name

342 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 12:23:17 +02:00
cd4f83f2cb docs: add proposal for transaction categories (what was sold)
Some checks failed
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 20s
CI / pytest (push) Failing after 2h34m11s
CI / validate (push) Successful in 35s
CI / dependency-scanning (push) Successful in 44s
CI / docs (push) Has been skipped
Client requirement: sellers must select a product category (e.g.,
Men, Women, Accessories, Kids) when entering loyalty transactions.
Categories are per-store, configured via admin/merchant CRUD.

Proposal covers: data model (StoreTransactionCategory + FK on
transactions), CRUD API for admin + store, web terminal UI, Android
terminal integration, and analytics extension path.

Priority: urgent for production launch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 00:29:41 +02:00
457350908a fix(android): resolve build errors in terminal scaffold
Some checks failed
CI / ruff (push) Successful in 20s
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
- Fix settings.gradle.kts: dependencyResolution → dependencyResolutionManagement
- Fix Hilt version: 2.54.1 (non-existent) → 2.51.1
- Fix LoyaltyApi.kt: remove decorative unicode comments causing
  "unclosed comment" errors, fix /api/v1/store/loyalty/* pattern
  in KDoc that Kotlin parsed as block comment start
- Add placeholder launcher icons (purple square, all densities)

App now builds and runs on Pixel Tablet emulator: Setup → PIN → Terminal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 00:11:30 +02:00
e759282116 refactor: rename apps/ to clients/ + update architecture docs
Some checks failed
CI / ruff (push) Successful in 20s
CI / pytest (push) Failing after 2h37m33s
CI / validate (push) Successful in 41s
CI / dependency-scanning (push) Successful in 42s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
Rename apps/ → clients/ for clarity:
- app/ (singular) = Python backend (FastAPI, server-rendered web UI)
- clients/ (plural) = standalone client applications (API consumers)

The web storefront/store/admin stays in app/ because it's server-
rendered Jinja2, not a standalone frontend. clients/ is for native
apps that connect to the API externally.

Updated:
- docs/architecture/overview.md — added clients/ to project structure
- clients/terminal-android/SETUP.md — updated path references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 18:09:24 +02:00
1df1b2bfca feat: scaffold Android terminal POS app (RewardFlow Terminal)
Some checks failed
CI / ruff (push) Successful in 25s
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 started running
Native Android tablet app for loyalty POS terminal. Replaces web
terminal for merchants who need device lockdown, camera QR scanning,
and offline operation.

Project at apps/terminal-android/ — Kotlin, Jetpack Compose, calls
the same /api/v1/store/loyalty/* API (no backend changes).

Scaffold includes:
- Gradle build (Kotlin DSL) with version catalog (libs.versions.toml)
- Hilt DI, Retrofit + Moshi networking, Room offline DB
- CameraX + ML Kit barcode scanning dependencies
- DataStore for device config persistence
- WorkManager for background sync
- Three-screen navigation: Setup → PIN → Terminal
- Stub screens with layout structure (ready to implement)
- API models matching all loyalty store endpoints
- PendingTransaction entity + DAO for offline queue
- RewardFlow brand theme (purple, light/dark)
- Landscape-only, fullscreen, Lock Task Mode ready
- SETUP.md with Android Studio installation guide
- .gitignore for Android build artifacts

Tech decisions:
- Min SDK 26 (Android 8.0, 95%+ tablet coverage)
- Firebase App Distribution for v1, Play Store later
- Staff PIN auth (no username/password on POS)
- One-time device setup via QR code from web settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 18:02:42 +02:00
51a2114e02 refactor(cms): migrate store theme UI from tenancy to CMS module
Move store theme admin pages, templates, and JS from tenancy module
to CMS module where the data layer (model, service, API, schemas)
already lives. Eliminates split ownership.

Moved:
- Route handlers: GET /store-themes, GET /stores/{code}/theme
- Templates: store-theme.html, store-themes.html
- JS: store-theme.js, store-themes.js
- Updated static references: tenancy_static → cms_static

Deleted old tenancy files (no remaining references).
Menu item in CMS definition already pointed to correct route.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 10:30:09 +02:00
21e4ac5124 docs(loyalty): update launch plan — Google Wallet already deployed
Some checks failed
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 25s
CI / pytest (push) Failing after 2h55m43s
CI / validate (push) Successful in 52s
CI / dependency-scanning (push) Successful in 56s
CI / docs (push) Has been skipped
Clarify Step 2: Google Wallet service account, Docker mount, and env
vars are already deployed on Hetzner (per Step 25 of server setup doc).
Only verification needed at deploy time.

Add Step 9 (post-launch): Google Wallet production access request.
Passes work in demo mode for test accounts at launch. Production
approval is a Google console step (1-3 business days, no code changes).
Google reviews the Issuer (platform), not individual merchants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:22 +02:00
3ade1b9354 docs(loyalty): rewrite launch plan with step-by-step pre-launch checklist
Some checks failed
CI / pytest (push) Failing after 2h31m6s
CI / validate (push) Successful in 29s
CI / dependency-scanning (push) Successful in 33s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 13s
Replace the old effort/critical-path sections with current status:
all dev phases 0-8 marked DONE with dates. Added a clear 8-step
pre-launch checklist (seed templates, deploy wallet certs, migrations,
translations, permissions, E2E testing, device test, go live) and a
post-launch roadmap table (Apple Wallet, marketing module, coverage,
trash UI, bulk PINs, cross-location enforcement).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:34:57 +02:00
b5bb9415f6 feat(cms): Phase A — page type selector, translation UI, SEO cleanup
Some checks failed
CI / ruff (push) Successful in 16s
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
Content page editor improvements:
- Page type selector: Content Page / Landing Page dropdown (sets template)
- Title language tabs: translate page titles per language (same pattern as sections)
- Content language tabs: translate page content per language
- Meta description language tabs: translatable SEO descriptions
- Template-driven section palette: template defines which sections are available
  (store landing pages hide Pricing, platform homepages show all)
- Hide content editor when Landing Page selected, hide sections when Content Page

Schema changes (migration cms_003):
- Add meta_description_translations column (JSON) to content_pages
- Drop meta_keywords column (obsolete, ignored by all search engines since 2009)
- Remove meta keywords tag from storefront and platform base templates

API + service updates:
- title_translations, content_translations, meta_description_translations
  added to create/update schemas, route handlers, and service methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:30:55 +02:00
bb3d6f0012 fix(loyalty): card detail — enrolled store name + copy buttons
Some checks failed
CI / pytest (push) Failing after 2h22m22s
CI / validate (push) Successful in 27s
CI / dependency-scanning (push) Successful in 31s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 12s
- Fix "Enrolled at: Unknown" by resolving enrolled_at_store_name from
  the store service and adding it to CardDetailResponse schema.
- Add clipboard-copy buttons next to card number, customer name,
  email, and phone fields using the shared Utils.copyToClipboard()
  utility with toast feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:31:53 +02:00
c92fe1261b fix(loyalty): use full pagination macro on card detail (match cards list)
Some checks failed
CI / ruff (push) Successful in 14s
CI / pytest (push) Has started running
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
Switch from pagination_simple to pagination — the same macro used on
the cards list page, with page number buttons and "Showing X-Y of Z".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:25:29 +02:00