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>
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>
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>
- 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>