includio-cms 0.36.9 → 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 (243) hide show
  1. package/API.md +178 -3
  2. package/CHANGELOG.md +54 -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 +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 +244 -0
  62. package/dist/admin/remote/booking.remote.js +283 -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 +39 -0
  76. package/dist/booking/client/index.js +44 -0
  77. package/dist/booking/client/use-booking.svelte.d.ts +25 -0
  78. package/dist/booking/client/use-booking.svelte.js +56 -0
  79. package/dist/booking/config.d.ts +156 -0
  80. package/dist/booking/config.js +36 -0
  81. package/dist/booking/draft.d.ts +77 -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 +136 -0
  121. package/dist/booking/server/bookings.d.ts +45 -0
  122. package/dist/booking/server/bookings.js +288 -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 +68 -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 +99 -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 +207 -0
  152. package/dist/booking/server/portal.js +234 -0
  153. package/dist/booking/server/refunds.d.ts +7 -0
  154. package/dist/booking/server/refunds.js +58 -0
  155. package/dist/booking/server/reminders.d.ts +9 -0
  156. package/dist/booking/server/reminders.js +24 -0
  157. package/dist/booking/server/validation.d.ts +17 -0
  158. package/dist/booking/server/validation.js +55 -0
  159. package/dist/booking/state-machine.d.ts +41 -0
  160. package/dist/booking/state-machine.js +73 -0
  161. package/dist/booking/templates/_partials/footer.en.html +4 -0
  162. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  163. package/dist/booking/templates/_partials/header.en.html +4 -0
  164. package/dist/booking/templates/_partials/header.pl.html +4 -0
  165. package/dist/booking/templates/balance-due.en.html +10 -0
  166. package/dist/booking/templates/balance-due.pl.html +10 -0
  167. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  168. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  169. package/dist/booking/templates/booking-received.en.html +9 -0
  170. package/dist/booking/templates/booking-received.pl.html +9 -0
  171. package/dist/booking/templates/deposit-paid.en.html +6 -0
  172. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  173. package/dist/booking/templates/payment-link.en.html +5 -0
  174. package/dist/booking/templates/payment-link.pl.html +5 -0
  175. package/dist/booking/templates/payment-received.en.html +10 -0
  176. package/dist/booking/templates/payment-received.pl.html +10 -0
  177. package/dist/booking/totals.d.ts +38 -0
  178. package/dist/booking/totals.js +61 -0
  179. package/dist/booking/webhook-logic.d.ts +28 -0
  180. package/dist/booking/webhook-logic.js +50 -0
  181. package/dist/cli/create-user.d.ts +7 -1
  182. package/dist/cli/create-user.js +23 -15
  183. package/dist/cli/index.js +55 -7
  184. package/dist/cli/scaffold/admin.d.ts +6 -0
  185. package/dist/cli/scaffold/admin.js +5 -3
  186. package/dist/cli/scaffold/booking.d.ts +12 -0
  187. package/dist/cli/scaffold/booking.js +103 -0
  188. package/dist/core/cms.d.ts +24 -9
  189. package/dist/core/cms.js +72 -34
  190. package/dist/core/config/validatePreviews.d.ts +12 -0
  191. package/dist/core/config/validatePreviews.js +90 -0
  192. package/dist/core/fields/slugPath.d.ts +9 -1
  193. package/dist/core/fields/slugPath.js +16 -2
  194. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  195. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  196. package/dist/core/server/generator/generator.js +7 -2
  197. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  198. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  199. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  200. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  201. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  202. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  203. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  204. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  205. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  206. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  207. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  208. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  209. package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
  210. package/dist/db-postgres/schema/booking/bookings.js +40 -0
  211. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  212. package/dist/db-postgres/schema/booking/index.js +7 -0
  213. package/dist/db-postgres/schema/index.d.ts +1 -0
  214. package/dist/db-postgres/schema/index.js +1 -0
  215. package/dist/db-postgres/schema-booking.d.ts +1 -0
  216. package/dist/db-postgres/schema-booking.js +1 -0
  217. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  218. package/dist/server/security/rate-limit-handler.js +34 -0
  219. package/dist/shop/adapters/payu/client.js +3 -1
  220. package/dist/shop/adapters/payu/index.js +37 -17
  221. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  222. package/dist/shop/adapters/payu/payload.js +1 -1
  223. package/dist/shop/adapters/stripe/index.js +36 -21
  224. package/dist/shop/index.d.ts +11 -2
  225. package/dist/shop/index.js +43 -1
  226. package/dist/shop/rate-limit.d.ts +1 -9
  227. package/dist/shop/rate-limit.js +3 -28
  228. package/dist/shop/types.d.ts +19 -0
  229. package/dist/sveltekit/config.d.ts +4 -0
  230. package/dist/sveltekit/config.js +6 -0
  231. package/dist/types/cms.d.ts +15 -1
  232. package/dist/types/cms.schema.d.ts +96 -2
  233. package/dist/types/cms.schema.js +84 -1
  234. package/dist/types/config.d.ts +33 -0
  235. package/dist/types/layout.d.ts +6 -0
  236. package/dist/types/modules.d.ts +133 -0
  237. package/dist/types/modules.js +27 -0
  238. package/dist/updates/0.37.0/index.d.ts +2 -0
  239. package/dist/updates/0.37.0/index.js +52 -0
  240. package/dist/updates/index.js +3 -1
  241. package/package.json +19 -1
  242. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  243. package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
@@ -0,0 +1,56 @@
1
+ import { createBookingClient, BookingClientError } from './index.js';
2
+ /**
3
+ * Reactive self-service portal state for a booking access token. Loads the view
4
+ * on creation; `edit`/`reload` refresh it, `pay` returns a payment link. Errors
5
+ * (including `CONTACT_STAFF` 409s) surface via `error`. A `PHONE_REQUIRED` 2FA
6
+ * gate surfaces via `needsPhone`; call `verifyPhone` to clear it and re-load the
7
+ * view.
8
+ *
9
+ * @public
10
+ */
11
+ export function useBooking(token, opts = {}) {
12
+ const client = createBookingClient(opts);
13
+ let view = $state(null);
14
+ let loading = $state(false);
15
+ let error = $state(null);
16
+ let needsPhone = $state(false);
17
+ async function run(fn) {
18
+ loading = true;
19
+ error = null;
20
+ try {
21
+ view = await fn();
22
+ needsPhone = false;
23
+ }
24
+ catch (e) {
25
+ if (e instanceof BookingClientError && e.code === 'PHONE_REQUIRED') {
26
+ needsPhone = true;
27
+ }
28
+ else {
29
+ error = e instanceof Error ? e.message : 'Error';
30
+ }
31
+ }
32
+ finally {
33
+ loading = false;
34
+ }
35
+ }
36
+ run(() => client.getByToken(token));
37
+ return {
38
+ get view() {
39
+ return view;
40
+ },
41
+ get loading() {
42
+ return loading;
43
+ },
44
+ get error() {
45
+ return error;
46
+ },
47
+ get needsPhone() {
48
+ return needsPhone;
49
+ },
50
+ reload: () => run(() => client.getByToken(token)),
51
+ edit: (changes) => run(() => client.edit(token, changes)),
52
+ pay: (amount) => client.pay(token, amount),
53
+ verifyPhone: (phone) => run(() => client.verifyPhone(token, phone)),
54
+ addTrips: (additions) => run(() => client.addTrips(token, additions))
55
+ };
56
+ }
@@ -0,0 +1,156 @@
1
+ import type { PaymentAdapter } from '../shop/types.js';
2
+ import type { PricingConfig } from './pricing.js';
3
+ import type { ParticipantPricingConfig } from './participant-pricing.js';
4
+ import type { FieldDef, AgeFlagConfig } from './participant-fields/types.js';
5
+ /** One scheduled reminder rule, relative to the earliest departure date. */
6
+ export interface ReminderRule {
7
+ /** Stable type tag for the reminder (e.g. `pre-departure`). */
8
+ type: string;
9
+ /** Whole days before departure to send. */
10
+ daysBefore: number;
11
+ /** Email template key to render (see booking email registry). */
12
+ templateKey: string;
13
+ }
14
+ /** Context handed to the client-supplied {@link BookingValidator} seam. */
15
+ export interface BookingValidationContext {
16
+ operation: 'create' | 'edit';
17
+ bookingId: string | null;
18
+ items: Array<{
19
+ tripRef: string;
20
+ departureDate: Date | null;
21
+ participantsCount: number;
22
+ }>;
23
+ customer: Record<string, unknown>;
24
+ }
25
+ /**
26
+ * Client-specific validation seam (e.g. weekly-model term conflicts). Throws to
27
+ * reject. The framework only enforces generic capacity + not-below-paid.
28
+ */
29
+ export type BookingValidator = (ctx: BookingValidationContext) => void | Promise<void>;
30
+ /**
31
+ * User-facing booking configuration. Passed to `defineBooking` and registered
32
+ * via `defineConfig({ modules: [defineBooking({...})] })`.
33
+ *
34
+ * @public
35
+ */
36
+ export interface BookingConfig {
37
+ /** ISO currency code, e.g. `PLN`. */
38
+ currency: string;
39
+ /** Slug of the (client-specific) trip catalog collection. Omit when none. */
40
+ catalogCollection?: string;
41
+ /**
42
+ * Dodatkowe kolekcje rdzenia wciągane do kontekstu „Rezerwacje" w adminie (obok katalogu) —
43
+ * np. kolekcja definiująca typy wypraw i pola uczestnika. Bez tego mieszkają w kontekście
44
+ * „Treści" i wyglądają jak doszyta kolekcja CMS-a, choć obsługują rezerwacje.
45
+ *
46
+ * Wyłącznie prezentacja: nie zmienia danych ani uprawnień.
47
+ *
48
+ * @experimental Docelowo moduł będzie wnosił typy wypraw sam (0.38) — patrz ROADMAP.
49
+ */
50
+ surfacedCollections?: string[];
51
+ /** Deposit that secures a reservation, in minor units (default 0). */
52
+ depositAmount?: number;
53
+ /** Discount rules (legacy per-line engine; booking now prices per-participant). */
54
+ pricing?: PricingConfig;
55
+ /**
56
+ * Per-participant pricing config (age bands + volume ladder + early-bird) as a
57
+ * resolver so values can come from an editable CMS singleton (globalSettings).
58
+ * Called per create/edit. Omit ⇒ no age/volume discounts (everyone full price).
59
+ */
60
+ participantPricing?: () => ParticipantPricingConfig | Promise<ParticipantPricingConfig>;
61
+ /** Customer field holding the arrival date — anchor for age + early-bird + reminders. Default `arrivalDate`. */
62
+ arrivalDateField?: string;
63
+ /** Payment adapters (reused from the shop module's adapter family). */
64
+ payment?: PaymentAdapter[];
65
+ /**
66
+ * Portal URL template handed to payment adapters as the post-payment return
67
+ * URL. Placeholder `{token}` → `booking.accessToken`. Must be absolute, e.g.
68
+ * `https://shop.example/rezerwacja/{token}`. Omit to disable redirect return.
69
+ */
70
+ portalUrlTemplate?: string;
71
+ /**
72
+ * Absolute return URL for the CHECKOUT deposit payment (post-payment landing,
73
+ * e.g. a thank-you page), when it should differ from the portal. Handed to the
74
+ * adapter as `ctx.continueUrl` for the deposit only; installments/balance keep
75
+ * `portalUrlTemplate`. Omit ⇒ deposit returns to the portal like other payments.
76
+ */
77
+ checkoutReturnUrl?: string;
78
+ /** Operator address for staff notifications. */
79
+ adminEmail?: string;
80
+ /** Business display name shown in transactional email footers; omit for none. */
81
+ brand?: string;
82
+ /** Minutes an unpaid draft is held before auto-cancel (default 60). */
83
+ depositTtlMinutes?: number;
84
+ /** Days before departure the balance is due; omit to disable forfeiture. */
85
+ balanceDueDaysBefore?: number;
86
+ /** Field on the catalog entry holding seat capacity; omit for unlimited. */
87
+ capacityField?: string;
88
+ /** Reminder schedule (offsets before departure). */
89
+ reminders?: ReminderRule[];
90
+ /** Client-specific validation seam. */
91
+ validateBooking?: BookingValidator;
92
+ /** In-process maintenance loop tuning. */
93
+ maintenance?: {
94
+ autoRun?: boolean;
95
+ intervalMinutes?: number;
96
+ };
97
+ /** Bearer token guarding the secured maintenance endpoint; omit to disable. */
98
+ maintenanceToken?: string;
99
+ /** Sekret HMAC dla sesji portalu (2FA telefonem); omit ⇒ portal fail-closed. */
100
+ portalSecret?: string;
101
+ /** Schemat pól uczestnika: statyczny lub resolver per wpis katalogu. */
102
+ participantFields?: FieldDef[] | ((catalogEntry: Record<string, unknown>) => FieldDef[]);
103
+ /** Flaga wieku (np. „Dziecko") z pola roli `birthDate` + próg. */
104
+ ageFlag?: AgeFlagConfig;
105
+ /**
106
+ * Opis pól worka `customer` — WYŁĄCZNIE dla prezentacji w adminie. Rdzeń nie zna kształtu
107
+ * `customer` (definiuje go projekt), więc bez tego panel „Klient" pokazuje uczłowieczone
108
+ * klucze (`referralSource` → „Referral source"). Podaj `label`, żeby nazwać pola po ludzku,
109
+ * i `role: 'birthDate'`, żeby rdzeń dopisał wiek obok daty.
110
+ *
111
+ * Klucze zagnieżdżone (kolumny tabel, np. manifest osób) adresuje się kropką:
112
+ * `{ key: 'people.birthDate', label: 'Data urodzenia', role: 'birthDate' }`.
113
+ *
114
+ * Nie waliduje ani nie zmienia danych — to czysta warstwa prezentacji.
115
+ *
116
+ * @experimental Świadomie NIEPEŁNE: pola uczestnika mają w rdzeniu prawdziwy schemat
117
+ * (`participantFields` — napędza walidację, render i formularz), a `customer` ma dziś tylko
118
+ * etykiety, bo formularz kontaktowy jest pisany ręcznie w projekcie. Skutek: worek `customer`
119
+ * ma DWIE deklaracje (etykiety tutaj, reguły w repo projektu) i może się rozjechać. Domknięcie
120
+ * w 0.38 — patrz ROADMAP.
121
+ */
122
+ customerFields?: FieldDef[];
123
+ }
124
+ /** Fully-defaulted booking config, carried on `CMSModule.config`. */
125
+ export interface ResolvedBookingConfig {
126
+ currency: string;
127
+ catalogCollection: string | null;
128
+ surfacedCollections: string[];
129
+ depositAmount: number;
130
+ pricing: PricingConfig;
131
+ participantPricing: (() => ParticipantPricingConfig | Promise<ParticipantPricingConfig>) | null;
132
+ arrivalDateField: string;
133
+ payment: PaymentAdapter[];
134
+ portalUrlTemplate: string | null;
135
+ checkoutReturnUrl: string | null;
136
+ adminEmail: string | null;
137
+ brand: string | null;
138
+ depositTtlMinutes: number;
139
+ balanceDueDaysBefore: number | null;
140
+ capacityField: string | null;
141
+ reminders: ReminderRule[];
142
+ validateBooking: BookingValidator | null;
143
+ maintenance: {
144
+ autoRun: boolean;
145
+ intervalMinutes: number;
146
+ };
147
+ maintenanceToken: string | null;
148
+ portalSecret: string | null;
149
+ participantFields: FieldDef[] | ((catalogEntry: Record<string, unknown>) => FieldDef[]);
150
+ ageFlag: AgeFlagConfig | null;
151
+ customerFields: FieldDef[];
152
+ }
153
+ /** Resolves a raw {@link BookingConfig} into a {@link ResolvedBookingConfig}. */
154
+ export declare function resolveBookingConfig(config: BookingConfig): ResolvedBookingConfig;
155
+ /** Rozwiązuje schemat pól dla danego wpisu katalogu (array lub resolver). */
156
+ export declare function resolveParticipantFields(config: ResolvedBookingConfig, catalogEntry: Record<string, unknown>): FieldDef[];
@@ -0,0 +1,36 @@
1
+ /** Resolves a raw {@link BookingConfig} into a {@link ResolvedBookingConfig}. */
2
+ export function resolveBookingConfig(config) {
3
+ return {
4
+ currency: config.currency,
5
+ catalogCollection: config.catalogCollection ?? null,
6
+ surfacedCollections: config.surfacedCollections ?? [],
7
+ depositAmount: config.depositAmount ?? 0,
8
+ pricing: config.pricing ?? { rules: [] },
9
+ participantPricing: config.participantPricing ?? null,
10
+ arrivalDateField: config.arrivalDateField ?? 'arrivalDate',
11
+ payment: config.payment ?? [],
12
+ portalUrlTemplate: config.portalUrlTemplate ?? null,
13
+ checkoutReturnUrl: config.checkoutReturnUrl ?? null,
14
+ adminEmail: config.adminEmail ?? null,
15
+ brand: config.brand ?? null,
16
+ depositTtlMinutes: config.depositTtlMinutes ?? 60,
17
+ balanceDueDaysBefore: config.balanceDueDaysBefore ?? null,
18
+ capacityField: config.capacityField ?? null,
19
+ reminders: config.reminders ?? [],
20
+ validateBooking: config.validateBooking ?? null,
21
+ maintenance: {
22
+ autoRun: config.maintenance?.autoRun ?? true,
23
+ intervalMinutes: config.maintenance?.intervalMinutes ?? 15
24
+ },
25
+ maintenanceToken: config.maintenanceToken ?? null,
26
+ portalSecret: config.portalSecret ?? null,
27
+ participantFields: config.participantFields ?? [],
28
+ ageFlag: config.ageFlag ?? null,
29
+ customerFields: config.customerFields ?? []
30
+ };
31
+ }
32
+ /** Rozwiązuje schemat pól dla danego wpisu katalogu (array lub resolver). */
33
+ export function resolveParticipantFields(config, catalogEntry) {
34
+ const pf = config.participantFields;
35
+ return typeof pf === 'function' ? pf(catalogEntry) : pf;
36
+ }
@@ -0,0 +1,77 @@
1
+ import type { ResolvedBookingConfig } from './config.js';
2
+ import { type PricingParticipant, type PricedTrip, type ParticipantPricingConfig } from './participant-pricing.js';
3
+ import { type BookingTotals } from './totals.js';
4
+ import type { FieldDef } from './participant-fields/types.js';
5
+ import type { AppliedRule } from './pricing.js';
6
+ /**
7
+ * A person on the booking. Identity fields (name, date of birth — the
8
+ * role:name / role:birthDate participant fields) are entered ONCE and shared
9
+ * across every trip the person takes. `ref` is a client-stable key used to
10
+ * correlate the person with their item assignments within a single payload.
11
+ */
12
+ export interface PersonInput {
13
+ ref: string;
14
+ identity: Record<string, unknown>;
15
+ }
16
+ /** Assignment of a person to a trip line, with that person's per-trip answers. */
17
+ export interface ItemAssignmentInput {
18
+ /** References a {@link PersonInput.ref} in the same payload. */
19
+ personRef: string;
20
+ /** Per-trip custom-field answers (trip-type-specific). */
21
+ data?: Record<string, unknown>;
22
+ }
23
+ /** One requested trip line in a create/edit payload. */
24
+ export interface BookingItemInput {
25
+ tripRef: string;
26
+ departureDate: Date | null;
27
+ /** Unit (adult) price (minor units) snapshotted at booking time. */
28
+ unitPrice: number;
29
+ /** Who takes this trip (+ their per-trip data). Count derives from this. */
30
+ assignments: ItemAssignmentInput[];
31
+ }
32
+ export interface CreateBookingInput {
33
+ customer: Record<string, unknown>;
34
+ /** People on the booking (identity, shared across trips). */
35
+ persons: PersonInput[];
36
+ items: BookingItemInput[];
37
+ language?: string;
38
+ }
39
+ /** Per-call pricing context (dynamic: from globalSettings + this booking's customer). */
40
+ export interface DraftPricingContext {
41
+ participantPricing: ParticipantPricingConfig;
42
+ /** Arrival date — anchor for age + early-bird. Null ⇒ everyone full price. */
43
+ anchor: Date | null;
44
+ /** Identity field schema, used to locate the role:birthDate key for age. */
45
+ identityFields: FieldDef[];
46
+ }
47
+ export interface BookingDraft {
48
+ totals: BookingTotals;
49
+ persons: PersonInput[];
50
+ items: Array<{
51
+ tripRef: string;
52
+ departureDate: Date | null;
53
+ participantsCount: number;
54
+ unitPriceSnapshot: number;
55
+ subtotalSnapshot: number | null;
56
+ appliedDiscounts: AppliedRule[];
57
+ assignments: ItemAssignmentInput[];
58
+ }>;
59
+ reminders: Array<{
60
+ type: string;
61
+ templateKey: string;
62
+ scheduledFor: Date;
63
+ }>;
64
+ balanceDueAt: Date | null;
65
+ holdExpiresAt: Date;
66
+ }
67
+ /** Build the per-participant pricing inputs from a create payload. */
68
+ export declare function pricingInputsFromCreate(input: CreateBookingInput, identityFields: FieldDef[]): {
69
+ participants: PricingParticipant[];
70
+ trips: PricedTrip[];
71
+ };
72
+ /**
73
+ * Pure: turn a create payload + config + pricing context + clock into the
74
+ * rows/totals to persist. Prices per-participant (age bands + volume ladder +
75
+ * early-bird), anchored to the arrival date in `ctx.anchor`.
76
+ */
77
+ export declare function buildBookingDraft(input: CreateBookingInput, config: ResolvedBookingConfig, ctx: DraftPricingContext, now: Date): BookingDraft;
@@ -0,0 +1,67 @@
1
+ import { computeBalanceDueAt, computeReminderSchedule } from './reminders-schedule.js';
2
+ import { priceByParticipant } from './participant-pricing.js';
3
+ import { resultToTotals, grossByTripRef } from './totals.js';
4
+ import { NO_ADJUSTMENTS } from './adjustments.js';
5
+ /** Build the per-participant pricing inputs from a create payload. */
6
+ export function pricingInputsFromCreate(input, identityFields) {
7
+ const dobKey = identityFields.find((f) => f.role === 'birthDate')?.key ?? null;
8
+ const seen = new Set();
9
+ const trips = [];
10
+ for (const it of input.items) {
11
+ if (seen.has(it.tripRef))
12
+ continue;
13
+ seen.add(it.tripRef);
14
+ trips.push({ ref: it.tripRef, adultPrice: it.unitPrice });
15
+ }
16
+ const tripsByPersonRef = new Map();
17
+ for (const it of input.items) {
18
+ for (const a of it.assignments) {
19
+ const list = tripsByPersonRef.get(a.personRef);
20
+ if (list) {
21
+ if (!list.includes(it.tripRef))
22
+ list.push(it.tripRef);
23
+ }
24
+ else {
25
+ tripsByPersonRef.set(a.personRef, [it.tripRef]);
26
+ }
27
+ }
28
+ }
29
+ const participants = input.persons.map((p) => {
30
+ const dobRaw = dobKey ? p.identity[dobKey] : null;
31
+ return {
32
+ id: p.ref,
33
+ dob: typeof dobRaw === 'string' ? dobRaw : null,
34
+ tripRefs: tripsByPersonRef.get(p.ref) ?? []
35
+ };
36
+ });
37
+ return { participants, trips };
38
+ }
39
+ /**
40
+ * Pure: turn a create payload + config + pricing context + clock into the
41
+ * rows/totals to persist. Prices per-participant (age bands + volume ladder +
42
+ * early-bird), anchored to the arrival date in `ctx.anchor`.
43
+ */
44
+ export function buildBookingDraft(input, config, ctx, now) {
45
+ const dates = input.items.map((it) => it.departureDate);
46
+ const { participants, trips } = pricingInputsFromCreate(input, ctx.identityFields);
47
+ const result = priceByParticipant(participants, trips, ctx.anchor, now, ctx.participantPricing);
48
+ const totals = resultToTotals(result, NO_ADJUSTMENTS, 0);
49
+ const subtotalByRef = grossByTripRef(result);
50
+ const items = input.items.map((it) => ({
51
+ tripRef: it.tripRef,
52
+ departureDate: it.departureDate,
53
+ participantsCount: it.assignments.length,
54
+ unitPriceSnapshot: it.unitPrice,
55
+ subtotalSnapshot: subtotalByRef.get(it.tripRef) ?? null,
56
+ appliedDiscounts: totals.appliedRules.filter((r) => r.targetRef === it.tripRef),
57
+ assignments: it.assignments
58
+ }));
59
+ return {
60
+ totals,
61
+ persons: input.persons,
62
+ items,
63
+ reminders: computeReminderSchedule(dates, config.reminders),
64
+ balanceDueAt: computeBalanceDueAt(dates, config.balanceDueDaysBefore),
65
+ holdExpiresAt: new Date(now.getTime() + config.depositTtlMinutes * 60_000)
66
+ };
67
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pure decision helpers for the booking maintenance cron. No clock/db access —
3
+ * the caller passes `now` and the rows. Tested in isolation; the DB wrappers in
4
+ * `server/maintenance.ts` just fetch rows and apply the results.
5
+ */
6
+ import type { BookingStatus } from './state-machine.js';
7
+ /** Ids of reminders that are due to send (unsent and `scheduledFor <= now`). */
8
+ export declare function selectDueReminders(rows: Array<{
9
+ id: string;
10
+ scheduledFor: Date;
11
+ sentAt: Date | null;
12
+ }>, now: Date): string[];
13
+ /**
14
+ * Partition active bookings into those to auto-cancel:
15
+ * - `abandon`: a `draft` whose deposit hold (`holdExpiresAt`) has lapsed.
16
+ * - `forfeit`: any non-terminal booking past `balanceDueAt` with balance left
17
+ * (deposit forfeited). `paid`/`completed`/`cancelled` are skipped.
18
+ */
19
+ export declare function decideExpirations(bookings: Array<{
20
+ id: string;
21
+ status: BookingStatus;
22
+ holdExpiresAt: Date | null;
23
+ balanceDueAt: Date | null;
24
+ totals: {
25
+ balance: number;
26
+ };
27
+ }>, now: Date): {
28
+ abandon: string[];
29
+ forfeit: string[];
30
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure decision helpers for the booking maintenance cron. No clock/db access —
3
+ * the caller passes `now` and the rows. Tested in isolation; the DB wrappers in
4
+ * `server/maintenance.ts` just fetch rows and apply the results.
5
+ */
6
+ /** Ids of reminders that are due to send (unsent and `scheduledFor <= now`). */
7
+ export function selectDueReminders(rows, now) {
8
+ return rows
9
+ .filter((r) => r.sentAt == null && r.scheduledFor.getTime() <= now.getTime())
10
+ .map((r) => r.id);
11
+ }
12
+ /**
13
+ * Partition active bookings into those to auto-cancel:
14
+ * - `abandon`: a `draft` whose deposit hold (`holdExpiresAt`) has lapsed.
15
+ * - `forfeit`: any non-terminal booking past `balanceDueAt` with balance left
16
+ * (deposit forfeited). `paid`/`completed`/`cancelled` are skipped.
17
+ */
18
+ export function decideExpirations(bookings, now) {
19
+ const abandon = [];
20
+ const forfeit = [];
21
+ for (const b of bookings) {
22
+ if (b.status === 'cancelled' || b.status === 'completed' || b.status === 'paid')
23
+ continue;
24
+ if (b.status === 'draft' && b.holdExpiresAt && b.holdExpiresAt.getTime() <= now.getTime()) {
25
+ abandon.push(b.id);
26
+ continue;
27
+ }
28
+ if (b.balanceDueAt && b.balanceDueAt.getTime() <= now.getTime() && b.totals.balance > 0) {
29
+ forfeit.push(b.id);
30
+ }
31
+ }
32
+ return { abandon, forfeit };
33
+ }
@@ -0,0 +1,18 @@
1
+ import type { CMSModule } from '../types/modules.js';
2
+ import { type BookingConfig } from './config.js';
3
+ /**
4
+ * Defines the booking module — a long-lived, mutable reservation system with a
5
+ * payment ledger, layered on the CMS core as a {@link CMSModule}. The resolved
6
+ * config is reachable via `getCMS().getModuleConfig('booking')`.
7
+ *
8
+ * @public
9
+ */
10
+ export declare function defineBooking(config: BookingConfig): CMSModule;
11
+ export { type BookingConfig, type ResolvedBookingConfig, type ReminderRule, type BookingValidator, type BookingValidationContext } from './config.js';
12
+ export { price, daysUntil, type Condition, type Effect, type DiscountRule, type PricingConfig, type PricingLine, type AppliedRule, type Perk, type PriceResult } from './pricing.js';
13
+ export { priceByParticipant, bandPctFor, ladderTierFor, type AgeBand, type LadderTier, type EarlyBird, type ParticipantPricingConfig, type PricedTrip, type PricingParticipant, type ParticipantLine, type ParticipantBreakdown, type ParticipantPriceResult } from './participant-pricing.js';
14
+ export { buildPricingInputs, type PersonRow, type ItemRow, type ItemPersonRow, type PricingInputs } from './pricing-inputs.js';
15
+ export { deriveStatus, canTransition, assertTransition, BookingStateError, BOOKING_STATUSES, type BookingStatus } from './state-machine.js';
16
+ export { signedAmount, sumAdjustments, assertAdjustmentMagnitude, NO_ADJUSTMENTS, type AdjustmentKind, type BookingAdjustment, type AdjustmentSums } from './adjustments.js';
17
+ export { totalPaid, totalRefunded, totalPending, balance, isFullyPaid, canReduceGrossTo, assertGrossNotBelowPaid, ledgerSummary, LedgerError, type PaymentEntry, type PaymentKind, type PaymentStatus, type LedgerSummary } from './ledger.js';
18
+ export * from './participant-fields/index.js';
@@ -0,0 +1,52 @@
1
+ import { resolveBookingConfig } from './config.js';
2
+ /**
3
+ * Defines the booking module — a long-lived, mutable reservation system with a
4
+ * payment ledger, layered on the CMS core as a {@link CMSModule}. The resolved
5
+ * config is reachable via `getCMS().getModuleConfig('booking')`.
6
+ *
7
+ * @public
8
+ */
9
+ export function defineBooking(config) {
10
+ const resolved = resolveBookingConfig(config);
11
+ // Katalog + kolekcje wspierające booking (np. typy wypraw definiujące pola uczestnika).
12
+ // Pusta lista ⇒ `undefined`, żeby nie zmieniać semantyki „brak wciągniętych kolekcji".
13
+ const surfaced = [
14
+ ...(resolved.catalogCollection ? [resolved.catalogCollection] : []),
15
+ ...resolved.surfacedCollections
16
+ ];
17
+ return {
18
+ id: 'booking',
19
+ label: { pl: 'Rezerwacje', en: 'Bookings' },
20
+ icon: 'calendar',
21
+ context: {
22
+ contextLabel: { pl: 'Rezerwacje', en: 'Bookings' },
23
+ landingUrl: '/admin/booking',
24
+ sections: [
25
+ {
26
+ key: 'reservations',
27
+ title: { pl: 'Rezerwacje', en: 'Reservations' },
28
+ url: '/admin/booking/reservations',
29
+ icon: 'clipboard-list'
30
+ }
31
+ ],
32
+ surfacedCollections: surfaced.length > 0 ? surfaced : undefined
33
+ },
34
+ config: resolved,
35
+ serverHooks: {
36
+ // A plain string, NOT an inline import: this barrel is client-safe (re-exported
37
+ // via `includio-cms/booking`), and inlining `import('./server/...')` here makes
38
+ // client bundlers pull server-only code (node:fs/url via the email template
39
+ // registry) and break browser builds. `initCMS` (server-only) imports this
40
+ // specifier at runtime and runs its `serverInit` — see ModuleServerHooks.initModule.
41
+ initModule: 'includio-cms/booking/server'
42
+ }
43
+ };
44
+ }
45
+ export {} from './config.js';
46
+ export { price, daysUntil } from './pricing.js';
47
+ export { priceByParticipant, bandPctFor, ladderTierFor } from './participant-pricing.js';
48
+ export { buildPricingInputs } from './pricing-inputs.js';
49
+ export { deriveStatus, canTransition, assertTransition, BookingStateError, BOOKING_STATUSES } from './state-machine.js';
50
+ export { signedAmount, sumAdjustments, assertAdjustmentMagnitude, NO_ADJUSTMENTS } from './adjustments.js';
51
+ export { totalPaid, totalRefunded, totalPending, balance, isFullyPaid, canReduceGrossTo, assertGrossNotBelowPaid, ledgerSummary, LedgerError } from './ledger.js';
52
+ export * from './participant-fields/index.js';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Booking payment ledger — pure helpers over a list of payment entries. The
3
+ * ledger is the source of truth for "how much is paid"; the booking status is
4
+ * {@link import('./state-machine.js').deriveStatus | derived} from the balance.
5
+ *
6
+ * All amounts are in **minor units** (grosze).
7
+ */
8
+ export type PaymentKind = 'deposit' | 'installment' | 'cash' | 'online';
9
+ export type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded';
10
+ export interface PaymentEntry {
11
+ amount: number;
12
+ status: PaymentStatus;
13
+ kind?: PaymentKind;
14
+ }
15
+ /** Thrown when an edit would push the gross below what has already been paid. */
16
+ export declare class LedgerError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ /** Sum of entries currently in `paid` state. */
20
+ export declare function totalPaid(payments: PaymentEntry[]): number;
21
+ /** Sum of entries that were refunded. */
22
+ export declare function totalRefunded(payments: PaymentEntry[]): number;
23
+ /** Sum of entries still pending (links generated, not yet settled). */
24
+ export declare function totalPending(payments: PaymentEntry[]): number;
25
+ /** Outstanding balance: `gross − paid` (never below 0). */
26
+ export declare function balance(gross: number, payments: PaymentEntry[]): number;
27
+ /** Whether the booking is fully settled (`gross > 0` and balance reached 0). */
28
+ export declare function isFullyPaid(gross: number, payments: PaymentEntry[]): boolean;
29
+ /** Whether the gross may be reduced to `newGross` without going below paid. */
30
+ export declare function canReduceGrossTo(newGross: number, payments: PaymentEntry[]): boolean;
31
+ /**
32
+ * Asserts an edit's new gross does not drop below the already-paid total.
33
+ * @throws {LedgerError} when `newGross < totalPaid`.
34
+ */
35
+ export declare function assertGrossNotBelowPaid(newGross: number, payments: PaymentEntry[]): void;
36
+ export interface LedgerSummary {
37
+ gross: number;
38
+ paid: number;
39
+ pending: number;
40
+ refunded: number;
41
+ balance: number;
42
+ /** Amount paid beyond gross (e.g. after a downward edit), 0 when none. */
43
+ overpaid: number;
44
+ }
45
+ /** Full ledger snapshot for a given gross. */
46
+ export declare function ledgerSummary(gross: number, payments: PaymentEntry[]): LedgerSummary;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Booking payment ledger — pure helpers over a list of payment entries. The
3
+ * ledger is the source of truth for "how much is paid"; the booking status is
4
+ * {@link import('./state-machine.js').deriveStatus | derived} from the balance.
5
+ *
6
+ * All amounts are in **minor units** (grosze).
7
+ */
8
+ /** Thrown when an edit would push the gross below what has already been paid. */
9
+ export class LedgerError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'LedgerError';
13
+ }
14
+ }
15
+ /** Sum of entries currently in `paid` state. */
16
+ export function totalPaid(payments) {
17
+ return payments.reduce((sum, p) => (p.status === 'paid' ? sum + p.amount : sum), 0);
18
+ }
19
+ /** Sum of entries that were refunded. */
20
+ export function totalRefunded(payments) {
21
+ return payments.reduce((sum, p) => (p.status === 'refunded' ? sum + p.amount : sum), 0);
22
+ }
23
+ /** Sum of entries still pending (links generated, not yet settled). */
24
+ export function totalPending(payments) {
25
+ return payments.reduce((sum, p) => (p.status === 'pending' ? sum + p.amount : sum), 0);
26
+ }
27
+ /** Outstanding balance: `gross − paid` (never below 0). */
28
+ export function balance(gross, payments) {
29
+ return Math.max(0, gross - totalPaid(payments));
30
+ }
31
+ /** Whether the booking is fully settled (`gross > 0` and balance reached 0). */
32
+ export function isFullyPaid(gross, payments) {
33
+ return gross > 0 && totalPaid(payments) >= gross;
34
+ }
35
+ /** Whether the gross may be reduced to `newGross` without going below paid. */
36
+ export function canReduceGrossTo(newGross, payments) {
37
+ return newGross >= totalPaid(payments);
38
+ }
39
+ /**
40
+ * Asserts an edit's new gross does not drop below the already-paid total.
41
+ * @throws {LedgerError} when `newGross < totalPaid`.
42
+ */
43
+ export function assertGrossNotBelowPaid(newGross, payments) {
44
+ const paid = totalPaid(payments);
45
+ if (newGross < paid) {
46
+ throw new LedgerError(`Cannot set gross to ${newGross}: already paid ${paid} (would owe a refund of ${paid - newGross}).`);
47
+ }
48
+ }
49
+ /** Full ledger snapshot for a given gross. */
50
+ export function ledgerSummary(gross, payments) {
51
+ const paid = totalPaid(payments);
52
+ return {
53
+ gross,
54
+ paid,
55
+ pending: totalPending(payments),
56
+ refunded: totalRefunded(payments),
57
+ balance: Math.max(0, gross - paid),
58
+ overpaid: Math.max(0, paid - gross)
59
+ };
60
+ }
@@ -0,0 +1,2 @@
1
+ /** Random `BK-XXXXX` booking number (~25 bits). Caller retries on UNIQUE collision. */
2
+ export declare function generateBookingNumber(): string;
@@ -0,0 +1,11 @@
1
+ // Crockford base32 alphabet — unambiguous (no I, L, O, U).
2
+ const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
3
+ /** Random `BK-XXXXX` booking number (~25 bits). Caller retries on UNIQUE collision. */
4
+ export function generateBookingNumber() {
5
+ const bytes = new Uint8Array(5);
6
+ crypto.getRandomValues(bytes);
7
+ let out = '';
8
+ for (let i = 0; i < 5; i++)
9
+ out += ALPHABET[bytes[i] % 32];
10
+ return `BK-${out}`;
11
+ }
@@ -0,0 +1,10 @@
1
+ import type { FieldDef } from './types.js';
2
+ /**
3
+ * Whether a given audience may fill/edit a field. Default `whoFills` is `'both'`.
4
+ *
5
+ * Staff (admin) is **privileged** — it may edit every field regardless of
6
+ * `whoFills` (operators must always be able to correct/complete any datum).
7
+ * `whoFills` therefore only gates the **customer/portal** side: a `'staff'`
8
+ * field is simply hidden from the portal, not locked away from the operator.
9
+ */
10
+ export declare function canFill(field: FieldDef, audience: 'customer' | 'staff'): boolean;