includio-cms 0.36.8 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +179 -3
- package/CHANGELOG.md +64 -0
- package/DOCS.md +28 -3
- package/ROADMAP.md +18 -0
- package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
- package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
- package/dist/admin/client/booking/customer-panel.svelte +102 -0
- package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
- package/dist/admin/client/booking/customer-view.d.ts +52 -0
- package/dist/admin/client/booking/customer-view.js +134 -0
- package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
- package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
- package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
- package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
- package/dist/admin/client/booking/reservations-page.svelte +221 -0
- package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
- package/dist/admin/client/collection/collection-entries.svelte +21 -2
- package/dist/admin/client/collection/entry-link.svelte +6 -4
- package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
- package/dist/admin/client/collection/row-actions.svelte +12 -4
- package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
- package/dist/admin/client/collection/status-badge.svelte +39 -3
- package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
- package/dist/admin/client/entry/entry.svelte +26 -9
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
- package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
- package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
- package/dist/admin/client/entry/preview-views.d.ts +27 -0
- package/dist/admin/client/entry/preview-views.js +33 -0
- package/dist/admin/client/index.d.ts +3 -0
- package/dist/admin/client/index.js +3 -0
- package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
- package/dist/admin/client/modules/dashboard-registry.js +11 -0
- package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
- package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
- package/dist/admin/client/modules/stat-card.svelte +22 -0
- package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
- package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
- package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
- package/dist/admin/components/fields/relation-field.svelte +11 -2
- package/dist/admin/components/icons/icon-map.js +8 -0
- package/dist/admin/components/layout/app-sidebar.svelte +45 -14
- package/dist/admin/components/layout/context-switcher.svelte +90 -0
- package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
- package/dist/admin/components/layout/lang.d.ts +4 -0
- package/dist/admin/components/layout/lang.js +8 -0
- package/dist/admin/components/layout/layout-renderer.svelte +2 -1
- package/dist/admin/components/layout/layout-tabs.svelte +14 -0
- package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
- package/dist/admin/components/layout/nav-collections.svelte +16 -9
- package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
- package/dist/admin/components/layout/nav-global.svelte +45 -0
- package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
- package/dist/admin/components/layout/nav-main.svelte +8 -32
- package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
- package/dist/admin/components/layout/nav-module.svelte +57 -0
- package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
- package/dist/admin/components/layout/nav-section.svelte +1 -1
- package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
- package/dist/admin/components/layout/nav-singletons.svelte +14 -7
- package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
- package/dist/admin/remote/booking.remote.d.ts +244 -0
- package/dist/admin/remote/booking.remote.js +283 -0
- package/dist/admin/remote/entry.remote.d.ts +3 -0
- package/dist/admin/remote/entry.remote.js +14 -3
- package/dist/admin/remote/index.d.ts +2 -0
- package/dist/admin/remote/index.js +2 -0
- package/dist/admin/remote/modules.remote.d.ts +10 -0
- package/dist/admin/remote/modules.remote.js +14 -0
- package/dist/admin/state/active-context-resolve.d.ts +37 -0
- package/dist/admin/state/active-context-resolve.js +66 -0
- package/dist/admin/state/active-context.svelte.d.ts +8 -0
- package/dist/admin/state/active-context.svelte.js +27 -0
- package/dist/booking/adjustments.d.ts +33 -0
- package/dist/booking/adjustments.js +26 -0
- package/dist/booking/capacity.d.ts +12 -0
- package/dist/booking/capacity.js +10 -0
- package/dist/booking/client/index.d.ts +39 -0
- package/dist/booking/client/index.js +44 -0
- package/dist/booking/client/use-booking.svelte.d.ts +25 -0
- package/dist/booking/client/use-booking.svelte.js +56 -0
- package/dist/booking/config.d.ts +156 -0
- package/dist/booking/config.js +36 -0
- package/dist/booking/draft.d.ts +77 -0
- package/dist/booking/draft.js +67 -0
- package/dist/booking/expiry-logic.d.ts +30 -0
- package/dist/booking/expiry-logic.js +33 -0
- package/dist/booking/index.d.ts +18 -0
- package/dist/booking/index.js +52 -0
- package/dist/booking/ledger.d.ts +46 -0
- package/dist/booking/ledger.js +60 -0
- package/dist/booking/number.d.ts +2 -0
- package/dist/booking/number.js +11 -0
- package/dist/booking/participant-fields/access.d.ts +10 -0
- package/dist/booking/participant-fields/access.js +14 -0
- package/dist/booking/participant-fields/age.d.ts +12 -0
- package/dist/booking/participant-fields/age.js +28 -0
- package/dist/booking/participant-fields/completeness.d.ts +8 -0
- package/dist/booking/participant-fields/completeness.js +13 -0
- package/dist/booking/participant-fields/group.d.ts +6 -0
- package/dist/booking/participant-fields/group.js +15 -0
- package/dist/booking/participant-fields/index.d.ts +6 -0
- package/dist/booking/participant-fields/index.js +6 -0
- package/dist/booking/participant-fields/types.d.ts +50 -0
- package/dist/booking/participant-fields/types.js +5 -0
- package/dist/booking/participant-fields/validate.d.ts +7 -0
- package/dist/booking/participant-fields/validate.js +44 -0
- package/dist/booking/participant-pricing.d.ts +93 -0
- package/dist/booking/participant-pricing.js +91 -0
- package/dist/booking/participants-view.d.ts +32 -0
- package/dist/booking/participants-view.js +22 -0
- package/dist/booking/payment-context.d.ts +19 -0
- package/dist/booking/payment-context.js +29 -0
- package/dist/booking/pricing-inputs.d.ts +28 -0
- package/dist/booking/pricing-inputs.js +35 -0
- package/dist/booking/pricing.d.ts +86 -0
- package/dist/booking/pricing.js +125 -0
- package/dist/booking/recompute.d.ts +22 -0
- package/dist/booking/recompute.js +31 -0
- package/dist/booking/reminders-schedule.d.ts +11 -0
- package/dist/booking/reminders-schedule.js +28 -0
- package/dist/booking/server/adjustments.d.ts +27 -0
- package/dist/booking/server/adjustments.js +136 -0
- package/dist/booking/server/bookings.d.ts +45 -0
- package/dist/booking/server/bookings.js +288 -0
- package/dist/booking/server/catalog.d.ts +9 -0
- package/dist/booking/server/catalog.js +20 -0
- package/dist/booking/server/db.d.ts +7 -0
- package/dist/booking/server/db.js +18 -0
- package/dist/booking/server/email.d.ts +23 -0
- package/dist/booking/server/email.js +59 -0
- package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
- package/dist/booking/server/emailTemplateRegistry.js +125 -0
- package/dist/booking/server/http/create-handler.d.ts +5 -0
- package/dist/booking/server/http/create-handler.js +68 -0
- package/dist/booking/server/http/index.d.ts +4 -0
- package/dist/booking/server/http/index.js +4 -0
- package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
- package/dist/booking/server/http/maintenance-handler.js +36 -0
- package/dist/booking/server/http/portal-handler.d.ts +13 -0
- package/dist/booking/server/http/portal-handler.js +99 -0
- package/dist/booking/server/http/webhook-handler.d.ts +9 -0
- package/dist/booking/server/http/webhook-handler.js +39 -0
- package/dist/booking/server/index.d.ts +12 -0
- package/dist/booking/server/index.js +14 -0
- package/dist/booking/server/maintenance.d.ts +28 -0
- package/dist/booking/server/maintenance.js +134 -0
- package/dist/booking/server/participants.d.ts +26 -0
- package/dist/booking/server/participants.js +92 -0
- package/dist/booking/server/payments.d.ts +34 -0
- package/dist/booking/server/payments.js +207 -0
- package/dist/booking/server/portal-session.d.ts +16 -0
- package/dist/booking/server/portal-session.js +42 -0
- package/dist/booking/server/portal.d.ts +207 -0
- package/dist/booking/server/portal.js +234 -0
- package/dist/booking/server/refunds.d.ts +7 -0
- package/dist/booking/server/refunds.js +58 -0
- package/dist/booking/server/reminders.d.ts +9 -0
- package/dist/booking/server/reminders.js +24 -0
- package/dist/booking/server/validation.d.ts +17 -0
- package/dist/booking/server/validation.js +55 -0
- package/dist/booking/state-machine.d.ts +41 -0
- package/dist/booking/state-machine.js +73 -0
- package/dist/booking/templates/_partials/footer.en.html +4 -0
- package/dist/booking/templates/_partials/footer.pl.html +4 -0
- package/dist/booking/templates/_partials/header.en.html +4 -0
- package/dist/booking/templates/_partials/header.pl.html +4 -0
- package/dist/booking/templates/balance-due.en.html +10 -0
- package/dist/booking/templates/balance-due.pl.html +10 -0
- package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
- package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
- package/dist/booking/templates/booking-received.en.html +9 -0
- package/dist/booking/templates/booking-received.pl.html +9 -0
- package/dist/booking/templates/deposit-paid.en.html +6 -0
- package/dist/booking/templates/deposit-paid.pl.html +6 -0
- package/dist/booking/templates/payment-link.en.html +5 -0
- package/dist/booking/templates/payment-link.pl.html +5 -0
- package/dist/booking/templates/payment-received.en.html +10 -0
- package/dist/booking/templates/payment-received.pl.html +10 -0
- package/dist/booking/totals.d.ts +38 -0
- package/dist/booking/totals.js +61 -0
- package/dist/booking/webhook-logic.d.ts +28 -0
- package/dist/booking/webhook-logic.js +50 -0
- package/dist/cli/create-user.d.ts +7 -1
- package/dist/cli/create-user.js +23 -15
- package/dist/cli/index.js +55 -7
- package/dist/cli/scaffold/admin.d.ts +6 -0
- package/dist/cli/scaffold/admin.js +5 -3
- package/dist/cli/scaffold/booking.d.ts +12 -0
- package/dist/cli/scaffold/booking.js +103 -0
- package/dist/core/cms.d.ts +24 -9
- package/dist/core/cms.js +72 -34
- package/dist/core/config/validatePreviews.d.ts +12 -0
- package/dist/core/config/validatePreviews.js +90 -0
- package/dist/core/fields/dotPath.d.ts +4 -0
- package/dist/core/fields/dotPath.js +18 -0
- package/dist/core/fields/slugPath.d.ts +9 -1
- package/dist/core/fields/slugPath.js +16 -2
- package/dist/core/fields/slugify.d.ts +3 -0
- package/dist/core/fields/slugify.js +4 -0
- package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
- package/dist/core/server/entries/operations/duplicate.js +73 -0
- package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
- package/dist/core/server/entries/operations/slugNormalize.js +20 -0
- package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
- package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
- package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
- package/dist/core/server/fields/resolveRelationFields.js +12 -3
- package/dist/core/server/generator/generator.js +7 -2
- package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
- package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
- package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
- package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
- package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
- package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
- package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
- package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
- package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
- package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
- package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
- package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
- package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
- package/dist/db-postgres/schema/booking/bookings.js +40 -0
- package/dist/db-postgres/schema/booking/index.d.ts +7 -0
- package/dist/db-postgres/schema/booking/index.js +7 -0
- package/dist/db-postgres/schema/index.d.ts +1 -0
- package/dist/db-postgres/schema/index.js +1 -0
- package/dist/db-postgres/schema-booking.d.ts +1 -0
- package/dist/db-postgres/schema-booking.js +1 -0
- package/dist/server/security/rate-limit-handler.d.ts +19 -0
- package/dist/server/security/rate-limit-handler.js +34 -0
- package/dist/shop/adapters/payu/client.js +3 -1
- package/dist/shop/adapters/payu/index.js +37 -17
- package/dist/shop/adapters/payu/payload.d.ts +6 -0
- package/dist/shop/adapters/payu/payload.js +1 -1
- package/dist/shop/adapters/stripe/index.js +36 -21
- package/dist/shop/index.d.ts +11 -2
- package/dist/shop/index.js +43 -1
- package/dist/shop/rate-limit.d.ts +1 -9
- package/dist/shop/rate-limit.js +3 -28
- package/dist/shop/types.d.ts +19 -0
- package/dist/sveltekit/config.d.ts +4 -0
- package/dist/sveltekit/config.js +6 -0
- package/dist/types/cms.d.ts +15 -1
- package/dist/types/cms.schema.d.ts +96 -2
- package/dist/types/cms.schema.js +84 -1
- package/dist/types/config.d.ts +33 -0
- package/dist/types/layout.d.ts +6 -0
- package/dist/types/modules.d.ts +133 -0
- package/dist/types/modules.js +27 -0
- package/dist/updates/0.36.9/index.d.ts +2 -0
- package/dist/updates/0.36.9/index.js +12 -0
- package/dist/updates/0.37.0/index.d.ts +2 -0
- package/dist/updates/0.37.0/index.js +52 -0
- package/dist/updates/index.js +5 -1
- package/package.json +19 -1
- package/dist/admin/components/layout/nav-shop.svelte +0 -33
- package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking email template registry — file-based Handlebars with project override
|
|
3
|
+
* → package default → fallback-lang resolution. Self-contained (no shop / no
|
|
4
|
+
* CMS-singleton auto-fetch) to keep booking independent of the shop module.
|
|
5
|
+
*
|
|
6
|
+
* @internal — exported for testing; not part of the public API.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import Handlebars from 'handlebars';
|
|
12
|
+
const PACKAGE_TEMPLATES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates');
|
|
13
|
+
const cache = new Map();
|
|
14
|
+
let helpersRegistered = false;
|
|
15
|
+
/** Long pl-PL (or given locale) date; '' for null/invalid. @internal */
|
|
16
|
+
export function formatBookingDate(value, locale = 'pl-PL') {
|
|
17
|
+
const d = value instanceof Date
|
|
18
|
+
? value
|
|
19
|
+
: typeof value === 'string' || typeof value === 'number'
|
|
20
|
+
? new Date(value)
|
|
21
|
+
: null;
|
|
22
|
+
if (!d || Number.isNaN(d.getTime()))
|
|
23
|
+
return '';
|
|
24
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeZone: 'Europe/Warsaw' }).format(d);
|
|
25
|
+
}
|
|
26
|
+
/** Whole days from `now` to `value`, floored at 0; '' for invalid. @internal */
|
|
27
|
+
export function daysUntilDate(value, now) {
|
|
28
|
+
const parse = (v) => v instanceof Date ? v : typeof v === 'string' || typeof v === 'number' ? new Date(v) : null;
|
|
29
|
+
const d = parse(value);
|
|
30
|
+
const n = parse(now);
|
|
31
|
+
if (!d || Number.isNaN(d.getTime()) || !n || Number.isNaN(n.getTime()))
|
|
32
|
+
return '';
|
|
33
|
+
return String(Math.max(0, Math.ceil((d.getTime() - n.getTime()) / 86_400_000)));
|
|
34
|
+
}
|
|
35
|
+
function registerHelpers() {
|
|
36
|
+
if (helpersRegistered)
|
|
37
|
+
return;
|
|
38
|
+
Handlebars.registerHelper('currency', (value, currency) => {
|
|
39
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
40
|
+
const cur = typeof currency === 'string' ? currency : 'PLN';
|
|
41
|
+
if (!Number.isFinite(num))
|
|
42
|
+
return '';
|
|
43
|
+
// Amounts are minor units (grosze) → convert to major for display.
|
|
44
|
+
return new Intl.NumberFormat('pl-PL', { style: 'currency', currency: cur }).format(num / 100);
|
|
45
|
+
});
|
|
46
|
+
Handlebars.registerHelper('date', (value, locale) => formatBookingDate(value, typeof locale === 'string' ? locale : 'pl-PL'));
|
|
47
|
+
Handlebars.registerHelper('daysUntil', (value, now) => daysUntilDate(value, now));
|
|
48
|
+
helpersRegistered = true;
|
|
49
|
+
}
|
|
50
|
+
function resolveTemplateSource(name, lang, fallbackLang, projectDir) {
|
|
51
|
+
const candidates = [
|
|
52
|
+
path.join(projectDir, `${name}.${lang}.html`),
|
|
53
|
+
path.join(projectDir, `${name}.html`),
|
|
54
|
+
path.join(PACKAGE_TEMPLATES_DIR, `${name}.${lang}.html`),
|
|
55
|
+
path.join(PACKAGE_TEMPLATES_DIR, `${name}.${fallbackLang}.html`)
|
|
56
|
+
];
|
|
57
|
+
for (const filePath of candidates) {
|
|
58
|
+
try {
|
|
59
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// try next candidate
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`[booking] No email template for "${name}" (lang=${lang}, fallback=${fallbackLang}). ` +
|
|
66
|
+
`Looked in "${projectDir}" and package defaults.`);
|
|
67
|
+
}
|
|
68
|
+
const partialSourceCache = new Map();
|
|
69
|
+
function readPartialSource(projectDir, name, lang, fallbackLang) {
|
|
70
|
+
const key = `${projectDir}|${name}|${lang}|${fallbackLang}`;
|
|
71
|
+
if (process.env.NODE_ENV === 'production' && partialSourceCache.has(key)) {
|
|
72
|
+
return partialSourceCache.get(key);
|
|
73
|
+
}
|
|
74
|
+
const candidates = [
|
|
75
|
+
path.join(projectDir, '_partials', `${name}.${lang}.html`),
|
|
76
|
+
path.join(projectDir, '_partials', `${name}.${fallbackLang}.html`),
|
|
77
|
+
path.join(PACKAGE_TEMPLATES_DIR, '_partials', `${name}.${lang}.html`),
|
|
78
|
+
path.join(PACKAGE_TEMPLATES_DIR, '_partials', `${name}.${fallbackLang}.html`)
|
|
79
|
+
];
|
|
80
|
+
for (const filePath of candidates) {
|
|
81
|
+
try {
|
|
82
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
83
|
+
partialSourceCache.set(key, source);
|
|
84
|
+
return source;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// try next candidate
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
partialSourceCache.set(key, null);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Register `header`/`footer` partials for a render. Re-registers on every call
|
|
95
|
+
* (cheap — source is cached in prod) so booking's partials always win over the
|
|
96
|
+
* shop module's identically-named partials right before a booking render.
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
export function registerBookingPartials(projectDir, lang, fallbackLang) {
|
|
100
|
+
for (const name of ['header', 'footer']) {
|
|
101
|
+
const source = readPartialSource(projectDir, name, lang, fallbackLang);
|
|
102
|
+
if (source != null)
|
|
103
|
+
Handlebars.registerPartial(name, source);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Render a booking email template by name + language. Project overrides live in
|
|
108
|
+
* `<projectDir>/<name>.<lang>.html` (default `src/emails/booking`); package
|
|
109
|
+
* defaults ship under `booking/templates`.
|
|
110
|
+
*/
|
|
111
|
+
export function renderEmailTemplate(name, lang, context, opts = {}) {
|
|
112
|
+
registerHelpers();
|
|
113
|
+
const fallbackLang = opts.fallbackLang ?? 'pl';
|
|
114
|
+
const projectDir = opts.projectDir ?? path.join(process.cwd(), 'src/emails/booking');
|
|
115
|
+
registerBookingPartials(projectDir, lang, fallbackLang);
|
|
116
|
+
const key = `${projectDir}|${name}|${lang}|${fallbackLang}`;
|
|
117
|
+
let compiled = cache.get(key);
|
|
118
|
+
if (!compiled) {
|
|
119
|
+
const source = resolveTemplateSource(name, lang, fallbackLang, projectDir);
|
|
120
|
+
compiled = Handlebars.compile(source, { noEscape: false });
|
|
121
|
+
if (process.env.NODE_ENV === 'production')
|
|
122
|
+
cache.set(key, compiled);
|
|
123
|
+
}
|
|
124
|
+
return compiled(context);
|
|
125
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { getCMS } from '../../../core/cms.js';
|
|
3
|
+
import { checkRateLimit, clientKey } from '../../../server/security/rate-limit-handler.js';
|
|
4
|
+
import { createBooking } from '../bookings.js';
|
|
5
|
+
import { createDepositPayment } from '../payments.js';
|
|
6
|
+
function bookingEnabled() {
|
|
7
|
+
try {
|
|
8
|
+
return getCMS().hasModule('booking');
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Public self-serve booking creation. Mount at `POST /api/booking`. */
|
|
15
|
+
export function createBookingHandler() {
|
|
16
|
+
return {
|
|
17
|
+
POST: async ({ request }) => {
|
|
18
|
+
if (!bookingEnabled())
|
|
19
|
+
return json({ error: 'Booking not enabled' }, { status: 404 });
|
|
20
|
+
const rl = checkRateLimit(clientKey(request, 'booking-create'), { limit: 5, windowSec: 60 });
|
|
21
|
+
if (!rl.allowed)
|
|
22
|
+
return json({ error: 'Rate limit exceeded' }, { status: 429 });
|
|
23
|
+
const body = (await request.json().catch(() => null));
|
|
24
|
+
if (!body || !Array.isArray(body.items) || body.items.length === 0) {
|
|
25
|
+
return json({ error: 'items required' }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const persons = Array.isArray(body.persons)
|
|
29
|
+
? body.persons.map((p) => ({
|
|
30
|
+
ref: String(p.ref),
|
|
31
|
+
identity: p.identity && typeof p.identity === 'object' ? p.identity : {}
|
|
32
|
+
}))
|
|
33
|
+
: [];
|
|
34
|
+
const items = body.items.map((it) => ({
|
|
35
|
+
tripRef: String(it.tripRef),
|
|
36
|
+
unitPrice: Number(it.unitPrice) || 0,
|
|
37
|
+
departureDate: it.departureDate ? new Date(it.departureDate) : null,
|
|
38
|
+
assignments: Array.isArray(it.assignments)
|
|
39
|
+
? it.assignments.map((a) => ({
|
|
40
|
+
personRef: String(a.personRef),
|
|
41
|
+
data: a.data && typeof a.data === 'object' ? a.data : undefined
|
|
42
|
+
}))
|
|
43
|
+
: []
|
|
44
|
+
}));
|
|
45
|
+
const { booking, accessToken } = await createBooking({
|
|
46
|
+
customer: body.customer ?? {},
|
|
47
|
+
persons,
|
|
48
|
+
items,
|
|
49
|
+
language: body.language
|
|
50
|
+
});
|
|
51
|
+
const payment = await createDepositPayment(booking.id);
|
|
52
|
+
return json({
|
|
53
|
+
bookingId: booking.id,
|
|
54
|
+
number: booking.number,
|
|
55
|
+
accessToken,
|
|
56
|
+
status: booking.status,
|
|
57
|
+
totals: booking.totals,
|
|
58
|
+
paymentStatus: payment.status,
|
|
59
|
+
requiresPaymentRedirect: payment.status === 'redirect',
|
|
60
|
+
redirectUrl: payment.redirectUrl
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return json({ error: err instanceof Error ? err.message : 'Booking failed' }, { status: 400 });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Secured external trigger for booking maintenance — the swap-in for the
|
|
4
|
+
* in-process loop (e.g. a system cron hitting this endpoint). Requires
|
|
5
|
+
* `Authorization: Bearer <maintenanceToken>`. Mount at `POST /api/booking/maintenance`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createMaintenanceHandler(): {
|
|
8
|
+
POST: RequestHandler;
|
|
9
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { requireBookingConfig } from '../db.js';
|
|
3
|
+
import { processDueReminders, processDueExpirations } from '../maintenance.js';
|
|
4
|
+
/** Constant-time string compare (avoids leaking the token via timing). */
|
|
5
|
+
function safeEqual(a, b) {
|
|
6
|
+
if (a.length !== b.length)
|
|
7
|
+
return false;
|
|
8
|
+
let r = 0;
|
|
9
|
+
for (let i = 0; i < a.length; i++)
|
|
10
|
+
r |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
11
|
+
return r === 0;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Secured external trigger for booking maintenance — the swap-in for the
|
|
15
|
+
* in-process loop (e.g. a system cron hitting this endpoint). Requires
|
|
16
|
+
* `Authorization: Bearer <maintenanceToken>`. Mount at `POST /api/booking/maintenance`.
|
|
17
|
+
*/
|
|
18
|
+
export function createMaintenanceHandler() {
|
|
19
|
+
return {
|
|
20
|
+
POST: async ({ request }) => {
|
|
21
|
+
const config = requireBookingConfig();
|
|
22
|
+
if (!config.maintenanceToken) {
|
|
23
|
+
return json({ error: 'Maintenance trigger disabled' }, { status: 404 });
|
|
24
|
+
}
|
|
25
|
+
const auth = request.headers.get('authorization') ?? '';
|
|
26
|
+
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
27
|
+
if (!safeEqual(token, config.maintenanceToken)) {
|
|
28
|
+
return json({ error: 'Unauthorized' }, { status: 401 });
|
|
29
|
+
}
|
|
30
|
+
const now = new Date();
|
|
31
|
+
const reminders = await processDueReminders(now);
|
|
32
|
+
const expirations = await processDueExpirations(now);
|
|
33
|
+
return json({ ok: true, reminders, ...expirations });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Self-service portal. Mount at `GET|POST /api/booking/portal/[token]`.
|
|
4
|
+
* Drugi czynnik (D9): token w linku = 1. czynnik; numer telefonu = 2. GET i
|
|
5
|
+
* wszystkie akcje poza `verify-phone` wymagają ważnego podpisanego cookie sesji
|
|
6
|
+
* (`?action=verify-phone` ustawia je po dopasowaniu numeru). Bez skonfigurowanego
|
|
7
|
+
* `portalSecret` portal jest fail-closed.
|
|
8
|
+
* `POST ?action=edit|pay|participant` jak dotąd; below-paid/terminal → 409.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createPortalHandler(): {
|
|
11
|
+
GET: RequestHandler;
|
|
12
|
+
POST: RequestHandler;
|
|
13
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { checkRateLimit, resetRateLimit, clientKey } from '../../../server/security/rate-limit-handler.js';
|
|
3
|
+
import { portalView, portalEdit, portalPay, portalUpdateParticipant, verifyPhone, portalAddTrips } from '../portal.js';
|
|
4
|
+
import { signSession, verifySession, portalCookieName } from '../portal-session.js';
|
|
5
|
+
import { requireBookingConfig } from '../db.js';
|
|
6
|
+
import { LedgerError } from '../../ledger.js';
|
|
7
|
+
import { BookingEditError } from '../../recompute.js';
|
|
8
|
+
import { BookingValidationError } from '../validation.js';
|
|
9
|
+
const SESSION_TTL = 7200;
|
|
10
|
+
/**
|
|
11
|
+
* Self-service portal. Mount at `GET|POST /api/booking/portal/[token]`.
|
|
12
|
+
* Drugi czynnik (D9): token w linku = 1. czynnik; numer telefonu = 2. GET i
|
|
13
|
+
* wszystkie akcje poza `verify-phone` wymagają ważnego podpisanego cookie sesji
|
|
14
|
+
* (`?action=verify-phone` ustawia je po dopasowaniu numeru). Bez skonfigurowanego
|
|
15
|
+
* `portalSecret` portal jest fail-closed.
|
|
16
|
+
* `POST ?action=edit|pay|participant` jak dotąd; below-paid/terminal → 409.
|
|
17
|
+
*/
|
|
18
|
+
export function createPortalHandler() {
|
|
19
|
+
const secret = () => requireBookingConfig().portalSecret;
|
|
20
|
+
const isVerified = (cookies, token) => {
|
|
21
|
+
const s = secret();
|
|
22
|
+
if (!s)
|
|
23
|
+
return false;
|
|
24
|
+
return verifySession(cookies.get(portalCookieName(token)) ?? '', token, Math.floor(Date.now() / 1000), s);
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
GET: async ({ params, cookies }) => {
|
|
28
|
+
const token = params.token ?? '';
|
|
29
|
+
if (!isVerified(cookies, token)) {
|
|
30
|
+
return json({ error: 'Wymagana weryfikacja', code: 'PHONE_REQUIRED' }, { status: 401 });
|
|
31
|
+
}
|
|
32
|
+
const view = await portalView(token);
|
|
33
|
+
return view ? json(view) : json({ error: 'Not found' }, { status: 404 });
|
|
34
|
+
},
|
|
35
|
+
POST: async ({ params, request, url, cookies }) => {
|
|
36
|
+
const rl = checkRateLimit(clientKey(request, 'booking-portal'), { limit: 10, windowSec: 60 });
|
|
37
|
+
if (!rl.allowed)
|
|
38
|
+
return json({ error: 'Rate limit exceeded' }, { status: 429 });
|
|
39
|
+
const token = params.token ?? '';
|
|
40
|
+
const action = url.searchParams.get('action');
|
|
41
|
+
const body = (await request.json().catch(() => ({})));
|
|
42
|
+
if (action === 'verify-phone') {
|
|
43
|
+
const s = secret();
|
|
44
|
+
if (!s)
|
|
45
|
+
return json({ error: 'Portal not configured' }, { status: 503 });
|
|
46
|
+
const lock = checkRateLimit(`portal-verify:${token}`, { limit: 5, windowSec: 600 });
|
|
47
|
+
if (!lock.allowed)
|
|
48
|
+
return json({ error: 'Za dużo prób', code: 'LOCKED' }, { status: 429 });
|
|
49
|
+
const booking = await verifyPhone(token, String(body.phone ?? ''));
|
|
50
|
+
if (!booking)
|
|
51
|
+
return json({ error: 'Numer nie pasuje', code: 'PHONE_MISMATCH' }, { status: 401 });
|
|
52
|
+
resetRateLimit(`portal-verify:${token}`);
|
|
53
|
+
cookies.set(portalCookieName(token), signSession(token, Math.floor(Date.now() / 1000), SESSION_TTL, s), {
|
|
54
|
+
httpOnly: true,
|
|
55
|
+
sameSite: 'lax',
|
|
56
|
+
secure: url.protocol === 'https:',
|
|
57
|
+
path: '/api/booking/portal',
|
|
58
|
+
maxAge: SESSION_TTL
|
|
59
|
+
});
|
|
60
|
+
const view = await portalView(token);
|
|
61
|
+
return view ? json(view) : json({ error: 'Not found' }, { status: 404 });
|
|
62
|
+
}
|
|
63
|
+
if (!isVerified(cookies, token)) {
|
|
64
|
+
return json({ error: 'Wymagana weryfikacja', code: 'PHONE_REQUIRED' }, { status: 401 });
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
if (action === 'pay') {
|
|
68
|
+
const out = await portalPay(token, Number(body.amount) || 0);
|
|
69
|
+
return out ? json(out) : json({ error: 'Not found' }, { status: 404 });
|
|
70
|
+
}
|
|
71
|
+
if (action === 'edit') {
|
|
72
|
+
const out = await portalEdit(token, body);
|
|
73
|
+
return out ? json(out) : json({ error: 'Not found' }, { status: 404 });
|
|
74
|
+
}
|
|
75
|
+
if (action === 'participant') {
|
|
76
|
+
const out = await portalUpdateParticipant(token, String(body.participantId ?? ''), (body.data ?? {}));
|
|
77
|
+
return out ? json(out) : json({ error: 'Not found' }, { status: 404 });
|
|
78
|
+
}
|
|
79
|
+
if (action === 'add-trips') {
|
|
80
|
+
const additions = Array.isArray(body.additions)
|
|
81
|
+
? body.additions
|
|
82
|
+
: [];
|
|
83
|
+
const out = await portalAddTrips(token, additions);
|
|
84
|
+
return out ? json(out) : json({ error: 'Not found' }, { status: 404 });
|
|
85
|
+
}
|
|
86
|
+
return json({ error: 'Unknown action' }, { status: 400 });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (err instanceof LedgerError || err instanceof BookingEditError) {
|
|
90
|
+
return json({ error: err.message, code: 'CONTACT_STAFF' }, { status: 409 });
|
|
91
|
+
}
|
|
92
|
+
if (err instanceof BookingValidationError) {
|
|
93
|
+
return json({ error: err.message, code: err.code }, { status: 400 });
|
|
94
|
+
}
|
|
95
|
+
return json({ error: err instanceof Error ? err.message : 'Edit failed' }, { status: 400 });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Payment-provider webhook. Mount at `POST /api/booking/webhook/[provider]`.
|
|
4
|
+
* Delegates parsing to the matching adapter's `handleWebhook`, applies the event
|
|
5
|
+
* idempotently, then sends the right confirmation email on a settled payment.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createWebhookHandler(): {
|
|
8
|
+
POST: RequestHandler;
|
|
9
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { getCMS } from '../../../core/cms.js';
|
|
3
|
+
import { requireBookingConfig } from '../db.js';
|
|
4
|
+
import { applyWebhookPayment } from '../payments.js';
|
|
5
|
+
import { sendBookingEmail } from '../email.js';
|
|
6
|
+
/**
|
|
7
|
+
* Payment-provider webhook. Mount at `POST /api/booking/webhook/[provider]`.
|
|
8
|
+
* Delegates parsing to the matching adapter's `handleWebhook`, applies the event
|
|
9
|
+
* idempotently, then sends the right confirmation email on a settled payment.
|
|
10
|
+
*/
|
|
11
|
+
export function createWebhookHandler() {
|
|
12
|
+
return {
|
|
13
|
+
POST: async ({ request, params }) => {
|
|
14
|
+
try {
|
|
15
|
+
getCMS();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return json({ error: 'not ready' }, { status: 503 });
|
|
19
|
+
}
|
|
20
|
+
const config = requireBookingConfig();
|
|
21
|
+
const adapter = config.payment.find((a) => a.id === params.provider);
|
|
22
|
+
if (!adapter?.handleWebhook)
|
|
23
|
+
return json({ error: 'Unknown provider' }, { status: 404 });
|
|
24
|
+
let event;
|
|
25
|
+
try {
|
|
26
|
+
event = await adapter.handleWebhook(request);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return json({ error: err instanceof Error ? err.message : 'bad webhook' }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
const { applied, bookingId, kind } = await applyWebhookPayment(event);
|
|
32
|
+
if (applied && bookingId) {
|
|
33
|
+
const emailKind = kind === 'deposit' ? 'deposit-paid' : 'payment-received';
|
|
34
|
+
await sendBookingEmail(bookingId, emailKind).catch((e) => console.error('[booking] email failed', e));
|
|
35
|
+
}
|
|
36
|
+
return json({ ok: true });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './db.js';
|
|
2
|
+
export * from './bookings.js';
|
|
3
|
+
export * from './payments.js';
|
|
4
|
+
export * from './adjustments.js';
|
|
5
|
+
export * from './refunds.js';
|
|
6
|
+
export * from './portal.js';
|
|
7
|
+
export * from './reminders.js';
|
|
8
|
+
export * from './maintenance.js';
|
|
9
|
+
export * from './validation.js';
|
|
10
|
+
export * from './catalog.js';
|
|
11
|
+
export * from './email.js';
|
|
12
|
+
export * from './http/index.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Node-only server barrel for the booking module → `includio-cms/booking/server`.
|
|
2
|
+
// Keeps server services + HTTP handlers out of any client bundle.
|
|
3
|
+
export * from './db.js';
|
|
4
|
+
export * from './bookings.js';
|
|
5
|
+
export * from './payments.js';
|
|
6
|
+
export * from './adjustments.js';
|
|
7
|
+
export * from './refunds.js';
|
|
8
|
+
export * from './portal.js';
|
|
9
|
+
export * from './reminders.js';
|
|
10
|
+
export * from './maintenance.js';
|
|
11
|
+
export * from './validation.js';
|
|
12
|
+
export * from './catalog.js';
|
|
13
|
+
export * from './email.js';
|
|
14
|
+
export * from './http/index.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send all due reminders (unsent, `scheduledFor <= now`) via the email adapter,
|
|
3
|
+
* stamping `sentAt` so they never re-send. Returns the count sent. Idempotent.
|
|
4
|
+
*/
|
|
5
|
+
export declare function processDueReminders(now?: Date): Promise<number>;
|
|
6
|
+
/**
|
|
7
|
+
* Auto-cancel expired bookings: abandoned drafts (deposit hold lapsed) and
|
|
8
|
+
* balance-overdue bookings (deposit forfeited + cancellation email). Idempotent.
|
|
9
|
+
*/
|
|
10
|
+
export declare function processDueExpirations(now?: Date): Promise<{
|
|
11
|
+
abandoned: number;
|
|
12
|
+
forfeited: number;
|
|
13
|
+
}>;
|
|
14
|
+
/** Run one maintenance pass (reminders + expirations), guarded against overlap. */
|
|
15
|
+
export declare function runBookingMaintenance(now?: Date): Promise<void>;
|
|
16
|
+
/** Start the in-process loop (config-gated). Idempotent. Called from serverHooks.init. */
|
|
17
|
+
export declare function startBookingMaintenance(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Conventional server-init entry run by `initCMS` via `serverHooks.initModule`
|
|
20
|
+
* (see the booking module in `../index.ts`). Server-only — kept out of the
|
|
21
|
+
* client-safe barrel so browser builds don't pull node:fs/url.
|
|
22
|
+
*/
|
|
23
|
+
export declare function serverInit(): void;
|
|
24
|
+
export declare function stopBookingMaintenance(): void;
|
|
25
|
+
export declare function getBookingMaintenanceStatus(): {
|
|
26
|
+
running: boolean;
|
|
27
|
+
lastRun: Date | null;
|
|
28
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { eq, inArray, isNull } from 'drizzle-orm';
|
|
2
|
+
import { getCMS } from '../../core/cms.js';
|
|
3
|
+
import { getBookingDb } from './db.js';
|
|
4
|
+
import { bookingsTable, bookingRemindersTable } from '../../db-postgres/schema/booking/index.js';
|
|
5
|
+
import { selectDueReminders, decideExpirations } from '../expiry-logic.js';
|
|
6
|
+
import { sendBookingEmail } from './email.js';
|
|
7
|
+
/**
|
|
8
|
+
* Send all due reminders (unsent, `scheduledFor <= now`) via the email adapter,
|
|
9
|
+
* stamping `sentAt` so they never re-send. Returns the count sent. Idempotent.
|
|
10
|
+
*/
|
|
11
|
+
export async function processDueReminders(now = new Date()) {
|
|
12
|
+
const db = getBookingDb();
|
|
13
|
+
const rows = await db
|
|
14
|
+
.select()
|
|
15
|
+
.from(bookingRemindersTable)
|
|
16
|
+
.where(isNull(bookingRemindersTable.sentAt));
|
|
17
|
+
const dueIds = selectDueReminders(rows.map((r) => ({ id: r.id, scheduledFor: r.scheduledFor, sentAt: r.sentAt })), now);
|
|
18
|
+
let sent = 0;
|
|
19
|
+
for (const id of dueIds) {
|
|
20
|
+
const row = rows.find((r) => r.id === id);
|
|
21
|
+
if (!row)
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
await sendBookingEmail(row.bookingId, row.templateKey);
|
|
25
|
+
await db
|
|
26
|
+
.update(bookingRemindersTable)
|
|
27
|
+
.set({ sentAt: now })
|
|
28
|
+
.where(eq(bookingRemindersTable.id, id));
|
|
29
|
+
sent++;
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
console.error('[booking] reminder send failed', id, e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return sent;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Auto-cancel expired bookings: abandoned drafts (deposit hold lapsed) and
|
|
39
|
+
* balance-overdue bookings (deposit forfeited + cancellation email). Idempotent.
|
|
40
|
+
*/
|
|
41
|
+
export async function processDueExpirations(now = new Date()) {
|
|
42
|
+
const db = getBookingDb();
|
|
43
|
+
const active = await db.select().from(bookingsTable).where(isNull(bookingsTable.deletedAt));
|
|
44
|
+
const { abandon, forfeit } = decideExpirations(active.map((b) => ({
|
|
45
|
+
id: b.id,
|
|
46
|
+
status: b.status,
|
|
47
|
+
holdExpiresAt: b.holdExpiresAt,
|
|
48
|
+
balanceDueAt: b.balanceDueAt,
|
|
49
|
+
totals: { balance: b.totals.balance }
|
|
50
|
+
})), now);
|
|
51
|
+
if (abandon.length > 0) {
|
|
52
|
+
await db
|
|
53
|
+
.update(bookingsTable)
|
|
54
|
+
.set({ status: 'cancelled', cancelReason: 'abandoned', updatedAt: now })
|
|
55
|
+
.where(inArray(bookingsTable.id, abandon));
|
|
56
|
+
}
|
|
57
|
+
for (const id of forfeit) {
|
|
58
|
+
await db
|
|
59
|
+
.update(bookingsTable)
|
|
60
|
+
.set({ status: 'cancelled', cancelReason: 'balance_overdue', updatedAt: now })
|
|
61
|
+
.where(eq(bookingsTable.id, id));
|
|
62
|
+
await sendBookingEmail(id, 'balance-overdue-cancelled').catch((e) => console.error('[booking] overdue email failed', e));
|
|
63
|
+
}
|
|
64
|
+
return { abandoned: abandon.length, forfeited: forfeit.length };
|
|
65
|
+
}
|
|
66
|
+
// --- In-process maintenance loop (no infra; secured endpoint is the swap-in) ---
|
|
67
|
+
const STATE_KEY = Symbol.for('includio.booking.maintenance.state');
|
|
68
|
+
function loopState() {
|
|
69
|
+
const g = globalThis;
|
|
70
|
+
if (!g[STATE_KEY])
|
|
71
|
+
g[STATE_KEY] = { timer: null, running: false, lastRun: null };
|
|
72
|
+
return g[STATE_KEY];
|
|
73
|
+
}
|
|
74
|
+
/** Run one maintenance pass (reminders + expirations), guarded against overlap. */
|
|
75
|
+
export async function runBookingMaintenance(now = new Date()) {
|
|
76
|
+
const st = loopState();
|
|
77
|
+
if (st.running)
|
|
78
|
+
return;
|
|
79
|
+
st.running = true;
|
|
80
|
+
try {
|
|
81
|
+
await processDueReminders(now);
|
|
82
|
+
await processDueExpirations(now);
|
|
83
|
+
st.lastRun = now;
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
console.warn('[booking-maintenance] error', e);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
st.running = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Start the in-process loop (config-gated). Idempotent. Called from serverHooks.init. */
|
|
93
|
+
export function startBookingMaintenance() {
|
|
94
|
+
const st = loopState();
|
|
95
|
+
if (st.timer)
|
|
96
|
+
return;
|
|
97
|
+
let cfg = null;
|
|
98
|
+
try {
|
|
99
|
+
cfg = getCMS().getModuleConfig('booking') ?? null;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!cfg || cfg.maintenance.autoRun === false)
|
|
105
|
+
return;
|
|
106
|
+
const ms = cfg.maintenance.intervalMinutes * 60_000;
|
|
107
|
+
st.timer = setInterval(() => void runBookingMaintenance(), ms);
|
|
108
|
+
console.info(`[booking-maintenance] scheduled every ${cfg.maintenance.intervalMinutes}m`);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Conventional server-init entry run by `initCMS` via `serverHooks.initModule`
|
|
112
|
+
* (see the booking module in `../index.ts`). Server-only — kept out of the
|
|
113
|
+
* client-safe barrel so browser builds don't pull node:fs/url.
|
|
114
|
+
*/
|
|
115
|
+
export function serverInit() {
|
|
116
|
+
startBookingMaintenance();
|
|
117
|
+
}
|
|
118
|
+
export function stopBookingMaintenance() {
|
|
119
|
+
const st = loopState();
|
|
120
|
+
if (st.timer) {
|
|
121
|
+
clearInterval(st.timer);
|
|
122
|
+
st.timer = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function getBookingMaintenanceStatus() {
|
|
126
|
+
const st = loopState();
|
|
127
|
+
return { running: st.running, lastRun: st.lastRun };
|
|
128
|
+
}
|
|
129
|
+
// Clean up the timer across Vite HMR so it doesn't accumulate.
|
|
130
|
+
if (import.meta.hot) {
|
|
131
|
+
import.meta.hot.dispose(() => {
|
|
132
|
+
stopBookingMaintenance();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type FieldDef } from '../participant-fields/index.js';
|
|
2
|
+
/** Who is editing — gates which fields a write may touch (see {@link canFill}). */
|
|
3
|
+
export type EditScope = 'staff' | 'customer';
|
|
4
|
+
/** Thrown when a write violates field types/min/max/option. Missing required is soft. */
|
|
5
|
+
export declare class ParticipantValidationError extends Error {
|
|
6
|
+
errors: Array<{
|
|
7
|
+
key: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}>;
|
|
10
|
+
constructor(errors: Array<{
|
|
11
|
+
key: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}>);
|
|
14
|
+
}
|
|
15
|
+
/** Resolve the participant-field schema for a booking item's catalog entry. */
|
|
16
|
+
export declare function resolveItemSchema(tripRef: string): Promise<FieldDef[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Merge a partial patch into a person's assignment, scoped to fields the
|
|
19
|
+
* `scope` audience may fill. Identity fields (role name/birthDate) are written
|
|
20
|
+
* once to the person row (shared across trips); the rest to this trip's
|
|
21
|
+
* assignment. Validates types/min/max/option (hard reject); missing required is
|
|
22
|
+
* allowed (soft status, surfaced in the UI elsewhere).
|
|
23
|
+
*
|
|
24
|
+
* @param assignmentId - `booking_item_persons.id` (the person-on-this-trip row).
|
|
25
|
+
*/
|
|
26
|
+
export declare function saveParticipantData(assignmentId: string, incoming: Record<string, unknown>, scope: EditScope): Promise<void>;
|