screen where users pick an eligible end‑customer purchase and confirm. Eligibility is enforced server‑side
(endcustomer_event, within 14 days, no event package created after purchase), refund is issued via Paddle, the
purchase is marked refunded, the tenant package is deactivated, and a new confirmation email is sent using resources/
views/emails/partials/layout.blade.php.
Details
- New controller + form request for the confirm flow: app/Http/Controllers/WithdrawalController.php, app/Http/
Requests/Marketing/WithdrawalConfirmRequest.php
- New confirmation page + CTA: resources/js/pages/marketing/WithdrawalConfirm.tsx, resources/js/pages/legal/Show.tsx
- Routes + locale rewrites: routes/web.php, resources/js/lib/localizedPath.ts
- New email notification + template: app/Notifications/Customer/WithdrawalConfirmed.php, resources/views/emails/
withdrawal-confirmation.blade.php
- Translations added for marketing UI + backend flash + email copy: public/lang/de/marketing.json, public/lang/en/
marketing.json, resources/lang/de/marketing.php, resources/lang/en/marketing.php, resources/lang/de/emails.php,
resources/lang/en/emails.php
- Tests: tests/Feature/Marketing/WithdrawalConfirmationTest.php
89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
export type LocaleRewriteMap = Record<string, Record<string, string>>;
|
|
|
|
export const defaultLocaleRewrites: LocaleRewriteMap = {
|
|
'/': {},
|
|
'/kontakt': { en: '/contact' },
|
|
'/contact': { de: '/kontakt' },
|
|
'/so-funktionierts': { en: '/how-it-works' },
|
|
'/how-it-works': { de: '/so-funktionierts' },
|
|
'/bestellen': { en: '/checkout' },
|
|
'/checkout': { de: '/bestellen' },
|
|
'/anlaesse': { en: '/occasions' },
|
|
'/anlaesse/hochzeit': { en: '/occasions/wedding' },
|
|
'/anlaesse/geburtstag': { en: '/occasions/birthday' },
|
|
'/anlaesse/firmenevent': { en: '/occasions/corporate-event' },
|
|
'/anlaesse/konfirmation': { en: '/occasions/confirmation' },
|
|
'/occasions/wedding': { de: '/anlaesse/hochzeit' },
|
|
'/occasions/birthday': { de: '/anlaesse/geburtstag' },
|
|
'/occasions/corporate-event': { de: '/anlaesse/firmenevent' },
|
|
'/occasions/confirmation': { de: '/anlaesse/konfirmation' },
|
|
'/widerruf': { en: '/withdrawal/confirm' },
|
|
'/withdrawal/confirm': { de: '/widerruf' },
|
|
};
|
|
|
|
const sanitizePath = (input: string): string => {
|
|
if (!input || input.trim().length === 0) {
|
|
return '/';
|
|
}
|
|
|
|
const withLeading = input.startsWith('/') ? input : `/${input}`;
|
|
const withoutTrailing = withLeading.replace(/\/{2,}/g, '/');
|
|
|
|
return withoutTrailing;
|
|
};
|
|
|
|
const resolveRewrite = (
|
|
path: string,
|
|
targetLocale: string,
|
|
rewrites: LocaleRewriteMap,
|
|
): string => {
|
|
const exact = rewrites[path];
|
|
if (exact) {
|
|
return exact[targetLocale] ?? path;
|
|
}
|
|
|
|
const prefixKey = Object.keys(rewrites)
|
|
.filter((key) => key !== '/' && path.startsWith(`${key}/`))
|
|
.sort((a, b) => b.length - a.length)[0];
|
|
|
|
if (!prefixKey) {
|
|
return path;
|
|
}
|
|
|
|
const replacement = rewrites[prefixKey]?.[targetLocale];
|
|
if (!replacement) {
|
|
return path;
|
|
}
|
|
|
|
const suffix = path.slice(prefixKey.length);
|
|
|
|
return `${replacement}${suffix}`;
|
|
};
|
|
|
|
export const buildLocalizedPath = (
|
|
path: string | null | undefined,
|
|
targetLocale: string | undefined,
|
|
supportedLocales: string[],
|
|
defaultLocale = 'de',
|
|
rewrites: LocaleRewriteMap = defaultLocaleRewrites,
|
|
): string => {
|
|
const fallbackLocale = supportedLocales.length > 0 ? supportedLocales[0] : defaultLocale;
|
|
const nextLocale = targetLocale && supportedLocales.includes(targetLocale)
|
|
? targetLocale
|
|
: fallbackLocale;
|
|
|
|
if (typeof path !== 'string' || path.trim().length === 0) {
|
|
return `/${fallbackLocale}`;
|
|
}
|
|
|
|
const trimmed = path.trim();
|
|
const [rawPath, rawQuery] = trimmed.split('?');
|
|
const normalizedPath = sanitizePath(rawPath);
|
|
const rewrittenPath = resolveRewrite(normalizedPath, nextLocale, rewrites);
|
|
const base = rewrittenPath === '/' ? `/${nextLocale}` : `/${nextLocale}${rewrittenPath}`;
|
|
const sanitisedBase = base.replace(/\/{2,}/g, '/');
|
|
const query = rawQuery ? `?${rawQuery}` : '';
|
|
|
|
return `${sanitisedBase}${query}`;
|
|
};
|