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,27 @@
1
+ import { type AdjustmentKind, type BookingAdjustment } from '../adjustments.js';
2
+ export interface AddAdjustmentInput {
3
+ kind: AdjustmentKind;
4
+ /** UI-entered POSITIVE magnitude (minor units). */
5
+ magnitude: number;
6
+ label: string;
7
+ reason?: string | null;
8
+ }
9
+ /** List all adjustments (active + voided) for a booking, newest first. */
10
+ export declare function listBookingAdjustments(bookingId: string): Promise<BookingAdjustment[]>;
11
+ /**
12
+ * Add a manual adjustment (surcharge/discount) and recompute the booking totals
13
+ * + derived status in one transaction. Rejects an adjustment that would push the
14
+ * net owed below zero. `amount` is stored signed (surcharge > 0, discount < 0).
15
+ */
16
+ export declare function addBookingAdjustment(bookingId: string, input: AddAdjustmentInput, opts: {
17
+ actor: string;
18
+ now?: Date;
19
+ }): Promise<BookingAdjustment>;
20
+ /**
21
+ * Soft-void an adjustment (kept for audit) and recompute the booking totals +
22
+ * status. Throws if the adjustment is missing or already voided.
23
+ */
24
+ export declare function voidBookingAdjustment(adjustmentId: string, opts: {
25
+ actor: string;
26
+ now?: Date;
27
+ }): Promise<void>;
@@ -0,0 +1,136 @@
1
+ import { and, desc, eq, isNull } from 'drizzle-orm';
2
+ import { getBookingDb, requireBookingConfig } from './db.js';
3
+ import { bookingsTable, bookingPaymentsTable, bookingAdjustmentsTable } from '../../db-postgres/schema/booking/index.js';
4
+ import { signedAmount, sumAdjustments, assertAdjustmentMagnitude } from '../adjustments.js';
5
+ import { recomputeTotals, assertEditAllowed } from '../recompute.js';
6
+ import { netOf } from '../totals.js';
7
+ import { deriveStatus } from '../state-machine.js';
8
+ import { priceFromExistingRows } from './bookings.js';
9
+ /** List all adjustments (active + voided) for a booking, newest first. */
10
+ export async function listBookingAdjustments(bookingId) {
11
+ const db = getBookingDb();
12
+ const rows = await db
13
+ .select()
14
+ .from(bookingAdjustmentsTable)
15
+ .where(eq(bookingAdjustmentsTable.bookingId, bookingId))
16
+ .orderBy(desc(bookingAdjustmentsTable.createdAt));
17
+ return rows;
18
+ }
19
+ /**
20
+ * Add a manual adjustment (surcharge/discount) and recompute the booking totals
21
+ * + derived status in one transaction. Rejects an adjustment that would push the
22
+ * net owed below zero. `amount` is stored signed (surcharge > 0, discount < 0).
23
+ */
24
+ export async function addBookingAdjustment(bookingId, input, opts) {
25
+ assertAdjustmentMagnitude(input.magnitude);
26
+ const config = requireBookingConfig();
27
+ const db = getBookingDb();
28
+ const now = opts.now ?? new Date();
29
+ const amount = signedAmount(input.kind, input.magnitude);
30
+ return db.transaction(async (tx) => {
31
+ const [booking] = await tx
32
+ .select()
33
+ .from(bookingsTable)
34
+ .where(eq(bookingsTable.id, bookingId))
35
+ .for('update');
36
+ if (!booking)
37
+ throw new Error(`Booking ${bookingId} not found`);
38
+ assertEditAllowed(booking.status);
39
+ const result = await priceFromExistingRows(tx, bookingId, config, now);
40
+ const active = await tx
41
+ .select()
42
+ .from(bookingAdjustmentsTable)
43
+ .where(and(eq(bookingAdjustmentsTable.bookingId, bookingId), isNull(bookingAdjustmentsTable.voidedAt)));
44
+ const prospective = sumAdjustments([...active, { amount, voidedAt: null }]);
45
+ const payments = (await tx
46
+ .select()
47
+ .from(bookingPaymentsTable)
48
+ .where(eq(bookingPaymentsTable.bookingId, bookingId))).map((p) => ({ amount: p.amount, status: p.status, kind: p.kind }));
49
+ const totals = recomputeTotals(result, prospective, payments);
50
+ // Raw (unclamped) net — netOf() floors at 0, so guard on the raw value.
51
+ const rawNet = totals.gross - totals.discount - totals.manualDiscount + totals.surcharge;
52
+ if (rawNet < 0) {
53
+ throw new Error('Adjustment would make the booking total negative');
54
+ }
55
+ const [row] = await tx
56
+ .insert(bookingAdjustmentsTable)
57
+ .values({
58
+ bookingId,
59
+ kind: input.kind,
60
+ amount,
61
+ label: input.label,
62
+ reason: input.reason ?? null,
63
+ createdBy: opts.actor
64
+ })
65
+ .returning();
66
+ await tx
67
+ .update(bookingsTable)
68
+ .set({
69
+ totals,
70
+ status: deriveStatus({
71
+ gross: netOf(totals),
72
+ paid: totals.paid,
73
+ depositRequired: config.depositAmount,
74
+ current: booking.status
75
+ }),
76
+ updatedAt: now
77
+ })
78
+ .where(eq(bookingsTable.id, bookingId));
79
+ return row;
80
+ });
81
+ }
82
+ /**
83
+ * Soft-void an adjustment (kept for audit) and recompute the booking totals +
84
+ * status. Throws if the adjustment is missing or already voided.
85
+ */
86
+ export async function voidBookingAdjustment(adjustmentId, opts) {
87
+ const config = requireBookingConfig();
88
+ const db = getBookingDb();
89
+ const now = opts.now ?? new Date();
90
+ await db.transaction(async (tx) => {
91
+ const [adj] = await tx
92
+ .select()
93
+ .from(bookingAdjustmentsTable)
94
+ .where(eq(bookingAdjustmentsTable.id, adjustmentId))
95
+ .for('update');
96
+ if (!adj)
97
+ throw new Error('Adjustment not found');
98
+ if (adj.voidedAt)
99
+ throw new Error('Adjustment already voided');
100
+ const [booking] = await tx
101
+ .select()
102
+ .from(bookingsTable)
103
+ .where(eq(bookingsTable.id, adj.bookingId))
104
+ .for('update');
105
+ if (!booking)
106
+ throw new Error('Booking not found');
107
+ assertEditAllowed(booking.status);
108
+ await tx
109
+ .update(bookingAdjustmentsTable)
110
+ .set({ voidedAt: now, voidedBy: opts.actor })
111
+ .where(eq(bookingAdjustmentsTable.id, adjustmentId));
112
+ const result = await priceFromExistingRows(tx, adj.bookingId, config, now);
113
+ const active = await tx
114
+ .select()
115
+ .from(bookingAdjustmentsTable)
116
+ .where(and(eq(bookingAdjustmentsTable.bookingId, adj.bookingId), isNull(bookingAdjustmentsTable.voidedAt)));
117
+ const payments = (await tx
118
+ .select()
119
+ .from(bookingPaymentsTable)
120
+ .where(eq(bookingPaymentsTable.bookingId, adj.bookingId))).map((p) => ({ amount: p.amount, status: p.status, kind: p.kind }));
121
+ const totals = recomputeTotals(result, sumAdjustments(active), payments);
122
+ await tx
123
+ .update(bookingsTable)
124
+ .set({
125
+ totals,
126
+ status: deriveStatus({
127
+ gross: netOf(totals),
128
+ paid: totals.paid,
129
+ depositRequired: config.depositAmount,
130
+ current: booking.status
131
+ }),
132
+ updatedAt: now
133
+ })
134
+ .where(eq(bookingsTable.id, adj.bookingId));
135
+ });
136
+ }
@@ -0,0 +1,45 @@
1
+ import { bookingsTable } from '../../db-postgres/schema/booking/index.js';
2
+ import { type CreateBookingInput, type PersonInput, type ItemAssignmentInput } from '../draft.js';
3
+ import { type ParticipantPriceResult } from '../participant-pricing.js';
4
+ import type { ResolvedBookingConfig } from '../config.js';
5
+ import { type BookingTx } from './reminders.js';
6
+ /** Edit payload: replace people + items (+assignments) and/or customer details. */
7
+ export interface BookingEditChanges {
8
+ persons?: PersonInput[];
9
+ items?: Array<{
10
+ tripRef: string;
11
+ departureDate: Date | null;
12
+ unitPrice: number;
13
+ assignments: ItemAssignmentInput[];
14
+ }>;
15
+ customer?: Record<string, unknown>;
16
+ }
17
+ /**
18
+ * Create a booking (public self-serve or staff). Validates catalog capacity +
19
+ * the client seam, prices the draft per-participant, and persists booking +
20
+ * people + items + assignments + reminders in one transaction. Status starts
21
+ * `draft`; the deposit payment (and its webhook) drives the status forward.
22
+ */
23
+ export declare function createBooking(input: CreateBookingInput, opts?: {
24
+ now?: Date;
25
+ }): Promise<{
26
+ booking: typeof bookingsTable.$inferSelect;
27
+ accessToken: string;
28
+ }>;
29
+ /**
30
+ * Edit a booking (staff or portal). Recomputes totals per-participant + derived
31
+ * status, enforces the actor's guards (`portal` blocks dropping below paid),
32
+ * re-runs capacity + validate seam when items change, replaces people/items/
33
+ * assignments, and reschedules reminders on a departure change. One transaction.
34
+ */
35
+ export declare function editBooking(bookingId: string, changes: BookingEditChanges, opts: {
36
+ actor: 'staff' | 'portal';
37
+ now?: Date;
38
+ }): Promise<typeof bookingsTable.$inferSelect>;
39
+ /**
40
+ * Price a booking from its CURRENTLY persisted people/items/assignments (no
41
+ * edit). Shared by adjustment recompute — mirrors the "no items change" branch
42
+ * of {@link editBooking}. Must run inside a transaction that has locked the
43
+ * booking row.
44
+ */
45
+ export declare function priceFromExistingRows(tx: BookingTx, bookingId: string, config: ResolvedBookingConfig, now: Date): Promise<ParticipantPriceResult>;
@@ -0,0 +1,288 @@
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
+ /** Resolve personRef → DB person id; throws on a dangling assignment reference. */
16
+ function personIdLookup(map, ref) {
17
+ const id = map.get(ref);
18
+ if (!id)
19
+ throw new Error(`Assignment references unknown personRef "${ref}"`);
20
+ return id;
21
+ }
22
+ /** Parse the arrival-date anchor from a customer field value (Date or ISO string). */
23
+ function parseAnchor(raw) {
24
+ if (raw instanceof Date)
25
+ return raw;
26
+ if (typeof raw === 'string' && !Number.isNaN(Date.parse(raw)))
27
+ return new Date(raw);
28
+ return null;
29
+ }
30
+ /**
31
+ * Resolve the per-call pricing context: the editable participant-pricing config
32
+ * (from the client resolver, e.g. globalSettings), the arrival-date anchor from
33
+ * the customer, and the identity field schema (to locate the dob key).
34
+ */
35
+ async function buildPricingContext(config, customer, tripRefForSchema) {
36
+ const participantPricing = config.participantPricing
37
+ ? await config.participantPricing()
38
+ : { ageBands: [], ladder: [], earlyBird: null };
39
+ const anchor = parseAnchor(customer[config.arrivalDateField]);
40
+ const identityFields = tripRefForSchema ? await resolveItemSchema(tripRefForSchema) : [];
41
+ return { participantPricing, anchor, identityFields };
42
+ }
43
+ /**
44
+ * Create a booking (public self-serve or staff). Validates catalog capacity +
45
+ * the client seam, prices the draft per-participant, and persists booking +
46
+ * people + items + assignments + reminders in one transaction. Status starts
47
+ * `draft`; the deposit payment (and its webhook) drives the status forward.
48
+ */
49
+ export async function createBooking(input, opts = {}) {
50
+ const config = requireBookingConfig();
51
+ const db = getBookingDb();
52
+ const now = opts.now ?? new Date();
53
+ const validationItems = input.items.map((it) => ({
54
+ tripRef: it.tripRef,
55
+ departureDate: it.departureDate,
56
+ participantsCount: it.assignments.length
57
+ }));
58
+ await runCapacityCheck(validationItems);
59
+ await runValidateBooking({
60
+ operation: 'create',
61
+ bookingId: null,
62
+ items: validationItems,
63
+ customer: input.customer
64
+ });
65
+ const ctx = await buildPricingContext(config, input.customer, input.items[0]?.tripRef);
66
+ const draft = buildBookingDraft(input, config, ctx, now);
67
+ // Unique booking number with collision retry (~25 bits → rare).
68
+ let number = generateBookingNumber();
69
+ for (let i = 0; i < 5; i++) {
70
+ const hit = await db
71
+ .select({ id: bookingsTable.id })
72
+ .from(bookingsTable)
73
+ .where(eq(bookingsTable.number, number));
74
+ if (hit.length === 0)
75
+ break;
76
+ number = generateBookingNumber();
77
+ }
78
+ return db.transaction(async (tx) => {
79
+ const [booking] = await tx
80
+ .insert(bookingsTable)
81
+ .values({
82
+ number,
83
+ status: 'draft',
84
+ currency: config.currency,
85
+ customer: input.customer,
86
+ totals: draft.totals,
87
+ balanceDueAt: draft.balanceDueAt,
88
+ holdExpiresAt: draft.holdExpiresAt,
89
+ language: input.language ?? null
90
+ })
91
+ .returning();
92
+ const personIdByRef = new Map();
93
+ for (const p of draft.persons) {
94
+ const [person] = await tx
95
+ .insert(bookingPersonsTable)
96
+ .values({ bookingId: booking.id, data: p.identity })
97
+ .returning();
98
+ personIdByRef.set(p.ref, person.id);
99
+ }
100
+ for (const it of draft.items) {
101
+ const [item] = await tx
102
+ .insert(bookingItemsTable)
103
+ .values({
104
+ bookingId: booking.id,
105
+ tripRef: it.tripRef,
106
+ departureDate: it.departureDate,
107
+ participantsCount: it.participantsCount,
108
+ unitPriceSnapshot: it.unitPriceSnapshot,
109
+ subtotalSnapshot: it.subtotalSnapshot,
110
+ appliedDiscounts: it.appliedDiscounts
111
+ })
112
+ .returning();
113
+ if (it.assignments.length > 0) {
114
+ await tx.insert(bookingItemPersonsTable).values(it.assignments.map((a) => ({
115
+ itemId: item.id,
116
+ personId: personIdLookup(personIdByRef, a.personRef),
117
+ data: a.data ?? {}
118
+ })));
119
+ }
120
+ }
121
+ if (draft.reminders.length > 0) {
122
+ await tx.insert(bookingRemindersTable).values(draft.reminders.map((r) => ({
123
+ bookingId: booking.id,
124
+ type: r.type,
125
+ templateKey: r.templateKey,
126
+ scheduledFor: r.scheduledFor
127
+ })));
128
+ }
129
+ return { booking, accessToken: booking.accessToken };
130
+ });
131
+ }
132
+ /**
133
+ * Edit a booking (staff or portal). Recomputes totals per-participant + derived
134
+ * status, enforces the actor's guards (`portal` blocks dropping below paid),
135
+ * re-runs capacity + validate seam when items change, replaces people/items/
136
+ * assignments, and reschedules reminders on a departure change. One transaction.
137
+ */
138
+ export async function editBooking(bookingId, changes, opts) {
139
+ const config = requireBookingConfig();
140
+ const db = getBookingDb();
141
+ const now = opts.now ?? new Date();
142
+ return db.transaction(async (tx) => {
143
+ const [booking] = await tx
144
+ .select()
145
+ .from(bookingsTable)
146
+ .where(eq(bookingsTable.id, bookingId))
147
+ .for('update');
148
+ if (!booking)
149
+ throw new Error(`Booking ${bookingId} not found`);
150
+ const existingItems = await tx
151
+ .select()
152
+ .from(bookingItemsTable)
153
+ .where(eq(bookingItemsTable.bookingId, bookingId));
154
+ const customerForCtx = changes.customer ?? booking.customer;
155
+ const tripRefForSchema = changes.items?.[0]?.tripRef ?? existingItems[0]?.tripRef;
156
+ const ctx = await buildPricingContext(config, customerForCtx, tripRefForSchema);
157
+ // Pricing inputs: from the edit when items change, else from existing rows.
158
+ let participants;
159
+ let trips;
160
+ if (changes.items) {
161
+ const pi = pricingInputsFromCreate({ customer: customerForCtx, persons: changes.persons ?? [], items: changes.items }, ctx.identityFields);
162
+ participants = pi.participants;
163
+ trips = pi.trips;
164
+ }
165
+ else {
166
+ const existingItemIds = existingItems.map((i) => i.id);
167
+ const persons = await tx
168
+ .select()
169
+ .from(bookingPersonsTable)
170
+ .where(eq(bookingPersonsTable.bookingId, bookingId));
171
+ const assignments = existingItemIds.length
172
+ ? await tx
173
+ .select()
174
+ .from(bookingItemPersonsTable)
175
+ .where(inArray(bookingItemPersonsTable.itemId, existingItemIds))
176
+ : [];
177
+ 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);
178
+ participants = pi.participants;
179
+ trips = pi.trips;
180
+ }
181
+ const result = priceByParticipant(participants, trips, ctx.anchor, now, ctx.participantPricing);
182
+ const dates = (changes.items ?? existingItems).map((it) => it.departureDate);
183
+ const payments = (await tx
184
+ .select()
185
+ .from(bookingPaymentsTable)
186
+ .where(eq(bookingPaymentsTable.bookingId, bookingId))).map((p) => ({ amount: p.amount, status: p.status, kind: p.kind }));
187
+ const adjustments = await tx
188
+ .select()
189
+ .from(bookingAdjustmentsTable)
190
+ .where(and(eq(bookingAdjustmentsTable.bookingId, bookingId), isNull(bookingAdjustmentsTable.voidedAt)));
191
+ const sums = sumAdjustments(adjustments);
192
+ const totals = recomputeTotals(result, sums, payments);
193
+ if (opts.actor === 'portal')
194
+ assertPortalEditAllowed(booking.status, netOf(totals), payments);
195
+ else
196
+ assertEditAllowed(booking.status);
197
+ if (changes.items) {
198
+ const validationItems = changes.items.map((it) => ({
199
+ tripRef: it.tripRef,
200
+ departureDate: it.departureDate,
201
+ participantsCount: it.assignments.length
202
+ }));
203
+ await runCapacityCheck(validationItems, bookingId);
204
+ await runValidateBooking({
205
+ operation: 'edit',
206
+ bookingId,
207
+ items: validationItems,
208
+ customer: customerForCtx
209
+ });
210
+ const subtotalByRef = grossByTripRef(result);
211
+ // Replace people + items + assignments (item delete cascades assignments).
212
+ await tx.delete(bookingItemsTable).where(eq(bookingItemsTable.bookingId, bookingId));
213
+ await tx.delete(bookingPersonsTable).where(eq(bookingPersonsTable.bookingId, bookingId));
214
+ const personIdByRef = new Map();
215
+ for (const p of changes.persons ?? []) {
216
+ const [person] = await tx
217
+ .insert(bookingPersonsTable)
218
+ .values({ bookingId, data: p.identity })
219
+ .returning();
220
+ personIdByRef.set(p.ref, person.id);
221
+ }
222
+ for (const it of changes.items) {
223
+ const [item] = await tx
224
+ .insert(bookingItemsTable)
225
+ .values({
226
+ bookingId,
227
+ tripRef: it.tripRef,
228
+ departureDate: it.departureDate,
229
+ participantsCount: it.assignments.length,
230
+ unitPriceSnapshot: it.unitPrice,
231
+ subtotalSnapshot: subtotalByRef.get(it.tripRef) ?? null,
232
+ appliedDiscounts: totals.appliedRules.filter((r) => r.targetRef === it.tripRef)
233
+ })
234
+ .returning();
235
+ if (it.assignments.length > 0) {
236
+ await tx.insert(bookingItemPersonsTable).values(it.assignments.map((a) => ({
237
+ itemId: item.id,
238
+ personId: personIdLookup(personIdByRef, a.personRef),
239
+ data: a.data ?? {}
240
+ })));
241
+ }
242
+ }
243
+ }
244
+ const status = deriveStatus({
245
+ gross: netOf(totals),
246
+ paid: totals.paid,
247
+ depositRequired: config.depositAmount,
248
+ current: booking.status
249
+ });
250
+ const [updated] = await tx
251
+ .update(bookingsTable)
252
+ .set({ totals, status, customer: customerForCtx, updatedAt: now })
253
+ .where(eq(bookingsTable.id, bookingId))
254
+ .returning();
255
+ if (changes.items)
256
+ await rescheduleReminders(tx, bookingId, dates);
257
+ return updated;
258
+ });
259
+ }
260
+ /**
261
+ * Price a booking from its CURRENTLY persisted people/items/assignments (no
262
+ * edit). Shared by adjustment recompute — mirrors the "no items change" branch
263
+ * of {@link editBooking}. Must run inside a transaction that has locked the
264
+ * booking row.
265
+ */
266
+ export async function priceFromExistingRows(tx, bookingId, config, now) {
267
+ const [booking] = await tx.select().from(bookingsTable).where(eq(bookingsTable.id, bookingId));
268
+ if (!booking)
269
+ throw new Error(`Booking ${bookingId} not found`);
270
+ const existingItems = await tx
271
+ .select()
272
+ .from(bookingItemsTable)
273
+ .where(eq(bookingItemsTable.bookingId, bookingId));
274
+ const ctx = await buildPricingContext(config, booking.customer, existingItems[0]?.tripRef);
275
+ const persons = await tx
276
+ .select()
277
+ .from(bookingPersonsTable)
278
+ .where(eq(bookingPersonsTable.bookingId, bookingId));
279
+ const itemIds = existingItems.map((i) => i.id);
280
+ const assignments = itemIds.length
281
+ ? await tx
282
+ .select()
283
+ .from(bookingItemPersonsTable)
284
+ .where(inArray(bookingItemPersonsTable.itemId, itemIds))
285
+ : [];
286
+ 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);
287
+ return priceByParticipant(pi.participants, pi.trips, ctx.anchor, now, ctx.participantPricing);
288
+ }
@@ -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 {};