includio-cms 0.36.8 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/API.md +179 -3
  2. package/CHANGELOG.md +64 -0
  3. package/DOCS.md +28 -3
  4. package/ROADMAP.md +18 -0
  5. package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
  6. package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
  7. package/dist/admin/client/booking/customer-panel.svelte +102 -0
  8. package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
  9. package/dist/admin/client/booking/customer-view.d.ts +52 -0
  10. package/dist/admin/client/booking/customer-view.js +134 -0
  11. package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
  12. package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
  13. package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
  14. package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
  15. package/dist/admin/client/booking/reservations-page.svelte +221 -0
  16. package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
  17. package/dist/admin/client/collection/collection-entries.svelte +21 -2
  18. package/dist/admin/client/collection/entry-link.svelte +6 -4
  19. package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
  20. package/dist/admin/client/collection/row-actions.svelte +12 -4
  21. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
  22. package/dist/admin/client/collection/status-badge.svelte +39 -3
  23. package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
  24. package/dist/admin/client/entry/entry.svelte +26 -9
  25. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
  26. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
  27. package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
  28. package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
  29. package/dist/admin/client/entry/preview-views.d.ts +27 -0
  30. package/dist/admin/client/entry/preview-views.js +33 -0
  31. package/dist/admin/client/index.d.ts +3 -0
  32. package/dist/admin/client/index.js +3 -0
  33. package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
  34. package/dist/admin/client/modules/dashboard-registry.js +11 -0
  35. package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
  36. package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
  37. package/dist/admin/client/modules/stat-card.svelte +22 -0
  38. package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
  39. package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
  40. package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
  41. package/dist/admin/components/fields/relation-field.svelte +11 -2
  42. package/dist/admin/components/icons/icon-map.js +8 -0
  43. package/dist/admin/components/layout/app-sidebar.svelte +45 -14
  44. package/dist/admin/components/layout/context-switcher.svelte +90 -0
  45. package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
  46. package/dist/admin/components/layout/lang.d.ts +4 -0
  47. package/dist/admin/components/layout/lang.js +8 -0
  48. package/dist/admin/components/layout/layout-renderer.svelte +2 -1
  49. package/dist/admin/components/layout/layout-tabs.svelte +14 -0
  50. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
  51. package/dist/admin/components/layout/nav-collections.svelte +16 -9
  52. package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
  53. package/dist/admin/components/layout/nav-global.svelte +45 -0
  54. package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
  55. package/dist/admin/components/layout/nav-main.svelte +8 -32
  56. package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
  57. package/dist/admin/components/layout/nav-module.svelte +57 -0
  58. package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
  59. package/dist/admin/components/layout/nav-section.svelte +1 -1
  60. package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
  61. package/dist/admin/components/layout/nav-singletons.svelte +14 -7
  62. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
  63. package/dist/admin/remote/booking.remote.d.ts +244 -0
  64. package/dist/admin/remote/booking.remote.js +283 -0
  65. package/dist/admin/remote/entry.remote.d.ts +3 -0
  66. package/dist/admin/remote/entry.remote.js +14 -3
  67. package/dist/admin/remote/index.d.ts +2 -0
  68. package/dist/admin/remote/index.js +2 -0
  69. package/dist/admin/remote/modules.remote.d.ts +10 -0
  70. package/dist/admin/remote/modules.remote.js +14 -0
  71. package/dist/admin/state/active-context-resolve.d.ts +37 -0
  72. package/dist/admin/state/active-context-resolve.js +66 -0
  73. package/dist/admin/state/active-context.svelte.d.ts +8 -0
  74. package/dist/admin/state/active-context.svelte.js +27 -0
  75. package/dist/booking/adjustments.d.ts +33 -0
  76. package/dist/booking/adjustments.js +26 -0
  77. package/dist/booking/capacity.d.ts +12 -0
  78. package/dist/booking/capacity.js +10 -0
  79. package/dist/booking/client/index.d.ts +39 -0
  80. package/dist/booking/client/index.js +44 -0
  81. package/dist/booking/client/use-booking.svelte.d.ts +25 -0
  82. package/dist/booking/client/use-booking.svelte.js +56 -0
  83. package/dist/booking/config.d.ts +156 -0
  84. package/dist/booking/config.js +36 -0
  85. package/dist/booking/draft.d.ts +77 -0
  86. package/dist/booking/draft.js +67 -0
  87. package/dist/booking/expiry-logic.d.ts +30 -0
  88. package/dist/booking/expiry-logic.js +33 -0
  89. package/dist/booking/index.d.ts +18 -0
  90. package/dist/booking/index.js +52 -0
  91. package/dist/booking/ledger.d.ts +46 -0
  92. package/dist/booking/ledger.js +60 -0
  93. package/dist/booking/number.d.ts +2 -0
  94. package/dist/booking/number.js +11 -0
  95. package/dist/booking/participant-fields/access.d.ts +10 -0
  96. package/dist/booking/participant-fields/access.js +14 -0
  97. package/dist/booking/participant-fields/age.d.ts +12 -0
  98. package/dist/booking/participant-fields/age.js +28 -0
  99. package/dist/booking/participant-fields/completeness.d.ts +8 -0
  100. package/dist/booking/participant-fields/completeness.js +13 -0
  101. package/dist/booking/participant-fields/group.d.ts +6 -0
  102. package/dist/booking/participant-fields/group.js +15 -0
  103. package/dist/booking/participant-fields/index.d.ts +6 -0
  104. package/dist/booking/participant-fields/index.js +6 -0
  105. package/dist/booking/participant-fields/types.d.ts +50 -0
  106. package/dist/booking/participant-fields/types.js +5 -0
  107. package/dist/booking/participant-fields/validate.d.ts +7 -0
  108. package/dist/booking/participant-fields/validate.js +44 -0
  109. package/dist/booking/participant-pricing.d.ts +93 -0
  110. package/dist/booking/participant-pricing.js +91 -0
  111. package/dist/booking/participants-view.d.ts +32 -0
  112. package/dist/booking/participants-view.js +22 -0
  113. package/dist/booking/payment-context.d.ts +19 -0
  114. package/dist/booking/payment-context.js +29 -0
  115. package/dist/booking/pricing-inputs.d.ts +28 -0
  116. package/dist/booking/pricing-inputs.js +35 -0
  117. package/dist/booking/pricing.d.ts +86 -0
  118. package/dist/booking/pricing.js +125 -0
  119. package/dist/booking/recompute.d.ts +22 -0
  120. package/dist/booking/recompute.js +31 -0
  121. package/dist/booking/reminders-schedule.d.ts +11 -0
  122. package/dist/booking/reminders-schedule.js +28 -0
  123. package/dist/booking/server/adjustments.d.ts +27 -0
  124. package/dist/booking/server/adjustments.js +136 -0
  125. package/dist/booking/server/bookings.d.ts +45 -0
  126. package/dist/booking/server/bookings.js +288 -0
  127. package/dist/booking/server/catalog.d.ts +9 -0
  128. package/dist/booking/server/catalog.js +20 -0
  129. package/dist/booking/server/db.d.ts +7 -0
  130. package/dist/booking/server/db.js +18 -0
  131. package/dist/booking/server/email.d.ts +23 -0
  132. package/dist/booking/server/email.js +59 -0
  133. package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
  134. package/dist/booking/server/emailTemplateRegistry.js +125 -0
  135. package/dist/booking/server/http/create-handler.d.ts +5 -0
  136. package/dist/booking/server/http/create-handler.js +68 -0
  137. package/dist/booking/server/http/index.d.ts +4 -0
  138. package/dist/booking/server/http/index.js +4 -0
  139. package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
  140. package/dist/booking/server/http/maintenance-handler.js +36 -0
  141. package/dist/booking/server/http/portal-handler.d.ts +13 -0
  142. package/dist/booking/server/http/portal-handler.js +99 -0
  143. package/dist/booking/server/http/webhook-handler.d.ts +9 -0
  144. package/dist/booking/server/http/webhook-handler.js +39 -0
  145. package/dist/booking/server/index.d.ts +12 -0
  146. package/dist/booking/server/index.js +14 -0
  147. package/dist/booking/server/maintenance.d.ts +28 -0
  148. package/dist/booking/server/maintenance.js +134 -0
  149. package/dist/booking/server/participants.d.ts +26 -0
  150. package/dist/booking/server/participants.js +92 -0
  151. package/dist/booking/server/payments.d.ts +34 -0
  152. package/dist/booking/server/payments.js +207 -0
  153. package/dist/booking/server/portal-session.d.ts +16 -0
  154. package/dist/booking/server/portal-session.js +42 -0
  155. package/dist/booking/server/portal.d.ts +207 -0
  156. package/dist/booking/server/portal.js +234 -0
  157. package/dist/booking/server/refunds.d.ts +7 -0
  158. package/dist/booking/server/refunds.js +58 -0
  159. package/dist/booking/server/reminders.d.ts +9 -0
  160. package/dist/booking/server/reminders.js +24 -0
  161. package/dist/booking/server/validation.d.ts +17 -0
  162. package/dist/booking/server/validation.js +55 -0
  163. package/dist/booking/state-machine.d.ts +41 -0
  164. package/dist/booking/state-machine.js +73 -0
  165. package/dist/booking/templates/_partials/footer.en.html +4 -0
  166. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  167. package/dist/booking/templates/_partials/header.en.html +4 -0
  168. package/dist/booking/templates/_partials/header.pl.html +4 -0
  169. package/dist/booking/templates/balance-due.en.html +10 -0
  170. package/dist/booking/templates/balance-due.pl.html +10 -0
  171. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  172. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  173. package/dist/booking/templates/booking-received.en.html +9 -0
  174. package/dist/booking/templates/booking-received.pl.html +9 -0
  175. package/dist/booking/templates/deposit-paid.en.html +6 -0
  176. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  177. package/dist/booking/templates/payment-link.en.html +5 -0
  178. package/dist/booking/templates/payment-link.pl.html +5 -0
  179. package/dist/booking/templates/payment-received.en.html +10 -0
  180. package/dist/booking/templates/payment-received.pl.html +10 -0
  181. package/dist/booking/totals.d.ts +38 -0
  182. package/dist/booking/totals.js +61 -0
  183. package/dist/booking/webhook-logic.d.ts +28 -0
  184. package/dist/booking/webhook-logic.js +50 -0
  185. package/dist/cli/create-user.d.ts +7 -1
  186. package/dist/cli/create-user.js +23 -15
  187. package/dist/cli/index.js +55 -7
  188. package/dist/cli/scaffold/admin.d.ts +6 -0
  189. package/dist/cli/scaffold/admin.js +5 -3
  190. package/dist/cli/scaffold/booking.d.ts +12 -0
  191. package/dist/cli/scaffold/booking.js +103 -0
  192. package/dist/core/cms.d.ts +24 -9
  193. package/dist/core/cms.js +72 -34
  194. package/dist/core/config/validatePreviews.d.ts +12 -0
  195. package/dist/core/config/validatePreviews.js +90 -0
  196. package/dist/core/fields/dotPath.d.ts +4 -0
  197. package/dist/core/fields/dotPath.js +18 -0
  198. package/dist/core/fields/slugPath.d.ts +9 -1
  199. package/dist/core/fields/slugPath.js +16 -2
  200. package/dist/core/fields/slugify.d.ts +3 -0
  201. package/dist/core/fields/slugify.js +4 -0
  202. package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
  203. package/dist/core/server/entries/operations/duplicate.js +73 -0
  204. package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
  205. package/dist/core/server/entries/operations/slugNormalize.js +20 -0
  206. package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
  207. package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
  208. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  209. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  210. package/dist/core/server/generator/generator.js +7 -2
  211. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  212. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  213. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  214. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  215. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  216. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  217. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  218. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  219. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  220. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  221. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  222. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  223. package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
  224. package/dist/db-postgres/schema/booking/bookings.js +40 -0
  225. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  226. package/dist/db-postgres/schema/booking/index.js +7 -0
  227. package/dist/db-postgres/schema/index.d.ts +1 -0
  228. package/dist/db-postgres/schema/index.js +1 -0
  229. package/dist/db-postgres/schema-booking.d.ts +1 -0
  230. package/dist/db-postgres/schema-booking.js +1 -0
  231. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  232. package/dist/server/security/rate-limit-handler.js +34 -0
  233. package/dist/shop/adapters/payu/client.js +3 -1
  234. package/dist/shop/adapters/payu/index.js +37 -17
  235. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  236. package/dist/shop/adapters/payu/payload.js +1 -1
  237. package/dist/shop/adapters/stripe/index.js +36 -21
  238. package/dist/shop/index.d.ts +11 -2
  239. package/dist/shop/index.js +43 -1
  240. package/dist/shop/rate-limit.d.ts +1 -9
  241. package/dist/shop/rate-limit.js +3 -28
  242. package/dist/shop/types.d.ts +19 -0
  243. package/dist/sveltekit/config.d.ts +4 -0
  244. package/dist/sveltekit/config.js +6 -0
  245. package/dist/types/cms.d.ts +15 -1
  246. package/dist/types/cms.schema.d.ts +96 -2
  247. package/dist/types/cms.schema.js +84 -1
  248. package/dist/types/config.d.ts +33 -0
  249. package/dist/types/layout.d.ts +6 -0
  250. package/dist/types/modules.d.ts +133 -0
  251. package/dist/types/modules.js +27 -0
  252. package/dist/updates/0.36.9/index.d.ts +2 -0
  253. package/dist/updates/0.36.9/index.js +12 -0
  254. package/dist/updates/0.37.0/index.d.ts +2 -0
  255. package/dist/updates/0.37.0/index.js +52 -0
  256. package/dist/updates/index.js +5 -1
  257. package/package.json +19 -1
  258. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  259. package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
@@ -0,0 +1,234 @@
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
+ const newItems = additions.map((a) => ({
227
+ tripRef: a.tripRef,
228
+ departureDate: anchor,
229
+ unitPrice: a.unitPrice,
230
+ assignments: a.assignments.map((ref) => ({ personRef: ref }))
231
+ }));
232
+ await editBooking(booking.id, { persons: personInputs, items: [...existingItems, ...newItems] }, { actor: 'portal' });
233
+ return portalView(token);
234
+ }
@@ -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;
@@ -0,0 +1,73 @@
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 const BOOKING_STATUSES = [
11
+ 'draft',
12
+ 'reserved',
13
+ 'partially_paid',
14
+ 'paid',
15
+ 'completed',
16
+ 'cancelled'
17
+ ];
18
+ /** Thrown when an illegal status transition is attempted. */
19
+ export class BookingStateError extends Error {
20
+ from;
21
+ to;
22
+ constructor(from, to) {
23
+ super(`Illegal booking status transition: ${from} → ${to}`);
24
+ this.from = from;
25
+ this.to = to;
26
+ this.name = 'BookingStateError';
27
+ }
28
+ }
29
+ /**
30
+ * Allowed manual transitions. Backward edges (e.g. `paid → partially_paid`)
31
+ * support refunds/edits that raise the balance. `completed` and `cancelled`
32
+ * are terminal.
33
+ */
34
+ const TRANSITIONS = {
35
+ draft: ['reserved', 'partially_paid', 'paid', 'cancelled'],
36
+ reserved: ['partially_paid', 'paid', 'cancelled'],
37
+ partially_paid: ['reserved', 'paid', 'cancelled'],
38
+ paid: ['partially_paid', 'completed', 'cancelled'],
39
+ completed: [],
40
+ cancelled: []
41
+ };
42
+ /** Whether `from → to` is a legal transition (self-transitions are allowed). */
43
+ export function canTransition(from, to) {
44
+ if (from === to)
45
+ return true;
46
+ return TRANSITIONS[from]?.includes(to) ?? false;
47
+ }
48
+ /** Asserts a legal transition, throwing {@link BookingStateError} otherwise. */
49
+ export function assertTransition(from, to) {
50
+ if (!canTransition(from, to))
51
+ throw new BookingStateError(from, to);
52
+ }
53
+ /**
54
+ * Derives the payment-progress status from the ledger.
55
+ *
56
+ * Money bands (after preserving terminal states):
57
+ * - `paid == 0` → `draft`
58
+ * - `paid >= gross` (gross > 0) → `paid`
59
+ * - `paid > depositRequired` → `partially_paid` (installments beyond deposit)
60
+ * - `0 < paid <= depositRequired` → `reserved` (deposit secured)
61
+ */
62
+ export function deriveStatus(input) {
63
+ const { gross, paid, depositRequired, current } = input;
64
+ if (current === 'completed' || current === 'cancelled')
65
+ return current;
66
+ if (paid <= 0)
67
+ return 'draft';
68
+ if (gross > 0 && paid >= gross)
69
+ return 'paid';
70
+ if (paid > depositRequired)
71
+ return 'partially_paid';
72
+ return 'reserved';
73
+ }
@@ -0,0 +1,4 @@
1
+ </div>
2
+ <p style="text-align:center;color:#8888A0;font-size:12px;margin-top:16px;">{{#if brand}}{{brand}} · {{/if}}{{booking.customer.email}}</p>
3
+ </div>
4
+ </body></html>
@@ -0,0 +1,4 @@
1
+ </div>
2
+ <p style="text-align:center;color:#8888A0;font-size:12px;margin-top:16px;">{{#if brand}}{{brand}} · {{/if}}{{booking.customer.email}}</p>
3
+ </div>
4
+ </body></html>
@@ -0,0 +1,4 @@
1
+ <!doctype html>
2
+ <html lang="en"><body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:#FAFAFE;color:#1A1A2E;">
3
+ <div style="max-width:560px;margin:0 auto;padding:24px;">
4
+ <div style="background:#fff;border-radius:12px;padding:28px;">
@@ -0,0 +1,4 @@
1
+ <!doctype html>
2
+ <html lang="pl"><body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:#FAFAFE;color:#1A1A2E;">
3
+ <div style="max-width:560px;margin:0 auto;padding:24px;">
4
+ <div style="background:#fff;border-radius:12px;padding:28px;">
@@ -0,0 +1,10 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Time to pay your balance — booking {{booking.number}}</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">A reminder about the remaining amount: <strong>{{currency booking.totals.balance currency}}</strong>.</p>
4
+ {{#if booking.balanceDueAt}}
5
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">You have until <strong>{{date booking.balanceDueAt "en-GB"}}</strong>{{#if now}} (about {{daysUntil booking.balanceDueAt now}} days left){{/if}}. By then, pay the remaining amount or change your departure date in your portal. Otherwise your booking will be cancelled and the deposit forfeited.</p>
6
+ {{else}}
7
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Please pay before your departure date — you can also change the date in your portal.</p>
8
+ {{/if}}
9
+ {{#if portalUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{portalUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">Pay or change the date</a></div>{{/if}}
10
+ {{> footer}}
@@ -0,0 +1,10 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Czas na dopłatę — rezerwacja {{booking.number}}</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Przypominamy o pozostałej kwocie: <strong>{{currency booking.totals.balance currency}}</strong>.</p>
4
+ {{#if booking.balanceDueAt}}
5
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Masz czas do <strong>{{date booking.balanceDueAt}}</strong>{{#if now}} (zostało około {{daysUntil booking.balanceDueAt now}} dni){{/if}}. Do tego dnia dopłać pozostałą kwotę albo zmień termin wyjazdu w swoim panelu. Inaczej rezerwacja zostanie anulowana, a wpłacony zadatek przepada.</p>
6
+ {{else}}
7
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Prosimy o dopłatę przed terminem wyjazdu — możesz też zmienić termin w swoim panelu.</p>
8
+ {{/if}}
9
+ {{#if portalUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{portalUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">Dopłać lub zmień termin</a></div>{{/if}}
10
+ {{> footer}}
@@ -0,0 +1,5 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Booking {{booking.number}} has been cancelled</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">The payment deadline has passed, so your booking was cancelled and the deposit forfeited.</p>
4
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">If this is a mistake or you'd like to rebook — just reply to this email. We'll help you find a new date.</p>
5
+ {{> footer}}
@@ -0,0 +1,5 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Rezerwacja {{booking.number}} została anulowana</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Termin dopłaty minął, więc Twoja rezerwacja została anulowana, a wpłacony zadatek przepadł.</p>
4
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Jeśli to nieporozumienie albo chcesz wrócić do tematu — po prostu odpowiedz na tę wiadomość. Pomożemy znaleźć nowy termin.</p>
5
+ {{> footer}}
@@ -0,0 +1,9 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">We've got your booking {{booking.number}}</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Thank you! We've saved your booking. To confirm it, pay the deposit — you can do it all from your booking portal.</p>
4
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Amount still due: <strong>{{currency booking.totals.balance currency}}</strong>.</p>
5
+ {{#if booking.totals.perks.length}}
6
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Your booking includes: {{#each booking.totals.perks}}<strong>{{this.label}}</strong>{{#unless @last}}, {{/unless}}{{/each}}.</p>
7
+ {{/if}}
8
+ {{#if portalUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{portalUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">Pay the deposit</a></div>{{/if}}
9
+ {{> footer}}
@@ -0,0 +1,9 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Mamy Twoją rezerwację {{booking.number}}</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Dziękujemy! Zapisaliśmy Twoją rezerwację. Żeby ją potwierdzić, opłać zadatek — wszystko zrobisz w swoim panelu rezerwacji.</p>
4
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Kwota do dopłaty: <strong>{{currency booking.totals.balance currency}}</strong>.</p>
5
+ {{#if booking.totals.perks.length}}
6
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">W ramach rezerwacji otrzymujesz: {{#each booking.totals.perks}}<strong>{{this.label}}</strong>{{#unless @last}}, {{/unless}}{{/each}}.</p>
7
+ {{/if}}
8
+ {{#if portalUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{portalUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">Opłać zadatek w panelu</a></div>{{/if}}
9
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Deposit received — booking {{booking.number}} confirmed</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Thank you! We've received your deposit, so your booking is confirmed.</p>
4
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Amount still due: <strong>{{currency booking.totals.balance currency}}</strong>. You can pay it anytime from your portal.</p>
5
+ {{#if portalUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{portalUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">Pay the balance</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,6 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Zadatek zaksięgowany — rezerwacja {{booking.number}} potwierdzona</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Dziękujemy! Zaksięgowaliśmy Twój zadatek, więc rezerwacja jest potwierdzona.</p>
4
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Pozostała kwota do dopłaty: <strong>{{currency booking.totals.balance currency}}</strong>. Możesz ją uregulować w dowolnej chwili w swoim panelu.</p>
5
+ {{#if portalUrl}}<div style="margin-top:24px;text-align:center;"><a href="{{portalUrl}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">Dopłać w panelu</a></div>{{/if}}
6
+ {{> footer}}
@@ -0,0 +1,5 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Payment link — booking {{booking.number}}</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">We've prepared a payment of <strong>{{currency amount currency}}</strong> for you.</p>
4
+ {{#if paymentLink}}<div style="margin-top:24px;text-align:center;"><a href="{{paymentLink}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">Pay now</a></div>{{/if}}
5
+ {{> footer}}
@@ -0,0 +1,5 @@
1
+ {{> header}}
2
+ <h1 style="font-size:20px;font-weight:800;margin:0 0 8px;">Link do płatności — rezerwacja {{booking.number}}</h1>
3
+ <p style="margin:0 0 16px;color:#555566;line-height:1.5;">Przygotowaliśmy dla Ciebie płatność na kwotę <strong>{{currency amount currency}}</strong>.</p>
4
+ {{#if paymentLink}}<div style="margin-top:24px;text-align:center;"><a href="{{paymentLink}}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">Zapłać teraz</a></div>{{/if}}
5
+ {{> footer}}