includio-cms 0.36.9 → 0.37.1

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 (247) hide show
  1. package/API.md +178 -3
  2. package/CHANGELOG.md +77 -0
  3. package/DOCS.md +28 -3
  4. package/ROADMAP.md +32 -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 +1 -1
  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/status-badge.svelte +39 -3
  21. package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
  22. package/dist/admin/client/entry/entry.svelte +26 -9
  23. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
  24. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
  25. package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
  26. package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
  27. package/dist/admin/client/entry/preview-views.d.ts +27 -0
  28. package/dist/admin/client/entry/preview-views.js +33 -0
  29. package/dist/admin/client/index.d.ts +3 -0
  30. package/dist/admin/client/index.js +3 -0
  31. package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
  32. package/dist/admin/client/modules/dashboard-registry.js +11 -0
  33. package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
  34. package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
  35. package/dist/admin/client/modules/stat-card.svelte +22 -0
  36. package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
  37. package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
  38. package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
  39. package/dist/admin/components/fields/relation-field.svelte +11 -2
  40. package/dist/admin/components/icons/icon-map.js +8 -0
  41. package/dist/admin/components/layout/app-sidebar.svelte +45 -14
  42. package/dist/admin/components/layout/context-switcher.svelte +90 -0
  43. package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
  44. package/dist/admin/components/layout/lang.d.ts +4 -0
  45. package/dist/admin/components/layout/lang.js +8 -0
  46. package/dist/admin/components/layout/layout-renderer.svelte +2 -1
  47. package/dist/admin/components/layout/layout-tabs.svelte +14 -0
  48. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
  49. package/dist/admin/components/layout/nav-collections.svelte +16 -9
  50. package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
  51. package/dist/admin/components/layout/nav-global.svelte +45 -0
  52. package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
  53. package/dist/admin/components/layout/nav-main.svelte +8 -32
  54. package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
  55. package/dist/admin/components/layout/nav-module.svelte +57 -0
  56. package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
  57. package/dist/admin/components/layout/nav-section.svelte +1 -1
  58. package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
  59. package/dist/admin/components/layout/nav-singletons.svelte +14 -7
  60. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
  61. package/dist/admin/remote/booking.remote.d.ts +247 -0
  62. package/dist/admin/remote/booking.remote.js +286 -0
  63. package/dist/admin/remote/index.d.ts +2 -0
  64. package/dist/admin/remote/index.js +2 -0
  65. package/dist/admin/remote/modules.remote.d.ts +10 -0
  66. package/dist/admin/remote/modules.remote.js +14 -0
  67. package/dist/admin/state/active-context-resolve.d.ts +37 -0
  68. package/dist/admin/state/active-context-resolve.js +66 -0
  69. package/dist/admin/state/active-context.svelte.d.ts +8 -0
  70. package/dist/admin/state/active-context.svelte.js +27 -0
  71. package/dist/booking/adjustments.d.ts +33 -0
  72. package/dist/booking/adjustments.js +26 -0
  73. package/dist/booking/capacity.d.ts +12 -0
  74. package/dist/booking/capacity.js +10 -0
  75. package/dist/booking/client/index.d.ts +42 -0
  76. package/dist/booking/client/index.js +48 -0
  77. package/dist/booking/client/use-booking.svelte.d.ts +24 -0
  78. package/dist/booking/client/use-booking.svelte.js +56 -0
  79. package/dist/booking/config.d.ts +176 -0
  80. package/dist/booking/config.js +37 -0
  81. package/dist/booking/draft.d.ts +96 -0
  82. package/dist/booking/draft.js +67 -0
  83. package/dist/booking/expiry-logic.d.ts +30 -0
  84. package/dist/booking/expiry-logic.js +33 -0
  85. package/dist/booking/index.d.ts +18 -0
  86. package/dist/booking/index.js +52 -0
  87. package/dist/booking/ledger.d.ts +46 -0
  88. package/dist/booking/ledger.js +60 -0
  89. package/dist/booking/number.d.ts +2 -0
  90. package/dist/booking/number.js +11 -0
  91. package/dist/booking/participant-fields/access.d.ts +10 -0
  92. package/dist/booking/participant-fields/access.js +14 -0
  93. package/dist/booking/participant-fields/age.d.ts +12 -0
  94. package/dist/booking/participant-fields/age.js +28 -0
  95. package/dist/booking/participant-fields/completeness.d.ts +8 -0
  96. package/dist/booking/participant-fields/completeness.js +13 -0
  97. package/dist/booking/participant-fields/group.d.ts +6 -0
  98. package/dist/booking/participant-fields/group.js +15 -0
  99. package/dist/booking/participant-fields/index.d.ts +6 -0
  100. package/dist/booking/participant-fields/index.js +6 -0
  101. package/dist/booking/participant-fields/types.d.ts +50 -0
  102. package/dist/booking/participant-fields/types.js +5 -0
  103. package/dist/booking/participant-fields/validate.d.ts +7 -0
  104. package/dist/booking/participant-fields/validate.js +44 -0
  105. package/dist/booking/participant-pricing.d.ts +93 -0
  106. package/dist/booking/participant-pricing.js +91 -0
  107. package/dist/booking/participants-view.d.ts +32 -0
  108. package/dist/booking/participants-view.js +22 -0
  109. package/dist/booking/payment-context.d.ts +19 -0
  110. package/dist/booking/payment-context.js +29 -0
  111. package/dist/booking/pricing-inputs.d.ts +28 -0
  112. package/dist/booking/pricing-inputs.js +35 -0
  113. package/dist/booking/pricing.d.ts +86 -0
  114. package/dist/booking/pricing.js +125 -0
  115. package/dist/booking/recompute.d.ts +22 -0
  116. package/dist/booking/recompute.js +31 -0
  117. package/dist/booking/reminders-schedule.d.ts +11 -0
  118. package/dist/booking/reminders-schedule.js +28 -0
  119. package/dist/booking/server/adjustments.d.ts +27 -0
  120. package/dist/booking/server/adjustments.js +139 -0
  121. package/dist/booking/server/bookings.d.ts +51 -0
  122. package/dist/booking/server/bookings.js +336 -0
  123. package/dist/booking/server/catalog.d.ts +9 -0
  124. package/dist/booking/server/catalog.js +20 -0
  125. package/dist/booking/server/db.d.ts +7 -0
  126. package/dist/booking/server/db.js +18 -0
  127. package/dist/booking/server/email.d.ts +23 -0
  128. package/dist/booking/server/email.js +59 -0
  129. package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
  130. package/dist/booking/server/emailTemplateRegistry.js +125 -0
  131. package/dist/booking/server/http/create-handler.d.ts +5 -0
  132. package/dist/booking/server/http/create-handler.js +70 -0
  133. package/dist/booking/server/http/index.d.ts +4 -0
  134. package/dist/booking/server/http/index.js +4 -0
  135. package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
  136. package/dist/booking/server/http/maintenance-handler.js +36 -0
  137. package/dist/booking/server/http/portal-handler.d.ts +13 -0
  138. package/dist/booking/server/http/portal-handler.js +103 -0
  139. package/dist/booking/server/http/webhook-handler.d.ts +9 -0
  140. package/dist/booking/server/http/webhook-handler.js +39 -0
  141. package/dist/booking/server/index.d.ts +12 -0
  142. package/dist/booking/server/index.js +14 -0
  143. package/dist/booking/server/maintenance.d.ts +28 -0
  144. package/dist/booking/server/maintenance.js +134 -0
  145. package/dist/booking/server/participants.d.ts +26 -0
  146. package/dist/booking/server/participants.js +92 -0
  147. package/dist/booking/server/payments.d.ts +34 -0
  148. package/dist/booking/server/payments.js +207 -0
  149. package/dist/booking/server/portal-session.d.ts +16 -0
  150. package/dist/booking/server/portal-session.js +42 -0
  151. package/dist/booking/server/portal.d.ts +206 -0
  152. package/dist/booking/server/portal.js +236 -0
  153. package/dist/booking/server/price-source.d.ts +73 -0
  154. package/dist/booking/server/price-source.js +94 -0
  155. package/dist/booking/server/refunds.d.ts +7 -0
  156. package/dist/booking/server/refunds.js +58 -0
  157. package/dist/booking/server/reminders.d.ts +9 -0
  158. package/dist/booking/server/reminders.js +24 -0
  159. package/dist/booking/server/validation.d.ts +17 -0
  160. package/dist/booking/server/validation.js +55 -0
  161. package/dist/booking/state-machine.d.ts +41 -0
  162. package/dist/booking/state-machine.js +73 -0
  163. package/dist/booking/templates/_partials/footer.en.html +4 -0
  164. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  165. package/dist/booking/templates/_partials/header.en.html +4 -0
  166. package/dist/booking/templates/_partials/header.pl.html +4 -0
  167. package/dist/booking/templates/balance-due.en.html +10 -0
  168. package/dist/booking/templates/balance-due.pl.html +10 -0
  169. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  170. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  171. package/dist/booking/templates/booking-received.en.html +9 -0
  172. package/dist/booking/templates/booking-received.pl.html +9 -0
  173. package/dist/booking/templates/deposit-paid.en.html +6 -0
  174. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  175. package/dist/booking/templates/payment-link.en.html +5 -0
  176. package/dist/booking/templates/payment-link.pl.html +5 -0
  177. package/dist/booking/templates/payment-received.en.html +10 -0
  178. package/dist/booking/templates/payment-received.pl.html +10 -0
  179. package/dist/booking/totals.d.ts +38 -0
  180. package/dist/booking/totals.js +61 -0
  181. package/dist/booking/webhook-logic.d.ts +28 -0
  182. package/dist/booking/webhook-logic.js +50 -0
  183. package/dist/cli/create-user.d.ts +7 -1
  184. package/dist/cli/create-user.js +23 -15
  185. package/dist/cli/index.js +55 -7
  186. package/dist/cli/scaffold/admin.d.ts +6 -0
  187. package/dist/cli/scaffold/admin.js +5 -3
  188. package/dist/cli/scaffold/booking.d.ts +12 -0
  189. package/dist/cli/scaffold/booking.js +103 -0
  190. package/dist/core/cms.d.ts +24 -9
  191. package/dist/core/cms.js +72 -34
  192. package/dist/core/config/validatePreviews.d.ts +12 -0
  193. package/dist/core/config/validatePreviews.js +90 -0
  194. package/dist/core/fields/slugPath.d.ts +9 -1
  195. package/dist/core/fields/slugPath.js +16 -2
  196. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  197. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  198. package/dist/core/server/generator/generator.js +7 -2
  199. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  200. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  201. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  202. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  203. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  204. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  205. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  206. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  207. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  208. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  209. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  210. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  211. package/dist/db-postgres/schema/booking/bookings.d.ts +324 -0
  212. package/dist/db-postgres/schema/booking/bookings.js +52 -0
  213. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  214. package/dist/db-postgres/schema/booking/index.js +7 -0
  215. package/dist/db-postgres/schema/index.d.ts +1 -0
  216. package/dist/db-postgres/schema/index.js +1 -0
  217. package/dist/db-postgres/schema-booking.d.ts +1 -0
  218. package/dist/db-postgres/schema-booking.js +1 -0
  219. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  220. package/dist/server/security/rate-limit-handler.js +34 -0
  221. package/dist/shop/adapters/payu/client.js +3 -1
  222. package/dist/shop/adapters/payu/index.js +37 -17
  223. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  224. package/dist/shop/adapters/payu/payload.js +1 -1
  225. package/dist/shop/adapters/stripe/index.js +36 -21
  226. package/dist/shop/index.d.ts +11 -2
  227. package/dist/shop/index.js +43 -1
  228. package/dist/shop/rate-limit.d.ts +1 -9
  229. package/dist/shop/rate-limit.js +3 -28
  230. package/dist/shop/types.d.ts +19 -0
  231. package/dist/sveltekit/config.d.ts +4 -0
  232. package/dist/sveltekit/config.js +6 -0
  233. package/dist/types/cms.d.ts +15 -1
  234. package/dist/types/cms.schema.d.ts +96 -2
  235. package/dist/types/cms.schema.js +84 -1
  236. package/dist/types/config.d.ts +33 -0
  237. package/dist/types/layout.d.ts +6 -0
  238. package/dist/types/modules.d.ts +133 -0
  239. package/dist/types/modules.js +27 -0
  240. package/dist/updates/0.37.0/index.d.ts +2 -0
  241. package/dist/updates/0.37.0/index.js +52 -0
  242. package/dist/updates/0.37.1/index.d.ts +2 -0
  243. package/dist/updates/0.37.1/index.js +24 -0
  244. package/dist/updates/index.js +5 -1
  245. package/package.json +19 -1
  246. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  247. package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
@@ -0,0 +1,336 @@
1
+ import { and, eq, inArray, isNull } from 'drizzle-orm';
2
+ import { getBookingDb, requireBookingConfig } from './db.js';
3
+ import { bookingsTable, bookingItemsTable, bookingPersonsTable, bookingItemPersonsTable, bookingPaymentsTable, bookingRemindersTable, bookingAdjustmentsTable } from '../../db-postgres/schema/booking/index.js';
4
+ import { buildBookingDraft, pricingInputsFromCreate } from '../draft.js';
5
+ import { buildPricingInputs } from '../pricing-inputs.js';
6
+ import { priceByParticipant } from '../participant-pricing.js';
7
+ import { grossByTripRef, netOf } from '../totals.js';
8
+ import { sumAdjustments } from '../adjustments.js';
9
+ import { resolveItemSchema } from './participants.js';
10
+ import { generateBookingNumber } from '../number.js';
11
+ import { recomputeTotals, assertEditAllowed, assertPortalEditAllowed } from '../recompute.js';
12
+ import { deriveStatus } from '../state-machine.js';
13
+ import { runCapacityCheck, runValidateBooking } from './validation.js';
14
+ import { rescheduleReminders } from './reminders.js';
15
+ import { priceItemsFromCatalog, fetchCatalogPrices, catalogRefsNeeded, pickItemPrices } from './price-source.js';
16
+ /**
17
+ * Data, na którą wycenia się TĘ rezerwację — nigdy „dzisiaj".
18
+ *
19
+ * Reguły zależne od czasu (early-bird) muszą widzieć moment złożenia rezerwacji, inaczej
20
+ * każde późniejsze dotknięcie (edycja, korekta) po cichu odbiera klientowi rabat.
21
+ * `pricedAt` jest NULL dla rezerwacji sprzed 0.37.1 — dla nich `createdAt` jest dokładnie
22
+ * tym, czym było wtedy `now`.
23
+ */
24
+ function pricingDateOf(booking) {
25
+ return booking.pricedAt ?? booking.createdAt;
26
+ }
27
+ /** Resolve personRef → DB person id; throws on a dangling assignment reference. */
28
+ function personIdLookup(map, ref) {
29
+ const id = map.get(ref);
30
+ if (!id)
31
+ throw new Error(`Assignment references unknown personRef "${ref}"`);
32
+ return id;
33
+ }
34
+ /** Parse the arrival-date anchor from a customer field value (Date or ISO string). */
35
+ function parseAnchor(raw) {
36
+ if (raw instanceof Date)
37
+ return raw;
38
+ if (typeof raw === 'string' && !Number.isNaN(Date.parse(raw)))
39
+ return new Date(raw);
40
+ return null;
41
+ }
42
+ /**
43
+ * Resolve the per-call pricing context: the editable participant-pricing config
44
+ * (from the client resolver, e.g. globalSettings), the arrival-date anchor from
45
+ * the customer, and the identity field schema (to locate the dob key).
46
+ */
47
+ async function buildPricingContext(config, customer, tripRefForSchema) {
48
+ const participantPricing = config.participantPricing
49
+ ? await config.participantPricing()
50
+ : { ageBands: [], ladder: [], earlyBird: null };
51
+ const anchor = parseAnchor(customer[config.arrivalDateField]);
52
+ const identityFields = tripRefForSchema ? await resolveItemSchema(tripRefForSchema) : [];
53
+ return { participantPricing, anchor, identityFields };
54
+ }
55
+ /**
56
+ * Create a booking (public self-serve or staff). Validates catalog capacity +
57
+ * the client seam, prices the draft per-participant, and persists booking +
58
+ * people + items + assignments + reminders in one transaction. Status starts
59
+ * `draft`; the deposit payment (and its webhook) drives the status forward.
60
+ */
61
+ export async function createBooking(input, opts = {}) {
62
+ const config = requireBookingConfig();
63
+ const db = getBookingDb();
64
+ const now = opts.now ?? new Date();
65
+ // Domyślnie NIEZAUFANY: nowy publiczny endpoint jest bezpieczny, dopóki ktoś jawnie
66
+ // nie napisze `actor: 'staff'`. Zapomnienie tego parametru nie może otworzyć dziury.
67
+ const actor = opts.actor ?? 'public';
68
+ const validationItems = input.items.map((it) => ({
69
+ tripRef: it.tripRef,
70
+ departureDate: it.departureDate,
71
+ participantsCount: it.assignments.length
72
+ }));
73
+ await runCapacityCheck(validationItems);
74
+ await runValidateBooking({
75
+ operation: 'create',
76
+ bookingId: null,
77
+ items: validationItems,
78
+ customer: input.customer
79
+ });
80
+ // Cena jest atrybutem katalogu, nie żądania. Wszystkie pozycje są tu nowe (brak
81
+ // snapshotów), więc dla klienta końcowego jedynym źródłem ceny jest szew `resolvePrice`.
82
+ const priced = await priceItemsFromCatalog(actor, input.items, new Map());
83
+ const pricedInput = {
84
+ ...input,
85
+ items: input.items.map((it, i) => ({ ...it, unitPrice: priced[i].unitPrice }))
86
+ };
87
+ const ctx = await buildPricingContext(config, input.customer, input.items[0]?.tripRef);
88
+ const draft = buildBookingDraft(pricedInput, config, ctx, now);
89
+ // Unique booking number with collision retry (~25 bits → rare).
90
+ let number = generateBookingNumber();
91
+ for (let i = 0; i < 5; i++) {
92
+ const hit = await db
93
+ .select({ id: bookingsTable.id })
94
+ .from(bookingsTable)
95
+ .where(eq(bookingsTable.number, number));
96
+ if (hit.length === 0)
97
+ break;
98
+ number = generateBookingNumber();
99
+ }
100
+ return db.transaction(async (tx) => {
101
+ const [booking] = await tx
102
+ .insert(bookingsTable)
103
+ .values({
104
+ number,
105
+ status: 'draft',
106
+ currency: config.currency,
107
+ customer: input.customer,
108
+ totals: draft.totals,
109
+ // Zamrażamy moment wyceny: wszystkie późniejsze przeliczenia (edycja, korekta)
110
+ // liczą early-bird na TĘ datę, nie na dzisiejszą.
111
+ pricedAt: now,
112
+ balanceDueAt: draft.balanceDueAt,
113
+ holdExpiresAt: draft.holdExpiresAt,
114
+ language: input.language ?? null
115
+ })
116
+ .returning();
117
+ const personIdByRef = new Map();
118
+ for (const p of draft.persons) {
119
+ const [person] = await tx
120
+ .insert(bookingPersonsTable)
121
+ .values({ bookingId: booking.id, data: p.identity })
122
+ .returning();
123
+ personIdByRef.set(p.ref, person.id);
124
+ }
125
+ for (const it of draft.items) {
126
+ const [item] = await tx
127
+ .insert(bookingItemsTable)
128
+ .values({
129
+ bookingId: booking.id,
130
+ tripRef: it.tripRef,
131
+ departureDate: it.departureDate,
132
+ participantsCount: it.participantsCount,
133
+ unitPriceSnapshot: it.unitPriceSnapshot,
134
+ subtotalSnapshot: it.subtotalSnapshot,
135
+ appliedDiscounts: it.appliedDiscounts
136
+ })
137
+ .returning();
138
+ if (it.assignments.length > 0) {
139
+ await tx.insert(bookingItemPersonsTable).values(it.assignments.map((a) => ({
140
+ itemId: item.id,
141
+ personId: personIdLookup(personIdByRef, a.personRef),
142
+ data: a.data ?? {}
143
+ })));
144
+ }
145
+ }
146
+ if (draft.reminders.length > 0) {
147
+ await tx.insert(bookingRemindersTable).values(draft.reminders.map((r) => ({
148
+ bookingId: booking.id,
149
+ type: r.type,
150
+ templateKey: r.templateKey,
151
+ scheduledFor: r.scheduledFor
152
+ })));
153
+ }
154
+ return { booking, accessToken: booking.accessToken };
155
+ });
156
+ }
157
+ /**
158
+ * Edit a booking (staff or portal). Recomputes totals per-participant + derived
159
+ * status, enforces the actor's guards (`portal` blocks dropping below paid),
160
+ * re-runs capacity + validate seam when items change, replaces people/items/
161
+ * assignments, and reschedules reminders on a departure change. One transaction.
162
+ */
163
+ export async function editBooking(bookingId, changes, opts) {
164
+ const config = requireBookingConfig();
165
+ const db = getBookingDb();
166
+ const now = opts.now ?? new Date();
167
+ // Katalog odpytujemy PRZED transakcją: szew `resolvePrice` należy do projektu i robi
168
+ // własne I/O. Wołany pod blokadą `FOR UPDATE` trzymałby wiersz rezerwacji przez cudzy
169
+ // odczyt i sięgał po drugie połączenie z tej samej puli — droga do zakleszczenia puli.
170
+ const catalogPrices = await fetchCatalogPrices(catalogRefsNeeded(opts.actor, changes.items ?? []));
171
+ return db.transaction(async (tx) => {
172
+ const [booking] = await tx
173
+ .select()
174
+ .from(bookingsTable)
175
+ .where(eq(bookingsTable.id, bookingId))
176
+ .for('update');
177
+ if (!booking)
178
+ throw new Error(`Booking ${bookingId} not found`);
179
+ const existingItems = await tx
180
+ .select()
181
+ .from(bookingItemsTable)
182
+ .where(eq(bookingItemsTable.bookingId, bookingId));
183
+ // Bramka cenowa. Portal nie dyktuje ceny: pozycja już w rezerwacji zachowuje swój
184
+ // zamrożony snapshot (dotknięcie rezerwacji nie może podnieść klientowi ceny, gdy
185
+ // cennik poszedł w górę), pozycja nowa dostaje cenę z katalogu.
186
+ const existingByRef = new Map(existingItems.map((i) => [i.tripRef, i.unitPriceSnapshot]));
187
+ const editItems = changes.items
188
+ ? pickItemPrices({
189
+ actor: opts.actor,
190
+ items: changes.items,
191
+ existingByRef,
192
+ catalogPrices
193
+ }).map((priced, i) => ({ ...changes.items[i], unitPrice: priced.unitPrice }))
194
+ : undefined;
195
+ const customerForCtx = changes.customer ?? booking.customer;
196
+ const tripRefForSchema = editItems?.[0]?.tripRef ?? existingItems[0]?.tripRef;
197
+ const ctx = await buildPricingContext(config, customerForCtx, tripRefForSchema);
198
+ // Pricing inputs: from the edit when items change, else from existing rows.
199
+ let participants;
200
+ let trips;
201
+ if (editItems) {
202
+ const pi = pricingInputsFromCreate({ customer: customerForCtx, persons: changes.persons ?? [], items: editItems }, ctx.identityFields);
203
+ participants = pi.participants;
204
+ trips = pi.trips;
205
+ }
206
+ else {
207
+ const existingItemIds = existingItems.map((i) => i.id);
208
+ const persons = await tx
209
+ .select()
210
+ .from(bookingPersonsTable)
211
+ .where(eq(bookingPersonsTable.bookingId, bookingId));
212
+ const assignments = existingItemIds.length
213
+ ? await tx
214
+ .select()
215
+ .from(bookingItemPersonsTable)
216
+ .where(inArray(bookingItemPersonsTable.itemId, existingItemIds))
217
+ : [];
218
+ const pi = buildPricingInputs(persons.map((p) => ({ id: p.id, data: p.data })), existingItems.map((i) => ({ id: i.id, tripRef: i.tripRef, unitPrice: i.unitPriceSnapshot })), assignments.map((a) => ({ itemId: a.itemId, personId: a.personId })), ctx.identityFields);
219
+ participants = pi.participants;
220
+ trips = pi.trips;
221
+ }
222
+ // Wycena idzie z ZAMROŻONEJ daty rezerwacji, nie z `now` — inaczej poprawienie
223
+ // literówki tydzień przed wylotem skasowałoby early-bird sprzed trzech miesięcy.
224
+ // `now` zostaje tam, gdzie naprawdę znaczy „teraz": `updatedAt`, terminy, przypomnienia.
225
+ const result = priceByParticipant(participants, trips, ctx.anchor, pricingDateOf(booking), ctx.participantPricing);
226
+ const dates = (editItems ?? existingItems).map((it) => it.departureDate);
227
+ const payments = (await tx
228
+ .select()
229
+ .from(bookingPaymentsTable)
230
+ .where(eq(bookingPaymentsTable.bookingId, bookingId))).map((p) => ({ amount: p.amount, status: p.status, kind: p.kind }));
231
+ const adjustments = await tx
232
+ .select()
233
+ .from(bookingAdjustmentsTable)
234
+ .where(and(eq(bookingAdjustmentsTable.bookingId, bookingId), isNull(bookingAdjustmentsTable.voidedAt)));
235
+ const sums = sumAdjustments(adjustments);
236
+ const totals = recomputeTotals(result, sums, payments);
237
+ if (opts.actor === 'portal')
238
+ assertPortalEditAllowed(booking.status, netOf(totals), payments);
239
+ else
240
+ assertEditAllowed(booking.status);
241
+ if (editItems) {
242
+ const validationItems = editItems.map((it) => ({
243
+ tripRef: it.tripRef,
244
+ departureDate: it.departureDate,
245
+ participantsCount: it.assignments.length
246
+ }));
247
+ await runCapacityCheck(validationItems, bookingId);
248
+ await runValidateBooking({
249
+ operation: 'edit',
250
+ bookingId,
251
+ items: validationItems,
252
+ customer: customerForCtx
253
+ });
254
+ const subtotalByRef = grossByTripRef(result);
255
+ // Replace people + items + assignments (item delete cascades assignments).
256
+ await tx.delete(bookingItemsTable).where(eq(bookingItemsTable.bookingId, bookingId));
257
+ await tx.delete(bookingPersonsTable).where(eq(bookingPersonsTable.bookingId, bookingId));
258
+ const personIdByRef = new Map();
259
+ for (const p of changes.persons ?? []) {
260
+ const [person] = await tx
261
+ .insert(bookingPersonsTable)
262
+ .values({ bookingId, data: p.identity })
263
+ .returning();
264
+ personIdByRef.set(p.ref, person.id);
265
+ }
266
+ for (const it of editItems) {
267
+ const [item] = await tx
268
+ .insert(bookingItemsTable)
269
+ .values({
270
+ bookingId,
271
+ tripRef: it.tripRef,
272
+ departureDate: it.departureDate,
273
+ participantsCount: it.assignments.length,
274
+ unitPriceSnapshot: it.unitPrice,
275
+ subtotalSnapshot: subtotalByRef.get(it.tripRef) ?? null,
276
+ appliedDiscounts: totals.appliedRules.filter((r) => r.targetRef === it.tripRef)
277
+ })
278
+ .returning();
279
+ if (it.assignments.length > 0) {
280
+ await tx.insert(bookingItemPersonsTable).values(it.assignments.map((a) => ({
281
+ itemId: item.id,
282
+ personId: personIdLookup(personIdByRef, a.personRef),
283
+ data: a.data ?? {}
284
+ })));
285
+ }
286
+ }
287
+ }
288
+ const status = deriveStatus({
289
+ gross: netOf(totals),
290
+ paid: totals.paid,
291
+ depositRequired: config.depositAmount,
292
+ current: booking.status
293
+ });
294
+ const [updated] = await tx
295
+ .update(bookingsTable)
296
+ .set({ totals, status, customer: customerForCtx, updatedAt: now })
297
+ .where(eq(bookingsTable.id, bookingId))
298
+ .returning();
299
+ if (changes.items)
300
+ await rescheduleReminders(tx, bookingId, dates);
301
+ return updated;
302
+ });
303
+ }
304
+ /**
305
+ * Price a booking from its CURRENTLY persisted people/items/assignments (no
306
+ * edit). Shared by adjustment recompute — mirrors the "no items change" branch
307
+ * of {@link editBooking}. Must run inside a transaction that has locked the
308
+ * booking row.
309
+ *
310
+ * **Wycenia na zamrożoną datę rezerwacji (`pricedAt`), nie na dziś.** To jest ścieżka
311
+ * korekt: obsługa księguje dopłatę ustaloną telefonicznie, a przeliczenie nie może przy
312
+ * okazji odebrać klientowi rabatu za wczesną rezerwację.
313
+ */
314
+ export async function priceFromExistingRows(tx, bookingId, config) {
315
+ const [booking] = await tx.select().from(bookingsTable).where(eq(bookingsTable.id, bookingId));
316
+ if (!booking)
317
+ throw new Error(`Booking ${bookingId} not found`);
318
+ const existingItems = await tx
319
+ .select()
320
+ .from(bookingItemsTable)
321
+ .where(eq(bookingItemsTable.bookingId, bookingId));
322
+ const ctx = await buildPricingContext(config, booking.customer, existingItems[0]?.tripRef);
323
+ const persons = await tx
324
+ .select()
325
+ .from(bookingPersonsTable)
326
+ .where(eq(bookingPersonsTable.bookingId, bookingId));
327
+ const itemIds = existingItems.map((i) => i.id);
328
+ const assignments = itemIds.length
329
+ ? await tx
330
+ .select()
331
+ .from(bookingItemPersonsTable)
332
+ .where(inArray(bookingItemPersonsTable.itemId, itemIds))
333
+ : [];
334
+ const pi = buildPricingInputs(persons.map((p) => ({ id: p.id, data: p.data })), existingItems.map((i) => ({ id: i.id, tripRef: i.tripRef, unitPrice: i.unitPriceSnapshot })), assignments.map((a) => ({ itemId: a.itemId, personId: a.personId })), ctx.identityFields);
335
+ return priceByParticipant(pi.participants, pi.trips, ctx.anchor, pricingDateOf(booking), ctx.participantPricing);
336
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Read a trip's seat capacity from the catalog collection.
3
+ *
4
+ * Contract: `tripRef` is the catalog **entry id** (`Entry._id`). The client
5
+ * storefront stores the resolved trip's `_id` on each booking item. Returns
6
+ * `null` (treated as unlimited) when no `capacityField` is configured, the entry
7
+ * is missing/unpublished, or the field is absent/non-numeric.
8
+ */
9
+ export declare function readTripCapacity(tripRef: string): Promise<number | null>;
@@ -0,0 +1,20 @@
1
+ import { resolveEntry } from '../../core/server/entries/operations/resolveEntry.js';
2
+ import { requireBookingConfig } from './db.js';
3
+ /**
4
+ * Read a trip's seat capacity from the catalog collection.
5
+ *
6
+ * Contract: `tripRef` is the catalog **entry id** (`Entry._id`). The client
7
+ * storefront stores the resolved trip's `_id` on each booking item. Returns
8
+ * `null` (treated as unlimited) when no `capacityField` is configured, the entry
9
+ * is missing/unpublished, or the field is absent/non-numeric.
10
+ */
11
+ export async function readTripCapacity(tripRef) {
12
+ const config = requireBookingConfig();
13
+ if (!config.capacityField)
14
+ return null;
15
+ const entry = await resolveEntry({ id: tripRef, status: 'published' });
16
+ if (!entry)
17
+ return null;
18
+ const raw = entry[config.capacityField];
19
+ return typeof raw === 'number' ? raw : null;
20
+ }
@@ -0,0 +1,7 @@
1
+ import type { ResolvedBookingConfig } from '../index.js';
2
+ /** Returns the Drizzle handle, asserting the postgres adapter is in use. */
3
+ export declare function getBookingDb(): import("drizzle-orm/postgres-js/driver", { with: { "resolution-mode": "require" } }).PostgresJsDatabase<typeof import("../../db-postgres/schema/index.js")> & {
4
+ $client: import("postgres").Sql<{}>;
5
+ };
6
+ /** Returns the resolved booking config, throwing when the module is not registered. */
7
+ export declare function requireBookingConfig(): ResolvedBookingConfig;
@@ -0,0 +1,18 @@
1
+ import { getCMS } from '../../core/cms.js';
2
+ /** Returns the Drizzle handle, asserting the postgres adapter is in use. */
3
+ export function getBookingDb() {
4
+ const cms = getCMS();
5
+ const adapter = cms.databaseAdapter;
6
+ if (!adapter._drizzle) {
7
+ throw new Error('Booking requires the db-postgres adapter (Drizzle instance missing).');
8
+ }
9
+ return adapter._drizzle;
10
+ }
11
+ /** Returns the resolved booking config, throwing when the module is not registered. */
12
+ export function requireBookingConfig() {
13
+ const cfg = getCMS().getModuleConfig('booking');
14
+ if (!cfg) {
15
+ throw new Error('Booking is not configured. Add `defineBooking({...})` to `modules` in your CMS config.');
16
+ }
17
+ return cfg;
18
+ }
@@ -0,0 +1,23 @@
1
+ /** The transactional emails the booking module can send. */
2
+ export type BookingEmailKind = 'booking-received' | 'deposit-paid' | 'payment-received' | 'payment-link' | 'balance-due' | 'balance-overdue-cancelled';
3
+ /** Template file name for a kind (1:1 — kept as a function for future indirection). */
4
+ export declare function pickEmailTemplate(kind: BookingEmailKind): string;
5
+ /** Localised subject for a kind, falling back to English. */
6
+ export declare function subjectFor(kind: BookingEmailKind, lang: string): string;
7
+ /** Render a booking email's HTML body for the given language + context. */
8
+ export declare function renderBookingEmail(kind: BookingEmailKind, lang: string, context: Record<string, unknown>): Promise<string>;
9
+ /** Pure render-context builder for a booking email. @internal */
10
+ export declare function buildEmailContext(booking: {
11
+ accessToken: string;
12
+ customer: Record<string, unknown>;
13
+ } & Record<string, unknown>, config: {
14
+ currency: string;
15
+ brand: string | null;
16
+ portalUrlTemplate: string | null;
17
+ }, now: Date, extra?: Record<string, unknown>): Record<string, unknown>;
18
+ /**
19
+ * Send a transactional booking email to the booker. No-op when no email adapter
20
+ * is configured, the booking is missing, or it has no customer email. `extra` is
21
+ * merged into the render context (e.g. `{ amount, paymentLink }` for payment-link).
22
+ */
23
+ export declare function sendBookingEmail(bookingId: string, kind: BookingEmailKind, extra?: Record<string, unknown>): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getCMS } from '../../core/cms.js';
3
+ import { getBookingDb, requireBookingConfig } from './db.js';
4
+ import { bookingsTable } from '../../db-postgres/schema/booking/index.js';
5
+ import { renderEmailTemplate } from './emailTemplateRegistry.js';
6
+ /** Template file name for a kind (1:1 — kept as a function for future indirection). */
7
+ export function pickEmailTemplate(kind) {
8
+ return kind;
9
+ }
10
+ const SUBJECTS = {
11
+ 'booking-received': { pl: 'Rezerwacja przyjęta', en: 'Booking received' },
12
+ 'deposit-paid': { pl: 'Zadatek zaksięgowany', en: 'Deposit received' },
13
+ 'payment-received': { pl: 'Płatność zaksięgowana', en: 'Payment received' },
14
+ 'payment-link': { pl: 'Link do płatności', en: 'Payment link' },
15
+ 'balance-due': { pl: 'Przypomnienie o dopłacie', en: 'Balance reminder' },
16
+ 'balance-overdue-cancelled': { pl: 'Rezerwacja anulowana', en: 'Booking cancelled' }
17
+ };
18
+ /** Localised subject for a kind, falling back to English. */
19
+ export function subjectFor(kind, lang) {
20
+ return SUBJECTS[kind][lang] ?? SUBJECTS[kind].en;
21
+ }
22
+ /** Render a booking email's HTML body for the given language + context. */
23
+ export async function renderBookingEmail(kind, lang, context) {
24
+ return renderEmailTemplate(pickEmailTemplate(kind), lang, context);
25
+ }
26
+ /** Pure render-context builder for a booking email. @internal */
27
+ export function buildEmailContext(booking, config, now, extra = {}) {
28
+ const portalUrl = config.portalUrlTemplate
29
+ ? config.portalUrlTemplate.replace('{token}', booking.accessToken)
30
+ : undefined;
31
+ return { booking, currency: config.currency, brand: config.brand, portalUrl, now, ...extra };
32
+ }
33
+ /**
34
+ * Send a transactional booking email to the booker. No-op when no email adapter
35
+ * is configured, the booking is missing, or it has no customer email. `extra` is
36
+ * merged into the render context (e.g. `{ amount, paymentLink }` for payment-link).
37
+ */
38
+ export async function sendBookingEmail(bookingId, kind, extra = {}) {
39
+ const cms = getCMS();
40
+ const email = cms.emailAdapter;
41
+ if (!email)
42
+ return;
43
+ const config = requireBookingConfig();
44
+ const db = getBookingDb();
45
+ const [booking] = await db.select().from(bookingsTable).where(eq(bookingsTable.id, bookingId));
46
+ if (!booking)
47
+ return;
48
+ const lang = booking.language ?? cms.languages[0] ?? 'pl';
49
+ const to = String(booking.customer.email ?? '');
50
+ if (!to)
51
+ return;
52
+ const context = buildEmailContext(booking, {
53
+ currency: config.currency,
54
+ brand: config.brand,
55
+ portalUrlTemplate: config.portalUrlTemplate
56
+ }, new Date(), extra);
57
+ const html = await renderBookingEmail(kind, lang, context);
58
+ await email.sendMail({ to, subject: subjectFor(kind, lang), html });
59
+ }
@@ -0,0 +1,29 @@
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
+ type Lang = string;
9
+ /** Long pl-PL (or given locale) date; '' for null/invalid. @internal */
10
+ export declare function formatBookingDate(value: unknown, locale?: string): string;
11
+ /** Whole days from `now` to `value`, floored at 0; '' for invalid. @internal */
12
+ export declare function daysUntilDate(value: unknown, now: unknown): string;
13
+ /**
14
+ * Register `header`/`footer` partials for a render. Re-registers on every call
15
+ * (cheap — source is cached in prod) so booking's partials always win over the
16
+ * shop module's identically-named partials right before a booking render.
17
+ * @internal
18
+ */
19
+ export declare function registerBookingPartials(projectDir: string, lang: Lang, fallbackLang: Lang): void;
20
+ /**
21
+ * Render a booking email template by name + language. Project overrides live in
22
+ * `<projectDir>/<name>.<lang>.html` (default `src/emails/booking`); package
23
+ * defaults ship under `booking/templates`.
24
+ */
25
+ export declare function renderEmailTemplate(name: string, lang: Lang, context: Record<string, unknown>, opts?: {
26
+ projectDir?: string;
27
+ fallbackLang?: Lang;
28
+ }): string;
29
+ export {};
@@ -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
+ };