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,207 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { getBookingDb, requireBookingConfig } from './db.js';
4
+ import { bookingsTable, bookingPaymentsTable } from '../../db-postgres/schema/booking/index.js';
5
+ import { applyPaymentEvent } from '../webhook-logic.js';
6
+ import { deriveStatus } from '../state-machine.js';
7
+ import { netOf } from '../totals.js';
8
+ import { buildPaymentContext } from '../payment-context.js';
9
+ /**
10
+ * Create the deposit payment for a freshly-created booking: ask the first
11
+ * configured adapter for a payment (charging only the deposit amount via an
12
+ * `OrderRef` with `totalGross = depositAmount`), and record a pending ledger row
13
+ * keyed by the provider ref. Returns the redirect for the storefront.
14
+ */
15
+ export async function createDepositPayment(bookingId) {
16
+ const config = requireBookingConfig();
17
+ const db = getBookingDb();
18
+ const [booking] = await db.select().from(bookingsTable).where(eq(bookingsTable.id, bookingId));
19
+ if (!booking)
20
+ throw new Error(`Booking ${bookingId} not found`);
21
+ const adapter = config.payment[0];
22
+ if (!adapter || config.depositAmount <= 0) {
23
+ return { redirectUrl: null, status: 'manual', providerRef: null };
24
+ }
25
+ const ref = {
26
+ id: booking.id,
27
+ number: booking.number,
28
+ totalGross: config.depositAmount,
29
+ currency: config.currency,
30
+ customerEmail: String(booking.customer.email ?? '')
31
+ };
32
+ const baseCtx = buildPaymentContext({
33
+ accessToken: booking.accessToken,
34
+ language: booking.language,
35
+ customer: booking.customer,
36
+ portalUrlTemplate: config.portalUrlTemplate
37
+ });
38
+ const ctx = {
39
+ ...baseCtx,
40
+ // Deposit lands on the checkout return page (e.g. thank-you) when configured;
41
+ // otherwise falls back to the portal like other payments.
42
+ continueUrl: config.checkoutReturnUrl ?? baseCtx.continueUrl,
43
+ // PayU's extOrderId must be unique per POS — a booking pays more than once
44
+ // (deposit + balance), so vary it per attempt while keeping the number visible.
45
+ externalId: `${booking.number}-${randomUUID().slice(0, 8)}`
46
+ };
47
+ const result = await adapter.createPayment(ref, ctx);
48
+ await db.insert(bookingPaymentsTable).values({
49
+ bookingId: booking.id,
50
+ kind: 'deposit',
51
+ amount: config.depositAmount,
52
+ status: 'pending',
53
+ providerRef: result.providerRef ?? null,
54
+ paymentLink: result.redirectUrl ?? null,
55
+ createdBy: 'system'
56
+ });
57
+ return {
58
+ redirectUrl: result.redirectUrl ?? null,
59
+ status: result.status,
60
+ providerRef: result.providerRef ?? null
61
+ };
62
+ }
63
+ /**
64
+ * Apply a provider webhook event to the ledger, idempotently. Locks the payment
65
+ * row (and its booking) `FOR UPDATE` so a concurrent duplicate webhook can't
66
+ * double-apply: the second caller sees a settled row and no-ops. Recomputes the
67
+ * booking totals + derived status and clears the draft hold on first deposit.
68
+ */
69
+ export async function applyWebhookPayment(event) {
70
+ const config = requireBookingConfig();
71
+ const db = getBookingDb();
72
+ if (!event.providerRef)
73
+ return { applied: false, bookingId: null, kind: null };
74
+ return db.transaction(async (tx) => {
75
+ const [payment] = await tx
76
+ .select()
77
+ .from(bookingPaymentsTable)
78
+ .where(eq(bookingPaymentsTable.providerRef, event.providerRef))
79
+ .for('update');
80
+ if (!payment)
81
+ return { applied: false, bookingId: null, kind: null };
82
+ const [booking] = await tx
83
+ .select()
84
+ .from(bookingsTable)
85
+ .where(eq(bookingsTable.id, payment.bookingId))
86
+ .for('update');
87
+ if (!booking)
88
+ return { applied: false, bookingId: null, kind: null };
89
+ const others = (await tx
90
+ .select()
91
+ .from(bookingPaymentsTable)
92
+ .where(eq(bookingPaymentsTable.bookingId, booking.id)))
93
+ .filter((p) => p.id !== payment.id)
94
+ .map((p) => ({ amount: p.amount, status: p.status, kind: p.kind }));
95
+ const r = applyPaymentEvent({
96
+ event,
97
+ payment: { status: payment.status, amount: payment.amount },
98
+ otherPayments: others,
99
+ gross: netOf(booking.totals),
100
+ depositRequired: config.depositAmount,
101
+ currentStatus: booking.status
102
+ });
103
+ if (r.alreadyApplied)
104
+ return { applied: false, bookingId: booking.id, kind: payment.kind };
105
+ await tx
106
+ .update(bookingPaymentsTable)
107
+ .set({
108
+ status: r.newPaymentStatus,
109
+ paidAt: r.newPaymentStatus === 'paid' ? new Date() : null,
110
+ updatedAt: new Date()
111
+ })
112
+ .where(eq(bookingPaymentsTable.id, payment.id));
113
+ await tx
114
+ .update(bookingsTable)
115
+ .set({
116
+ status: r.newStatus,
117
+ totals: {
118
+ ...booking.totals,
119
+ paid: r.newTotalsPaid,
120
+ balance: Math.max(0, netOf(booking.totals) - r.newTotalsPaid)
121
+ },
122
+ holdExpiresAt: r.clearHold ? null : booking.holdExpiresAt,
123
+ updatedAt: new Date()
124
+ })
125
+ .where(eq(bookingsTable.id, booking.id));
126
+ return { applied: r.newPaymentStatus === 'paid', bookingId: booking.id, kind: payment.kind };
127
+ });
128
+ }
129
+ /**
130
+ * Generate an installment/balance payment link for an arbitrary amount (staff or
131
+ * portal). Records a pending ledger row keyed by the provider ref; the webhook
132
+ * settles it later.
133
+ */
134
+ export async function createInstallmentPayment(bookingId, amount) {
135
+ const config = requireBookingConfig();
136
+ const db = getBookingDb();
137
+ const [booking] = await db.select().from(bookingsTable).where(eq(bookingsTable.id, bookingId));
138
+ if (!booking)
139
+ throw new Error('Booking not found');
140
+ const adapter = config.payment[0];
141
+ if (!adapter)
142
+ throw new Error('No payment adapter configured');
143
+ const ctx = {
144
+ ...buildPaymentContext({
145
+ accessToken: booking.accessToken,
146
+ language: booking.language,
147
+ customer: booking.customer,
148
+ portalUrlTemplate: config.portalUrlTemplate
149
+ }),
150
+ // PayU's extOrderId must be unique per POS — a booking pays more than once
151
+ // (deposit + balance), so vary it per attempt while keeping the number visible.
152
+ externalId: `${booking.number}-${randomUUID().slice(0, 8)}`
153
+ };
154
+ const result = await adapter.createPayment({
155
+ id: booking.id,
156
+ number: booking.number,
157
+ totalGross: amount,
158
+ currency: config.currency,
159
+ customerEmail: String(booking.customer.email ?? '')
160
+ }, ctx);
161
+ await db.insert(bookingPaymentsTable).values({
162
+ bookingId,
163
+ kind: 'installment',
164
+ amount,
165
+ status: 'pending',
166
+ providerRef: result.providerRef ?? null,
167
+ paymentLink: result.redirectUrl ?? null,
168
+ createdBy: 'staff'
169
+ });
170
+ return { paymentLink: result.redirectUrl ?? null };
171
+ }
172
+ /** Record a manual cash payment (staff). Settles immediately and recomputes status. */
173
+ export async function recordCash(bookingId, amount) {
174
+ const config = requireBookingConfig();
175
+ const db = getBookingDb();
176
+ await db.transaction(async (tx) => {
177
+ const [b] = await tx
178
+ .select()
179
+ .from(bookingsTable)
180
+ .where(eq(bookingsTable.id, bookingId))
181
+ .for('update');
182
+ if (!b)
183
+ throw new Error('Booking not found');
184
+ await tx.insert(bookingPaymentsTable).values({
185
+ bookingId,
186
+ kind: 'cash',
187
+ amount,
188
+ status: 'paid',
189
+ createdBy: 'staff',
190
+ paidAt: new Date()
191
+ });
192
+ const paid = b.totals.paid + amount;
193
+ await tx
194
+ .update(bookingsTable)
195
+ .set({
196
+ totals: { ...b.totals, paid, balance: Math.max(0, netOf(b.totals) - paid) },
197
+ status: deriveStatus({
198
+ gross: netOf(b.totals),
199
+ paid,
200
+ depositRequired: config.depositAmount,
201
+ current: b.status
202
+ }),
203
+ updatedAt: new Date()
204
+ })
205
+ .where(eq(bookingsTable.id, bookingId));
206
+ });
207
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bezstanowa sesja portalu (2. czynnik: telefon) + normalizacja numeru.
3
+ * Czyste funkcje — bez dostępu do zegara/env (wstrzykiwane), testowalne wprost.
4
+ * Sesja = podpisane HMAC cookie `"<expSec>.<hmac>"`; token w linku pozostaje
5
+ * 1. czynnikiem, numer telefonu = 2.
6
+ */
7
+ /** Numer do porównania: same cyfry, ostatnie 9. */
8
+ export declare function normalizePhone(raw: string): string;
9
+ /** True gdy oba numery mają identyczną, pełną (9-cyfrową) końcówkę. */
10
+ export declare function phonesMatch(a: string, b: string): boolean;
11
+ /** Wartość cookie sesji portalu: `"<expSec>.<hmac>"`. */
12
+ export declare function signSession(token: string, issuedAtSec: number, ttlSec: number, secret: string): string;
13
+ /** Poprawny podpis dla tokenu i nie wygasł (`nowSec < exp`). Stałoczasowe porównanie. */
14
+ export declare function verifySession(cookieValue: string, token: string, nowSec: number, secret: string): boolean;
15
+ /** Nazwa cookie per-rezerwacja (hash tokenu — nie wycieka tokenu w nazwie). */
16
+ export declare function portalCookieName(token: string): string;
@@ -0,0 +1,42 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ /**
3
+ * Bezstanowa sesja portalu (2. czynnik: telefon) + normalizacja numeru.
4
+ * Czyste funkcje — bez dostępu do zegara/env (wstrzykiwane), testowalne wprost.
5
+ * Sesja = podpisane HMAC cookie `"<expSec>.<hmac>"`; token w linku pozostaje
6
+ * 1. czynnikiem, numer telefonu = 2.
7
+ */
8
+ /** Numer do porównania: same cyfry, ostatnie 9. */
9
+ export function normalizePhone(raw) {
10
+ return (raw ?? '').replace(/\D+/g, '').slice(-9);
11
+ }
12
+ /** True gdy oba numery mają identyczną, pełną (9-cyfrową) końcówkę. */
13
+ export function phonesMatch(a, b) {
14
+ const na = normalizePhone(a);
15
+ return na.length === 9 && na === normalizePhone(b);
16
+ }
17
+ function hmac(secret, payload) {
18
+ return createHmac('sha256', secret).update(payload).digest('base64url');
19
+ }
20
+ /** Wartość cookie sesji portalu: `"<expSec>.<hmac>"`. */
21
+ export function signSession(token, issuedAtSec, ttlSec, secret) {
22
+ const exp = issuedAtSec + ttlSec;
23
+ return `${exp}.${hmac(secret, `${token}.${exp}`)}`;
24
+ }
25
+ /** Poprawny podpis dla tokenu i nie wygasł (`nowSec < exp`). Stałoczasowe porównanie. */
26
+ export function verifySession(cookieValue, token, nowSec, secret) {
27
+ if (!cookieValue || !secret)
28
+ return false;
29
+ const dot = cookieValue.indexOf('.');
30
+ if (dot <= 0)
31
+ return false;
32
+ const exp = Number(cookieValue.slice(0, dot));
33
+ if (!Number.isFinite(exp) || nowSec >= exp)
34
+ return false;
35
+ const sig = Buffer.from(cookieValue.slice(dot + 1));
36
+ const expected = Buffer.from(hmac(secret, `${token}.${exp}`));
37
+ return sig.length === expected.length && timingSafeEqual(sig, expected);
38
+ }
39
+ /** Nazwa cookie per-rezerwacja (hash tokenu — nie wycieka tokenu w nazwie). */
40
+ export function portalCookieName(token) {
41
+ return 'pz_portal_' + createHmac('sha256', 'portal-cookie').update(token).digest('hex').slice(0, 16);
42
+ }
@@ -0,0 +1,206 @@
1
+ import { bookingsTable, bookingItemsTable } from '../../db-postgres/schema/booking/index.js';
2
+ import { type PaymentEntry } from '../ledger.js';
3
+ import { type BookingEditChanges } from './bookings.js';
4
+ import { type FieldDef } from '../participant-fields/index.js';
5
+ import { type FlatParticipant } from '../participants-view.js';
6
+ type BookingRow = typeof bookingsTable.$inferSelect;
7
+ type ItemRow = typeof bookingItemsTable.$inferSelect;
8
+ /**
9
+ * Pure: shape a portal-safe DTO. Explicitly whitelists fields — `staffNotes`,
10
+ * `deletedBy`, and other internals never reach the customer.
11
+ */
12
+ export declare function toPortalView(booking: BookingRow, items: ItemRow[], participants: FlatParticipant[], payments: Array<PaymentEntry & {
13
+ paidAt?: Date | null;
14
+ }>, schemaByItem?: Record<string, FieldDef[]>): {
15
+ number: string;
16
+ status: import("../state-machine.js").BookingStatus;
17
+ currency: string;
18
+ customer: Record<string, unknown>;
19
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
20
+ balanceDueAt: Date | null;
21
+ items: {
22
+ participantFields: FieldDef[];
23
+ id: string;
24
+ createdAt: Date;
25
+ updatedAt: Date;
26
+ bookingId: string;
27
+ tripRef: string;
28
+ departureDate: Date | null;
29
+ participantsCount: number;
30
+ unitPriceSnapshot: number;
31
+ subtotalSnapshot: number | null;
32
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
33
+ }[];
34
+ participants: FlatParticipant[];
35
+ payments: {
36
+ amount: number;
37
+ status: import("../ledger.js").PaymentStatus;
38
+ kind: import("../ledger.js").PaymentKind | undefined;
39
+ paidAt: Date | null;
40
+ }[];
41
+ ledger: import("../ledger.js").LedgerSummary;
42
+ };
43
+ /** Resolve a booking by its unguessable portal access token (active only). */
44
+ export declare function resolveByToken(token: string): Promise<BookingRow | null>;
45
+ /** Drugi czynnik: zwraca booking gdy podany numer pasuje do `customer.phone`. */
46
+ export declare function verifyPhone(token: string, phone: string): Promise<BookingRow | null>;
47
+ export interface BookingStatusItem {
48
+ tripRef: string;
49
+ departureDate: string | null;
50
+ unitPrice: number;
51
+ }
52
+ export interface BookingStatus {
53
+ number: string;
54
+ status: string;
55
+ currency: string;
56
+ totals: {
57
+ gross: number;
58
+ paid: number;
59
+ balance: number;
60
+ };
61
+ items: BookingStatusItem[];
62
+ /** Payment link of a still-pending deposit (drives a „Zapłać zadatek" CTA), or null. */
63
+ depositLink: string | null;
64
+ }
65
+ export declare function bookingStatusByToken(token: string): Promise<BookingStatus | null>;
66
+ /** Portal view: booking + items + participants + ledger (no staff data). */
67
+ export declare function portalView(token: string): Promise<{
68
+ number: string;
69
+ status: import("../state-machine.js").BookingStatus;
70
+ currency: string;
71
+ customer: Record<string, unknown>;
72
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
73
+ balanceDueAt: Date | null;
74
+ items: {
75
+ participantFields: FieldDef[];
76
+ id: string;
77
+ createdAt: Date;
78
+ updatedAt: Date;
79
+ bookingId: string;
80
+ tripRef: string;
81
+ departureDate: Date | null;
82
+ participantsCount: number;
83
+ unitPriceSnapshot: number;
84
+ subtotalSnapshot: number | null;
85
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
86
+ }[];
87
+ participants: FlatParticipant[];
88
+ payments: {
89
+ amount: number;
90
+ status: import("../ledger.js").PaymentStatus;
91
+ kind: import("../ledger.js").PaymentKind | undefined;
92
+ paidAt: Date | null;
93
+ }[];
94
+ ledger: import("../ledger.js").LedgerSummary;
95
+ } | null>;
96
+ /** Portal self-service edit (actor:'portal' guards). Throws → handler maps to 409/400. */
97
+ export declare function portalEdit(token: string, changes: BookingEditChanges): Promise<{
98
+ number: string;
99
+ status: import("../state-machine.js").BookingStatus;
100
+ currency: string;
101
+ customer: Record<string, unknown>;
102
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
103
+ balanceDueAt: Date | null;
104
+ items: {
105
+ participantFields: FieldDef[];
106
+ id: string;
107
+ createdAt: Date;
108
+ updatedAt: Date;
109
+ bookingId: string;
110
+ tripRef: string;
111
+ departureDate: Date | null;
112
+ participantsCount: number;
113
+ unitPriceSnapshot: number;
114
+ subtotalSnapshot: number | null;
115
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
116
+ }[];
117
+ participants: FlatParticipant[];
118
+ payments: {
119
+ amount: number;
120
+ status: import("../ledger.js").PaymentStatus;
121
+ kind: import("../ledger.js").PaymentKind | undefined;
122
+ paidAt: Date | null;
123
+ }[];
124
+ ledger: import("../ledger.js").LedgerSummary;
125
+ } | null>;
126
+ /** Portal: create an installment/balance payment link. */
127
+ export declare function portalPay(token: string, amount: number): Promise<{
128
+ paymentLink: string | null;
129
+ } | null>;
130
+ /**
131
+ * Portal self-service: customer fills their own participant data. Verifies the
132
+ * assignment belongs to the token's booking (no cross-booking writes), then
133
+ * saves with `'customer'` scope (staff-only fields are dropped, not an error).
134
+ * Throws {@link ParticipantValidationError} on type/min/max/option violations.
135
+ *
136
+ * @param assignmentId - `booking_item_persons.id` (person-on-this-trip row).
137
+ */
138
+ export declare function portalUpdateParticipant(token: string, assignmentId: string, data: Record<string, unknown>): Promise<{
139
+ number: string;
140
+ status: import("../state-machine.js").BookingStatus;
141
+ currency: string;
142
+ customer: Record<string, unknown>;
143
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
144
+ balanceDueAt: Date | null;
145
+ items: {
146
+ participantFields: FieldDef[];
147
+ id: string;
148
+ createdAt: Date;
149
+ updatedAt: Date;
150
+ bookingId: string;
151
+ tripRef: string;
152
+ departureDate: Date | null;
153
+ participantsCount: number;
154
+ unitPriceSnapshot: number;
155
+ subtotalSnapshot: number | null;
156
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
157
+ }[];
158
+ participants: FlatParticipant[];
159
+ payments: {
160
+ amount: number;
161
+ status: import("../ledger.js").PaymentStatus;
162
+ kind: import("../ledger.js").PaymentKind | undefined;
163
+ paidAt: Date | null;
164
+ }[];
165
+ ledger: import("../ledger.js").LedgerSummary;
166
+ } | null>;
167
+ /**
168
+ * Self-service dobieranie wycieczki: doczytaj obecny stan, scal z `additions`
169
+ * (tylko istniejące osoby), przelicz przez `editBooking` (full-replace → recompute
170
+ * tym samym silnikiem per-uczestnik co koszyk). Saldo rośnie; dopłata istniejącym
171
+ * flow. `assignments` w additions = lista `booking_persons.id`. Kotwica nowej
172
+ * pozycji (`departureDate`) = `customer.arrivalDate`.
173
+ */
174
+ export declare function portalAddTrips(token: string, additions: Array<{
175
+ tripRef: string;
176
+ assignments: string[];
177
+ }>): Promise<{
178
+ number: string;
179
+ status: import("../state-machine.js").BookingStatus;
180
+ currency: string;
181
+ customer: Record<string, unknown>;
182
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
183
+ balanceDueAt: Date | null;
184
+ items: {
185
+ participantFields: FieldDef[];
186
+ id: string;
187
+ createdAt: Date;
188
+ updatedAt: Date;
189
+ bookingId: string;
190
+ tripRef: string;
191
+ departureDate: Date | null;
192
+ participantsCount: number;
193
+ unitPriceSnapshot: number;
194
+ subtotalSnapshot: number | null;
195
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
196
+ }[];
197
+ participants: FlatParticipant[];
198
+ payments: {
199
+ amount: number;
200
+ status: import("../ledger.js").PaymentStatus;
201
+ kind: import("../ledger.js").PaymentKind | undefined;
202
+ paidAt: Date | null;
203
+ }[];
204
+ ledger: import("../ledger.js").LedgerSummary;
205
+ } | null>;
206
+ export {};