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,236 @@
1
+ import { and, eq, inArray, isNull } from 'drizzle-orm';
2
+ import { getBookingDb } from './db.js';
3
+ import { bookingsTable, bookingItemsTable, bookingPersonsTable, bookingItemPersonsTable, bookingPaymentsTable } from '../../db-postgres/schema/booking/index.js';
4
+ import { ledgerSummary } from '../ledger.js';
5
+ import { editBooking } from './bookings.js';
6
+ import { createInstallmentPayment } from './payments.js';
7
+ import { resolveItemSchema, saveParticipantData } from './participants.js';
8
+ import { canFill } from '../participant-fields/index.js';
9
+ import { flattenAssignments } from '../participants-view.js';
10
+ import { phonesMatch } from './portal-session.js';
11
+ import { BookingValidationError } from './validation.js';
12
+ /**
13
+ * Pure: shape a portal-safe DTO. Explicitly whitelists fields — `staffNotes`,
14
+ * `deletedBy`, and other internals never reach the customer.
15
+ */
16
+ export function toPortalView(booking, items, participants, payments, schemaByItem = {}) {
17
+ return {
18
+ number: booking.number,
19
+ status: booking.status,
20
+ currency: booking.currency,
21
+ customer: booking.customer,
22
+ totals: booking.totals,
23
+ balanceDueAt: booking.balanceDueAt,
24
+ items: items.map((it) => ({ ...it, participantFields: schemaByItem[it.id] ?? [] })),
25
+ participants,
26
+ payments: payments.map((p) => ({
27
+ amount: p.amount,
28
+ status: p.status,
29
+ kind: p.kind,
30
+ paidAt: p.paidAt ?? null
31
+ })),
32
+ ledger: ledgerSummary(booking.totals.gross, payments)
33
+ };
34
+ }
35
+ /** Resolve a booking by its unguessable portal access token (active only). */
36
+ export async function resolveByToken(token) {
37
+ const db = getBookingDb();
38
+ const [booking] = await db
39
+ .select()
40
+ .from(bookingsTable)
41
+ .where(and(eq(bookingsTable.accessToken, token), isNull(bookingsTable.deletedAt)));
42
+ return booking ?? null;
43
+ }
44
+ /** Drugi czynnik: zwraca booking gdy podany numer pasuje do `customer.phone`. */
45
+ export async function verifyPhone(token, phone) {
46
+ const booking = await resolveByToken(token);
47
+ if (!booking)
48
+ return null;
49
+ const stored = booking.customer?.phone ?? '';
50
+ return phonesMatch(phone, stored) ? booking : null;
51
+ }
52
+ /**
53
+ * Non-PII booking status by portal token — for the reservation panel reachable by
54
+ * token alone (no 2FA). Deliberately WITHOUT customer / participants / phone /
55
+ * email: PII stays gated behind portal 2FA ({@link portalView}). Safe to render
56
+ * server-side on a token-addressed page and to poll. Trip titles are resolved
57
+ * client-side from `tripRef` (the module is catalog-title agnostic).
58
+ */
59
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
60
+ export async function bookingStatusByToken(token) {
61
+ // Guard: accessToken is a uuid column — a malformed token can't match any row,
62
+ // and querying it raw would throw (→ 500). Treat as not-found.
63
+ if (!UUID_RE.test(token))
64
+ return null;
65
+ const booking = await resolveByToken(token);
66
+ if (!booking)
67
+ return null;
68
+ const db = getBookingDb();
69
+ const items = await db
70
+ .select()
71
+ .from(bookingItemsTable)
72
+ .where(eq(bookingItemsTable.bookingId, booking.id));
73
+ const payments = await db
74
+ .select()
75
+ .from(bookingPaymentsTable)
76
+ .where(eq(bookingPaymentsTable.bookingId, booking.id));
77
+ const pendingDeposit = payments.find((p) => p.kind === 'deposit' && p.status === 'pending');
78
+ return {
79
+ number: booking.number,
80
+ status: booking.status,
81
+ currency: booking.currency,
82
+ totals: {
83
+ gross: booking.totals.gross,
84
+ paid: booking.totals.paid,
85
+ balance: booking.totals.balance
86
+ },
87
+ items: items.map((it) => ({
88
+ tripRef: it.tripRef,
89
+ departureDate: it.departureDate ? it.departureDate.toISOString().slice(0, 10) : null,
90
+ unitPrice: it.unitPriceSnapshot
91
+ })),
92
+ depositLink: pendingDeposit?.paymentLink ?? null
93
+ };
94
+ }
95
+ /** Load a booking's people + assignments, folded into the flat participant shape. */
96
+ async function loadParticipants(db, bookingId, itemIds) {
97
+ const persons = await db
98
+ .select()
99
+ .from(bookingPersonsTable)
100
+ .where(eq(bookingPersonsTable.bookingId, bookingId));
101
+ const assignments = itemIds.length
102
+ ? await db
103
+ .select()
104
+ .from(bookingItemPersonsTable)
105
+ .where(inArray(bookingItemPersonsTable.itemId, itemIds))
106
+ : [];
107
+ return flattenAssignments(persons, assignments);
108
+ }
109
+ /** Portal view: booking + items + participants + ledger (no staff data). */
110
+ export async function portalView(token) {
111
+ const booking = await resolveByToken(token);
112
+ if (!booking)
113
+ return null;
114
+ const db = getBookingDb();
115
+ const items = await db
116
+ .select()
117
+ .from(bookingItemsTable)
118
+ .where(eq(bookingItemsTable.bookingId, booking.id));
119
+ const itemIds = items.map((i) => i.id);
120
+ const participants = await loadParticipants(db, booking.id, itemIds);
121
+ const payments = await db
122
+ .select()
123
+ .from(bookingPaymentsTable)
124
+ .where(eq(bookingPaymentsTable.bookingId, booking.id));
125
+ // Per-item participant schema, customer-visible only (staff fields hidden).
126
+ const schemaByItem = {};
127
+ for (const it of items) {
128
+ const fields = await resolveItemSchema(it.tripRef);
129
+ schemaByItem[it.id] = fields.filter((f) => canFill(f, 'customer'));
130
+ }
131
+ return toPortalView(booking, items, participants, payments.map((p) => ({ amount: p.amount, status: p.status, kind: p.kind, paidAt: p.paidAt })), schemaByItem);
132
+ }
133
+ /** Portal self-service edit (actor:'portal' guards). Throws → handler maps to 409/400. */
134
+ export async function portalEdit(token, changes) {
135
+ const booking = await resolveByToken(token);
136
+ if (!booking)
137
+ return null;
138
+ await editBooking(booking.id, changes, { actor: 'portal' });
139
+ return portalView(token);
140
+ }
141
+ /** Portal: create an installment/balance payment link. */
142
+ export async function portalPay(token, amount) {
143
+ const booking = await resolveByToken(token);
144
+ if (!booking)
145
+ return null;
146
+ return createInstallmentPayment(booking.id, amount);
147
+ }
148
+ /**
149
+ * Portal self-service: customer fills their own participant data. Verifies the
150
+ * assignment belongs to the token's booking (no cross-booking writes), then
151
+ * saves with `'customer'` scope (staff-only fields are dropped, not an error).
152
+ * Throws {@link ParticipantValidationError} on type/min/max/option violations.
153
+ *
154
+ * @param assignmentId - `booking_item_persons.id` (person-on-this-trip row).
155
+ */
156
+ export async function portalUpdateParticipant(token, assignmentId, data) {
157
+ const booking = await resolveByToken(token);
158
+ if (!booking)
159
+ return null;
160
+ const db = getBookingDb();
161
+ const items = await db
162
+ .select({ id: bookingItemsTable.id })
163
+ .from(bookingItemsTable)
164
+ .where(eq(bookingItemsTable.bookingId, booking.id));
165
+ const itemIds = new Set(items.map((i) => i.id));
166
+ const [assignment] = await db
167
+ .select()
168
+ .from(bookingItemPersonsTable)
169
+ .where(eq(bookingItemPersonsTable.id, assignmentId));
170
+ if (!assignment || !itemIds.has(assignment.itemId))
171
+ return null;
172
+ await saveParticipantData(assignmentId, data, 'customer');
173
+ return portalView(token);
174
+ }
175
+ /**
176
+ * Self-service dobieranie wycieczki: doczytaj obecny stan, scal z `additions`
177
+ * (tylko istniejące osoby), przelicz przez `editBooking` (full-replace → recompute
178
+ * tym samym silnikiem per-uczestnik co koszyk). Saldo rośnie; dopłata istniejącym
179
+ * flow. `assignments` w additions = lista `booking_persons.id`. Kotwica nowej
180
+ * pozycji (`departureDate`) = `customer.arrivalDate`.
181
+ */
182
+ export async function portalAddTrips(token, additions) {
183
+ const booking = await resolveByToken(token);
184
+ if (!booking)
185
+ return null;
186
+ const db = getBookingDb();
187
+ const persons = await db
188
+ .select()
189
+ .from(bookingPersonsTable)
190
+ .where(eq(bookingPersonsTable.bookingId, booking.id));
191
+ const items = await db
192
+ .select()
193
+ .from(bookingItemsTable)
194
+ .where(eq(bookingItemsTable.bookingId, booking.id));
195
+ const itemIds = items.map((i) => i.id);
196
+ const assigns = itemIds.length
197
+ ? await db
198
+ .select()
199
+ .from(bookingItemPersonsTable)
200
+ .where(inArray(bookingItemPersonsTable.itemId, itemIds))
201
+ : [];
202
+ const personIds = new Set(persons.map((p) => p.id));
203
+ const existingRefs = new Set(items.map((i) => i.tripRef));
204
+ for (const a of additions) {
205
+ if (existingRefs.has(a.tripRef)) {
206
+ throw new BookingValidationError('VALIDATION', `Wycieczka już w rezerwacji: ${a.tripRef}`);
207
+ }
208
+ for (const ref of a.assignments) {
209
+ if (!personIds.has(ref)) {
210
+ throw new BookingValidationError('VALIDATION', `Nieznana osoba: ${ref}`);
211
+ }
212
+ }
213
+ }
214
+ const personInputs = persons.map((p) => ({ ref: p.id, identity: p.data }));
215
+ const byItem = new Map();
216
+ for (const a of assigns)
217
+ byItem.set(a.itemId, [...(byItem.get(a.itemId) ?? []), a]);
218
+ const existingItems = items.map((it) => ({
219
+ tripRef: it.tripRef,
220
+ departureDate: it.departureDate,
221
+ unitPrice: it.unitPriceSnapshot,
222
+ assignments: (byItem.get(it.id) ?? []).map((a) => ({ personRef: a.personId, data: a.data }))
223
+ }));
224
+ const arrivalRaw = booking.customer?.arrivalDate;
225
+ const anchor = arrivalRaw ? new Date(arrivalRaw) : null;
226
+ // Bez `unitPrice`: cenę nowej pozycji ustala bramka cenowa w `editBooking` z katalogu
227
+ // (`actor: 'portal'`). Klient z ważnym tokenem nie może sobie dorzucić wyprawy za grosz.
228
+ const newItems = additions.map((a) => ({
229
+ tripRef: a.tripRef,
230
+ departureDate: anchor,
231
+ unitPrice: 0, // nadpisywane przez bramkę
232
+ assignments: a.assignments.map((ref) => ({ personRef: ref }))
233
+ }));
234
+ await editBooking(booking.id, { persons: personInputs, items: [...existingItems, ...newItems] }, { actor: 'portal' });
235
+ return portalView(token);
236
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Kto pisze do rezerwacji. Steruje zaufaniem do ceny przyniesionej w żądaniu:
3
+ * `staff` jest zalogowaną obsługą (ręczna korekta ceny w adminie jest zamierzoną
4
+ * funkcją), `public`/`portal` to klient końcowy — jego ceny nie honorujemy nigdy.
5
+ */
6
+ export type PriceActor = 'public' | 'portal' | 'staff';
7
+ /** Cena nie do przyjęcia: brak w katalogu, ujemna albo w ułamku grosza. */
8
+ export declare class PriceSourceError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ /**
12
+ * Ustala cenę bazową każdej pozycji. **Niezmiennik: cena jest atrybutem katalogu,
13
+ * nie żądania.** Bez tego publiczny endpoint przyjmował `unitPrice` prosto z ciała
14
+ * żądania i rezerwację dało się złożyć za grosz.
15
+ *
16
+ * Kolejność źródeł:
17
+ * 1. `staff` z jawną ceną → jego cena (ręczna korekta w adminie),
18
+ * 2. pozycja już w rezerwacji → jej **snapshot** (zamrożony przy zakupie; dotknięcie
19
+ * rezerwacji nie może podnieść klientowi ceny, gdy cennik poszedł w górę),
20
+ * 3. katalog przez szew `resolvePrice`.
21
+ *
22
+ * Czysta funkcja — I/O (odczyt katalogu) robi {@link fetchCatalogPrices}.
23
+ *
24
+ * @throws {PriceSourceError} gdy pozycji nie da się wycenić (fail-closed: brak ceny
25
+ * to odmowa, nigdy „zero").
26
+ */
27
+ export declare function pickItemPrices(input: {
28
+ actor: PriceActor;
29
+ items: Array<{
30
+ tripRef: string;
31
+ unitPrice?: number;
32
+ }>;
33
+ /** `tripRef` → `unitPriceSnapshot` pozycji już zapisanych w tej rezerwacji. */
34
+ existingByRef: Map<string, number>;
35
+ /** `tripRef` → cena bazowa z katalogu (wynik szwu `resolvePrice`). */
36
+ catalogPrices: Map<string, number>;
37
+ }): Array<{
38
+ tripRef: string;
39
+ unitPrice: number;
40
+ }>;
41
+ /**
42
+ * Odpytuje szew `resolvePrice` o cenę bazową podanych pozycji katalogu (równolegle,
43
+ * bez duplikatów). Szew jest **serwerowy** — czyta katalog projektu, więc przeglądarka
44
+ * nie ma na tę cenę wpływu.
45
+ *
46
+ * @throws {PriceSourceError} gdy `resolvePrice` nie jest skonfigurowany. Fail-closed:
47
+ * publiczny booking bez źródła cen musi odmówić, a nie po cichu zaufać żądaniu.
48
+ */
49
+ export declare function fetchCatalogPrices(tripRefs: string[]): Promise<Map<string, number>>;
50
+ /**
51
+ * Pozycje, o których trzeba zapytać katalog. Obsługa podająca cenę jawnie nie wymaga
52
+ * odczytu — dzięki temu instalacja bez publicznego bookingu (sam admin) działa bez
53
+ * skonfigurowanego `resolvePrice`.
54
+ *
55
+ * `existingByRef` bywa nieznane przed wejściem w transakcję (patrz `editBooking`) — wtedy
56
+ * pomiń je, a katalog zostanie odpytany też o pozycje, które ostatecznie wezmą snapshot.
57
+ * Kilka odczytów więcej jest tańsze niż odczyt katalogu pod blokadą wiersza.
58
+ */
59
+ export declare function catalogRefsNeeded(actor: PriceActor, items: Array<{
60
+ tripRef: string;
61
+ unitPrice?: number;
62
+ }>, existingByRef?: Map<string, number>): string[];
63
+ /**
64
+ * Wycenia pozycje: odpytuje katalog **tylko o te, których nie da się wycenić inaczej**,
65
+ * a potem stosuje {@link pickItemPrices}. Wołaj poza transakcją — robi I/O.
66
+ */
67
+ export declare function priceItemsFromCatalog(actor: PriceActor, items: Array<{
68
+ tripRef: string;
69
+ unitPrice?: number;
70
+ }>, existingByRef: Map<string, number>): Promise<Array<{
71
+ tripRef: string;
72
+ unitPrice: number;
73
+ }>>;
@@ -0,0 +1,94 @@
1
+ import { requireBookingConfig } from './db.js';
2
+ /** Cena nie do przyjęcia: brak w katalogu, ujemna albo w ułamku grosza. */
3
+ export class PriceSourceError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'PriceSourceError';
7
+ }
8
+ }
9
+ function assertValidPrice(tripRef, value) {
10
+ if (!Number.isInteger(value) || value < 0) {
11
+ throw new PriceSourceError(`Niepoprawna cena pozycji ${tripRef}: ${value} (oczekiwano nieujemnej liczby całkowitej groszy)`);
12
+ }
13
+ return value;
14
+ }
15
+ /**
16
+ * Ustala cenę bazową każdej pozycji. **Niezmiennik: cena jest atrybutem katalogu,
17
+ * nie żądania.** Bez tego publiczny endpoint przyjmował `unitPrice` prosto z ciała
18
+ * żądania i rezerwację dało się złożyć za grosz.
19
+ *
20
+ * Kolejność źródeł:
21
+ * 1. `staff` z jawną ceną → jego cena (ręczna korekta w adminie),
22
+ * 2. pozycja już w rezerwacji → jej **snapshot** (zamrożony przy zakupie; dotknięcie
23
+ * rezerwacji nie może podnieść klientowi ceny, gdy cennik poszedł w górę),
24
+ * 3. katalog przez szew `resolvePrice`.
25
+ *
26
+ * Czysta funkcja — I/O (odczyt katalogu) robi {@link fetchCatalogPrices}.
27
+ *
28
+ * @throws {PriceSourceError} gdy pozycji nie da się wycenić (fail-closed: brak ceny
29
+ * to odmowa, nigdy „zero").
30
+ */
31
+ export function pickItemPrices(input) {
32
+ const { actor, items, existingByRef, catalogPrices } = input;
33
+ return items.map((it) => {
34
+ if (actor === 'staff' && it.unitPrice !== undefined) {
35
+ return { tripRef: it.tripRef, unitPrice: assertValidPrice(it.tripRef, it.unitPrice) };
36
+ }
37
+ const snapshot = existingByRef.get(it.tripRef);
38
+ if (snapshot !== undefined) {
39
+ return { tripRef: it.tripRef, unitPrice: assertValidPrice(it.tripRef, snapshot) };
40
+ }
41
+ const fromCatalog = catalogPrices.get(it.tripRef);
42
+ if (fromCatalog === undefined) {
43
+ throw new PriceSourceError(`Brak ceny w katalogu dla pozycji ${it.tripRef} — rezerwacji nie da się wycenić`);
44
+ }
45
+ return { tripRef: it.tripRef, unitPrice: assertValidPrice(it.tripRef, fromCatalog) };
46
+ });
47
+ }
48
+ /**
49
+ * Odpytuje szew `resolvePrice` o cenę bazową podanych pozycji katalogu (równolegle,
50
+ * bez duplikatów). Szew jest **serwerowy** — czyta katalog projektu, więc przeglądarka
51
+ * nie ma na tę cenę wpływu.
52
+ *
53
+ * @throws {PriceSourceError} gdy `resolvePrice` nie jest skonfigurowany. Fail-closed:
54
+ * publiczny booking bez źródła cen musi odmówić, a nie po cichu zaufać żądaniu.
55
+ */
56
+ export async function fetchCatalogPrices(tripRefs) {
57
+ const refs = [...new Set(tripRefs)];
58
+ if (refs.length === 0)
59
+ return new Map();
60
+ const { resolvePrice } = requireBookingConfig();
61
+ if (!resolvePrice) {
62
+ throw new PriceSourceError('defineBooking({ resolvePrice }) nie jest skonfigurowany — publiczna rezerwacja nie ma z czego wziąć ceny. ' +
63
+ 'Podaj szew czytający cenę z katalogu, np. ' +
64
+ '`resolvePrice: async (tripRef) => Math.round(Number((await resolveEntry({ id: tripRef, status: "published" }))?.price ?? 0) * 100)`.');
65
+ }
66
+ const prices = await Promise.all(refs.map(async (ref) => [ref, await resolvePrice(ref)]));
67
+ return new Map(prices);
68
+ }
69
+ /**
70
+ * Pozycje, o których trzeba zapytać katalog. Obsługa podająca cenę jawnie nie wymaga
71
+ * odczytu — dzięki temu instalacja bez publicznego bookingu (sam admin) działa bez
72
+ * skonfigurowanego `resolvePrice`.
73
+ *
74
+ * `existingByRef` bywa nieznane przed wejściem w transakcję (patrz `editBooking`) — wtedy
75
+ * pomiń je, a katalog zostanie odpytany też o pozycje, które ostatecznie wezmą snapshot.
76
+ * Kilka odczytów więcej jest tańsze niż odczyt katalogu pod blokadą wiersza.
77
+ */
78
+ export function catalogRefsNeeded(actor, items, existingByRef) {
79
+ return items
80
+ .filter((it) => {
81
+ if (actor === 'staff' && it.unitPrice !== undefined)
82
+ return false;
83
+ return !existingByRef?.has(it.tripRef);
84
+ })
85
+ .map((it) => it.tripRef);
86
+ }
87
+ /**
88
+ * Wycenia pozycje: odpytuje katalog **tylko o te, których nie da się wycenić inaczej**,
89
+ * a potem stosuje {@link pickItemPrices}. Wołaj poza transakcją — robi I/O.
90
+ */
91
+ export async function priceItemsFromCatalog(actor, items, existingByRef) {
92
+ const catalogPrices = await fetchCatalogPrices(catalogRefsNeeded(actor, items, existingByRef));
93
+ return pickItemPrices({ actor, items, existingByRef, catalogPrices });
94
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Refund a paid ledger entry via the adapter (staff-only manual action), then
3
+ * mark it `refunded` and recompute the booking's paid/balance + derived status.
4
+ * Full-payment refund semantics (the row is marked refunded); `amount` is passed
5
+ * to the provider for partial captures but the ledger treats the row as settled.
6
+ */
7
+ export declare function refundPayment(paymentId: string, amount?: number): Promise<void>;
@@ -0,0 +1,58 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getBookingDb, requireBookingConfig } from './db.js';
3
+ import { bookingsTable, bookingPaymentsTable } from '../../db-postgres/schema/booking/index.js';
4
+ import { deriveStatus } from '../state-machine.js';
5
+ import { netOf } from '../totals.js';
6
+ /**
7
+ * Refund a paid ledger entry via the adapter (staff-only manual action), then
8
+ * mark it `refunded` and recompute the booking's paid/balance + derived status.
9
+ * Full-payment refund semantics (the row is marked refunded); `amount` is passed
10
+ * to the provider for partial captures but the ledger treats the row as settled.
11
+ */
12
+ export async function refundPayment(paymentId, amount) {
13
+ const config = requireBookingConfig();
14
+ const db = getBookingDb();
15
+ const [payment] = await db
16
+ .select()
17
+ .from(bookingPaymentsTable)
18
+ .where(eq(bookingPaymentsTable.id, paymentId));
19
+ if (!payment || payment.status !== 'paid' || !payment.providerRef) {
20
+ throw new Error('Payment not refundable');
21
+ }
22
+ const adapter = config.payment[0];
23
+ if (!adapter?.refund)
24
+ throw new Error('Adapter does not support refunds');
25
+ // Provider call first (outside the DB transaction — it's a network op).
26
+ await adapter.refund({ providerRef: payment.providerRef, amount, currency: config.currency });
27
+ await db.transaction(async (tx) => {
28
+ await tx
29
+ .update(bookingPaymentsTable)
30
+ .set({ status: 'refunded', updatedAt: new Date() })
31
+ .where(eq(bookingPaymentsTable.id, paymentId));
32
+ const [b] = await tx
33
+ .select()
34
+ .from(bookingsTable)
35
+ .where(eq(bookingsTable.id, payment.bookingId))
36
+ .for('update');
37
+ if (!b)
38
+ return;
39
+ const rows = await tx
40
+ .select()
41
+ .from(bookingPaymentsTable)
42
+ .where(eq(bookingPaymentsTable.bookingId, payment.bookingId));
43
+ const paid = rows.filter((p) => p.status === 'paid').reduce((s, p) => s + p.amount, 0);
44
+ await tx
45
+ .update(bookingsTable)
46
+ .set({
47
+ totals: { ...b.totals, paid, balance: Math.max(0, netOf(b.totals) - paid) },
48
+ status: deriveStatus({
49
+ gross: netOf(b.totals),
50
+ paid,
51
+ depositRequired: config.depositAmount,
52
+ current: b.status
53
+ }),
54
+ updatedAt: new Date()
55
+ })
56
+ .where(eq(bookingsTable.id, payment.bookingId));
57
+ });
58
+ }
@@ -0,0 +1,9 @@
1
+ import { getBookingDb } from './db.js';
2
+ /** Exact drizzle transaction handle, derived from the booking db's `transaction`. */
3
+ export type BookingTx = Parameters<Parameters<ReturnType<typeof getBookingDb>['transaction']>[0]>[0];
4
+ /**
5
+ * Recompute the reminder schedule for a booking after a departure-date change:
6
+ * drop the unsent reminders and re-insert from the current departures. Sent
7
+ * reminders are preserved (audit). Runs inside the caller's transaction.
8
+ */
9
+ export declare function rescheduleReminders(tx: BookingTx, bookingId: string, dates: Array<Date | null>): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { and, eq, isNull } from 'drizzle-orm';
2
+ import { bookingRemindersTable } from '../../db-postgres/schema/booking/index.js';
3
+ import { computeReminderSchedule } from '../reminders-schedule.js';
4
+ import { getBookingDb, requireBookingConfig } from './db.js';
5
+ /**
6
+ * Recompute the reminder schedule for a booking after a departure-date change:
7
+ * drop the unsent reminders and re-insert from the current departures. Sent
8
+ * reminders are preserved (audit). Runs inside the caller's transaction.
9
+ */
10
+ export async function rescheduleReminders(tx, bookingId, dates) {
11
+ const config = requireBookingConfig();
12
+ await tx
13
+ .delete(bookingRemindersTable)
14
+ .where(and(eq(bookingRemindersTable.bookingId, bookingId), isNull(bookingRemindersTable.sentAt)));
15
+ const rows = computeReminderSchedule(dates, config.reminders);
16
+ if (rows.length > 0) {
17
+ await tx.insert(bookingRemindersTable).values(rows.map((r) => ({
18
+ bookingId,
19
+ type: r.type,
20
+ templateKey: r.templateKey,
21
+ scheduledFor: r.scheduledFor
22
+ })));
23
+ }
24
+ }
@@ -0,0 +1,17 @@
1
+ import type { BookingValidationContext } from '../config.js';
2
+ /** Thrown when a booking fails capacity (core) or the client `validateBooking` seam. */
3
+ export declare class BookingValidationError extends Error {
4
+ code: 'CAPACITY' | 'VALIDATION';
5
+ constructor(code: 'CAPACITY' | 'VALIDATION', message: string);
6
+ }
7
+ /**
8
+ * Generic seat-capacity check (core). Sums requested participants per trip and
9
+ * compares against the catalog capacity minus what active bookings already hold.
10
+ * `excludeBookingId` drops the booking being edited from the existing tally.
11
+ */
12
+ export declare function runCapacityCheck(items: Array<{
13
+ tripRef: string;
14
+ participantsCount: number;
15
+ }>, excludeBookingId?: string): Promise<void>;
16
+ /** Invoke the client-supplied validation seam, wrapping failures uniformly. */
17
+ export declare function runValidateBooking(ctx: BookingValidationContext): Promise<void>;
@@ -0,0 +1,55 @@
1
+ import { and, eq, isNull, ne } from 'drizzle-orm';
2
+ import { getBookingDb, requireBookingConfig } from './db.js';
3
+ import { bookingsTable, bookingItemsTable } from '../../db-postgres/schema/booking/index.js';
4
+ import { decideCapacity } from '../capacity.js';
5
+ import { readTripCapacity } from './catalog.js';
6
+ /** Thrown when a booking fails capacity (core) or the client `validateBooking` seam. */
7
+ export class BookingValidationError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ this.name = 'BookingValidationError';
13
+ }
14
+ }
15
+ /**
16
+ * Generic seat-capacity check (core). Sums requested participants per trip and
17
+ * compares against the catalog capacity minus what active bookings already hold.
18
+ * `excludeBookingId` drops the booking being edited from the existing tally.
19
+ */
20
+ export async function runCapacityCheck(items, excludeBookingId) {
21
+ const db = getBookingDb();
22
+ const requestedByTrip = new Map();
23
+ for (const it of items) {
24
+ requestedByTrip.set(it.tripRef, (requestedByTrip.get(it.tripRef) ?? 0) + it.participantsCount);
25
+ }
26
+ for (const [tripRef, requested] of requestedByTrip) {
27
+ const capacity = await readTripCapacity(tripRef);
28
+ if (capacity == null)
29
+ continue; // unlimited / unconfigured
30
+ const rows = await db
31
+ .select({ count: bookingItemsTable.participantsCount, bid: bookingsTable.id })
32
+ .from(bookingItemsTable)
33
+ .innerJoin(bookingsTable, eq(bookingItemsTable.bookingId, bookingsTable.id))
34
+ .where(and(eq(bookingItemsTable.tripRef, tripRef), isNull(bookingsTable.deletedAt), ne(bookingsTable.status, 'cancelled')));
35
+ const alreadyBooked = rows
36
+ .filter((r) => r.bid !== excludeBookingId)
37
+ .reduce((s, r) => s + r.count, 0);
38
+ const { ok, available } = decideCapacity({ capacity, alreadyBooked, requested });
39
+ if (!ok) {
40
+ throw new BookingValidationError('CAPACITY', `Trip ${tripRef} has only ${available} seats left (requested ${requested}).`);
41
+ }
42
+ }
43
+ }
44
+ /** Invoke the client-supplied validation seam, wrapping failures uniformly. */
45
+ export async function runValidateBooking(ctx) {
46
+ const config = requireBookingConfig();
47
+ if (!config.validateBooking)
48
+ return;
49
+ try {
50
+ await config.validateBooking(ctx);
51
+ }
52
+ catch (err) {
53
+ throw new BookingValidationError('VALIDATION', err instanceof Error ? err.message : 'Validation failed');
54
+ }
55
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Booking state machine — pure, deterministic. Two concerns:
3
+ *
4
+ * 1. {@link deriveStatus} computes the payment-progress status from money
5
+ * (gross, paid, deposit) — the source of truth is the ledger balance.
6
+ * 2. {@link assertTransition} / {@link canTransition} validate **manual**
7
+ * status moves (notably `→ completed` and `→ cancelled`), rejecting illegal
8
+ * transitions with a {@link BookingStateError}.
9
+ */
10
+ export type BookingStatus = 'draft' | 'reserved' | 'partially_paid' | 'paid' | 'completed' | 'cancelled';
11
+ export declare const BOOKING_STATUSES: readonly BookingStatus[];
12
+ /** Thrown when an illegal status transition is attempted. */
13
+ export declare class BookingStateError extends Error {
14
+ from: BookingStatus;
15
+ to: BookingStatus;
16
+ constructor(from: BookingStatus, to: BookingStatus);
17
+ }
18
+ /** Whether `from → to` is a legal transition (self-transitions are allowed). */
19
+ export declare function canTransition(from: BookingStatus, to: BookingStatus): boolean;
20
+ /** Asserts a legal transition, throwing {@link BookingStateError} otherwise. */
21
+ export declare function assertTransition(from: BookingStatus, to: BookingStatus): void;
22
+ export interface DeriveStatusInput {
23
+ /** Total gross owed, in minor units. */
24
+ gross: number;
25
+ /** Total amount paid (ledger sum of paid entries), in minor units. */
26
+ paid: number;
27
+ /** Deposit threshold that secures a reservation, in minor units. */
28
+ depositRequired: number;
29
+ /** Current status — terminal states (`completed`/`cancelled`) are preserved. */
30
+ current?: BookingStatus;
31
+ }
32
+ /**
33
+ * Derives the payment-progress status from the ledger.
34
+ *
35
+ * Money bands (after preserving terminal states):
36
+ * - `paid == 0` → `draft`
37
+ * - `paid >= gross` (gross > 0) → `paid`
38
+ * - `paid > depositRequired` → `partially_paid` (installments beyond deposit)
39
+ * - `0 < paid <= depositRequired` → `reserved` (deposit secured)
40
+ */
41
+ export declare function deriveStatus(input: DeriveStatusInput): BookingStatus;