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.
- package/API.md +179 -3
- package/CHANGELOG.md +64 -0
- package/DOCS.md +28 -3
- package/ROADMAP.md +18 -0
- package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
- package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
- package/dist/admin/client/booking/customer-panel.svelte +102 -0
- package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
- package/dist/admin/client/booking/customer-view.d.ts +52 -0
- package/dist/admin/client/booking/customer-view.js +134 -0
- package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
- package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
- package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
- package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
- package/dist/admin/client/booking/reservations-page.svelte +221 -0
- package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
- package/dist/admin/client/collection/collection-entries.svelte +21 -2
- package/dist/admin/client/collection/entry-link.svelte +6 -4
- package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
- package/dist/admin/client/collection/row-actions.svelte +12 -4
- package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
- package/dist/admin/client/collection/status-badge.svelte +39 -3
- package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
- package/dist/admin/client/entry/entry.svelte +26 -9
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
- package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
- package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
- package/dist/admin/client/entry/preview-views.d.ts +27 -0
- package/dist/admin/client/entry/preview-views.js +33 -0
- package/dist/admin/client/index.d.ts +3 -0
- package/dist/admin/client/index.js +3 -0
- package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
- package/dist/admin/client/modules/dashboard-registry.js +11 -0
- package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
- package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
- package/dist/admin/client/modules/stat-card.svelte +22 -0
- package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
- package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
- package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
- package/dist/admin/components/fields/relation-field.svelte +11 -2
- package/dist/admin/components/icons/icon-map.js +8 -0
- package/dist/admin/components/layout/app-sidebar.svelte +45 -14
- package/dist/admin/components/layout/context-switcher.svelte +90 -0
- package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
- package/dist/admin/components/layout/lang.d.ts +4 -0
- package/dist/admin/components/layout/lang.js +8 -0
- package/dist/admin/components/layout/layout-renderer.svelte +2 -1
- package/dist/admin/components/layout/layout-tabs.svelte +14 -0
- package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
- package/dist/admin/components/layout/nav-collections.svelte +16 -9
- package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
- package/dist/admin/components/layout/nav-global.svelte +45 -0
- package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
- package/dist/admin/components/layout/nav-main.svelte +8 -32
- package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
- package/dist/admin/components/layout/nav-module.svelte +57 -0
- package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
- package/dist/admin/components/layout/nav-section.svelte +1 -1
- package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
- package/dist/admin/components/layout/nav-singletons.svelte +14 -7
- package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
- package/dist/admin/remote/booking.remote.d.ts +244 -0
- package/dist/admin/remote/booking.remote.js +283 -0
- package/dist/admin/remote/entry.remote.d.ts +3 -0
- package/dist/admin/remote/entry.remote.js +14 -3
- package/dist/admin/remote/index.d.ts +2 -0
- package/dist/admin/remote/index.js +2 -0
- package/dist/admin/remote/modules.remote.d.ts +10 -0
- package/dist/admin/remote/modules.remote.js +14 -0
- package/dist/admin/state/active-context-resolve.d.ts +37 -0
- package/dist/admin/state/active-context-resolve.js +66 -0
- package/dist/admin/state/active-context.svelte.d.ts +8 -0
- package/dist/admin/state/active-context.svelte.js +27 -0
- package/dist/booking/adjustments.d.ts +33 -0
- package/dist/booking/adjustments.js +26 -0
- package/dist/booking/capacity.d.ts +12 -0
- package/dist/booking/capacity.js +10 -0
- package/dist/booking/client/index.d.ts +39 -0
- package/dist/booking/client/index.js +44 -0
- package/dist/booking/client/use-booking.svelte.d.ts +25 -0
- package/dist/booking/client/use-booking.svelte.js +56 -0
- package/dist/booking/config.d.ts +156 -0
- package/dist/booking/config.js +36 -0
- package/dist/booking/draft.d.ts +77 -0
- package/dist/booking/draft.js +67 -0
- package/dist/booking/expiry-logic.d.ts +30 -0
- package/dist/booking/expiry-logic.js +33 -0
- package/dist/booking/index.d.ts +18 -0
- package/dist/booking/index.js +52 -0
- package/dist/booking/ledger.d.ts +46 -0
- package/dist/booking/ledger.js +60 -0
- package/dist/booking/number.d.ts +2 -0
- package/dist/booking/number.js +11 -0
- package/dist/booking/participant-fields/access.d.ts +10 -0
- package/dist/booking/participant-fields/access.js +14 -0
- package/dist/booking/participant-fields/age.d.ts +12 -0
- package/dist/booking/participant-fields/age.js +28 -0
- package/dist/booking/participant-fields/completeness.d.ts +8 -0
- package/dist/booking/participant-fields/completeness.js +13 -0
- package/dist/booking/participant-fields/group.d.ts +6 -0
- package/dist/booking/participant-fields/group.js +15 -0
- package/dist/booking/participant-fields/index.d.ts +6 -0
- package/dist/booking/participant-fields/index.js +6 -0
- package/dist/booking/participant-fields/types.d.ts +50 -0
- package/dist/booking/participant-fields/types.js +5 -0
- package/dist/booking/participant-fields/validate.d.ts +7 -0
- package/dist/booking/participant-fields/validate.js +44 -0
- package/dist/booking/participant-pricing.d.ts +93 -0
- package/dist/booking/participant-pricing.js +91 -0
- package/dist/booking/participants-view.d.ts +32 -0
- package/dist/booking/participants-view.js +22 -0
- package/dist/booking/payment-context.d.ts +19 -0
- package/dist/booking/payment-context.js +29 -0
- package/dist/booking/pricing-inputs.d.ts +28 -0
- package/dist/booking/pricing-inputs.js +35 -0
- package/dist/booking/pricing.d.ts +86 -0
- package/dist/booking/pricing.js +125 -0
- package/dist/booking/recompute.d.ts +22 -0
- package/dist/booking/recompute.js +31 -0
- package/dist/booking/reminders-schedule.d.ts +11 -0
- package/dist/booking/reminders-schedule.js +28 -0
- package/dist/booking/server/adjustments.d.ts +27 -0
- package/dist/booking/server/adjustments.js +136 -0
- package/dist/booking/server/bookings.d.ts +45 -0
- package/dist/booking/server/bookings.js +288 -0
- package/dist/booking/server/catalog.d.ts +9 -0
- package/dist/booking/server/catalog.js +20 -0
- package/dist/booking/server/db.d.ts +7 -0
- package/dist/booking/server/db.js +18 -0
- package/dist/booking/server/email.d.ts +23 -0
- package/dist/booking/server/email.js +59 -0
- package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
- package/dist/booking/server/emailTemplateRegistry.js +125 -0
- package/dist/booking/server/http/create-handler.d.ts +5 -0
- package/dist/booking/server/http/create-handler.js +68 -0
- package/dist/booking/server/http/index.d.ts +4 -0
- package/dist/booking/server/http/index.js +4 -0
- package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
- package/dist/booking/server/http/maintenance-handler.js +36 -0
- package/dist/booking/server/http/portal-handler.d.ts +13 -0
- package/dist/booking/server/http/portal-handler.js +99 -0
- package/dist/booking/server/http/webhook-handler.d.ts +9 -0
- package/dist/booking/server/http/webhook-handler.js +39 -0
- package/dist/booking/server/index.d.ts +12 -0
- package/dist/booking/server/index.js +14 -0
- package/dist/booking/server/maintenance.d.ts +28 -0
- package/dist/booking/server/maintenance.js +134 -0
- package/dist/booking/server/participants.d.ts +26 -0
- package/dist/booking/server/participants.js +92 -0
- package/dist/booking/server/payments.d.ts +34 -0
- package/dist/booking/server/payments.js +207 -0
- package/dist/booking/server/portal-session.d.ts +16 -0
- package/dist/booking/server/portal-session.js +42 -0
- package/dist/booking/server/portal.d.ts +207 -0
- package/dist/booking/server/portal.js +234 -0
- package/dist/booking/server/refunds.d.ts +7 -0
- package/dist/booking/server/refunds.js +58 -0
- package/dist/booking/server/reminders.d.ts +9 -0
- package/dist/booking/server/reminders.js +24 -0
- package/dist/booking/server/validation.d.ts +17 -0
- package/dist/booking/server/validation.js +55 -0
- package/dist/booking/state-machine.d.ts +41 -0
- package/dist/booking/state-machine.js +73 -0
- package/dist/booking/templates/_partials/footer.en.html +4 -0
- package/dist/booking/templates/_partials/footer.pl.html +4 -0
- package/dist/booking/templates/_partials/header.en.html +4 -0
- package/dist/booking/templates/_partials/header.pl.html +4 -0
- package/dist/booking/templates/balance-due.en.html +10 -0
- package/dist/booking/templates/balance-due.pl.html +10 -0
- package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
- package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
- package/dist/booking/templates/booking-received.en.html +9 -0
- package/dist/booking/templates/booking-received.pl.html +9 -0
- package/dist/booking/templates/deposit-paid.en.html +6 -0
- package/dist/booking/templates/deposit-paid.pl.html +6 -0
- package/dist/booking/templates/payment-link.en.html +5 -0
- package/dist/booking/templates/payment-link.pl.html +5 -0
- package/dist/booking/templates/payment-received.en.html +10 -0
- package/dist/booking/templates/payment-received.pl.html +10 -0
- package/dist/booking/totals.d.ts +38 -0
- package/dist/booking/totals.js +61 -0
- package/dist/booking/webhook-logic.d.ts +28 -0
- package/dist/booking/webhook-logic.js +50 -0
- package/dist/cli/create-user.d.ts +7 -1
- package/dist/cli/create-user.js +23 -15
- package/dist/cli/index.js +55 -7
- package/dist/cli/scaffold/admin.d.ts +6 -0
- package/dist/cli/scaffold/admin.js +5 -3
- package/dist/cli/scaffold/booking.d.ts +12 -0
- package/dist/cli/scaffold/booking.js +103 -0
- package/dist/core/cms.d.ts +24 -9
- package/dist/core/cms.js +72 -34
- package/dist/core/config/validatePreviews.d.ts +12 -0
- package/dist/core/config/validatePreviews.js +90 -0
- package/dist/core/fields/dotPath.d.ts +4 -0
- package/dist/core/fields/dotPath.js +18 -0
- package/dist/core/fields/slugPath.d.ts +9 -1
- package/dist/core/fields/slugPath.js +16 -2
- package/dist/core/fields/slugify.d.ts +3 -0
- package/dist/core/fields/slugify.js +4 -0
- package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
- package/dist/core/server/entries/operations/duplicate.js +73 -0
- package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
- package/dist/core/server/entries/operations/slugNormalize.js +20 -0
- package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
- package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
- package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
- package/dist/core/server/fields/resolveRelationFields.js +12 -3
- package/dist/core/server/generator/generator.js +7 -2
- package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
- package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
- package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
- package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
- package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
- package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
- package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
- package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
- package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
- package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
- package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
- package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
- package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
- package/dist/db-postgres/schema/booking/bookings.js +40 -0
- package/dist/db-postgres/schema/booking/index.d.ts +7 -0
- package/dist/db-postgres/schema/booking/index.js +7 -0
- package/dist/db-postgres/schema/index.d.ts +1 -0
- package/dist/db-postgres/schema/index.js +1 -0
- package/dist/db-postgres/schema-booking.d.ts +1 -0
- package/dist/db-postgres/schema-booking.js +1 -0
- package/dist/server/security/rate-limit-handler.d.ts +19 -0
- package/dist/server/security/rate-limit-handler.js +34 -0
- package/dist/shop/adapters/payu/client.js +3 -1
- package/dist/shop/adapters/payu/index.js +37 -17
- package/dist/shop/adapters/payu/payload.d.ts +6 -0
- package/dist/shop/adapters/payu/payload.js +1 -1
- package/dist/shop/adapters/stripe/index.js +36 -21
- package/dist/shop/index.d.ts +11 -2
- package/dist/shop/index.js +43 -1
- package/dist/shop/rate-limit.d.ts +1 -9
- package/dist/shop/rate-limit.js +3 -28
- package/dist/shop/types.d.ts +19 -0
- package/dist/sveltekit/config.d.ts +4 -0
- package/dist/sveltekit/config.js +6 -0
- package/dist/types/cms.d.ts +15 -1
- package/dist/types/cms.schema.d.ts +96 -2
- package/dist/types/cms.schema.js +84 -1
- package/dist/types/config.d.ts +33 -0
- package/dist/types/layout.d.ts +6 -0
- package/dist/types/modules.d.ts +133 -0
- package/dist/types/modules.js +27 -0
- package/dist/updates/0.36.9/index.d.ts +2 -0
- package/dist/updates/0.36.9/index.js +12 -0
- package/dist/updates/0.37.0/index.d.ts +2 -0
- package/dist/updates/0.37.0/index.js +52 -0
- package/dist/updates/index.js +5 -1
- package/package.json +19 -1
- package/dist/admin/components/layout/nav-shop.svelte +0 -33
- package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-participant booking pricing — pure, deterministic, module-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the per-line {@link price} engine, this prices each *participant ×
|
|
5
|
+
* trip* unit, so age-based pricing and volume discounts compose correctly:
|
|
6
|
+
*
|
|
7
|
+
* line base = round(adultPrice * ageBandPct / 100)
|
|
8
|
+
* package = participant's cheapest trip * ladder% (full-price only)
|
|
9
|
+
* early-bird = participant's gross * earlyBird% (full-price only)
|
|
10
|
+
*
|
|
11
|
+
* INVARIANT: volume discounts (package + early-bird) apply ONLY to full-price
|
|
12
|
+
* participants (age band pct === 100). Any participant with an age discount is
|
|
13
|
+
* excluded — "a child already on −30% does not also stack the bundle".
|
|
14
|
+
*
|
|
15
|
+
* Amounts are in **minor units** (grosze). No clock access: `now` and `anchor`
|
|
16
|
+
* (the arrival date) are injected by the caller.
|
|
17
|
+
*/
|
|
18
|
+
import type { Perk } from './pricing.js';
|
|
19
|
+
/** A global age band: participants aged [minAge, maxAge] pay `pct`% of adult price. */
|
|
20
|
+
export interface AgeBand {
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Inclusive lower bound (whole years). */
|
|
23
|
+
minAge: number;
|
|
24
|
+
/** Inclusive upper bound (whole years); `null` ⇒ no upper bound. */
|
|
25
|
+
maxAge: number | null;
|
|
26
|
+
/** Percent of the adult price, 0–100. */
|
|
27
|
+
pct: number;
|
|
28
|
+
}
|
|
29
|
+
/** Resolve the age-band percent for an age; 100 when null/unmatched (full price). */
|
|
30
|
+
export declare function bandPctFor(age: number | null, bands: AgeBand[]): number;
|
|
31
|
+
/** One ladder tier: from `minTrips` trips, the cheapest trip gets `percent`% off. */
|
|
32
|
+
export interface LadderTier {
|
|
33
|
+
minTrips: number;
|
|
34
|
+
percent: number;
|
|
35
|
+
perk?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Highest ladder tier whose `minTrips` ≤ tripCount; null when none qualifies. */
|
|
38
|
+
export declare function ladderTierFor(tripCount: number, ladder: LadderTier[]): LadderTier | null;
|
|
39
|
+
/** Early-bird: `percent`% off a full-price participant's gross when booked early. */
|
|
40
|
+
export interface EarlyBird {
|
|
41
|
+
daysBefore: number;
|
|
42
|
+
percent: number;
|
|
43
|
+
}
|
|
44
|
+
/** Serializable per-participant pricing config (sourced from client globalSettings). */
|
|
45
|
+
export interface ParticipantPricingConfig {
|
|
46
|
+
ageBands: AgeBand[];
|
|
47
|
+
ladder: LadderTier[];
|
|
48
|
+
earlyBird?: EarlyBird | null;
|
|
49
|
+
}
|
|
50
|
+
/** A trip available in the booking, with its full (adult) price (minor units). */
|
|
51
|
+
export interface PricedTrip {
|
|
52
|
+
ref: string;
|
|
53
|
+
adultPrice: number;
|
|
54
|
+
}
|
|
55
|
+
/** A participant and the trips they take. */
|
|
56
|
+
export interface PricingParticipant {
|
|
57
|
+
id: string;
|
|
58
|
+
/** ISO date of birth; `null`/invalid ⇒ treated as a full-price adult. */
|
|
59
|
+
dob: string | null;
|
|
60
|
+
tripRefs: string[];
|
|
61
|
+
}
|
|
62
|
+
export interface ParticipantLine {
|
|
63
|
+
tripRef: string;
|
|
64
|
+
/** Age-adjusted base for this participant on this trip (minor units). */
|
|
65
|
+
base: number;
|
|
66
|
+
}
|
|
67
|
+
export interface ParticipantBreakdown {
|
|
68
|
+
participantId: string;
|
|
69
|
+
/** Resolved age-band percent (100 when no band matches / no dob). */
|
|
70
|
+
ageBandPct: number;
|
|
71
|
+
/** True ⇔ ageBandPct === 100 (eligible for volume discounts). */
|
|
72
|
+
fullPrice: boolean;
|
|
73
|
+
lines: ParticipantLine[];
|
|
74
|
+
/** Sum of line bases. */
|
|
75
|
+
gross: number;
|
|
76
|
+
/** Package (ladder) discount, minor units (0 when ineligible). */
|
|
77
|
+
packageDiscount: number;
|
|
78
|
+
/** Trip ref the package discount targets, or null. */
|
|
79
|
+
packageTargetRef: string | null;
|
|
80
|
+
/** Early-bird discount, minor units (0 when ineligible). */
|
|
81
|
+
earlyBirdDiscount: number;
|
|
82
|
+
perks: string[];
|
|
83
|
+
/** gross − packageDiscount − earlyBirdDiscount. */
|
|
84
|
+
net: number;
|
|
85
|
+
}
|
|
86
|
+
export interface ParticipantPriceResult {
|
|
87
|
+
gross: number;
|
|
88
|
+
discountTotal: number;
|
|
89
|
+
net: number;
|
|
90
|
+
participants: ParticipantBreakdown[];
|
|
91
|
+
perks: Perk[];
|
|
92
|
+
}
|
|
93
|
+
export declare function priceByParticipant(participants: PricingParticipant[], trips: PricedTrip[], anchor: Date | null, now: Date, config: ParticipantPricingConfig): ParticipantPriceResult;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { daysUntil } from './pricing.js';
|
|
2
|
+
import { ageAt } from './participant-fields/age.js';
|
|
3
|
+
/** Resolve the age-band percent for an age; 100 when null/unmatched (full price). */
|
|
4
|
+
export function bandPctFor(age, bands) {
|
|
5
|
+
if (age === null)
|
|
6
|
+
return 100;
|
|
7
|
+
for (const b of bands) {
|
|
8
|
+
const lo = age >= b.minAge;
|
|
9
|
+
const hi = b.maxAge === null || age <= b.maxAge;
|
|
10
|
+
if (lo && hi)
|
|
11
|
+
return b.pct;
|
|
12
|
+
}
|
|
13
|
+
return 100;
|
|
14
|
+
}
|
|
15
|
+
/** Highest ladder tier whose `minTrips` ≤ tripCount; null when none qualifies. */
|
|
16
|
+
export function ladderTierFor(tripCount, ladder) {
|
|
17
|
+
let best = null;
|
|
18
|
+
for (const t of ladder) {
|
|
19
|
+
if (tripCount >= t.minTrips && (!best || t.minTrips > best.minTrips))
|
|
20
|
+
best = t;
|
|
21
|
+
}
|
|
22
|
+
return best;
|
|
23
|
+
}
|
|
24
|
+
export function priceByParticipant(participants, trips, anchor, now, config) {
|
|
25
|
+
const priceOf = new Map(trips.map((t) => [t.ref, t.adultPrice]));
|
|
26
|
+
const daysBefore = anchor ? daysUntil(anchor, now) : null;
|
|
27
|
+
const breakdowns = participants.map((p) => {
|
|
28
|
+
const age = p.dob && anchor ? ageAt(p.dob, anchor) : null;
|
|
29
|
+
const pct = bandPctFor(age, config.ageBands);
|
|
30
|
+
const fullPrice = pct === 100;
|
|
31
|
+
const lines = p.tripRefs.map((ref) => ({
|
|
32
|
+
tripRef: ref,
|
|
33
|
+
base: Math.round(((priceOf.get(ref) ?? 0) * pct) / 100)
|
|
34
|
+
}));
|
|
35
|
+
const gross = lines.reduce((s, l) => s + l.base, 0);
|
|
36
|
+
let packageDiscount = 0;
|
|
37
|
+
let packageTargetRef = null;
|
|
38
|
+
const perks = [];
|
|
39
|
+
if (fullPrice) {
|
|
40
|
+
const tier = ladderTierFor(p.tripRefs.length, config.ladder);
|
|
41
|
+
if (tier) {
|
|
42
|
+
const cheapest = lines.reduce((min, l) => (!min || l.base < min.base ? l : min), null);
|
|
43
|
+
if (cheapest) {
|
|
44
|
+
packageDiscount = Math.round((cheapest.base * tier.percent) / 100);
|
|
45
|
+
packageTargetRef = cheapest.tripRef;
|
|
46
|
+
}
|
|
47
|
+
if (tier.perk)
|
|
48
|
+
perks.push(tier.perk);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let earlyBirdDiscount = 0;
|
|
52
|
+
if (fullPrice &&
|
|
53
|
+
config.earlyBird &&
|
|
54
|
+
daysBefore !== null &&
|
|
55
|
+
daysBefore >= config.earlyBird.daysBefore) {
|
|
56
|
+
earlyBirdDiscount = Math.round((gross * config.earlyBird.percent) / 100);
|
|
57
|
+
}
|
|
58
|
+
const net = Math.max(0, gross - packageDiscount - earlyBirdDiscount);
|
|
59
|
+
return {
|
|
60
|
+
participantId: p.id,
|
|
61
|
+
ageBandPct: pct,
|
|
62
|
+
fullPrice,
|
|
63
|
+
lines,
|
|
64
|
+
gross,
|
|
65
|
+
packageDiscount,
|
|
66
|
+
packageTargetRef,
|
|
67
|
+
earlyBirdDiscount,
|
|
68
|
+
perks,
|
|
69
|
+
net
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
const gross = breakdowns.reduce((s, b) => s + b.gross, 0);
|
|
73
|
+
const discountTotal = breakdowns.reduce((s, b) => s + b.packageDiscount + b.earlyBirdDiscount, 0);
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const perks = [];
|
|
76
|
+
for (const b of breakdowns) {
|
|
77
|
+
for (const label of b.perks) {
|
|
78
|
+
if (seen.has(label))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(label);
|
|
81
|
+
perks.push({ label });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
gross,
|
|
86
|
+
discountTotal: Math.min(gross, discountTotal),
|
|
87
|
+
net: Math.max(0, gross - discountTotal),
|
|
88
|
+
participants: breakdowns,
|
|
89
|
+
perks
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure read adapter: fold person + assignment rows into the flat
|
|
3
|
+
* "participant per trip line" shape the admin/portal UI was written against
|
|
4
|
+
* (one row per person×item, with identity + per-trip answers merged into
|
|
5
|
+
* `data`). Keeps the read DTOs backward-compatible while the storage moved to
|
|
6
|
+
* the person-centric model. UI modernization (split identity vs per-trip) is a
|
|
7
|
+
* later plan.
|
|
8
|
+
*/
|
|
9
|
+
/** Person row (subset). `data` holds identity answers (name, dob, …). */
|
|
10
|
+
export interface PersonRecord {
|
|
11
|
+
id: string;
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
/** Assignment (junction) row (subset). `data` holds per-trip answers. */
|
|
15
|
+
export interface AssignmentRecord {
|
|
16
|
+
id: string;
|
|
17
|
+
itemId: string;
|
|
18
|
+
personId: string;
|
|
19
|
+
data: Record<string, unknown>;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
}
|
|
23
|
+
/** Flat participant: one per assignment, identity + per-trip `data` merged. */
|
|
24
|
+
export interface FlatParticipant {
|
|
25
|
+
id: string;
|
|
26
|
+
itemId: string;
|
|
27
|
+
personId: string;
|
|
28
|
+
data: Record<string, unknown>;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
updatedAt: Date;
|
|
31
|
+
}
|
|
32
|
+
export declare function flattenAssignments(persons: PersonRecord[], assignments: AssignmentRecord[]): FlatParticipant[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure read adapter: fold person + assignment rows into the flat
|
|
3
|
+
* "participant per trip line" shape the admin/portal UI was written against
|
|
4
|
+
* (one row per person×item, with identity + per-trip answers merged into
|
|
5
|
+
* `data`). Keeps the read DTOs backward-compatible while the storage moved to
|
|
6
|
+
* the person-centric model. UI modernization (split identity vs per-trip) is a
|
|
7
|
+
* later plan.
|
|
8
|
+
*/
|
|
9
|
+
export function flattenAssignments(persons, assignments) {
|
|
10
|
+
const personById = new Map(persons.map((p) => [p.id, p]));
|
|
11
|
+
return assignments.map((a) => {
|
|
12
|
+
const person = personById.get(a.personId);
|
|
13
|
+
return {
|
|
14
|
+
id: a.id,
|
|
15
|
+
itemId: a.itemId,
|
|
16
|
+
personId: a.personId,
|
|
17
|
+
data: { ...(person?.data ?? {}), ...a.data },
|
|
18
|
+
createdAt: a.createdAt,
|
|
19
|
+
updatedAt: a.updatedAt
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PaymentCreateContext } from '../shop/types.js';
|
|
2
|
+
/** Split "Imię Nazwisko" → first/last (first whitespace splits). */
|
|
3
|
+
export declare function splitName(name: string): {
|
|
4
|
+
firstName?: string;
|
|
5
|
+
lastName?: string;
|
|
6
|
+
};
|
|
7
|
+
export interface BuildPaymentContextInput {
|
|
8
|
+
accessToken: string;
|
|
9
|
+
language: string | null;
|
|
10
|
+
customer: Record<string, unknown>;
|
|
11
|
+
portalUrlTemplate: string | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build the {@link PaymentCreateContext} a booking hands to its payment adapter:
|
|
15
|
+
* a fully-resolved portal `continueUrl` (template `{token}` → accessToken) plus
|
|
16
|
+
* buyer identity derived from `customer`. Lets any shop adapter serve booking
|
|
17
|
+
* without a shop module.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildPaymentContext(input: BuildPaymentContextInput): PaymentCreateContext;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Split "Imię Nazwisko" → first/last (first whitespace splits). */
|
|
2
|
+
export function splitName(name) {
|
|
3
|
+
const trimmed = (name ?? '').trim();
|
|
4
|
+
if (!trimmed)
|
|
5
|
+
return {};
|
|
6
|
+
const firstSpace = trimmed.search(/\s/);
|
|
7
|
+
if (firstSpace === -1)
|
|
8
|
+
return { firstName: trimmed };
|
|
9
|
+
return {
|
|
10
|
+
firstName: trimmed.slice(0, firstSpace),
|
|
11
|
+
lastName: trimmed.slice(firstSpace + 1).trim() || undefined
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the {@link PaymentCreateContext} a booking hands to its payment adapter:
|
|
16
|
+
* a fully-resolved portal `continueUrl` (template `{token}` → accessToken) plus
|
|
17
|
+
* buyer identity derived from `customer`. Lets any shop adapter serve booking
|
|
18
|
+
* without a shop module.
|
|
19
|
+
*/
|
|
20
|
+
export function buildPaymentContext(input) {
|
|
21
|
+
const { accessToken, language, customer, portalUrlTemplate } = input;
|
|
22
|
+
const { firstName, lastName } = splitName(String(customer.name ?? ''));
|
|
23
|
+
const phone = String(customer.phone ?? '').trim() || undefined;
|
|
24
|
+
return {
|
|
25
|
+
language,
|
|
26
|
+
continueUrl: portalUrlTemplate ? portalUrlTemplate.replace('{token}', accessToken) : undefined,
|
|
27
|
+
buyer: { firstName, lastName, phone }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure adapter: person + item + assignment rows → {@link priceByParticipant}
|
|
3
|
+
* inputs. Resolves each person's date of birth from identity `data` via the
|
|
4
|
+
* role:'birthDate' field; dedupes trips by `tripRef`.
|
|
5
|
+
*/
|
|
6
|
+
import type { FieldDef } from './participant-fields/types.js';
|
|
7
|
+
import type { PricingParticipant, PricedTrip } from './participant-pricing.js';
|
|
8
|
+
/** Person row (subset needed for pricing). */
|
|
9
|
+
export interface PersonRow {
|
|
10
|
+
id: string;
|
|
11
|
+
data: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/** Trip-line row (subset needed for pricing). `unitPrice` = adult price (minor units). */
|
|
14
|
+
export interface ItemRow {
|
|
15
|
+
id: string;
|
|
16
|
+
tripRef: string;
|
|
17
|
+
unitPrice: number;
|
|
18
|
+
}
|
|
19
|
+
/** Assignment (junction) row. */
|
|
20
|
+
export interface ItemPersonRow {
|
|
21
|
+
itemId: string;
|
|
22
|
+
personId: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PricingInputs {
|
|
25
|
+
participants: PricingParticipant[];
|
|
26
|
+
trips: PricedTrip[];
|
|
27
|
+
}
|
|
28
|
+
export declare function buildPricingInputs(persons: PersonRow[], items: ItemRow[], assignments: ItemPersonRow[], identityFields: FieldDef[]): PricingInputs;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function buildPricingInputs(persons, items, assignments, identityFields) {
|
|
2
|
+
const dobKey = identityFields.find((f) => f.role === 'birthDate')?.key ?? null;
|
|
3
|
+
const tripRefByItem = new Map(items.map((it) => [it.id, it.tripRef]));
|
|
4
|
+
const tripsByPerson = new Map();
|
|
5
|
+
for (const a of assignments) {
|
|
6
|
+
const tripRef = tripRefByItem.get(a.itemId);
|
|
7
|
+
if (tripRef === undefined)
|
|
8
|
+
continue;
|
|
9
|
+
const list = tripsByPerson.get(a.personId);
|
|
10
|
+
if (list) {
|
|
11
|
+
if (!list.includes(tripRef))
|
|
12
|
+
list.push(tripRef);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
tripsByPerson.set(a.personId, [tripRef]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const participants = persons.map((p) => {
|
|
19
|
+
const dobRaw = dobKey ? p.data[dobKey] : null;
|
|
20
|
+
return {
|
|
21
|
+
id: p.id,
|
|
22
|
+
dob: typeof dobRaw === 'string' ? dobRaw : null,
|
|
23
|
+
tripRefs: tripsByPerson.get(p.id) ?? []
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const trips = [];
|
|
28
|
+
for (const it of items) {
|
|
29
|
+
if (seen.has(it.tripRef))
|
|
30
|
+
continue;
|
|
31
|
+
seen.add(it.tripRef);
|
|
32
|
+
trips.push({ ref: it.tripRef, adultPrice: it.unitPrice });
|
|
33
|
+
}
|
|
34
|
+
return { participants, trips };
|
|
35
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking discount rules engine — a pure, deterministic, module-agnostic
|
|
3
|
+
* function. No side effects, no clock access (the caller passes `now`).
|
|
4
|
+
*
|
|
5
|
+
* A rule is serializable data (`when` conditions → `then` effects). Rules carry
|
|
6
|
+
* an optional `group`: within a group the single largest-discount rule wins;
|
|
7
|
+
* across groups discounts sum. Amounts are in **minor units** (grosze).
|
|
8
|
+
*/
|
|
9
|
+
/** A condition; all conditions in a rule's `when` must hold (logical AND). */
|
|
10
|
+
export type Condition = {
|
|
11
|
+
kind: 'minTripCount';
|
|
12
|
+
value: number;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'minParticipants';
|
|
15
|
+
value: number;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'minDaysBefore';
|
|
18
|
+
value: number;
|
|
19
|
+
};
|
|
20
|
+
/** An effect applied when a rule wins its group. */
|
|
21
|
+
export type Effect = {
|
|
22
|
+
kind: 'percentOff';
|
|
23
|
+
value: number;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'amountOff';
|
|
26
|
+
value: number;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'percentOffCheapestLine';
|
|
29
|
+
value: number;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'grantPerk';
|
|
32
|
+
label: string;
|
|
33
|
+
code?: string;
|
|
34
|
+
};
|
|
35
|
+
/** A serializable discount rule. */
|
|
36
|
+
export interface DiscountRule {
|
|
37
|
+
/** Stable id (labels, audit, future UI). */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Human label for admin/portal/email; defaults to `id`. */
|
|
40
|
+
label?: string;
|
|
41
|
+
/** Exclusivity group; rules in the same group compete (one wins). Omitted ⇒ own singleton group. */
|
|
42
|
+
group?: string;
|
|
43
|
+
/** Tiebreak within a group when discount amounts are equal (higher wins). Default 0. */
|
|
44
|
+
priority?: number;
|
|
45
|
+
when: Condition[];
|
|
46
|
+
then: Effect[];
|
|
47
|
+
}
|
|
48
|
+
export interface PricingConfig {
|
|
49
|
+
rules?: DiscountRule[];
|
|
50
|
+
}
|
|
51
|
+
/** A single priced line that retains trip identity (for cheapest/distinct-count). */
|
|
52
|
+
export interface PricingLine {
|
|
53
|
+
/** Trip reference — identity for distinct-count and discount targeting. */
|
|
54
|
+
ref: string;
|
|
55
|
+
/** Unit price (minor units), per participant. */
|
|
56
|
+
unitPrice: number;
|
|
57
|
+
/** Participant count on this line. */
|
|
58
|
+
quantity: number;
|
|
59
|
+
/** Departure date — drives early-bird (earliest across lines). */
|
|
60
|
+
departureDate: Date | null;
|
|
61
|
+
/** Precomputed line gross (minor units); overrides unitPrice*quantity when set. */
|
|
62
|
+
subtotal?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface AppliedRule {
|
|
65
|
+
ruleId: string;
|
|
66
|
+
/** Human label (rule.label ?? rule.id). */
|
|
67
|
+
label: string;
|
|
68
|
+
/** Monetary discount contributed, in minor units. */
|
|
69
|
+
amount: number;
|
|
70
|
+
/** Line `ref` this discount targets (set for line-targeted effects only). */
|
|
71
|
+
targetRef?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface Perk {
|
|
74
|
+
code?: string;
|
|
75
|
+
label: string;
|
|
76
|
+
}
|
|
77
|
+
export interface PriceResult {
|
|
78
|
+
gross: number;
|
|
79
|
+
discountTotal: number;
|
|
80
|
+
net: number;
|
|
81
|
+
appliedRules: AppliedRule[];
|
|
82
|
+
perks: Perk[];
|
|
83
|
+
}
|
|
84
|
+
/** Whole days from `now` until `departureDate` (negative if departure is past). */
|
|
85
|
+
export declare function daysUntil(departureDate: Date, now: Date): number;
|
|
86
|
+
export declare function price(lines: PricingLine[], now: Date, config: PricingConfig): PriceResult;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking discount rules engine — a pure, deterministic, module-agnostic
|
|
3
|
+
* function. No side effects, no clock access (the caller passes `now`).
|
|
4
|
+
*
|
|
5
|
+
* A rule is serializable data (`when` conditions → `then` effects). Rules carry
|
|
6
|
+
* an optional `group`: within a group the single largest-discount rule wins;
|
|
7
|
+
* across groups discounts sum. Amounts are in **minor units** (grosze).
|
|
8
|
+
*/
|
|
9
|
+
const MS_PER_DAY = 86_400_000;
|
|
10
|
+
/** Whole days from `now` until `departureDate` (negative if departure is past). */
|
|
11
|
+
export function daysUntil(departureDate, now) {
|
|
12
|
+
return Math.floor((departureDate.getTime() - now.getTime()) / MS_PER_DAY);
|
|
13
|
+
}
|
|
14
|
+
function conditionHolds(c, ctx) {
|
|
15
|
+
switch (c.kind) {
|
|
16
|
+
case 'minTripCount':
|
|
17
|
+
return ctx.distinctTrips >= c.value;
|
|
18
|
+
case 'minParticipants':
|
|
19
|
+
return ctx.totalQty >= c.value;
|
|
20
|
+
case 'minDaysBefore':
|
|
21
|
+
return ctx.daysBefore >= c.value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Monetary amount of an effect, clamped to [0, gross]; null for non-monetary effects. */
|
|
25
|
+
function effectMonetary(e, ctx) {
|
|
26
|
+
switch (e.kind) {
|
|
27
|
+
case 'percentOff':
|
|
28
|
+
return { amount: Math.max(0, Math.min(ctx.gross, Math.round((ctx.gross * e.value) / 100))) };
|
|
29
|
+
case 'amountOff':
|
|
30
|
+
return { amount: Math.max(0, Math.min(ctx.gross, Math.round(e.value))) };
|
|
31
|
+
case 'percentOffCheapestLine': {
|
|
32
|
+
if (!ctx.cheapest)
|
|
33
|
+
return null;
|
|
34
|
+
const amt = Math.round((ctx.cheapest.total * e.value) / 100);
|
|
35
|
+
return { amount: Math.max(0, Math.min(ctx.gross, amt)), targetRef: ctx.cheapest.ref };
|
|
36
|
+
}
|
|
37
|
+
case 'grantPerk':
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Prices a set of lines against discount rules.
|
|
43
|
+
*
|
|
44
|
+
* @param lines - Priced lines retaining trip identity (minor units).
|
|
45
|
+
* @param now - Reference "current" time (injected for determinism).
|
|
46
|
+
* @param config - Discount rules (values from client config).
|
|
47
|
+
*/
|
|
48
|
+
/** Line gross: explicit subtotal when provided, else unitPrice * quantity. */
|
|
49
|
+
function lineTotal(l) {
|
|
50
|
+
return l.subtotal ?? l.unitPrice * l.quantity;
|
|
51
|
+
}
|
|
52
|
+
export function price(lines, now, config) {
|
|
53
|
+
const gross = lines.reduce((s, l) => s + lineTotal(l), 0);
|
|
54
|
+
const totalQty = lines.reduce((s, l) => s + l.quantity, 0);
|
|
55
|
+
const distinctTrips = new Set(lines.map((l) => l.ref)).size;
|
|
56
|
+
const dated = lines.map((l) => l.departureDate).filter((d) => d instanceof Date);
|
|
57
|
+
const basis = dated.length ? new Date(Math.min(...dated.map((d) => d.getTime()))) : now;
|
|
58
|
+
const daysBefore = daysUntil(basis, now);
|
|
59
|
+
let cheapest = null;
|
|
60
|
+
for (const l of lines) {
|
|
61
|
+
const total = lineTotal(l);
|
|
62
|
+
if (!cheapest || total < cheapest.total)
|
|
63
|
+
cheapest = { ref: l.ref, total };
|
|
64
|
+
}
|
|
65
|
+
const ctx = { gross, totalQty, distinctTrips, daysBefore, cheapest };
|
|
66
|
+
// Eligible rules, preserving declaration order.
|
|
67
|
+
const eligible = (config.rules ?? [])
|
|
68
|
+
.map((r, i) => ({ r, i }))
|
|
69
|
+
.filter(({ r }) => r.when.every((c) => conditionHolds(c, ctx)));
|
|
70
|
+
// Bucket by exclusivity group (omitted group ⇒ unique singleton).
|
|
71
|
+
const groups = new Map();
|
|
72
|
+
for (const e of eligible) {
|
|
73
|
+
const key = e.r.group ?? `__single__${e.i}`;
|
|
74
|
+
const bucket = groups.get(key);
|
|
75
|
+
if (bucket)
|
|
76
|
+
bucket.push(e);
|
|
77
|
+
else
|
|
78
|
+
groups.set(key, [e]);
|
|
79
|
+
}
|
|
80
|
+
const appliedRules = [];
|
|
81
|
+
const perks = [];
|
|
82
|
+
for (const members of groups.values()) {
|
|
83
|
+
const evaluated = members.map(({ r, i }) => {
|
|
84
|
+
const effects = r.then.map((e) => ({ e, m: effectMonetary(e, ctx) }));
|
|
85
|
+
const monetary = effects.reduce((s, x) => s + (x.m?.amount ?? 0), 0);
|
|
86
|
+
return { r, i, effects, monetary };
|
|
87
|
+
});
|
|
88
|
+
// Winner: largest monetary, tie → higher priority, tie → earlier order.
|
|
89
|
+
evaluated.sort((a, b) => b.monetary - a.monetary || (b.r.priority ?? 0) - (a.r.priority ?? 0) || a.i - b.i);
|
|
90
|
+
const winner = evaluated[0];
|
|
91
|
+
if (!winner)
|
|
92
|
+
continue;
|
|
93
|
+
for (const { e, m } of winner.effects) {
|
|
94
|
+
if (e.kind === 'grantPerk') {
|
|
95
|
+
perks.push({ code: e.code, label: e.label });
|
|
96
|
+
}
|
|
97
|
+
else if (m && m.amount > 0) {
|
|
98
|
+
const ar = {
|
|
99
|
+
ruleId: winner.r.id,
|
|
100
|
+
label: winner.r.label ?? winner.r.id,
|
|
101
|
+
amount: m.amount
|
|
102
|
+
};
|
|
103
|
+
if (m.targetRef)
|
|
104
|
+
ar.targetRef = m.targetRef;
|
|
105
|
+
appliedRules.push(ar);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const discountTotal = Math.min(gross, appliedRules.reduce((s, a) => s + a.amount, 0));
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
const dedupedPerks = perks.filter((p) => {
|
|
112
|
+
const k = p.code ?? p.label;
|
|
113
|
+
if (seen.has(k))
|
|
114
|
+
return false;
|
|
115
|
+
seen.add(k);
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
gross,
|
|
120
|
+
discountTotal,
|
|
121
|
+
net: Math.max(0, gross - discountTotal),
|
|
122
|
+
appliedRules,
|
|
123
|
+
perks: dedupedPerks
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type PaymentEntry } from './ledger.js';
|
|
2
|
+
import { type BookingTotals } from './totals.js';
|
|
3
|
+
import type { ParticipantPriceResult } from './participant-pricing.js';
|
|
4
|
+
import type { AdjustmentSums } from './adjustments.js';
|
|
5
|
+
import type { BookingStatus } from './state-machine.js';
|
|
6
|
+
/** Thrown when an edit is not allowed (terminal booking). */
|
|
7
|
+
export declare class BookingEditError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Pure recompute of a booking's money snapshot after an edit. Takes the
|
|
12
|
+
* already-computed per-participant pricing result (the caller prices from the
|
|
13
|
+
* edited or existing people/items) and folds in payments.
|
|
14
|
+
*/
|
|
15
|
+
export declare function recomputeTotals(result: ParticipantPriceResult, sums: AdjustmentSums, payments: PaymentEntry[]): BookingTotals;
|
|
16
|
+
/** Block edits to terminal bookings (cancelled/completed). */
|
|
17
|
+
export declare function assertEditAllowed(status: BookingStatus): void;
|
|
18
|
+
/**
|
|
19
|
+
* Portal (self-service) edit guard: block terminal bookings AND any edit that
|
|
20
|
+
* would drop the gross below what's already paid (would owe a refund → staff).
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertPortalEditAllowed(status: BookingStatus, newGross: number, payments: PaymentEntry[]): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { totalPaid, assertGrossNotBelowPaid } from './ledger.js';
|
|
2
|
+
import { resultToTotals } from './totals.js';
|
|
3
|
+
/** Thrown when an edit is not allowed (terminal booking). */
|
|
4
|
+
export class BookingEditError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'BookingEditError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Pure recompute of a booking's money snapshot after an edit. Takes the
|
|
12
|
+
* already-computed per-participant pricing result (the caller prices from the
|
|
13
|
+
* edited or existing people/items) and folds in payments.
|
|
14
|
+
*/
|
|
15
|
+
export function recomputeTotals(result, sums, payments) {
|
|
16
|
+
return resultToTotals(result, sums, totalPaid(payments));
|
|
17
|
+
}
|
|
18
|
+
/** Block edits to terminal bookings (cancelled/completed). */
|
|
19
|
+
export function assertEditAllowed(status) {
|
|
20
|
+
if (status === 'cancelled' || status === 'completed') {
|
|
21
|
+
throw new BookingEditError(`Cannot edit a ${status} booking`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Portal (self-service) edit guard: block terminal bookings AND any edit that
|
|
26
|
+
* would drop the gross below what's already paid (would owe a refund → staff).
|
|
27
|
+
*/
|
|
28
|
+
export function assertPortalEditAllowed(status, newGross, payments) {
|
|
29
|
+
assertEditAllowed(status);
|
|
30
|
+
assertGrossNotBelowPaid(newGross, payments); // throws LedgerError when newGross < paid
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReminderRule } from './config.js';
|
|
2
|
+
/** The earliest non-null departure across booking items, or null when none. */
|
|
3
|
+
export declare function earliestDeparture(dates: Array<Date | null>): Date | null;
|
|
4
|
+
/** Balance-due deadline = earliest departure − `balanceDueDaysBefore`; null if unconfigured. */
|
|
5
|
+
export declare function computeBalanceDueAt(dates: Array<Date | null>, balanceDueDaysBefore: number | null): Date | null;
|
|
6
|
+
/** One scheduled-reminder row per rule, anchored to the earliest departure. */
|
|
7
|
+
export declare function computeReminderSchedule(dates: Array<Date | null>, rules: ReminderRule[]): Array<{
|
|
8
|
+
type: string;
|
|
9
|
+
templateKey: string;
|
|
10
|
+
scheduledFor: Date;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const MS_PER_DAY = 86_400_000;
|
|
2
|
+
/** The earliest non-null departure across booking items, or null when none. */
|
|
3
|
+
export function earliestDeparture(dates) {
|
|
4
|
+
const valid = dates.filter((d) => d instanceof Date);
|
|
5
|
+
if (valid.length === 0)
|
|
6
|
+
return null;
|
|
7
|
+
return valid.reduce((min, d) => (d.getTime() < min.getTime() ? d : min));
|
|
8
|
+
}
|
|
9
|
+
/** Balance-due deadline = earliest departure − `balanceDueDaysBefore`; null if unconfigured. */
|
|
10
|
+
export function computeBalanceDueAt(dates, balanceDueDaysBefore) {
|
|
11
|
+
if (balanceDueDaysBefore == null)
|
|
12
|
+
return null;
|
|
13
|
+
const dep = earliestDeparture(dates);
|
|
14
|
+
if (!dep)
|
|
15
|
+
return null;
|
|
16
|
+
return new Date(dep.getTime() - balanceDueDaysBefore * MS_PER_DAY);
|
|
17
|
+
}
|
|
18
|
+
/** One scheduled-reminder row per rule, anchored to the earliest departure. */
|
|
19
|
+
export function computeReminderSchedule(dates, rules) {
|
|
20
|
+
const dep = earliestDeparture(dates);
|
|
21
|
+
if (!dep || rules.length === 0)
|
|
22
|
+
return [];
|
|
23
|
+
return rules.map((r) => ({
|
|
24
|
+
type: r.type,
|
|
25
|
+
templateKey: r.templateKey,
|
|
26
|
+
scheduledFor: new Date(dep.getTime() - r.daysBefore * MS_PER_DAY)
|
|
27
|
+
}));
|
|
28
|
+
}
|