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.
Files changed (259) hide show
  1. package/API.md +179 -3
  2. package/CHANGELOG.md +64 -0
  3. package/DOCS.md +28 -3
  4. package/ROADMAP.md +18 -0
  5. package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
  6. package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
  7. package/dist/admin/client/booking/customer-panel.svelte +102 -0
  8. package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
  9. package/dist/admin/client/booking/customer-view.d.ts +52 -0
  10. package/dist/admin/client/booking/customer-view.js +134 -0
  11. package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
  12. package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
  13. package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
  14. package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
  15. package/dist/admin/client/booking/reservations-page.svelte +221 -0
  16. package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
  17. package/dist/admin/client/collection/collection-entries.svelte +21 -2
  18. package/dist/admin/client/collection/entry-link.svelte +6 -4
  19. package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
  20. package/dist/admin/client/collection/row-actions.svelte +12 -4
  21. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
  22. package/dist/admin/client/collection/status-badge.svelte +39 -3
  23. package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
  24. package/dist/admin/client/entry/entry.svelte +26 -9
  25. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
  26. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
  27. package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
  28. package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
  29. package/dist/admin/client/entry/preview-views.d.ts +27 -0
  30. package/dist/admin/client/entry/preview-views.js +33 -0
  31. package/dist/admin/client/index.d.ts +3 -0
  32. package/dist/admin/client/index.js +3 -0
  33. package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
  34. package/dist/admin/client/modules/dashboard-registry.js +11 -0
  35. package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
  36. package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
  37. package/dist/admin/client/modules/stat-card.svelte +22 -0
  38. package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
  39. package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
  40. package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
  41. package/dist/admin/components/fields/relation-field.svelte +11 -2
  42. package/dist/admin/components/icons/icon-map.js +8 -0
  43. package/dist/admin/components/layout/app-sidebar.svelte +45 -14
  44. package/dist/admin/components/layout/context-switcher.svelte +90 -0
  45. package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
  46. package/dist/admin/components/layout/lang.d.ts +4 -0
  47. package/dist/admin/components/layout/lang.js +8 -0
  48. package/dist/admin/components/layout/layout-renderer.svelte +2 -1
  49. package/dist/admin/components/layout/layout-tabs.svelte +14 -0
  50. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
  51. package/dist/admin/components/layout/nav-collections.svelte +16 -9
  52. package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
  53. package/dist/admin/components/layout/nav-global.svelte +45 -0
  54. package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
  55. package/dist/admin/components/layout/nav-main.svelte +8 -32
  56. package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
  57. package/dist/admin/components/layout/nav-module.svelte +57 -0
  58. package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
  59. package/dist/admin/components/layout/nav-section.svelte +1 -1
  60. package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
  61. package/dist/admin/components/layout/nav-singletons.svelte +14 -7
  62. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
  63. package/dist/admin/remote/booking.remote.d.ts +244 -0
  64. package/dist/admin/remote/booking.remote.js +283 -0
  65. package/dist/admin/remote/entry.remote.d.ts +3 -0
  66. package/dist/admin/remote/entry.remote.js +14 -3
  67. package/dist/admin/remote/index.d.ts +2 -0
  68. package/dist/admin/remote/index.js +2 -0
  69. package/dist/admin/remote/modules.remote.d.ts +10 -0
  70. package/dist/admin/remote/modules.remote.js +14 -0
  71. package/dist/admin/state/active-context-resolve.d.ts +37 -0
  72. package/dist/admin/state/active-context-resolve.js +66 -0
  73. package/dist/admin/state/active-context.svelte.d.ts +8 -0
  74. package/dist/admin/state/active-context.svelte.js +27 -0
  75. package/dist/booking/adjustments.d.ts +33 -0
  76. package/dist/booking/adjustments.js +26 -0
  77. package/dist/booking/capacity.d.ts +12 -0
  78. package/dist/booking/capacity.js +10 -0
  79. package/dist/booking/client/index.d.ts +39 -0
  80. package/dist/booking/client/index.js +44 -0
  81. package/dist/booking/client/use-booking.svelte.d.ts +25 -0
  82. package/dist/booking/client/use-booking.svelte.js +56 -0
  83. package/dist/booking/config.d.ts +156 -0
  84. package/dist/booking/config.js +36 -0
  85. package/dist/booking/draft.d.ts +77 -0
  86. package/dist/booking/draft.js +67 -0
  87. package/dist/booking/expiry-logic.d.ts +30 -0
  88. package/dist/booking/expiry-logic.js +33 -0
  89. package/dist/booking/index.d.ts +18 -0
  90. package/dist/booking/index.js +52 -0
  91. package/dist/booking/ledger.d.ts +46 -0
  92. package/dist/booking/ledger.js +60 -0
  93. package/dist/booking/number.d.ts +2 -0
  94. package/dist/booking/number.js +11 -0
  95. package/dist/booking/participant-fields/access.d.ts +10 -0
  96. package/dist/booking/participant-fields/access.js +14 -0
  97. package/dist/booking/participant-fields/age.d.ts +12 -0
  98. package/dist/booking/participant-fields/age.js +28 -0
  99. package/dist/booking/participant-fields/completeness.d.ts +8 -0
  100. package/dist/booking/participant-fields/completeness.js +13 -0
  101. package/dist/booking/participant-fields/group.d.ts +6 -0
  102. package/dist/booking/participant-fields/group.js +15 -0
  103. package/dist/booking/participant-fields/index.d.ts +6 -0
  104. package/dist/booking/participant-fields/index.js +6 -0
  105. package/dist/booking/participant-fields/types.d.ts +50 -0
  106. package/dist/booking/participant-fields/types.js +5 -0
  107. package/dist/booking/participant-fields/validate.d.ts +7 -0
  108. package/dist/booking/participant-fields/validate.js +44 -0
  109. package/dist/booking/participant-pricing.d.ts +93 -0
  110. package/dist/booking/participant-pricing.js +91 -0
  111. package/dist/booking/participants-view.d.ts +32 -0
  112. package/dist/booking/participants-view.js +22 -0
  113. package/dist/booking/payment-context.d.ts +19 -0
  114. package/dist/booking/payment-context.js +29 -0
  115. package/dist/booking/pricing-inputs.d.ts +28 -0
  116. package/dist/booking/pricing-inputs.js +35 -0
  117. package/dist/booking/pricing.d.ts +86 -0
  118. package/dist/booking/pricing.js +125 -0
  119. package/dist/booking/recompute.d.ts +22 -0
  120. package/dist/booking/recompute.js +31 -0
  121. package/dist/booking/reminders-schedule.d.ts +11 -0
  122. package/dist/booking/reminders-schedule.js +28 -0
  123. package/dist/booking/server/adjustments.d.ts +27 -0
  124. package/dist/booking/server/adjustments.js +136 -0
  125. package/dist/booking/server/bookings.d.ts +45 -0
  126. package/dist/booking/server/bookings.js +288 -0
  127. package/dist/booking/server/catalog.d.ts +9 -0
  128. package/dist/booking/server/catalog.js +20 -0
  129. package/dist/booking/server/db.d.ts +7 -0
  130. package/dist/booking/server/db.js +18 -0
  131. package/dist/booking/server/email.d.ts +23 -0
  132. package/dist/booking/server/email.js +59 -0
  133. package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
  134. package/dist/booking/server/emailTemplateRegistry.js +125 -0
  135. package/dist/booking/server/http/create-handler.d.ts +5 -0
  136. package/dist/booking/server/http/create-handler.js +68 -0
  137. package/dist/booking/server/http/index.d.ts +4 -0
  138. package/dist/booking/server/http/index.js +4 -0
  139. package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
  140. package/dist/booking/server/http/maintenance-handler.js +36 -0
  141. package/dist/booking/server/http/portal-handler.d.ts +13 -0
  142. package/dist/booking/server/http/portal-handler.js +99 -0
  143. package/dist/booking/server/http/webhook-handler.d.ts +9 -0
  144. package/dist/booking/server/http/webhook-handler.js +39 -0
  145. package/dist/booking/server/index.d.ts +12 -0
  146. package/dist/booking/server/index.js +14 -0
  147. package/dist/booking/server/maintenance.d.ts +28 -0
  148. package/dist/booking/server/maintenance.js +134 -0
  149. package/dist/booking/server/participants.d.ts +26 -0
  150. package/dist/booking/server/participants.js +92 -0
  151. package/dist/booking/server/payments.d.ts +34 -0
  152. package/dist/booking/server/payments.js +207 -0
  153. package/dist/booking/server/portal-session.d.ts +16 -0
  154. package/dist/booking/server/portal-session.js +42 -0
  155. package/dist/booking/server/portal.d.ts +207 -0
  156. package/dist/booking/server/portal.js +234 -0
  157. package/dist/booking/server/refunds.d.ts +7 -0
  158. package/dist/booking/server/refunds.js +58 -0
  159. package/dist/booking/server/reminders.d.ts +9 -0
  160. package/dist/booking/server/reminders.js +24 -0
  161. package/dist/booking/server/validation.d.ts +17 -0
  162. package/dist/booking/server/validation.js +55 -0
  163. package/dist/booking/state-machine.d.ts +41 -0
  164. package/dist/booking/state-machine.js +73 -0
  165. package/dist/booking/templates/_partials/footer.en.html +4 -0
  166. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  167. package/dist/booking/templates/_partials/header.en.html +4 -0
  168. package/dist/booking/templates/_partials/header.pl.html +4 -0
  169. package/dist/booking/templates/balance-due.en.html +10 -0
  170. package/dist/booking/templates/balance-due.pl.html +10 -0
  171. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  172. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  173. package/dist/booking/templates/booking-received.en.html +9 -0
  174. package/dist/booking/templates/booking-received.pl.html +9 -0
  175. package/dist/booking/templates/deposit-paid.en.html +6 -0
  176. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  177. package/dist/booking/templates/payment-link.en.html +5 -0
  178. package/dist/booking/templates/payment-link.pl.html +5 -0
  179. package/dist/booking/templates/payment-received.en.html +10 -0
  180. package/dist/booking/templates/payment-received.pl.html +10 -0
  181. package/dist/booking/totals.d.ts +38 -0
  182. package/dist/booking/totals.js +61 -0
  183. package/dist/booking/webhook-logic.d.ts +28 -0
  184. package/dist/booking/webhook-logic.js +50 -0
  185. package/dist/cli/create-user.d.ts +7 -1
  186. package/dist/cli/create-user.js +23 -15
  187. package/dist/cli/index.js +55 -7
  188. package/dist/cli/scaffold/admin.d.ts +6 -0
  189. package/dist/cli/scaffold/admin.js +5 -3
  190. package/dist/cli/scaffold/booking.d.ts +12 -0
  191. package/dist/cli/scaffold/booking.js +103 -0
  192. package/dist/core/cms.d.ts +24 -9
  193. package/dist/core/cms.js +72 -34
  194. package/dist/core/config/validatePreviews.d.ts +12 -0
  195. package/dist/core/config/validatePreviews.js +90 -0
  196. package/dist/core/fields/dotPath.d.ts +4 -0
  197. package/dist/core/fields/dotPath.js +18 -0
  198. package/dist/core/fields/slugPath.d.ts +9 -1
  199. package/dist/core/fields/slugPath.js +16 -2
  200. package/dist/core/fields/slugify.d.ts +3 -0
  201. package/dist/core/fields/slugify.js +4 -0
  202. package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
  203. package/dist/core/server/entries/operations/duplicate.js +73 -0
  204. package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
  205. package/dist/core/server/entries/operations/slugNormalize.js +20 -0
  206. package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
  207. package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
  208. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  209. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  210. package/dist/core/server/generator/generator.js +7 -2
  211. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  212. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  213. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  214. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  215. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  216. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  217. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  218. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  219. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  220. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  221. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  222. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  223. package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
  224. package/dist/db-postgres/schema/booking/bookings.js +40 -0
  225. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  226. package/dist/db-postgres/schema/booking/index.js +7 -0
  227. package/dist/db-postgres/schema/index.d.ts +1 -0
  228. package/dist/db-postgres/schema/index.js +1 -0
  229. package/dist/db-postgres/schema-booking.d.ts +1 -0
  230. package/dist/db-postgres/schema-booking.js +1 -0
  231. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  232. package/dist/server/security/rate-limit-handler.js +34 -0
  233. package/dist/shop/adapters/payu/client.js +3 -1
  234. package/dist/shop/adapters/payu/index.js +37 -17
  235. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  236. package/dist/shop/adapters/payu/payload.js +1 -1
  237. package/dist/shop/adapters/stripe/index.js +36 -21
  238. package/dist/shop/index.d.ts +11 -2
  239. package/dist/shop/index.js +43 -1
  240. package/dist/shop/rate-limit.d.ts +1 -9
  241. package/dist/shop/rate-limit.js +3 -28
  242. package/dist/shop/types.d.ts +19 -0
  243. package/dist/sveltekit/config.d.ts +4 -0
  244. package/dist/sveltekit/config.js +6 -0
  245. package/dist/types/cms.d.ts +15 -1
  246. package/dist/types/cms.schema.d.ts +96 -2
  247. package/dist/types/cms.schema.js +84 -1
  248. package/dist/types/config.d.ts +33 -0
  249. package/dist/types/layout.d.ts +6 -0
  250. package/dist/types/modules.d.ts +133 -0
  251. package/dist/types/modules.js +27 -0
  252. package/dist/updates/0.36.9/index.d.ts +2 -0
  253. package/dist/updates/0.36.9/index.js +12 -0
  254. package/dist/updates/0.37.0/index.d.ts +2 -0
  255. package/dist/updates/0.37.0/index.js +52 -0
  256. package/dist/updates/index.js +5 -1
  257. package/package.json +19 -1
  258. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  259. 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,5 @@
1
+ import { type RequestHandler } from '@sveltejs/kit';
2
+ /** Public self-serve booking creation. Mount at `POST /api/booking`. */
3
+ export declare function createBookingHandler(): {
4
+ POST: RequestHandler;
5
+ };
@@ -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,4 @@
1
+ export { createBookingHandler } from './create-handler.js';
2
+ export { createWebhookHandler } from './webhook-handler.js';
3
+ export { createPortalHandler } from './portal-handler.js';
4
+ export { createMaintenanceHandler } from './maintenance-handler.js';
@@ -0,0 +1,4 @@
1
+ export { createBookingHandler } from './create-handler.js';
2
+ export { createWebhookHandler } from './webhook-handler.js';
3
+ export { createPortalHandler } from './portal-handler.js';
4
+ export { createMaintenanceHandler } from './maintenance-handler.js';
@@ -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>;