From c267452dc6350444330df6f84ca24a1bbffcabc6 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Tue, 5 May 2026 21:03:33 +0200 Subject: [PATCH] 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) --- app/modules/loyalty/routes/api/admin.py | 12 ++++++++---- app/modules/loyalty/routes/api/merchant.py | 12 ++++++++++-- .../loyalty/static/shared/js/loyalty-devices-list.js | 12 +++--------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/app/modules/loyalty/routes/api/admin.py b/app/modules/loyalty/routes/api/admin.py index e38ce7cb..6dd948d9 100644 --- a/app/modules/loyalty/routes/api/admin.py +++ b/app/modules/loyalty/routes/api/admin.py @@ -482,13 +482,17 @@ def list_merchant_locations( current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): - """Get active store locations for a merchant.""" + """Get active store locations for a merchant. + + Shape matches what the shared loyalty list partials bind to + (`loc.store_id` / `loc.store_name` / `loc.store_code`). + """ locations = program_service.get_merchant_locations(db, merchant_id) return [ { - "id": store.id, - "name": store.name, - "code": store.store_code, + "store_id": store.id, + "store_name": store.name, + "store_code": store.store_code, } for store in locations ] diff --git a/app/modules/loyalty/routes/api/merchant.py b/app/modules/loyalty/routes/api/merchant.py index e479bd2a..87bdddb0 100644 --- a/app/modules/loyalty/routes/api/merchant.py +++ b/app/modules/loyalty/routes/api/merchant.py @@ -530,9 +530,17 @@ def list_locations( merchant: Merchant = Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): - """List merchant stores (for filter dropdowns).""" + """List merchant stores (for filter dropdowns). + + Shape matches what the shared loyalty list partials bind to + (`loc.store_id` / `loc.store_name` / `loc.store_code`). + """ locations = program_service.get_merchant_locations(db, merchant.id) return [ - {"id": loc.id, "name": loc.name, "code": loc.store_code} + { + "store_id": loc.id, + "store_name": loc.name, + "store_code": loc.store_code, + } for loc in locations ] diff --git a/app/modules/loyalty/static/shared/js/loyalty-devices-list.js b/app/modules/loyalty/static/shared/js/loyalty-devices-list.js index 8de43ea8..2c331ba0 100644 --- a/app/modules/loyalty/static/shared/js/loyalty-devices-list.js +++ b/app/modules/loyalty/static/shared/js/loyalty-devices-list.js @@ -95,15 +95,9 @@ function loyaltyDevicesList(config) { async loadLocations() { try { const response = await apiClient.get(locationsPrefix + '/locations'); - if (!response) return; - const raw = Array.isArray(response) ? response : (response.locations || []); - // Endpoint returns {id, name, code}; templates bind to store_id/store_name. - // Normalize so callers don't have to care about either shape. - this.locations = raw.map(loc => ({ - store_id: loc.store_id ?? loc.id, - store_name: loc.store_name ?? loc.name, - store_code: loc.store_code ?? loc.code, - })); + if (response) { + this.locations = Array.isArray(response) ? response : (response.locations || []); + } } catch (error) { loyaltyDevicesListLog.warn('Failed to load locations:', error.message); }