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>
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>
- 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>
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>
Replace custom pagination with the shared pagination_simple macro
to match the cards list page pattern. Always shows "Showing X-Y of Z"
with Previous/Next — no longer hidden when only 1 page. Uses standard
Alpine.js pagination interface (pagination.page, totalPages, startIndex,
endIndex, pageNumbers, previousPage, nextPage, goToPage).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The store card detail page now shows paginated transaction history
instead of a flat list of 50. Uses PlatformSettings.getRowsPerPage()
for the page size (default 20), with Previous/Next navigation and
"Page X of Y" indicator using server-rendered i18n.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The terminal redeem failed with "card not found: unknown" because
CardLookupResponse used card_id while CardDetailResponse (from
refreshCard) used id. After refresh, selectedCard.card_id was
undefined.
Fix: standardize on 'id' everywhere (the universal convention):
- CardLookupResponse: card_id → id
- _build_card_lookup_response: card_id= → id=
- loyalty-terminal.js: selectedCard.card_id → selectedCard.id (5 refs)
- Removed the card_id/model_validator approach as unnecessary
Also fixes Chart.js recursion error on analytics page (inline CDN
script instead of optional-libs.html include which caused infinite
template recursion in test context).
342 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire the Phase 7 analytics API endpoints into the store analytics
page with interactive visualizations:
- Revenue chart (Chart.js bar+line combo): monthly points earned as
bars + active customers as line overlay with dual Y axes.
- At-risk members panel: ranked list of churning cards showing
customer name and days inactive, with count badge.
- Cohort retention table: enrollment month rows × M0-M5 retention
columns with color-coded percentage cells (green >60%, yellow
>30%, red <30%).
Chart.js loaded on-demand via existing CDN loader with local fallback.
Data fetched in parallel via Promise.all for the 3 analytics endpoints.
All sections gracefully degrade to "not enough data" message when empty.
7 new i18n keys (EN only — FR/DE/LB translations to be added).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New analytics_service.py with three analytics features:
- Cohort retention: groups cards by enrollment month, tracks % with
any transaction in each subsequent month. Returns matrix suitable
for Chart.js heatmap. GET /analytics/cohorts?months_back=6
- Churn detection: flags cards as "at risk" when inactive > 2x their
average inter-transaction interval (default 60d for new cards).
Returns ranked list. GET /analytics/churn?limit=50
- Revenue attribution: monthly and per-store aggregation of point-
earning transactions. GET /analytics/revenue?months_back=6
Endpoints added to both admin API (/admin/loyalty/merchants/{id}/
analytics/*) and store API (/store/loyalty/analytics/*) so merchants
can see their own analytics.
342 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admin operations for production management:
- GDPR anonymization: DELETE /admin/loyalty/cards/customer/{id}
Nulls customer_id, deactivates cards, scrubs PII from transaction
notes. Keeps aggregate data for reporting.
- Bulk deactivate: POST /admin/loyalty/merchants/{id}/cards/bulk/deactivate
and POST /store/loyalty/cards/bulk/deactivate (merchant_owner only).
Deactivates multiple cards with audit trail.
- Point restore: POST /admin/loyalty/cards/{id}/restore-points
Creates ADMIN_ADJUSTMENT transaction with positive delta. Reuses
existing adjust_points service method.
- Cascade restore: POST /admin/loyalty/merchants/{id}/restore-deleted
Restores all soft-deleted programs and cards for a merchant.
Service methods: anonymize_cards_for_customer, bulk_deactivate_cards,
restore_deleted_cards, restore_deleted_programs.
342 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add support for linking a loyalty program's Terms & Conditions to a
CMS page, replacing the simple terms_text textarea with a scalable
content source that supports rich HTML, multi-language, and store
overrides.
- Migration loyalty_006: adds terms_cms_page_slug column to
loyalty_programs (nullable, String 200).
- Model + schemas: new field on LoyaltyProgram, ProgramCreate,
ProgramUpdate, ProgramResponse.
- Program form: new "CMS Page Slug" input field with hint text,
placed above the legacy terms_text (now labeled as "fallback").
- Enrollment page: when terms_cms_page_slug is set, JS fetches the
CMS page content via /storefront/cms/pages/{slug} and displays
rendered HTML in the modal. Falls back to terms_text when no slug.
- i18n: 3 new keys in 4 locales (terms_cms_page, terms_cms_page_hint,
terms_fallback_hint).
Legacy terms_text field preserved as fallback for existing programs.
342 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 of the production launch plan: task reliability improvements
to prevent DB lock issues at scale and handle transient wallet API
failures gracefully.
- 3.1 Batched point expiration: rewrite per-card Python loop to chunked
processing (LIMIT 500 FOR UPDATE SKIP LOCKED). Each chunk commits
independently, releasing row locks before processing the next batch.
Notifications sent after commit (outside lock window). Warning emails
also chunked with same pattern.
- 3.2 Wallet sync exponential backoff: replace time.sleep(2) single
retry with 4 attempts using [1s, 4s, 16s] backoff delays. Per-card
try/except ensures one failing card doesn't block the batch.
Failed card IDs logged for observability.
342 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add async email notifications for 5 loyalty lifecycle events, using
the existing messaging module infrastructure (EmailService, EmailLog,
store template overrides).
- New seed script: scripts/seed/seed_email_templates_loyalty.py
Seeds 5 templates × 4 locales (en/fr/de/lb) = 20 rows. Idempotent.
Renamed existing script to seed_email_templates_core.py.
- Celery task: loyalty.send_notification_email — async dispatch with
3 retries and 60s backoff. Opens own DB session.
- Notification service: LoyaltyNotificationService with 5 methods
that resolve customer/card/program into template variables and
enqueue via Celery (never blocks request handlers).
- Enrollment: sends loyalty_enrollment + loyalty_welcome_bonus (if
bonus > 0) after card creation commit.
- Stamps: sends loyalty_reward_ready when stamp target reached.
- Expiration task: sends loyalty_points_expiring 14 days before expiry
(tracked via new last_expiration_warning_at column to prevent dupes),
and loyalty_points_expired after points are zeroed.
- Migration loyalty_005: adds last_expiration_warning_at to cards.
- 8 new unit tests for notification service dispatch.
- Fix: rate limiter autouse fixture in integration tests to prevent
state bleed between tests.
Templates: loyalty_enrollment, loyalty_welcome_bonus,
loyalty_points_expiring, loyalty_points_expired, loyalty_reward_ready.
All support store-level overrides via the existing email template UI.
Birthday + re-engagement emails deferred to future marketing module
(cross-platform: OMS, loyalty, hosting).
342 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix duplicate card creation when the same email enrolls at different
stores under the same merchant, and implement cross-location-aware
enrollment behavior.
- Cross-location enabled (default): one card per customer per merchant.
Re-enrolling at another store returns the existing card with a
"works at all our locations" message + store list.
- Cross-location disabled: one card per customer per store. Enrolling
at a different store creates a separate card for that store.
Changes:
- Migration loyalty_004: replace (merchant_id, customer_id) unique
index with (enrolled_at_store_id, customer_id). Per-merchant
uniqueness enforced at application layer when cross-location enabled.
- card_service.resolve_customer_id: cross-store email lookup via
merchant_id param to find existing cardholders at other stores.
- card_service.enroll_customer: branch duplicate check on
allow_cross_location_redemption setting.
- card_service.search_card_for_store: cross-store email search when
cross-location enabled so staff at store2 can find cards from store1.
- card_service.get_card_by_customer_and_store: new service method.
- storefront enrollment: catch LoyaltyCardAlreadyExistsException,
return existing card with already_enrolled flag, locations, and
cross-location context. Server-rendered i18n via Jinja2 tojson.
- enroll-success.html: conditional cross-store/single-store messaging,
server-rendered translations and context, i18n_modules block added.
- dashboard.html, history.html: replace $t() with server-side _() to
fix i18n flicker across all storefront templates.
- Fix device-mobile icon → phone icon.
- 4 new i18n keys in 4 locales (en, fr, de, lb).
- Docs: updated data-model, business-logic, production-launch-plan,
user-journeys with cross-location behavior and E2E test checklist.
- 12 new unit tests + 3 new integration tests (334 total pass).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 45 new preset queries covering all database tables, reorganize into
platform-aligned sections (Infrastructure, Core, OMS, Loyalty, Hosting,
Internal) with search/filter input. Fix column headers not appearing on
SELECT * queries by capturing result.keys() before fetchmany().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of the loyalty production launch plan: config & security
hardening, dropped-data fix, DB integrity guards, rate limiting, and
constant-time auth compare. 362 tests pass.
- 1.4 Persist customer birth_date (new column + migration). Enrollment
form was collecting it but the value was silently dropped because
create_customer_for_enrollment never received it. Backfills existing
customers without overwriting.
- 1.1 LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON validated at startup (file
must exist and be readable; ~ expanded). Adds is_google_wallet_enabled
and is_apple_wallet_enabled derived flags. Prod path documented as
~/apps/orion/google-wallet-sa.json.
- 1.5 CHECK constraints on loyalty_cards (points_balance, stamp_count
non-negative) and loyalty_programs (min_purchase, points_per_euro,
welcome_bonus non-negative; stamps_target >= 1). Mirrored as
CheckConstraint in models. Pre-flight scan showed zero violations.
- 1.3 @rate_limit on store mutating endpoints: stamp 60/min,
redeem/points-earn 30-60/min, void/adjust 20/min, pin unlock 10/min.
- 1.2 Constant-time hmac.compare_digest for Apple Wallet auth token
(pulled forward from Phase 9 — code is safe whenever Apple ships).
See app/modules/loyalty/docs/production-launch-plan.md for the full
launch plan and remaining phases.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
E-commerce nav (Products, Cart, Account) shows on hosting POC sites.
Preview mode should render only CMS pages (Services, Projects, Contact)
in the nav, not module-defined e-commerce items.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Details the gap between scraped content (21 paragraphs, 30 headings,
13 images) and what appears on POC pages (only placeholder fields).
Phase 1 plan: programmatic mapping of scraped headings/paragraphs/
images into template sections (hero subtitle, gallery, about text).
Phase 2: AI-powered content enhancement (deferred, provider TBD).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CTA buttons in section macros use short paths like /contact which
resolve to site root instead of the storefront path. The preview
JS now detects short paths (not starting with /platforms/, /storefront/,
etc.) and prepends the store's base_url before adding _preview token.
Example: /contact → /platforms/hosting/storefront/batirenovation-strasbourg/contact?_preview=...
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Preview token propagation:
- JavaScript in storefront base.html appends _preview query param to
all internal links when in preview mode, so clicking nav items
(Services, Contact, etc.) preserves the preview bypass
Scraped content enrichment:
- POC builder now appends first 5 scraped paragraphs to about/services/
projects pages, so the POC shows actual content from the prospect's
site instead of just generic template text
- Extracts tagline from second scraped heading
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
testimonials.items in Jinja2 calls dict.items() method instead of
accessing the 'items' key when sections are raw JSON dicts (POC builder
pages). Fixed by using .get('items', []) with mapping check.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The hero and CTA section macros expect button.text.translations but
template JSONs used button.label.translations. Changed all 5 template
homepage files: label → text in button objects.
Also fixed existing CMS pages in DB (page 56) to match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two critical fixes for POC site rendering:
1. Storefront content page route now selects template based on
page.template field: 'full' → landing-full.html (section-based),
'modern'/'minimal' → their respective templates. Default stays
content-page.html (plain HTML). Previously always used content-page
which ignores page.sections JSON.
2. Storefront base_url uses store.subdomain (lowercase, hyphens)
instead of store.store_code (uppercase, underscores) for URL
building. Nav links now point to correct paths that the store
context middleware can resolve.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Build POC button on site detail now passes site_id to the POC
builder, which populates the existing site's store with CMS content
instead of trying to create a new site (which failed with duplicate
slug error).
- poc_builder_service.build_poc() accepts optional site_id param
- If site_id given: uses existing site, skips hosted_site_service.create()
- If not given: creates new site (standalone POC build)
- API schema: added site_id to BuildPocRequest
- Frontend: passes this.site.id in the build request
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same issue as Create Site button — bg-teal-600 not in Tailwind purge.
Switched to bg-purple-600 and removed $icon('sparkles') which may
not exist in the icon set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When deleting a hosted site, the associated Store is now soft-deleted
(sets deleted_at). This frees the subdomain for reuse via the partial
unique index (WHERE deleted_at IS NULL).
Previously the Store was orphaned, blocking subdomain reuse.
Closes docs/proposals/hosting-cascade-delete.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deleting a HostedSite leaves the Store orphaned, blocking subdomain
reuse. Proposal: cascade delete the Store when deleting the site.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Button was invisible — likely due to bg-teal-600 not being in the
Tailwind CSS purge or $icon rendering issue. Switched to bg-purple-600
(known to work) and simplified button content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Draft sites with a linked prospect show a "Build POC from Template"
section with:
- Template dropdown (generic, restaurant, construction, auto-parts,
professional-services) loaded from /admin/hosting/sites/templates API
- "Build POC" button that calls POST /admin/hosting/sites/poc/build
- Loading state + success message with pages created count
- Auto-refreshes site detail after build (status changes to POC_READY)
Visible only for draft sites with a prospect_id.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace raw ID inputs with a live search dropdown that queries
/admin/prospecting/prospects?search= as you type
- Shows matching prospects with business name, domain, and ID
- Clicking a prospect auto-fills business name, email, phone
- Selected prospect shown as badge with clear button
- Optional merchant ID field for existing merchants
- Remove stale "Create from Prospect" link on sites list (was just
a link to the prospects page)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Site creation form:
- Replace old "Create from Prospect" button (called removed endpoint)
with inline prospect_id + merchant_id fields
- Schema requires at least one — form validates before submit
- Clean payload (strip nulls/empty strings before POST)
Sites list:
- Add trash icon delete button with confirmation dialog
- Calls DELETE /admin/hosting/sites/{id} (existing endpoint)
- Reloads list after successful deletion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename "Score Breakdown" → "Opportunity Score" with subtitle
"Higher = more issues = better sales opportunity"
- "No issues detected" at 0 points shows green "✓ No issues found —
low opportunity" instead of ambiguous gray text
- Explains why Technical Health 0/40 is actually good (no problems)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Trash icon button in Actions column with confirmation dialog
- Calls DELETE /admin/prospecting/prospects/{id} (existing endpoint)
- Reloads list after successful deletion
- Toast notification on success/failure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slugify now handles both domains and business names gracefully:
- Domain: strip protocol/www/TLD → batirenovation-strasbourg
- Business name: take first 3 meaningful words, skip filler
(le, la, du, des, the, and) → boulangerie-coin
- Cap at 30 chars
Clients without a domain get clean slugs from their business name
instead of the full title truncated mid-word.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Site detail template: x-show → x-if to prevent Alpine evaluating
expressions when site is null during async loading
- Slugify: prefer domain_name over business_name for subdomain
generation (batirenovation-strasbourg vs bati-rnovation-strasbourg-
peinture-ravalement-dans). Cap at 30 chars. Strip protocol/TLD.
- POC builder passes domain_name for clean slugs
- Remove .lu/.fr/.com TLD from slugs automatically
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The POC viewer was loading the storefront in an iframe, which hit the
StorefrontAccessMiddleware subscription check (POC sites don't have
subscriptions yet). Fixed by rendering CMS sections directly in the
preview template:
- Load ContentPages and StoreTheme from DB
- Render hero, features, testimonials, CTA sections inline
- Apply template colors/fonts via Tailwind CSS config
- HostWizard preview banner with nav links
- Footer with contact info
- No iframe, no subscription check needed
Also fixed Jinja2 dict.items collision (dict.items is the method,
not the 'items' key — use dict.get('items') instead).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Register hosting public page router in main.py (POC preview at
/hosting/sites/{id}/preview was returning 404 because the
public_page_router was set on module definition but never mounted)
- Suppress urllib3 InsecureRequestWarning in enrichment service
(intentional verify=False for prospect site scanning)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New section partials for hosting templates:
- _testimonials.html: customer review cards with star ratings, avatars
- _gallery.html: responsive image grid with hover captions
- _contact_info.html: phone/email/address cards with icons + hours
Updated renderers:
- Platform homepage-default.html: imports + renders new section types
- Storefront landing-full.html: added section-based rendering path
that takes over when page.sections is set (POC builder pages),
falls back to hardcoded HTML layout for non-section pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New scrape_content() method in enrichment_service: extracts meta
description, H1/H2 headings, paragraphs, images (filtered for size),
social links, service items, and detected languages using BeautifulSoup
- Scans 6 pages per prospect: /, /about, /a-propos, /services,
/nos-services, /contact
- Results stored as JSON in prospect.scraped_content_json
- New endpoints: POST /content-scrape/{id} and /content-scrape/batch
- Added to full_enrichment pipeline (Step 5, before security audit)
- CONTENT_SCRAPE job type for scan-jobs tracking
- "Content Scrape" batch button on scan-jobs page
- Add beautifulsoup4 to requirements.txt
Tested on batirenovation-strasbourg.fr: extracted 30 headings,
21 paragraphs, 13 images.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add PROSPECTING_BATCH_DELAY_SECONDS config (default 1.0s) — polite
delay between prospects in batch scans to avoid rate limiting
- Apply delay to all 5 batch API endpoints and all Celery tasks
- Fix Celery tasks: error_message → error_log (matches model field)
- Add batch-scanning.md docs with rate limiting guide, scaling estimates
for 70k+ URL imports, and pipeline order recommendations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SecurityReportService generates standalone branded HTML reports from
stored audit data (grade badge, simulated hacked site, detailed
findings, business impact, call-to-action with contact info)
- GET /security-audit/report/{prospect_id} returns HTMLResponse
- "Generate Report" button on prospect detail security tab opens
report in new browser tab (printable to PDF)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete security audit integration into the enrichment pipeline:
Backend:
- SecurityAuditService with 7 passive checks: HTTPS, SSL cert, security
headers, exposed files, cookies, server info, technology detection
- Constants file with SECURITY_HEADERS, EXPOSED_PATHS, SEVERITY_SCORES
- SecurityAuditResponse schema with JSON field validators + aliases
- Endpoints: POST /security-audit/{id}, POST /security-audit/batch
- Added to full_enrichment pipeline (Step 5, before scoring)
- get_pending_security_audit() query in prospect_service
Frontend:
- Security tab on prospect detail page with grade badge (A+ to F),
score/100, severity counts, HTTPS/SSL status, missing headers,
exposed files, technologies, and full findings list
- "Run Security Audit" button with loading state
- "Security Audit" batch button on scan-jobs page
Tested on batirenovation-strasbourg.fr: Grade D (50/100), 11 issues
found (missing headers, exposed wp-login, server version disclosure).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Schema: add merchant_id/prospect_id with model_validator requiring
at least one. Remove from-prospect endpoint (unified into POST /sites)
- Service: rewrite create() — if merchant_id use it directly, if
prospect_id auto-create merchant from prospect data. Remove system
merchant hack entirely. Extract _create_merchant_from_prospect helper.
- Simplify accept_proposal() — merchant already exists at creation,
only creates subscription and marks prospect converted
- Tests: update all create calls with merchant_id, replace from-prospect
tests with prospect_id + validation tests
Closes docs/proposals/hosting-site-creation-fix.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Master plan covering 4 workstreams:
1. Fix hosting foundation (merchant/prospect required)
2. Security audit pipeline + report + live demo
3. POC builder with industry templates (restaurant, construction,
auto-parts, professional-services, generic)
4. AI content enhancement (deferred, provider TBD)
Target: 10-step journey from prospect discovery to live website.
Steps 1-3 work today, steps 4-10 need the work described.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hosted sites leverage existing CMS module (ContentPage, StoreTheme,
MediaFile) instead of building a separate site rendering system. Industry
templates (restaurant, construction, auto-parts, professional-services,
generic) are JSON presets that populate CMS entities for a new Store.
POC phase uses subdomain routing (acme.hostwizard.lu), go-live adds
custom domain via StoreDomain (acme.lu). All routing handled by existing
StoreContextMiddleware + Caddy wildcards.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4-phase plan for integrating scripts/security-audit/ into the
prospecting module: security audit pipeline, report generation,
live demo server, and POC site builder architecture.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New model: ProspectSecurityAudit with score, grade, findings_json,
severity counts, has_https, has_valid_ssl, missing_headers, exposed
files, technologies, scan_error
- Add last_security_audit_at timestamp to Prospect model
- Add security_audit 1:1 relationship on Prospect
Part of Phase 1: Security Audit in Enrichment Pipeline. Service,
constants, migration, endpoints, and frontend to follow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Detect API-level errors (quota exceeded, invalid URL) in response JSON
and store in scan_error instead of silently writing zeros
- Show scan error message on the performance card when present
- Show "No performance data — configure PAGESPEED_API_KEY" when all
scores are 0 and no error was recorded
- Add accessibility and best practices scores to performance card
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Table actions now show view + edit + delete (trash icon) for non-owner
members. Delete opens the existing remove-from-all-stores modal.
Edit modal enhanced with "Add to another store" section:
- Shows a dashed-border card with store dropdown + role dropdown + add button
- Only appears when the member is not yet in all merchant stores
- Uses the existing invite API to add the member to the selected store
i18n: 2 new keys (add_to_store, select_store) in 4 locales.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Restructure score_breakdown from flat dict to grouped by category:
{technical_health: {flag: pts}, modernity: {...}, ...}
- Each category row shows score/max with progress bar + per-flag detail
(e.g. Technical Health 15/40 → "very slow: 15 pts")
- Color-coded: green for positive flags, orange for issues
- "No issues detected" shown for clean categories
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Score Breakdown: show point-by-point contributions from score_breakdown
dict, sorted by value, color-coded green (positive) vs red (negative)
- Tech Profile: prominent CMS badge (WordPress, Shopify, etc.) with
e-commerce platform tag, "Custom / Unknown CMS" fallback
- Add SSL issuer and expiry date to tech profile card
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix contact_type column: Enum(ContactType) → String(20) to match the
migration (fixes "type contacttype does not exist" on insert)
- Rewrite scrape_contacts with structured-first approach:
Phase 1: tel:/mailto: href extraction (high confidence)
Phase 2: regex fallback with SVG/script stripping, international phone
pattern (requires + prefix, min 10 digits)
Phase 3: address extraction from Schema.org JSON-LD, <address> tags,
and European street address regex (FR/DE/EN street keywords)
- URL-decode email values, strip tags to plain text for cross-element
address matching
- Add /mentions-legales to scanned paths
Tested on batirenovation-strasbourg.fr: finds 3 contacts (email, phone,
address) vs 120+ false positives and a crash before.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The role dropdown was hidden for pending stores (x-show="!store.is_pending").
Pending members already have an assigned role that should be changeable
before acceptance.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts the expandable sub-row design back to a clean one-row-per-member
table. All per-store management now happens inside the edit modal.
Table: simple 4-column layout (Member | Stores & Roles | Status | Actions)
with view + edit buttons. Store badges show orange for pending stores.
Edit modal enhanced with per-store cards showing:
- Store name, code, and status badge (Active/Pending)
- Role dropdown + Update button (for active stores)
- Resend invitation button (for pending stores)
- Remove from store button
- "Remove from all stores" link at bottom
Removed: expandedMembers, flattenedRows, toggleMemberExpand,
resendStoreInvitation, resendInvitation (member-level).
Added: resendForStore, removeFromStore (work inside edit modal).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The nested tbody approach caused browsers to collapse all cells into
one column. Replaced with a single flat x-for loop over flattenedRows
(computed property that interleaves member rows and store sub-rows).
Each row is a single <tr> with 4 proper <td> cells, using x-if to
conditionally render member-level or store-level content per column.
Sub-rows are hidden/shown via expandedMembers array.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fixed header/column alignment: Member | Role | Status | Actions
- Store count + chevron moved inline with member name (not a separate column)
- Role column shows single role, "Owner", or "Multiple roles" on main row
- Actions use fixed 4-slot grid (resend | view | edit | remove) ensuring
icons always align vertically between main rows and sub-rows
- Empty slots render as blank space to maintain alignment
i18n: added multiple_roles key in 4 locales.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Member rows now show a store count with expand/collapse chevron.
Clicking expands sub-rows showing each store with:
- Store name and code
- Per-store role badge
- Per-store status (active/pending independently)
- Per-store actions: resend invitation (pending), remove from store
This fixes the issue where a member active on one store but pending
on another showed misleading combined status and actions.
Member-level actions (view, edit profile) stay on the main row.
Store-level actions (resend, remove) are on each sub-row.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
getMemberStatus() showed "pending" if ANY store had a pending invitation,
even if the member was already active in another store. Now checks for
active stores first — a member who is active in at least one store
shows as "active", not "pending".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New standalone page at /store/{store_code}/invitation/accept?token=xxx
where invited team members can:
- Review their name and email (pre-filled from invitation)
- Set their password
- Accept the invitation
Page handles all routing modes (dev path, platform path, prod subdomain,
custom domain) via store context middleware. After acceptance, redirects
to the platform-aware store login page.
New service method get_invitation_info() validates the token and returns
invitation details without modifying anything.
Error states: expired token, already accepted, invalid token.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 new tests in TestResendInvitation:
- test_resend_invitation_for_pending_member: verifies token regeneration
and invitation_sent_at update
- test_resend_invitation_nonexistent_user: verifies 404
Total: 17 store team member integration tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Using debug flag for environment detection is unreliable — if left
True in prod, links would point to localhost. Now uses the proper
is_production() from environment module.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Email clients need absolute URLs to make links clickable. The
acceptance_link was a relative path (/store/invitation/accept?token=...)
which rendered as plain text. Now prepends the platform domain with
the correct protocol.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New resend_invitation() service method regenerates the token and
resends the invitation email for pending members.
Available on all frontends:
- Merchant: POST /merchants/account/team/stores/{sid}/members/{uid}/resend
- Store: POST /store/team/members/{uid}/resend
UI: paper-airplane icon appears on pending members in both merchant
and store team pages.
i18n: resend_invitation + invitation_resent keys in 4 locales.
Also translated previously untranslated invitation_sent_successfully
in fr/de/lb.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
x-model bindings crash when selectedMember is null because x-show
keeps DOM elements alive. x-if removes them entirely, preventing
the "can't access property of null" errors on page load.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Edit modal now has editable first_name, last_name, email fields with
a "Save Profile" button, alongside the existing per-store role management.
New:
- PUT /merchants/account/team/members/{user_id}/profile endpoint
- MerchantTeamProfileUpdate schema
- update_team_member_profile() service method with ownership validation
- 2 new i18n keys across 4 locales (personal_info, save_profile)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merchant team page:
- Consistent member display (full_name + email on every row)
- New view button (eye icon) on all members including owner
- View modal shows account info (username, role, email verified,
last login, account created) and store memberships with roles
- API enriched with user metadata (username, role, is_email_verified,
last_login, created_at)
Invite fix (both merchant and store routes):
- first_name and last_name from invite form were never passed to the
service that creates the User account. Now passed through correctly.
i18n: 6 new keys across 4 locales (view_member, account_information,
username, email_verified, last_login, account_created).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
The block name validation (scripts → extra_scripts, etc.) only checked
admin and store templates, missing merchant. Added is_merchant flag.
This would have caught the {% block scripts %} bug in merchant/team.html.
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>
The shared apiClient unconditionally called response.json() on every
response, including 204 No Content (returned by DELETE endpoints).
This caused "Invalid JSON response from server" errors on all delete
operations across all modules and personas.
Now returns null for 204 responses without attempting JSON parse.
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>
- hetzner-server-setup: runner timeout 3h, shutdown_timeout 300s,
deploy.sh now writes .build-info and uses explicit -f flag
- gitea: document unit-only CI tests and xdist incompatibility
- docker: add build info section, document volume mount approach
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Docker build cache can skip picking up the .build-info file during
COPY. Mounting it as a read-only volume ensures the container always
reads the current host file.
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>
2,484 unit tests take ~13min locally but ~2h on the 2-vCPU CAX11.
pytest-xdist doesn't work with the shared DB session setup, so
increase the job timeout instead. Runner config also bumped to 3h.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add docker-compose.override.yml exposing db/redis ports for local dev
- Remove override from .gitignore so all devs get port mappings
- Use explicit -f in deploy.sh to skip override in production
- Document production safety rule: always use -f on the server
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>
On 2-core ARM runner, 2893 tests with verbose output and live log
capture take 2.5h+. Major bottlenecks:
- Coverage: disabled (previous commit)
- Verbose output (-v): generates huge I/O over Docker bridge
- Live log capture: logs every HTTP request per test
- Integration tests: heavy DB fixture setup (~7s each)
Now: unit tests only (2484), quiet mode (-q), no log capture,
LOG_LEVEL=WARNING. Integration tests run locally via make test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2893 tests with DB fixture setup take 2.5h+ on 2-core ARM runner.
Split into unit tests (2484, fast) and integration tests (341, DB-heavy)
as separate steps for better visibility into what's slow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Coverage instrumentation (--cov) in pyproject.toml addopts was adding
3-5x overhead on the 2-core ARM CI runner. Disable it in CI with
--no-cov and --override-ini to clear addopts. Add --durations=20 to
identify slowest tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- deploy.sh writes .build-info with commit SHA and timestamp after git pull
- /health endpoint now returns version, commit, and deployed_at fields
- Admin sidebar footer shows version and commit SHA
- Hetzner docs updated: runner --config flag, swap, and runner timeout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
node-exporter only uses ~20MB so 32m is safe. cadvisor was at 98% of
128m and crashing during CI runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <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>
Adds WizaMart S.à r.l. as a demo merchant with:
- OMS platform subscription (essential tier, 30-day trial)
- Custom domain wizamart.com linked to OMS platform
- Idempotent: safe to run multiple times
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update origin cert config: wildcards for omsflow.lu, rewardflow.lu, hostwizard.lu
- Add wildcard Caddy blocks to production Caddyfile example
- Replace "Future" section with actionable runbooks:
- Add a Store Subdomain (self-service, no infra changes)
- Add a Custom Store Domain (Cloudflare + Caddy + DB)
- Add a New Platform Domain (full setup)
- Document wizard.lu exception (no wildcard due to git.wizard.lu DNS-only)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Register Alpine magic $t() for reactive translations in templates
- Dispatch i18n:ready event when translations load
- Fix base.html to use current_language instead of storefront_language
Co-Authored-By: Claude Opus 4.6 (1M context) <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>
Remove marketplace page redirect-to-onboarding tests that no longer
match the route behavior. Add can_create_store mock to tenancy store
creation tests to bypass billing limit checks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
RSASigner doesn't expose a .key attribute. Load the private key string
directly from the service account JSON file for PyJWT encoding. Also
adds fat JWT fallback for demo mode where DRAFT classes reject object
creation via REST API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add run_affected_tests.py script that uses module dependency graph to
run only tests for changed modules and their dependents. Fix CI and
Makefile to use pyproject.toml testpaths (was missing 9 of 18 modules).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test was failing because can_create_store() called the billing
module's check_resource_limit() which returned False for the test
merchant (no subscription). Patched the limit check since billing
is not what this unit test exercises.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- API-004: Add noqa for factory-pattern auth in user_account routes and payments admin
- MDL-003: Add from_attributes to MerchantStoreDetailResponse schema
- EXC-003: Suppress broad except in merchant_store_service and admin_subscription_service
(intentional fallbacks for optional billing module)
- NAM-002: Rename onboarding files to *_service.py suffix and update all imports
- JS-001: Add file-level noqa for dev-toolbar.js (console interceptor by design)
- JS-005: Add init guards to dashboard.js and customer-detail.js
- IMPORT-004: Break circular deps by removing orders from inventory requires and
marketplace from orders requires; add IMPORT-002 suppression for lazy cross-imports
- MOD-025: Remove unused OnboardingAlreadyCompletedException
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 23:43:12 +01:00
488 changed files with 30793 additions and 2908 deletions
"failed_to_load_dashboard_data":"Failed to load dashboard data",
"dashboard_refreshed":"Dashboard refreshed"
"failed_to_load_dashboard_data":"Échec du chargement des données du tableau de bord",
"dashboard_refreshed":"Tableau de bord actualisé",
"item_removed_from_cart":"Article retiré du panier",
"cart_cleared":"Panier vidé"
},
"confirmations":{
"show_all_menu_items":"Ceci affichera tous les éléments de menu. Continuer ?",
"hide_all_menu_items":"Ceci masquera tous les éléments de menu (sauf les obligatoires). Vous pourrez ensuite activer ceux que vous souhaitez. Continuer ?",
"reset_email_settings":"Ceci réinitialisera tous les paramètres e-mail aux valeurs par défaut .env. Continuer ?",
"cleanup_logs":"Ceci supprimera tous les journaux de plus de {days} jours. Continuer ?"
@@ -34,6 +35,7 @@ class Customer(Base, TimestampMixin):
first_name=Column(String(100))
last_name=Column(String(100))
phone=Column(String(50))
birth_date=Column(Date,nullable=True)
customer_number=Column(
String(100),nullable=False,index=True
)# Store-specific ID
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.