Menu tests (6): Tests expected merchant menu item id "loyalty-program"
but the actual definition in loyalty/definition.py uses "program".
Updated assertions to match the actual menu item IDs.
Wallet test (1): test_enrollment_succeeds_without_wallet_config didn't
mock the Google Wallet config, so is_configured returned True when
GOOGLE_ISSUER_ID is set in .env. Added @patch to mock config as
unconfigured.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Delete program tests now verify soft-delete (deleted_at set, record
hidden from normal queries) instead of expecting hard deletion.
Uses db.query() instead of db.get() since the soft-delete filter
only applies to ORM queries, not identity map lookups.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The storefront login template uses $icon() in Alpine expressions but
didn't load icons.js, causing "$icon is not defined" errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Login pages don't extend base templates, so they need the
FRONTEND_TYPE injection directly. Fixes "unknown" frontend
in dev toolbar and log prefixes on login pages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server now injects window.FRONTEND_TYPE in all base templates via
get_context_for_frontend(). Both log-config.js and dev-toolbar.js read
this instead of guessing from URL paths, fixing:
- UNKNOWN prefix on merchant pages
- Incorrect detection on custom domains/subdomains in prod
Also adds frontend_type to login page contexts (admin, merchant, store).
Renames all [SHOP] logger prefixes to [STOREFRONT] across 7 files
(storefront-layout.js + 6 storefront templates).
Adds 'merchant' and 'storefront' to log-config.js frontend detection,
log levels, and logger selection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Store team page:
- Fix undefined user_id (API returns `id`, JS used `user_id`)
- Fix wrong URL path in updateMember (remove redundant storeCode)
- Fix update_member_role route passing wrong kwarg (new_role_id → new_role_name)
- Add update_member() service method for role_id + is_active updates
- Add :selected binding for role pre-selection in edit modal
Merchant team page:
- Add missing db.commit() on invite, update, and remove endpoints
- Fix forward-reference string type annotation on MerchantTeamInvite
- Add :selected binding for role pre-selection in edit modal
Shared fixes:
- Replace removed subscription_service.check_team_limit with usage_service
- Replace removed subscription_service.get_current_tier in email service
- Fix email config bool settings crashing on .lower() (value_type=boolean)
Tests: 15 new integration tests for store team member API endpoints.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The merchant team page was read-only. Now merchant owners can invite,
edit roles, and remove team members across all their stores from a
single hub view.
Architecture: No new models — delegates to existing store_team_service.
Members are deduplicated across stores with per-store role badges.
New:
- 5 API endpoints: GET team (member-centric), GET store roles, POST
invite (multi-store), PUT update role, DELETE remove member
- merchant-team.js Alpine component with invite/edit/remove modals
- Full CRUD template with stats cards, store filter, member table
- 7 Pydantic schemas for merchant team request/response
- 2 service methods: validate_store_ownership, get_merchant_team_members
- 25 new i18n keys across 4 tenancy locales + 1 core common key
Tests: 434 tenancy tests passing, arch-check green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
merchant_store_service referenced merchant.business_name and
merchant.brand_name which don't exist on the Merchant model.
The field is simply merchant.name.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JS was calling /merchants/tenancy/account/team but the endpoint is
mounted at /merchants/account/team (no tenancy prefix in the path).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Template used {% block scripts %} but merchant base.html defines
{% block extra_scripts %}. The merchantTeam() function never rendered,
causing "merchantTeam is not defined" errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merchant team page called .toLowerCase() on a Jinja2 string (Python),
causing UndefinedError. Fixed to .lower().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The first-letter avatar adds visual noise on a dense transactions table
without meaningful value. Simplified to plain text customer name.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TransactionResponse doesn't include card_number, so the template was
showing '-' under every customer name. Removed the nonexistent field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When editing a PIN, only the PIN code should be changeable. Staff name,
staff ID, and store are now displayed as read-only fields. This prevents
accidentally reassigning a PIN to a different staff member.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PIN create/edit modals were showing "Customer not found" (terminal
message) when no staff members matched. Now shows "No staff members
found" with a proper locale key.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace custom inline autocomplete HTML in both create and edit PIN
modals with the shared search_autocomplete macro from inputs.html.
Refactored JS to use staffSearchResults array populated by searchStaff()
(client-side filter) matching the macro's conventions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
list_cards() was calling Python .replace() on a SQLAlchemy column
object instead of SQL func.replace(), causing AttributeError when
searching cards by card number.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace custom inline autocomplete HTML with the shared
search_autocomplete macro from inputs.html. Same behavior (debounced
search, dropdown with name + email, loading/no-results states) but
using the established reusable component.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Terminal search now shows live autocomplete suggestions as the user
types (debounced 300ms, min 2 chars). Dropdown shows matching customers
with avatar, name, email, card number, and points balance. Uses the
existing GET /store/loyalty/cards?search= endpoint (limit=5).
Selecting a result loads the full card details via the lookup endpoint.
Enter key still works for exact lookup. No new dependencies — uses
native Alpine.js dropdown, no Tom Select needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The autocomplete dropdown appeared immediately when the name field
gained focus (even when empty). Now only shows when there's text to
filter by.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a staff member was selected and then the name field was edited or
cleared, the staff_id (email) remained set. Now tracks the selected
member name and clears staff_id when the search text diverges.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pins list template included the pagination macro but the JS has no
pagination state (PINs are few and don't need pagination). The empty
macro rendered a broken pagination bar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Template called openEditPin() and confirmDeletePin() but JS methods
are openEditModal() and openDeleteModal(). Buttons were silently
failing on click.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When creating or editing a staff PIN in the store context, the name
field now shows an autocomplete dropdown with the store's team members
(loaded from GET /store/team/members). Selecting a member auto-fills
name and staff_id (email). The dropdown filters as you type.
Only active in store context (where staffApiPrefix is configured).
Merchant and admin PIN views are unaffected — merchant has no
staffApiPrefix, admin is read-only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The terminal's selectedCard comes from CardLookupResponse which uses
card_id field, but the JS was referencing selectedCard.id (undefined).
This caused all terminal transactions to fail with "LoyaltyCard with
identifier 'unknown' not found" instead of processing the transaction
or showing proper PIN validation errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The store and merchant init-alpine.js derive currentPage from the URL's
last segment (e.g., /loyalty/program -> 'program'). Loyalty menu items
used prefixed IDs like 'loyalty-program' which never matched, so sidebar
items never highlighted.
Fixed by renaming all store/merchant menu item IDs and JS currentPage
values to match URL segments: program, cards, analytics, transactions,
pins, settings — consistent with how every other module works.
Also reverted the init-alpine.js guard that broke storeCode extraction,
and added missing loyalty.common.contact_admin_setup translation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The store init-alpine.js init() was unconditionally setting currentPage
from the URL path segment, overwriting the value set by child components
like storeLoyaltyProgram (currentPage: 'loyalty-program'). This caused
sidebar menu items to not highlight on pages where the URL segment
doesn't match the menu item ID (e.g., /loyalty/program vs loyalty-program).
Now only sets currentPage from URL if the child hasn't already set it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Store templates (cards, card-detail, terminal) reference col_member,
col_date etc. but locale files had table_member, table_date. Renamed
16 keys across all 4 locale files (en/fr/de/lb) to match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The tenancy merchant detail page now reads an optional ?back= query
parameter to determine the back button destination. Falls back to
/admin/merchants when no param is present (default behavior preserved).
The loyalty merchant detail "View Merchant" link now passes
?back=/admin/loyalty/merchants/{id} so clicking back from the tenancy
page returns to the loyalty context instead of the merchants list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "View Merchant" quick action on the loyalty merchant detail hub
links to the tenancy merchant page, which has its own back button going
to /admin/merchants. Opening in a new tab prevents losing the loyalty
context. Added external link icon as visual indicator.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Load merchant name in page route handlers and pass to template context.
Headers now render as "Cards: Fashion Group S.A." using server-side
Jinja2 variables instead of relying on JS program.merchant_name which
was not in the ProgramResponse schema.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch admin sub-pages (cards, pins, transactions) from page_header_flex
to detail_page_header with merchant name context, matching the settings
page pattern. Headers now show "MerchantName — Cards" with back button
to merchant detail hub.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The shared JS modules (cards-list, pins-list, transactions-list) all
call {apiPrefix}/program to load the program before fetching data. For
admin on-behalf pages, this resolved to GET /admin/loyalty/merchants/
{id}/program which only had a POST endpoint, causing 405 Method Not
Allowed errors on all admin on-behalf pages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix template references to match existing locale key names (11 renames
in pins-list.html and settings.html) and add 29 missing keys to all 4
locale files (en/fr/de/lb). All 299 template keys now resolve correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align loyalty pages across admin, merchant, and store personas so each
sees the same page set scoped to their access level. Admin acts as a
superset of merchant with "on behalf" capabilities.
New pages:
- Store: Staff PINs management (CRUD)
- Merchant: Cards, Card Detail, Transactions, Staff PINs (CRUD), Settings (read-only)
- Admin: Merchant Cards, Card Detail, Transactions, PINs (read-only)
Architecture:
- 4 shared Jinja2 partials (cards-list, card-detail, transactions, pins)
- 4 shared JS factory modules parameterized by apiPrefix/scope
- Persona templates are thin wrappers including shared partials
- PinDetailResponse schema for cross-store PIN listings
API: 17 new endpoints (11 merchant, 6 admin on-behalf)
Tests: 38 new integration tests, arch-check green
i18n: ~130 new keys across en/fr/de/lb
Docs: pages-and-navigation.md with full page matrix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add wallet diagnostics page at /admin/loyalty/wallet-debug (super admin only)
with explorer-sidebar pattern: config validation, class status, card inspector,
save URL tester, recent enrollments, and Apple Wallet status panels
- Fix Google Wallet fat JWT: include both loyaltyClasses and loyaltyObjects in
payload, use UNDER_REVIEW instead of DRAFT for class reviewStatus
- Fix StorefrontProgramResponse schema: accept google_class_id values while
keeping exclude=True (was rejecting non-None values)
- Standardize all module configs to read from .env file directly
(env_file=".env", extra="ignore") matching core Settings pattern
- Add MOD-026 architecture rule enforcing env_file in module configs
- Add SVC-005 noqa support in architecture validator
- Add test files for dev_tools domain_health and isolation_audit services
- Add google_wallet_status.py script for querying Google Wallet API
- Use table_wrapper macro in wallet-debug.html (FE-005 compliance)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Security:
- Fix TOCTOU race conditions: move balance/limit checks after row lock in redeem_points, add_stamp, redeem_stamps
- Add PIN ownership verification to update/delete/unlock store routes
- Gate adjust_points endpoint to merchant_owner role only
Data integrity:
- Track total_points_voided in void_points
- Add order_reference idempotency guard in earn_points
Correctness:
- Fix LoyaltyProgramAlreadyExistsException to use merchant_id parameter
- Add StorefrontProgramResponse excluding wallet IDs from public API
- Add bounds (±100000) to PointsAdjustRequest.points_delta
Audit & config:
- Add CARD_REACTIVATED transaction type with audit record
- Improve admin audit logging with actor identity and old values
- Use merchant-specific PIN lockout settings with global fallback
- Guard MerchantLoyaltySettings creation with get_or_create pattern
Tests: 27 new tests (265 total) covering all 12 items — unit and integration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix rate limiter to extract real client IP and handle sync/async endpoints
- Rate-limit public enrollment (10/min) and program info (30/min) endpoints
- Add 409 Conflict to non-retryable status codes in retry decorator
- Cache private key in get_save_url() to avoid re-reading JSON per call
- Make update_class() return bool success status with error-level logging
- Move Google Wallet config from core to loyalty module config
- Document time.sleep() safety in retry decorator (threadpool execution)
- Add per-card retry (1 retry, 2s delay) to wallet sync task
- Add logo URL reachability check (HEAD request) to validate_config()
- Add 26 comprehensive unit tests for GoogleWalletService
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix IPv6 host parsing with _strip_port() utility
- Remove dangerous StorePlatform→Store.subdomain silent fallback
- Close storefront gate bypass when frontend_type is None
- Add custom subdomain management UI and API for stores
- Add domain health diagnostic tool
- Convert db.add() in loops to db.add_all() (24 PERF-006 fixes)
- Add tests for all new functionality (18 subdomain service tests)
- Add .github templates for validator compliance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new "Tenant Isolation" diagnostic tool that scans all stores and
reports where configuration values come from — flagging silent inheritance,
missing data, and potential data commingling. Also fix merchant dashboard
and onboarding integration tests that were missing require_platform
dependency override.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs causing "Program Not Available" on storefront enrollment:
1. extract_store_from_referer() was not platform-aware — used
settings.main_domain (wizard.lu) instead of platform.domain
(rewardflow.lu) for subdomain detection, and restricted path-based
extraction to localhost only. Now mirrors the platform-aware logic
from _detect_store_from_host_and_path(): checks platform.domain for
subdomain detection (fashionhub.rewardflow.lu → fashionhub) and
allows path-based extraction on platform domains
(rewardflow.lu/storefront/FASHIONHUB/... → FASHIONHUB).
2. Storefront JS scripts (enroll, dashboard, history) were missing
defer attribute, causing them to execute before log-config.js and
crash on window.LogConfig access. Also fix quote escaping in
server-side rendered x-text expressions for French translations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Convert storefront enrollment $t() calls to server-side _() to silence
dev-toolbar warnings (welcome bonus + join button)
- Fix store base template I18n.init() to use current_language (from middleware)
instead of dashboard_language (hardcoded store config) so language changes
take effect immediately
- Switch admin loyalty routes to use get_admin_context() for proper i18n support
- Switch store loyalty routes to use core get_store_context() from page_context
- Pass program object to storefront enrollment context for server-side rendering
- Add LANG-011 architecture rule: enforce $t()/_() over I18n.t() in templates
- Fix duplicate file_pattern key in LANG-004 rule (YAML validation error)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removed the duplicated store detection logic in the debug trace endpoint
and calls StoreContextManager._detect_store_from_host_and_path() directly,
which also picks up the platform domain guards.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The setting `settings.platform_domain` (the global/main domain like "wizard.lu")
was easily confused with `platform.domain` (per-platform domain like "rewardflow.lu").
Renamed to `settings.main_domain` / `MAIN_DOMAIN` env var across the entire codebase.
Also updated docs to reflect the refactored store detection logic with
`is_platform_domain` / `is_subdomain_of_platform` guards.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add pessimistic locking (SELECT FOR UPDATE) on card write operations
to prevent race conditions in stamp_service and points_service
- Replace 16 console.log/error/warn calls with LogConfig.createLogger()
in 3 storefront JS files (dashboard, history, enroll)
- Delete all stale lu.json locale files across 8 modules (lb is the
correct ISO 639-1 code for Luxembourgish)
- Update architecture rules and docs to reference lb.json not lu.json
- Add production-readiness.md report for loyalty module
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
StoreContextMiddleware was treating platform domains (e.g. rewardflow.lu)
as custom store domains, causing store lookup to fail before reaching
path-based detection (/storefront/FASHIONHUB/...). Now skips custom
domain detection when the host matches the platform's own domain.
Also fixes menu tests to use loyalty-program instead of loyalty-overview,
and adds LOYALTY_DEFAULT_LOGO_URL and LOYALTY_GOOGLE_WALLET_ORIGINS to
Hetzner deployment docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded English strings across all 22 templates, 10 JS files,
and 4 locale files (en/fr/de/lb) with ~300 translation keys per language.
Uses server-side _() for Jinja2 templates and I18n.t() for JS toast
messages and dynamic Alpine.js expressions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>