feat(loyalty): Phase 4.1 — T&C via CMS integration
Some checks failed
Some checks failed
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>
This commit is contained in:
@@ -247,9 +247,11 @@
|
||||
</h3>
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.terms_conditions') }}</label>
|
||||
<textarea x-model="settings.terms_text" rows="3"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"></textarea>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.terms_cms_page') }}</label>
|
||||
<input type="text" x-model="settings.terms_cms_page_slug" maxlength="200"
|
||||
placeholder="e.g. terms-and-conditions"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ _('loyalty.shared.program_form.terms_cms_page_hint') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.privacy_policy_url') }}</label>
|
||||
@@ -257,6 +259,12 @@
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.shared.program_form.terms_conditions') }}</label>
|
||||
<textarea x-model="settings.terms_text" rows="3"
|
||||
placeholder="{{ _('loyalty.shared.program_form.terms_fallback_hint') }}"
|
||||
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Program Status -->
|
||||
|
||||
@@ -96,10 +96,10 @@
|
||||
style="color: var(--color-primary)">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ _('loyalty.enrollment.form.terms_agree') }}
|
||||
<template x-if="program?.terms_text">
|
||||
<template x-if="program?.terms_text || program?.terms_cms_page_slug">
|
||||
<a href="#" @click.prevent="showTerms = true" class="underline" style="color: var(--color-primary)">{{ _('loyalty.enrollment.form.terms') }}</a>
|
||||
</template>
|
||||
<template x-if="!program?.terms_text">
|
||||
<template x-if="!program?.terms_text && !program?.terms_cms_page_slug">
|
||||
<span class="underline" style="color: var(--color-primary)">{{ _('loyalty.enrollment.form.terms') }}</span>
|
||||
</template>
|
||||
</span>
|
||||
@@ -137,9 +137,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TODO: Rework T&C strategy - current approach (small text field on program model) won't scale
|
||||
for full legal T&C. Options: (1) leverage the CMS module to host T&C pages, or
|
||||
(2) create a dedicated T&C page within the loyalty module. Decision pending. #}
|
||||
<!-- Terms & Conditions Modal -->
|
||||
<div x-show="showTerms" x-cloak
|
||||
role="dialog" aria-modal="true" aria-labelledby="terms-modal-title"
|
||||
@@ -153,7 +150,9 @@
|
||||
<span x-html="$icon('x-mark', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4 overflow-y-auto text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line" x-text="program?.terms_text"></div>
|
||||
{# CMS page content takes priority over legacy terms_text #}
|
||||
<div x-show="termsHtml" class="p-4 overflow-y-auto text-sm text-gray-700 dark:text-gray-300 prose dark:prose-invert max-w-none" x-html="termsHtml"></div>
|
||||
<div x-show="!termsHtml && program?.terms_text" class="p-4 overflow-y-auto text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line" x-text="program?.terms_text"></div>
|
||||
<template x-if="program?.privacy_url">
|
||||
<div class="px-4 pb-2">
|
||||
<a :href="program.privacy_url" target="_blank" class="text-sm underline" style="color: var(--color-primary)">{{ _('loyalty.enrollment.privacy_policy') }}</a>
|
||||
|
||||
Reference in New Issue
Block a user