Some checks failed
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>
176 lines
9.5 KiB
HTML
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 %}
|