Files
orion/app/modules/loyalty/templates/loyalty/storefront/enroll-success.html
Samir Boulahtit f804ff8442
Some checks failed
CI / ruff (push) Successful in 16s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
fix(loyalty): cross-store enrollment, card scoping, i18n flicker
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>
2026-04-11 18:28:19 +02:00

176 lines
9.5 KiB
HTML

{# app/modules/loyalty/templates/loyalty/storefront/enroll-success.html #}
{% extends "storefront/base.html" %}
{% block title %}{{ _('loyalty.enrollment.success.title') }} - {{ store.name }}{% endblock %}
{% block i18n_modules %}['loyalty']{% endblock %}
{% block alpine_data %}customerLoyaltyEnrollSuccess(){% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center px-4 py-12 bg-gray-50 dark:bg-gray-900">
<div class="max-w-md w-full text-center">
<!-- Success Icon -->
<div class="w-20 h-20 mx-auto mb-6 rounded-full flex items-center justify-center"
style="background-color: var(--color-primary); opacity: 0.1">
<div class="w-20 h-20 rounded-full flex items-center justify-center" style="background-color: var(--color-primary)">
<span x-html="$icon('check', 'w-10 h-10 text-white')"></span>
</div>
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2"
x-text="enrollContext.already_enrolled ? i18nStrings.already_enrolled_title : i18nStrings.success_title">
{{ _('loyalty.enrollment.success.title') }}
</h1>
<p class="text-gray-600 dark:text-gray-400 mb-8">{{ _('loyalty.enrollment.success.message') }}</p>
<!-- Card Number Display -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-8">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">{{ _('loyalty.enrollment.success.card_number') }}</p>
<p class="text-2xl font-mono font-bold text-gray-900 dark:text-white">{{ enrolled_card_number or 'Loading...' }}</p>
<div x-show="walletUrls.apple_wallet_url || walletUrls.google_wallet_url"
class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{{ _('loyalty.enrollment.success.wallet_prompt') }}
</p>
<div class="space-y-2">
<template x-if="walletUrls.apple_wallet_url">
<a :href="walletUrls.apple_wallet_url" target="_blank" rel="noopener"
class="w-full flex items-center justify-center px-4 py-3 bg-black text-white rounded-lg hover:bg-gray-800 transition-colors">
<span x-html="$icon('phone', 'w-5 h-5 mr-2')"></span>
{{ _('loyalty.loyalty.wallet.apple') }}
</a>
</template>
<template x-if="walletUrls.google_wallet_url">
<a :href="walletUrls.google_wallet_url" target="_blank" rel="noopener"
class="w-full flex items-center justify-center px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<span x-html="$icon('phone', 'w-5 h-5 mr-2')"></span>
{{ _('loyalty.loyalty.wallet.google') }}
</a>
</template>
</div>
</div>
</div>
<!-- Cross-location info -->
<template x-if="enrollContext.already_enrolled || (enrollContext.merchant_locations?.length > 1 && enrollContext.allow_cross_location)">
<div class="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-8 text-left">
<template x-if="enrollContext.already_enrolled && enrollContext.allow_cross_location">
<div>
<p class="font-medium text-blue-800 dark:text-blue-200 mb-2" x-text="i18nStrings.cross_location_message"></p>
<ul class="space-y-1">
<template x-for="loc in enrollContext.merchant_locations" :key="loc.id">
<li class="flex items-center text-sm text-blue-700 dark:text-blue-300">
<span x-html="$icon('map-pin', 'w-4 h-4 mr-2 flex-shrink-0')"></span>
<span x-text="loc.name"></span>
</li>
</template>
</ul>
</div>
</template>
<template x-if="enrollContext.already_enrolled && !enrollContext.allow_cross_location">
<p class="text-sm text-blue-700 dark:text-blue-300" x-text="i18nStrings.single_location_message.replace('{store_name}', enrollContext.enrolled_at_store_name || '')"></p>
</template>
<template x-if="!enrollContext.already_enrolled && enrollContext.merchant_locations?.length > 1 && enrollContext.allow_cross_location">
<div>
<p class="font-medium text-blue-800 dark:text-blue-200 mb-2" x-text="i18nStrings.available_locations"></p>
<ul class="space-y-1">
<template x-for="loc in enrollContext.merchant_locations" :key="loc.id">
<li class="flex items-center text-sm text-blue-700 dark:text-blue-300">
<span x-html="$icon('map-pin', 'w-4 h-4 mr-2 flex-shrink-0')"></span>
<span x-text="loc.name"></span>
</li>
</template>
</ul>
</div>
</template>
</div>
</template>
<!-- Next Steps -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 text-left mb-8">
<h2 class="font-semibold text-gray-900 dark:text-white mb-4">{{ _('loyalty.enrollment.success.next_steps_title') }}</h2>
<ul class="space-y-3 text-sm text-gray-600 dark:text-gray-400">
<li class="flex items-start">
<span x-html="$icon('check-circle', 'w-5 h-5 mr-2 text-green-500 flex-shrink-0')"></span>
<span>{{ _('loyalty.enrollment.success.step_earn') }}</span>
</li>
<li class="flex items-start">
<span x-html="$icon('check-circle', 'w-5 h-5 mr-2 text-green-500 flex-shrink-0')"></span>
<span>{{ _('loyalty.enrollment.success.step_balance') }}</span>
</li>
<li class="flex items-start">
<span x-html="$icon('check-circle', 'w-5 h-5 mr-2 text-green-500 flex-shrink-0')"></span>
<span>{{ _('loyalty.enrollment.success.step_redeem') }}</span>
</li>
</ul>
</div>
<!-- Actions -->
<div class="space-y-3">
<a href="{{ base_url }}account/loyalty"
class="block w-full py-3 px-4 text-white font-semibold rounded-lg transition-colors text-center"
style="background-color: var(--color-primary)">
{{ _('loyalty.enrollment.success.view_dashboard') }}
</a>
<a href="{{ base_url }}"
class="block w-full py-3 px-4 text-gray-700 dark:text-gray-300 font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-center">
{{ _('loyalty.enrollment.success.continue_shopping') }}
</a>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function customerLoyaltyEnrollSuccess() {
return {
...storefrontLayoutData(),
walletUrls: { google_wallet_url: null, apple_wallet_url: null },
// Server-rendered context — no flicker, survives refreshes
enrollContext: {
already_enrolled: {{ server_already_enrolled|tojson }},
allow_cross_location: {{ server_allow_cross_location|tojson }},
enrolled_at_store_name: null,
merchant_locations: {{ server_merchant_locations|tojson }},
},
i18nStrings: {
success_title: {{ _('loyalty.enrollment.success.title')|tojson }},
already_enrolled_title: {{ _('loyalty.enrollment.already_enrolled_title')|tojson }},
cross_location_message: {{ _('loyalty.enrollment.cross_location_message')|tojson }},
single_location_message: {{ _('loyalty.enrollment.single_location_message')|tojson }},
available_locations: {{ _('loyalty.enrollment.available_locations')|tojson }},
},
init() {
// Read wallet URLs saved during enrollment (no auth needed)
try {
const stored = sessionStorage.getItem('loyalty_wallet_urls');
if (stored) {
this.walletUrls = JSON.parse(stored);
sessionStorage.removeItem('loyalty_wallet_urls');
}
} catch (e) {
console.log('Could not load wallet URLs:', e.message);
}
// Merge sessionStorage context (has enrolled_at_store_name from
// the enrollment API response) into the server-rendered defaults
try {
const ctx = sessionStorage.getItem('loyalty_enroll_context');
if (ctx) {
const parsed = JSON.parse(ctx);
if (parsed.enrolled_at_store_name) {
this.enrollContext.enrolled_at_store_name = parsed.enrolled_at_store_name;
}
}
} catch (e) {
console.log('Could not load enroll context:', e.message);
}
}
};
}
</script>
{% endblock %}