@medalsocial/booking 0.0.0-placeholder.0 → 0.2.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/CHANGELOG.md +27 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +220 -1
- package/dist/core/age.d.mts +34 -0
- package/dist/core/age.mjs +95 -0
- package/dist/core/age.mjs.map +1 -0
- package/dist/core/attempt-store.d.mts +111 -0
- package/dist/core/attempt-store.mjs +159 -0
- package/dist/core/attempt-store.mjs.map +1 -0
- package/dist/core/categories.d.mts +27 -0
- package/dist/core/categories.mjs +42 -0
- package/dist/core/categories.mjs.map +1 -0
- package/dist/core/clock.d.mts +33 -0
- package/dist/core/clock.mjs +251 -0
- package/dist/core/clock.mjs.map +1 -0
- package/dist/core/config.d.mts +178 -0
- package/dist/core/config.mjs +394 -0
- package/dist/core/config.mjs.map +1 -0
- package/dist/core/consent.d.mts +14 -0
- package/dist/core/consent.mjs +17 -0
- package/dist/core/consent.mjs.map +1 -0
- package/dist/core/deep-link.d.mts +40 -0
- package/dist/core/deep-link.mjs +160 -0
- package/dist/core/deep-link.mjs.map +1 -0
- package/dist/core/display-name.d.mts +18 -0
- package/dist/core/display-name.mjs +30 -0
- package/dist/core/display-name.mjs.map +1 -0
- package/dist/core/draft-store.d.mts +89 -0
- package/dist/core/draft-store.mjs +131 -0
- package/dist/core/draft-store.mjs.map +1 -0
- package/dist/core/dto.d.mts +46 -0
- package/dist/core/dto.mjs +157 -0
- package/dist/core/dto.mjs.map +1 -0
- package/dist/core/ics.d.mts +63 -0
- package/dist/core/ics.mjs +130 -0
- package/dist/core/ics.mjs.map +1 -0
- package/dist/core/index.d.mts +35 -0
- package/dist/core/index.mjs +32 -0
- package/dist/core/labels.d.mts +88 -0
- package/dist/core/labels.mjs +93 -0
- package/dist/core/labels.mjs.map +1 -0
- package/dist/core/machine.d.mts +645 -0
- package/dist/core/machine.mjs +988 -0
- package/dist/core/machine.mjs.map +1 -0
- package/dist/core/money.d.mts +12 -0
- package/dist/core/money.mjs +51 -0
- package/dist/core/money.mjs.map +1 -0
- package/dist/core/next-available.d.mts +20 -0
- package/dist/core/next-available.mjs +36 -0
- package/dist/core/next-available.mjs.map +1 -0
- package/dist/core/next-free.d.mts +25 -0
- package/dist/core/next-free.mjs +44 -0
- package/dist/core/next-free.mjs.map +1 -0
- package/dist/core/party-slots.d.mts +79 -0
- package/dist/core/party-slots.mjs +229 -0
- package/dist/core/party-slots.mjs.map +1 -0
- package/dist/core/paths.d.mts +11 -0
- package/dist/core/paths.mjs +56 -0
- package/dist/core/paths.mjs.map +1 -0
- package/dist/core/phone.d.mts +17 -0
- package/dist/core/phone.mjs +58 -0
- package/dist/core/phone.mjs.map +1 -0
- package/dist/core/portal/cookies.d.mts +29 -0
- package/dist/core/portal/dto.d.mts +140 -0
- package/dist/core/portal/dto.mjs +142 -0
- package/dist/core/portal/dto.mjs.map +1 -0
- package/dist/core/portal/family.d.mts +39 -0
- package/dist/core/portal/family.mjs +39 -0
- package/dist/core/portal/family.mjs.map +1 -0
- package/dist/core/portal/login-input.d.mts +29 -0
- package/dist/core/portal/login-input.mjs +29 -0
- package/dist/core/portal/login-input.mjs.map +1 -0
- package/dist/core/portal/next-path.d.mts +12 -0
- package/dist/core/portal/next-path.mjs +98 -0
- package/dist/core/portal/next-path.mjs.map +1 -0
- package/dist/core/portal/return-path.d.mts +7 -0
- package/dist/core/portal/return-path.mjs +49 -0
- package/dist/core/portal/return-path.mjs.map +1 -0
- package/dist/core/portal/session-cookie.d.mts +27 -0
- package/dist/core/portal/session-cookie.mjs +30 -0
- package/dist/core/portal/session-cookie.mjs.map +1 -0
- package/dist/core/portal/vipps-link.d.mts +56 -0
- package/dist/core/portal/vipps-link.mjs +136 -0
- package/dist/core/portal/vipps-link.mjs.map +1 -0
- package/dist/core/portal/vipps-return.d.mts +39 -0
- package/dist/core/portal/vipps-return.mjs +69 -0
- package/dist/core/portal/vipps-return.mjs.map +1 -0
- package/dist/core/rebook-store.d.mts +28 -0
- package/dist/core/rebook-store.mjs +60 -0
- package/dist/core/rebook-store.mjs.map +1 -0
- package/dist/core/restore-gate.d.mts +40 -0
- package/dist/core/restore-gate.mjs +96 -0
- package/dist/core/restore-gate.mjs.map +1 -0
- package/dist/core/stores.d.mts +8 -0
- package/dist/core/stores.mjs +23 -0
- package/dist/core/stores.mjs.map +1 -0
- package/dist/core/types.d.mts +245 -0
- package/dist/core/visit.d.mts +26 -0
- package/dist/core/visit.mjs +40 -0
- package/dist/core/visit.mjs.map +1 -0
- package/dist/core/wire.d.mts +37 -0
- package/dist/next/cache/memory.d.mts +14 -0
- package/dist/next/cache/memory.mjs +51 -0
- package/dist/next/cache/memory.mjs.map +1 -0
- package/dist/next/cache/next-data.d.mts +6 -0
- package/dist/next/cache/next-data.mjs +35 -0
- package/dist/next/cache/next-data.mjs.map +1 -0
- package/dist/next/cache/noop.d.mts +6 -0
- package/dist/next/cache/noop.mjs +15 -0
- package/dist/next/cache/noop.mjs.map +1 -0
- package/dist/next/cache/workers.d.mts +29 -0
- package/dist/next/cache/workers.mjs +74 -0
- package/dist/next/cache/workers.mjs.map +1 -0
- package/dist/next/catalogue.d.mts +38 -0
- package/dist/next/catalogue.mjs +215 -0
- package/dist/next/catalogue.mjs.map +1 -0
- package/dist/next/handler.d.mts +11 -0
- package/dist/next/handler.mjs +150 -0
- package/dist/next/handler.mjs.map +1 -0
- package/dist/next/index.d.mts +17 -0
- package/dist/next/index.mjs +14 -0
- package/dist/next/loaders.d.mts +79 -0
- package/dist/next/loaders.mjs +195 -0
- package/dist/next/loaders.mjs.map +1 -0
- package/dist/next/manage-dto.d.mts +6 -0
- package/dist/next/manage-dto.mjs +28 -0
- package/dist/next/manage-dto.mjs.map +1 -0
- package/dist/next/medal.d.mts +105 -0
- package/dist/next/medal.mjs +155 -0
- package/dist/next/medal.mjs.map +1 -0
- package/dist/next/options.d.mts +158 -0
- package/dist/next/options.mjs +65 -0
- package/dist/next/options.mjs.map +1 -0
- package/dist/next/portal/guardian.d.mts +42 -0
- package/dist/next/portal/guardian.mjs +76 -0
- package/dist/next/portal/guardian.mjs.map +1 -0
- package/dist/next/portal/medal-portal.d.mts +161 -0
- package/dist/next/portal/medal-portal.mjs +825 -0
- package/dist/next/portal/medal-portal.mjs.map +1 -0
- package/dist/next/portal/session.d.mts +38 -0
- package/dist/next/portal/session.mjs +40 -0
- package/dist/next/portal/session.mjs.map +1 -0
- package/dist/next/portal/vipps-flash.d.mts +14 -0
- package/dist/next/portal/vipps-flash.mjs +41 -0
- package/dist/next/portal/vipps-flash.mjs.map +1 -0
- package/dist/next/portal.d.mts +205 -0
- package/dist/next/portal.mjs +469 -0
- package/dist/next/portal.mjs.map +1 -0
- package/dist/next/request.d.mts +51 -0
- package/dist/next/request.mjs +123 -0
- package/dist/next/request.mjs.map +1 -0
- package/dist/next/routes/catalogue-routes.mjs +344 -0
- package/dist/next/routes/catalogue-routes.mjs.map +1 -0
- package/dist/next/routes/create.mjs +460 -0
- package/dist/next/routes/create.mjs.map +1 -0
- package/dist/next/routes/manage.mjs +191 -0
- package/dist/next/routes/manage.mjs.map +1 -0
- package/dist/next/routes/portal-routes.mjs +312 -0
- package/dist/next/routes/portal-routes.mjs.map +1 -0
- package/dist/next/routes/shared.mjs +162 -0
- package/dist/next/routes/shared.mjs.map +1 -0
- package/dist/next/routes/vipps-return.mjs +166 -0
- package/dist/next/routes/vipps-return.mjs.map +1 -0
- package/dist/next/runtime.d.mts +49 -0
- package/dist/next/runtime.mjs +94 -0
- package/dist/next/runtime.mjs.map +1 -0
- package/dist/next/seed.d.mts +71 -0
- package/dist/next/seed.mjs +495 -0
- package/dist/next/seed.mjs.map +1 -0
- package/dist/next/server.d.mts +26 -0
- package/dist/next/server.mjs +63 -0
- package/dist/next/server.mjs.map +1 -0
- package/dist/react/BookingLink.d.mts +61 -0
- package/dist/react/BookingLink.mjs +102 -0
- package/dist/react/BookingLink.mjs.map +1 -0
- package/dist/react/BookingWizard.d.mts +49 -0
- package/dist/react/BookingWizard.mjs +598 -0
- package/dist/react/BookingWizard.mjs.map +1 -0
- package/dist/react/LoginPageQuery.d.mts +37 -0
- package/dist/react/LoginPageQuery.mjs +106 -0
- package/dist/react/LoginPageQuery.mjs.map +1 -0
- package/dist/react/LoginSheet.d.mts +49 -0
- package/dist/react/LoginSheet.mjs +98 -0
- package/dist/react/LoginSheet.mjs.map +1 -0
- package/dist/react/ManageBooking.d.mts +50 -0
- package/dist/react/ManageBooking.mjs +197 -0
- package/dist/react/ManageBooking.mjs.map +1 -0
- package/dist/react/PortalDashboard.d.mts +10 -0
- package/dist/react/Provider.d.mts +82 -0
- package/dist/react/Provider.mjs +132 -0
- package/dist/react/Provider.mjs.map +1 -0
- package/dist/react/actions.d.mts +133 -0
- package/dist/react/actions.mjs +90 -0
- package/dist/react/actions.mjs.map +1 -0
- package/dist/react/index.d.mts +28 -0
- package/dist/react/index.mjs +20 -0
- package/dist/react/kit.d.mts +54 -0
- package/dist/react/kit.mjs +127 -0
- package/dist/react/kit.mjs.map +1 -0
- package/dist/react/labels/index.d.mts +23 -0
- package/dist/react/labels/index.mjs +44 -0
- package/dist/react/labels/index.mjs.map +1 -0
- package/dist/react/labels/login.d.mts +10 -0
- package/dist/react/labels/login.mjs +13 -0
- package/dist/react/labels/login.mjs.map +1 -0
- package/dist/react/labels/manage.d.mts +15 -0
- package/dist/react/labels/manage.mjs +19 -0
- package/dist/react/labels/manage.mjs.map +1 -0
- package/dist/react/labels/portal.d.mts +25 -0
- package/dist/react/labels/portal.mjs +41 -0
- package/dist/react/labels/portal.mjs.map +1 -0
- package/dist/react/labels/screens.d.mts +11 -0
- package/dist/react/labels/screens.mjs +721 -0
- package/dist/react/labels/screens.mjs.map +1 -0
- package/dist/react/labels/wizard.d.mts +76 -0
- package/dist/react/labels/wizard.mjs +97 -0
- package/dist/react/labels/wizard.mjs.map +1 -0
- package/dist/react/link/index.d.mts +5 -0
- package/dist/react/link/index.mjs +5 -0
- package/dist/react/portal/SessionTouch.d.mts +19 -0
- package/dist/react/portal/SessionTouch.mjs +40 -0
- package/dist/react/portal/SessionTouch.mjs.map +1 -0
- package/dist/react/portal/age-prompt.d.mts +10 -0
- package/dist/react/portal/age-prompt.mjs +21 -0
- package/dist/react/portal/age-prompt.mjs.map +1 -0
- package/dist/react/portal/book-again.d.mts +18 -0
- package/dist/react/portal/book-again.mjs +21 -0
- package/dist/react/portal/book-again.mjs.map +1 -0
- package/dist/react/portal/child-summary.d.mts +17 -0
- package/dist/react/portal/child-summary.mjs +45 -0
- package/dist/react/portal/child-summary.mjs.map +1 -0
- package/dist/react/portal/dashboard.d.mts +74 -0
- package/dist/react/portal/dashboard.mjs +204 -0
- package/dist/react/portal/dashboard.mjs.map +1 -0
- package/dist/react/portal/leave.d.mts +17 -0
- package/dist/react/portal/leave.mjs +21 -0
- package/dist/react/portal/leave.mjs.map +1 -0
- package/dist/react/portal/sections.d.mts +12 -0
- package/dist/react/portal/sections.mjs +48 -0
- package/dist/react/portal/sections.mjs.map +1 -0
- package/dist/react/portal/tabs.d.mts +23 -0
- package/dist/react/portal/tabs.mjs +36 -0
- package/dist/react/portal/tabs.mjs.map +1 -0
- package/dist/react/portal/vipps-flash.d.mts +11 -0
- package/dist/react/portal/vipps-flash.mjs +18 -0
- package/dist/react/portal/vipps-flash.mjs.map +1 -0
- package/dist/react/portal/wired.mjs +202 -0
- package/dist/react/portal/wired.mjs.map +1 -0
- package/dist/react/screen-labels.mjs +14 -0
- package/dist/react/screen-labels.mjs.map +1 -0
- package/dist/react/shared.d.mts +13 -0
- package/dist/react/shared.mjs +7 -0
- package/dist/react/use-next-free.d.mts +19 -0
- package/dist/react/use-next-free.mjs +56 -0
- package/dist/react/use-next-free.mjs.map +1 -0
- package/dist/react/useBooking.d.mts +179 -0
- package/dist/react/useBooking.mjs +1331 -0
- package/dist/react/useBooking.mjs.map +1 -0
- package/dist/react/wizard/adapters.mjs +189 -0
- package/dist/react/wizard/adapters.mjs.map +1 -0
- package/dist/react/wizard/index.d.mts +11 -0
- package/dist/react/wizard/index.mjs +6 -0
- package/dist/react/wizard-error.mjs +29 -0
- package/dist/react/wizard-error.mjs.map +1 -0
- package/package.json +126 -5
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { WizardService } from "./machine.mjs";
|
|
2
|
+
//#region src/core/types.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The wizard-shaped contract: what this site's own route handlers emit and what
|
|
5
|
+
* the step components take as props. Deliberately not Medal's wire types —
|
|
6
|
+
* camelCase, no nulls where a null would only mean "the serialiser had nothing"
|
|
7
|
+
* (`medal-client.ts` explains why every field arrives nullable), and epoch
|
|
8
|
+
* milliseconds rather than ISO strings for anything the machine has to format.
|
|
9
|
+
*
|
|
10
|
+
* Shared from here rather than declared beside each route so a field added to
|
|
11
|
+
* the payload cannot be added to only one of the two ends that read it.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* One service as step 1 wants it: `WizardService` plus the flag that decides
|
|
15
|
+
* whether the card is a button or a phone number.
|
|
16
|
+
*
|
|
17
|
+
* `bookableOnline` is not on `WizardService` because the wizard never carries a
|
|
18
|
+
* service it cannot book. It has to travel anyway — J1 step 1 has «Hull i
|
|
19
|
+
* ørene» rendering as «Ring oss for denne» rather than disappearing, so the
|
|
20
|
+
* price list stays complete — and the card, not the machine, is what needs it.
|
|
21
|
+
*/
|
|
22
|
+
export interface BookingServiceDto extends WizardService {
|
|
23
|
+
bookableOnline: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One stylist as step 2 wants it.
|
|
27
|
+
*
|
|
28
|
+
* `serviceIds` is what makes the step honest: offering a stylist who cannot do
|
|
29
|
+
* the chosen service is a server refusal three steps later, dressed up as a
|
|
30
|
+
* choice. `photoUrl` and `bio` stay nullable because an unphotographed stylist
|
|
31
|
+
* is the ordinary case, and the card falls back to initials.
|
|
32
|
+
*/
|
|
33
|
+
export interface BookingResourceDto {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
photoUrl: string | null;
|
|
37
|
+
bio: string | null;
|
|
38
|
+
serviceIds: string[];
|
|
39
|
+
sortOrder: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One bookable slot, as step 3 wants it.
|
|
43
|
+
*
|
|
44
|
+
* `startTs` is epoch milliseconds, which is a translation and not a
|
|
45
|
+
* relabelling: `formatBookingSlot` puts every timestamp through
|
|
46
|
+
* `toISOString()`, but `pickSlot` stores `startTs` as a number and the sticky
|
|
47
|
+
* summary bar formats it with `Intl.DateTimeFormat.format`. Relaying the string
|
|
48
|
+
* would seat one in `state.startTs`, and the bar would throw on the visitor's
|
|
49
|
+
* next keystroke.
|
|
50
|
+
*
|
|
51
|
+
* No `endTs`: the wizard has nowhere to put one. A slot's end is its start plus
|
|
52
|
+
* the service's `durationMinutes`, which step 3 already has.
|
|
53
|
+
*
|
|
54
|
+
* `resourceId` stays nullable, unlike everything the services route emits. Here
|
|
55
|
+
* `null` is an answer rather than a gap — it is «Første ledige» not yet
|
|
56
|
+
* resolved to a stylist — and `pickSlot` takes it as one.
|
|
57
|
+
*/
|
|
58
|
+
export interface BookingSlotDto {
|
|
59
|
+
startTs: number;
|
|
60
|
+
resourceId: string | null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* One date the salon keeps opening hours on, as step 3 wants it.
|
|
64
|
+
*
|
|
65
|
+
* The list is what lets the step tell «stengt» from «fullt». Availability
|
|
66
|
+
* answers what can still be booked and its empty array says three things at
|
|
67
|
+
* once — closed today, past closing today, every chair taken today — and a step
|
|
68
|
+
* with one card for all three announced «Fullt i dag» at 18:45 on a Saturday
|
|
69
|
+
* the salon shut at 17:00. A date MISSING from this list is one the salon keeps
|
|
70
|
+
* no hours on; that absence is the answer, which is why there is no `closed`
|
|
71
|
+
* field to read.
|
|
72
|
+
*
|
|
73
|
+
* `dayKey` is `salonDayKey`'s `2026-08-29`, so the step can look a day up
|
|
74
|
+
* without re-deriving the salon's calendar date from an instant.
|
|
75
|
+
*
|
|
76
|
+
* `lastStartTs` is the last start THIS visit could occupy on the date, not the
|
|
77
|
+
* closing time — the gap between them is a whole service's duration, and it is
|
|
78
|
+
* the difference between «fullt» and «for sent i dag». Null when the salon
|
|
79
|
+
* posts hours for the date but is shut on it outright, which is how a public
|
|
80
|
+
* holiday arrives.
|
|
81
|
+
*/
|
|
82
|
+
export interface BookingDayDto {
|
|
83
|
+
dayKey: string;
|
|
84
|
+
opensTs: number;
|
|
85
|
+
closesTs: number;
|
|
86
|
+
lastStartTs: number | null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* One booking as the manage page wants it — the screen reached from the link in
|
|
90
|
+
* the confirmation e-mail.
|
|
91
|
+
*
|
|
92
|
+
* Deliberately carries no token. The token is the credential that produced this
|
|
93
|
+
* object and it stays on the server that fetched it; the page puts it back into
|
|
94
|
+
* its own URLs and nowhere else, and nothing here would let it leak into a
|
|
95
|
+
* client-side prop, a log line or an analytics payload.
|
|
96
|
+
*
|
|
97
|
+
* `canCancel` / `canReschedule` are **the engine's answers, relayed** — never
|
|
98
|
+
* this site's opinion. The engine enforces both windows on the write, and it
|
|
99
|
+
* computes these two with the identical comparison at read time, so relaying
|
|
100
|
+
* them is what stops the page offering a button that is certain to 409. There
|
|
101
|
+
* is no deadline field on the wire, only the window in hours, so the *sentence*
|
|
102
|
+
* about when free changes end is derived (`startTs - cancelWindowHours`); the
|
|
103
|
+
* *buttons* are not.
|
|
104
|
+
*/
|
|
105
|
+
export interface BookingManageDto {
|
|
106
|
+
bookingId: string;
|
|
107
|
+
status: 'pending' | 'confirmed' | 'completed' | 'cancelled' | 'no_show';
|
|
108
|
+
/** The booking this one was moved from, when it is itself a move — the id the
|
|
109
|
+
* customer's calendar entry was created under. */
|
|
110
|
+
rescheduledFromId: string | null;
|
|
111
|
+
startTs: number;
|
|
112
|
+
endTs: number;
|
|
113
|
+
serviceId: string | null;
|
|
114
|
+
/** «Unknown service» when the catalogue row was deleted; never blank. */
|
|
115
|
+
serviceName: string;
|
|
116
|
+
resourceId: string | null;
|
|
117
|
+
resourceName: string;
|
|
118
|
+
/** The child, when the parent named one. */
|
|
119
|
+
bookedForName: string | null;
|
|
120
|
+
/** Non-null when this booking is one child of a family visit. */
|
|
121
|
+
partySequenceId: string | null;
|
|
122
|
+
/** What the salon charged for THIS booking, surcharge included. */
|
|
123
|
+
amountOre: number;
|
|
124
|
+
cancelWindowHours: number;
|
|
125
|
+
rescheduleWindowHours: number;
|
|
126
|
+
canCancel: boolean;
|
|
127
|
+
canReschedule: boolean;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One line of a submission: a service, an instant, and whichever of the two
|
|
131
|
+
* optional answers about the child the parent actually gave.
|
|
132
|
+
*
|
|
133
|
+
* Every optional field here is optional in the strict sense — **absent**, never
|
|
134
|
+
* blank. The engine takes `booked_for_name` as `z.string().trim().min(1)
|
|
135
|
+
* .optional()`, where `''` is a 400 rather than a shrug, and
|
|
136
|
+
* `booked_for_birth_year` as `z.number().optional()`, which refuses the `null`
|
|
137
|
+
* an emptied input serialises to. The create route coalesces as a second line
|
|
138
|
+
* of defence; the step still has to send nothing rather than send emptiness,
|
|
139
|
+
* because the moment anything else consumes this shape the coalescing is gone.
|
|
140
|
+
*/
|
|
141
|
+
export interface BookingSubmissionItem {
|
|
142
|
+
serviceId: string;
|
|
143
|
+
/** The rest of this person's visit after `serviceId`, in order — booked as
|
|
144
|
+
* ONE appointment with one stylist. Absent for a one-service visit, never
|
|
145
|
+
* `[]`. */
|
|
146
|
+
extraServiceIds?: string[];
|
|
147
|
+
/** The stylist the slot RESOLVED to, not the visitor's «Første ledige»
|
|
148
|
+
* preference — absent when the engine is still to choose. */
|
|
149
|
+
resourceId?: string;
|
|
150
|
+
startTs: number;
|
|
151
|
+
bookedForName?: string;
|
|
152
|
+
bookedForBirthYear?: number;
|
|
153
|
+
/**
|
|
154
|
+
* The saved child this line is for (SP10). Sent only when the parent is
|
|
155
|
+
* logged in AND the phone being submitted is theirs — Medal resolves the
|
|
156
|
+
* contact by phone, and an id under somebody else's number fails the whole
|
|
157
|
+
* booking. The create route checks the same rule again.
|
|
158
|
+
*/
|
|
159
|
+
bookedForPersonId?: string;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* What step 4 hands its caller, and what `POST /api/booking/create` takes.
|
|
163
|
+
*
|
|
164
|
+
* `consentMarketing` travels even though the engine's create body has nowhere
|
|
165
|
+
* to put it: it is written separately to the GDPR consent endpoint once the
|
|
166
|
+
* booking has succeeded. Leaving it off this shape would mean a box that is
|
|
167
|
+
* ticked and then forgotten, which is worse than not offering it.
|
|
168
|
+
*/
|
|
169
|
+
export interface BookingSubmission {
|
|
170
|
+
items: BookingSubmissionItem[];
|
|
171
|
+
contact: {
|
|
172
|
+
phone: string;
|
|
173
|
+
name?: string;
|
|
174
|
+
email?: string;
|
|
175
|
+
};
|
|
176
|
+
notes?: string;
|
|
177
|
+
consentTerms: boolean;
|
|
178
|
+
consentMarketing: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Identifies the submission *attempt*, not the booking — one value minted
|
|
181
|
+
* when step 4 comes up and resent unchanged on every retry of it.
|
|
182
|
+
*
|
|
183
|
+
* It is what turns a double-tapped «Bekreft» into one booking: the create
|
|
184
|
+
* route hashes it together with this body to derive the `Idempotency-Key`, so
|
|
185
|
+
* an identical retry derives an identical key. Absent, the route falls back to
|
|
186
|
+
* a per-request key and the retry books twice — which is why it is on this
|
|
187
|
+
* shape rather than added by whoever posts it.
|
|
188
|
+
*/
|
|
189
|
+
submissionNonce?: string;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The logged-in parent, as the wizard's «Bekreft» step wants them.
|
|
193
|
+
*
|
|
194
|
+
* A narrowing of `PortalProfileDto`, and the narrowing is the point. The
|
|
195
|
+
* booking page reads the portal profile on the server when the visitor happens
|
|
196
|
+
* to hold a Min side cookie, and hands the browser only what a booking form
|
|
197
|
+
* has a use for: the name and number that prefill step 4, the address the
|
|
198
|
+
* confirmation is sent to, and the children whose names step 4 asks for.
|
|
199
|
+
* `contactId`, the marketing flag and the bransjemal labels stay on the server
|
|
200
|
+
* — a prop that travels is a prop in the HTML, and none of those three is
|
|
201
|
+
* anything the wizard renders.
|
|
202
|
+
*
|
|
203
|
+
* Nulls stay nulls, as in the portal DTO: `''` would make «Vipps gave us no
|
|
204
|
+
* surname» indistinguishable from «the parent cleared the field».
|
|
205
|
+
*/
|
|
206
|
+
export interface BookingGuardian {
|
|
207
|
+
firstName: string | null;
|
|
208
|
+
lastName: string | null;
|
|
209
|
+
email: string;
|
|
210
|
+
phone: string | null;
|
|
211
|
+
/** The children on the profile, for step 1's «Hvem skal klippes?». */
|
|
212
|
+
family: BookingFamilyMember[];
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* One child on the logged-in parent's profile, as the wizard sees them.
|
|
216
|
+
*
|
|
217
|
+
* `personId` is the id Medal keeps for the child (SP10/SP11), absent when the
|
|
218
|
+
* profile could not name one — see `PortalFamilyMemberDto`. It is what a line
|
|
219
|
+
* item is booked for (`booked_for_person_id`) and what «Samme som sist» joins
|
|
220
|
+
* a past visit on. Optional fields are ABSENT rather than null, the rule every
|
|
221
|
+
* wizard-shaped value follows.
|
|
222
|
+
*/
|
|
223
|
+
export interface BookingFamilyMember {
|
|
224
|
+
name: string;
|
|
225
|
+
birthYear: number;
|
|
226
|
+
personId?: string;
|
|
227
|
+
/** 1–12, when the parent gave one — makes the age exact across the birthday. */
|
|
228
|
+
birthMonth?: number;
|
|
229
|
+
/** «Fast frisør»: the stylist this child usually sees. A preference only. */
|
|
230
|
+
preferredResourceId?: string;
|
|
231
|
+
/**
|
|
232
|
+
* The child's last COMPLETED visit, for the card's «Sist: Gutteklipp, 12.
|
|
233
|
+
* aug» and step 2's «Samme som sist». Absent for a child with none, or one
|
|
234
|
+
* the site cannot tell apart from a sibling.
|
|
235
|
+
*/
|
|
236
|
+
lastVisit?: BookingLastVisit;
|
|
237
|
+
}
|
|
238
|
+
export interface BookingLastVisit {
|
|
239
|
+
serviceId: string;
|
|
240
|
+
serviceName: string | null;
|
|
241
|
+
resourceId: string | null;
|
|
242
|
+
startTs: number;
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//# sourceMappingURL=types.d.mts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { WizardItem, WizardService } from "./machine.mjs";
|
|
2
|
+
//#region src/core/visit.d.ts
|
|
3
|
+
/** The timing of a whole visit. */
|
|
4
|
+
export interface VisitTiming {
|
|
5
|
+
durationMinutes: number;
|
|
6
|
+
bufferBeforeMinutes: number;
|
|
7
|
+
bufferAfterMinutes: number;
|
|
8
|
+
}
|
|
9
|
+
/** A person's services in order: the first, then the extras. */
|
|
10
|
+
export declare function visitServicesOf(item: Pick<WizardItem, 'service' | 'extraServices'>): WizardService[];
|
|
11
|
+
/** The timing of an ordered, non-empty list of services. Throws on an empty list. */
|
|
12
|
+
export declare function visitOfServices(services: readonly WizardService[]): VisitTiming;
|
|
13
|
+
/** The timing of a person's visit, from a line item or a list of services. */
|
|
14
|
+
export declare function visitOf(itemOrServices: Pick<WizardItem, 'service' | 'extraServices'> | readonly WizardService[]): VisitTiming;
|
|
15
|
+
/**
|
|
16
|
+
* A visit's ids joined with `+`, in order; a single service is its id
|
|
17
|
+
* unchanged. The server keys its slot cache and its `freshSlots` answer with
|
|
18
|
+
* this too, so the wizard and the server can never disagree on a visit's name.
|
|
19
|
+
*/
|
|
20
|
+
export declare function visitKeyOfIds(serviceIds: readonly string[]): string;
|
|
21
|
+
/** The service ids joined with `+`, in order; a single service is its id unchanged. */
|
|
22
|
+
export declare function visitKey(itemOrServices: Pick<WizardItem, 'service' | 'extraServices'> | readonly WizardService[]): string;
|
|
23
|
+
/** The visit's price: each service priced on its own, then summed. */
|
|
24
|
+
export declare function visitPriceOre(services: readonly WizardService[], priceOf: (service: WizardService) => number): number;
|
|
25
|
+
//#endregion
|
|
26
|
+
//# sourceMappingURL=visit.d.mts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/core/visit.ts
|
|
2
|
+
/** A person's services in order: the first, then the extras. */
|
|
3
|
+
function visitServicesOf(item) {
|
|
4
|
+
return [item.service, ...item.extraServices ?? []];
|
|
5
|
+
}
|
|
6
|
+
/** The timing of an ordered, non-empty list of services. Throws on an empty list. */
|
|
7
|
+
function visitOfServices(services) {
|
|
8
|
+
const first = services[0];
|
|
9
|
+
const last = services[services.length - 1];
|
|
10
|
+
if (!first || !last) throw new Error("a visit needs at least one service");
|
|
11
|
+
return {
|
|
12
|
+
durationMinutes: services.reduce((sum, s) => sum + s.durationMinutes, 0),
|
|
13
|
+
bufferBeforeMinutes: first.bufferBeforeMinutes,
|
|
14
|
+
bufferAfterMinutes: last.bufferAfterMinutes
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** The timing of a person's visit, from a line item or a list of services. */
|
|
18
|
+
function visitOf(itemOrServices) {
|
|
19
|
+
return visitOfServices(Array.isArray(itemOrServices) ? itemOrServices : visitServicesOf(itemOrServices));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A visit's ids joined with `+`, in order; a single service is its id
|
|
23
|
+
* unchanged. The server keys its slot cache and its `freshSlots` answer with
|
|
24
|
+
* this too, so the wizard and the server can never disagree on a visit's name.
|
|
25
|
+
*/
|
|
26
|
+
function visitKeyOfIds(serviceIds) {
|
|
27
|
+
return serviceIds.join("+");
|
|
28
|
+
}
|
|
29
|
+
/** The service ids joined with `+`, in order; a single service is its id unchanged. */
|
|
30
|
+
function visitKey(itemOrServices) {
|
|
31
|
+
return visitKeyOfIds((Array.isArray(itemOrServices) ? itemOrServices : visitServicesOf(itemOrServices)).map((s) => s.id));
|
|
32
|
+
}
|
|
33
|
+
/** The visit's price: each service priced on its own, then summed. */
|
|
34
|
+
function visitPriceOre(services, priceOf) {
|
|
35
|
+
return services.reduce((sum, s) => sum + priceOf(s), 0);
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { visitKey, visitKeyOfIds, visitOf, visitOfServices, visitPriceOre, visitServicesOf };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=visit.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visit.mjs","names":[],"sources":["../../src/core/visit.ts"],"sourcesContent":["/**\n * One person, several services, ONE visit — performed back to back by one\n * stylist. The engine's model: the duration is the SUM of the services', the\n * buffer before is the FIRST service's, the buffer after is the LAST's, there\n * is no buffer between a person's own services, and each service is priced on\n * its own. Everything that times or prices a line goes through these helpers so\n * a one-service line and a three-service line share one path.\n */\n\nimport type { WizardItem, WizardService } from './machine';\n\n/** The timing of a whole visit. */\nexport interface VisitTiming {\n durationMinutes: number;\n bufferBeforeMinutes: number;\n bufferAfterMinutes: number;\n}\n\n/** A person's services in order: the first, then the extras. */\nexport function visitServicesOf(\n item: Pick<WizardItem, 'service' | 'extraServices'>\n): WizardService[] {\n return [item.service, ...(item.extraServices ?? [])];\n}\n\n/** The timing of an ordered, non-empty list of services. Throws on an empty list. */\nexport function visitOfServices(services: readonly WizardService[]): VisitTiming {\n const first = services[0];\n const last = services[services.length - 1];\n if (!first || !last) throw new Error('a visit needs at least one service');\n return {\n durationMinutes: services.reduce((sum, s) => sum + s.durationMinutes, 0),\n bufferBeforeMinutes: first.bufferBeforeMinutes,\n bufferAfterMinutes: last.bufferAfterMinutes,\n };\n}\n\n/** The timing of a person's visit, from a line item or a list of services. */\nexport function visitOf(\n itemOrServices: Pick<WizardItem, 'service' | 'extraServices'> | readonly WizardService[]\n): VisitTiming {\n return visitOfServices(\n Array.isArray(itemOrServices)\n ? itemOrServices\n : visitServicesOf(itemOrServices as Pick<WizardItem, 'service' | 'extraServices'>)\n );\n}\n\n/**\n * A visit's ids joined with `+`, in order; a single service is its id\n * unchanged. The server keys its slot cache and its `freshSlots` answer with\n * this too, so the wizard and the server can never disagree on a visit's name.\n */\nexport function visitKeyOfIds(serviceIds: readonly string[]): string {\n return serviceIds.join('+');\n}\n\n/** The service ids joined with `+`, in order; a single service is its id unchanged. */\nexport function visitKey(\n itemOrServices: Pick<WizardItem, 'service' | 'extraServices'> | readonly WizardService[]\n): string {\n const services = Array.isArray(itemOrServices)\n ? (itemOrServices as readonly WizardService[])\n : visitServicesOf(itemOrServices as Pick<WizardItem, 'service' | 'extraServices'>);\n return visitKeyOfIds(services.map((s) => s.id));\n}\n\n/** The visit's price: each service priced on its own, then summed. */\nexport function visitPriceOre(\n services: readonly WizardService[],\n priceOf: (service: WizardService) => number\n): number {\n return services.reduce((sum, s) => sum + priceOf(s), 0);\n}\n"],"mappings":";;AAmBA,SAAgB,gBACd,MACiB;CACjB,OAAO,CAAC,KAAK,SAAS,GAAI,KAAK,iBAAiB,CAAC,CAAE;AACrD;;AAGA,SAAgB,gBAAgB,UAAiD;CAC/E,MAAM,QAAQ,SAAS;CACvB,MAAM,OAAO,SAAS,SAAS,SAAS;CACxC,IAAI,CAAC,SAAS,CAAC,MAAM,MAAM,IAAI,MAAM,oCAAoC;CACzE,OAAO;EACL,iBAAiB,SAAS,QAAQ,KAAK,MAAM,MAAM,EAAE,iBAAiB,CAAC;EACvE,qBAAqB,MAAM;EAC3B,oBAAoB,KAAK;CAC3B;AACF;;AAGA,SAAgB,QACd,gBACa;CACb,OAAO,gBACL,MAAM,QAAQ,cAAc,IACxB,iBACA,gBAAgB,cAA+D,CACrF;AACF;;;;;;AAOA,SAAgB,cAAc,YAAuC;CACnE,OAAO,WAAW,KAAK,GAAG;AAC5B;;AAGA,SAAgB,SACd,gBACQ;CAIR,OAAO,eAHU,MAAM,QAAQ,cAAc,IACxC,iBACD,gBAAgB,cAA+D,EAAA,CACrD,KAAK,MAAM,EAAE,EAAE,CAAC;AAChD;;AAGA,SAAgB,cACd,UACA,SACQ;CACR,OAAO,SAAS,QAAQ,KAAK,MAAM,MAAM,QAAQ,CAAC,GAAG,CAAC;AACxD"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BookingResource, BookingScheduleDay, BookingService, BookingSlot, ManageSummary } from "@medalsocial/sdk";
|
|
2
|
+
//#region src/core/wire.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Wire types are the SDK's, `Pick`ed down to exactly the fields this site
|
|
5
|
+
* reads: the SDK growing a column (it has several this site never looks at)
|
|
6
|
+
* then neither widens what the routes pass to the browser nor breaks a
|
|
7
|
+
* fixture, while a change to the TYPE of a field the site does read still
|
|
8
|
+
* fails the build.
|
|
9
|
+
*
|
|
10
|
+
* Nullable, not optional: the serialiser emits every field as `value ?? null`,
|
|
11
|
+
* so each key is always present and may be `null` — a different thing from
|
|
12
|
+
* being absent. Coalescing the nulls away belongs in the route handler.
|
|
13
|
+
*/
|
|
14
|
+
export type MedalService = Pick<BookingService, 'id' | 'name' | 'description' | 'category' | 'duration_minutes' | 'buffer_before_minutes' | 'buffer_after_minutes' | 'price_ore' | 'bookable_online' | 'max_per_booking' | 'weekend_surcharge_pct'> &
|
|
15
|
+
/**
|
|
16
|
+
* SP10's service age range, inclusive whole years — `null` where the salon
|
|
17
|
+
* set none. Optional here although the SDK's `BookingService` always has
|
|
18
|
+
* them: a Medal that predates SP10 sends neither, and `toServiceDto` reads
|
|
19
|
+
* them defensively.
|
|
20
|
+
*/
|
|
21
|
+
Partial<Pick<BookingService, 'age_min_years' | 'age_max_years'>>;
|
|
22
|
+
export type MedalResource = Pick<BookingResource, 'id' | 'name' | 'photo_url' | 'bio' | 'service_ids' | 'sort_order'>;
|
|
23
|
+
export type MedalSlot = BookingSlot;
|
|
24
|
+
export type MedalScheduleDay = BookingScheduleDay;
|
|
25
|
+
/**
|
|
26
|
+
* Three fields are narrower than the SDK's, on the engine's own word: it falls
|
|
27
|
+
* back to «Unknown service» / «Unknown resource» rather than `null` when the
|
|
28
|
+
* catalogue row is gone, and a booking always has a status. `getManage`
|
|
29
|
+
* makes the narrowing true by construction instead of asserting it.
|
|
30
|
+
*/
|
|
31
|
+
export type MedalManageSummary = Omit<ManageSummary, 'status' | 'service_name' | 'resource_name'> & {
|
|
32
|
+
status: NonNullable<ManageSummary['status']>;
|
|
33
|
+
service_name: string;
|
|
34
|
+
resource_name: string;
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//# sourceMappingURL=wire.d.mts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BookingCacheAdapter } from "../options.mjs";
|
|
2
|
+
import "server-only";
|
|
3
|
+
//#region src/next/cache/memory.d.ts
|
|
4
|
+
export interface MemoryCacheAdapterOptions {
|
|
5
|
+
/** Most entries held at once. Default 500. */
|
|
6
|
+
maxEntries?: number;
|
|
7
|
+
/** A ceiling on every entry's TTL, in seconds. Default: none. */
|
|
8
|
+
maxTtlSeconds?: number;
|
|
9
|
+
/** The clock, for tests. Default `Date.now`. */
|
|
10
|
+
now?: () => number;
|
|
11
|
+
}
|
|
12
|
+
export declare function memoryCacheAdapter(options?: MemoryCacheAdapterOptions): BookingCacheAdapter;
|
|
13
|
+
//#endregion
|
|
14
|
+
//# sourceMappingURL=memory.d.mts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
//#region src/next/cache/memory.ts
|
|
3
|
+
function memoryCacheAdapter(options = {}) {
|
|
4
|
+
const maxEntries = Math.max(1, options.maxEntries ?? 500);
|
|
5
|
+
const now = options.now ?? Date.now;
|
|
6
|
+
const entries = /* @__PURE__ */ new Map();
|
|
7
|
+
function live(key) {
|
|
8
|
+
const entry = entries.get(key);
|
|
9
|
+
if (entry === void 0) return void 0;
|
|
10
|
+
if (entry.expiresAt <= now()) {
|
|
11
|
+
entries.delete(key);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
return entry;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
async get(key) {
|
|
18
|
+
const entry = live(key);
|
|
19
|
+
if (entry === void 0) return void 0;
|
|
20
|
+
entries.delete(key);
|
|
21
|
+
entries.set(key, entry);
|
|
22
|
+
return JSON.parse(entry.json);
|
|
23
|
+
},
|
|
24
|
+
async put(key, value, ttlSeconds, putOptions) {
|
|
25
|
+
const ttl = Math.min(ttlSeconds, options.maxTtlSeconds ?? ttlSeconds);
|
|
26
|
+
if (!(ttl > 0)) return;
|
|
27
|
+
const json = JSON.stringify(value);
|
|
28
|
+
if (json === void 0) return;
|
|
29
|
+
entries.delete(key);
|
|
30
|
+
entries.set(key, {
|
|
31
|
+
json,
|
|
32
|
+
expiresAt: now() + ttl * 1e3,
|
|
33
|
+
tags: putOptions?.tags ?? []
|
|
34
|
+
});
|
|
35
|
+
while (entries.size > maxEntries) {
|
|
36
|
+
const oldest = entries.keys().next().value;
|
|
37
|
+
entries.delete(oldest);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async delete(keys) {
|
|
41
|
+
for (const key of keys) entries.delete(key);
|
|
42
|
+
},
|
|
43
|
+
async expireTag(tag) {
|
|
44
|
+
for (const [key, entry] of entries) if (entry.tags.includes(tag)) entries.delete(key);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { memoryCacheAdapter };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=memory.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.mjs","names":[],"sources":["../../../src/next/cache/memory.ts"],"sourcesContent":["/**\n * `@medalsocial/booking/next/cache/memory` — a per-process LRU cache.\n *\n * For a site on Node (`next start`, Docker, a VM): one map in this process,\n * bounded by `maxEntries` (least recently used goes first) and by each\n * entry's TTL, with tag expiry for the data cache. Nothing is shared between\n * processes, so each instance warms on its own; the TTLs the package writes\n * (30 s to an hour) bound how far two instances can disagree.\n *\n * Values are stored as JSON and parsed on the way out, so a caller can never\n * mutate a cached entry through the object it was handed — the same\n * guarantee a real cache gives.\n */\n\nimport 'server-only';\n\nimport type { BookingCacheAdapter } from '../options';\n\nexport interface MemoryCacheAdapterOptions {\n /** Most entries held at once. Default 500. */\n maxEntries?: number;\n /** A ceiling on every entry's TTL, in seconds. Default: none. */\n maxTtlSeconds?: number;\n /** The clock, for tests. Default `Date.now`. */\n now?: () => number;\n}\n\ninterface Entry {\n json: string;\n expiresAt: number;\n tags: readonly string[];\n}\n\nexport function memoryCacheAdapter(options: MemoryCacheAdapterOptions = {}): BookingCacheAdapter {\n const maxEntries = Math.max(1, options.maxEntries ?? 500);\n const now = options.now ?? Date.now;\n const entries = new Map<string, Entry>();\n\n function live(key: string): Entry | undefined {\n const entry = entries.get(key);\n if (entry === undefined) return undefined;\n if (entry.expiresAt <= now()) {\n entries.delete(key);\n return undefined;\n }\n return entry;\n }\n\n return {\n async get<T>(key: string) {\n const entry = live(key);\n if (entry === undefined) return undefined;\n // Most recently used moves to the end of the insertion order.\n entries.delete(key);\n entries.set(key, entry);\n return JSON.parse(entry.json) as T;\n },\n\n async put(key, value, ttlSeconds, putOptions) {\n const ttl = Math.min(ttlSeconds, options.maxTtlSeconds ?? ttlSeconds);\n if (!(ttl > 0)) return;\n const json = JSON.stringify(value);\n if (json === undefined) return;\n entries.delete(key);\n entries.set(key, { json, expiresAt: now() + ttl * 1000, tags: putOptions?.tags ?? [] });\n while (entries.size > maxEntries) {\n const oldest = entries.keys().next().value as string;\n entries.delete(oldest);\n }\n },\n\n async delete(keys) {\n for (const key of keys) entries.delete(key);\n },\n\n async expireTag(tag) {\n for (const [key, entry] of entries) {\n if (entry.tags.includes(tag)) entries.delete(key);\n }\n },\n };\n}\n"],"mappings":";;AAiCA,SAAgB,mBAAmB,UAAqC,CAAC,GAAwB;CAC/F,MAAM,aAAa,KAAK,IAAI,GAAG,QAAQ,cAAc,GAAG;CACxD,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,0BAAU,IAAI,IAAmB;CAEvC,SAAS,KAAK,KAAgC;EAC5C,MAAM,QAAQ,QAAQ,IAAI,GAAG;EAC7B,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAChC,IAAI,MAAM,aAAa,IAAI,GAAG;GAC5B,QAAQ,OAAO,GAAG;GAClB;EACF;EACA,OAAO;CACT;CAEA,OAAO;EACL,MAAM,IAAO,KAAa;GACxB,MAAM,QAAQ,KAAK,GAAG;GACtB,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;GAEhC,QAAQ,OAAO,GAAG;GAClB,QAAQ,IAAI,KAAK,KAAK;GACtB,OAAO,KAAK,MAAM,MAAM,IAAI;EAC9B;EAEA,MAAM,IAAI,KAAK,OAAO,YAAY,YAAY;GAC5C,MAAM,MAAM,KAAK,IAAI,YAAY,QAAQ,iBAAiB,UAAU;GACpE,IAAI,EAAE,MAAM,IAAI;GAChB,MAAM,OAAO,KAAK,UAAU,KAAK;GACjC,IAAI,SAAS,KAAA,GAAW;GACxB,QAAQ,OAAO,GAAG;GAClB,QAAQ,IAAI,KAAK;IAAE;IAAM,WAAW,IAAI,IAAI,MAAM;IAAM,MAAM,YAAY,QAAQ,CAAC;GAAE,CAAC;GACtF,OAAO,QAAQ,OAAO,YAAY;IAChC,MAAM,SAAS,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;IACrC,QAAQ,OAAO,MAAM;GACvB;EACF;EAEA,MAAM,OAAO,MAAM;GACjB,KAAK,MAAM,OAAO,MAAM,QAAQ,OAAO,GAAG;EAC5C;EAEA,MAAM,UAAU,KAAK;GACnB,KAAK,MAAM,CAAC,KAAK,UAAU,SACzB,IAAI,MAAM,KAAK,SAAS,GAAG,GAAG,QAAQ,OAAO,GAAG;EAEpD;CACF;AACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { after } from "next/server";
|
|
3
|
+
import { revalidateTag, unstable_cache } from "next/cache";
|
|
4
|
+
//#region src/next/cache/next-data.ts
|
|
5
|
+
function nextDataCacheAdapter() {
|
|
6
|
+
return {
|
|
7
|
+
async get() {},
|
|
8
|
+
async put() {},
|
|
9
|
+
async delete() {},
|
|
10
|
+
load(keyParts, args, fill, options) {
|
|
11
|
+
return unstable_cache(async () => fill(), [...keyParts], {
|
|
12
|
+
revalidate: options.ttlSeconds,
|
|
13
|
+
tags: [...options.tags]
|
|
14
|
+
})(...args);
|
|
15
|
+
},
|
|
16
|
+
/** Synchronous under the promise, so a throw reaches the caller's `try`. */
|
|
17
|
+
expireTag(tag) {
|
|
18
|
+
revalidateTag(tag, { expire: 0 });
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
},
|
|
21
|
+
/** `after()`, inside a request; outside one there is nothing to hand to. */
|
|
22
|
+
defer(work) {
|
|
23
|
+
try {
|
|
24
|
+
after(() => work);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { nextDataCacheAdapter };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=next-data.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next-data.mjs","names":[],"sources":["../../../src/next/cache/next-data.ts"],"sourcesContent":["/**\n * `@medalsocial/booking/next/cache/next-data` — Next's data cache.\n *\n * `unstable_cache` for the reads and `revalidateTag` for the expiry: the data\n * cache a Next deployment already has (on OpenNext, R2 plus the Durable Object\n * tag cache; on Vercel, its own). Use it for `cache.data`.\n *\n * READ-THROUGH ONLY. Next's data cache wraps a function; it has no `get` or\n * `put` by key and no per-key delete. So this adapter does everything through\n * `load` (keys are the key parts plus the call's arguments, exactly as\n * `unstable_cache` forms them) and `expireTag`; its `get` is always a miss and\n * `put`/`delete` do nothing. As `cache.edge` it would therefore cache nothing —\n * pair it with the `workers` or `memory` adapter there.\n */\n\nimport 'server-only';\n\nimport { revalidateTag, unstable_cache } from 'next/cache';\nimport { after } from 'next/server';\nimport type { BookingCacheAdapter } from '../options';\n\nexport function nextDataCacheAdapter(): BookingCacheAdapter {\n return {\n async get() {\n return undefined;\n },\n async put() {},\n async delete() {},\n\n load(keyParts, args, fill, options) {\n // One wrapper per call: the tags can name the call's own service, and\n // the callback's text is part of Next's key, so it is the same text for\n // every call.\n const loader = unstable_cache(async () => fill(), [...keyParts], {\n revalidate: options.ttlSeconds,\n tags: [...options.tags],\n });\n return (loader as (...callArgs: unknown[]) => Promise<Awaited<ReturnType<typeof fill>>>)(\n ...args\n );\n },\n\n /** Synchronous under the promise, so a throw reaches the caller's `try`. */\n expireTag(tag) {\n revalidateTag(tag, { expire: 0 });\n return Promise.resolve();\n },\n\n /** `after()`, inside a request; outside one there is nothing to hand to. */\n defer(work) {\n try {\n after(() => work);\n return true;\n } catch {\n return false;\n }\n },\n };\n}\n"],"mappings":";;;;AAqBA,SAAgB,uBAA4C;CAC1D,OAAO;EACL,MAAM,MAAM,CAEZ;EACA,MAAM,MAAM,CAAC;EACb,MAAM,SAAS,CAAC;EAEhB,KAAK,UAAU,MAAM,MAAM,SAAS;GAQlC,OAJe,eAAe,YAAY,KAAK,GAAG,CAAC,GAAG,QAAQ,GAAG;IAC/D,YAAY,QAAQ;IACpB,MAAM,CAAC,GAAG,QAAQ,IAAI;GACxB,CACa,CAAC,CACZ,GAAG,IACL;EACF;;EAGA,UAAU,KAAK;GACb,cAAc,KAAK,EAAE,QAAQ,EAAE,CAAC;GAChC,OAAO,QAAQ,QAAQ;EACzB;;EAGA,MAAM,MAAM;GACV,IAAI;IACF,YAAY,IAAI;IAChB,OAAO;GACT,QAAQ;IACN,OAAO;GACT;EACF;CACF;AACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
//#region src/next/cache/noop.ts
|
|
3
|
+
function noopCacheAdapter() {
|
|
4
|
+
return {
|
|
5
|
+
async get() {},
|
|
6
|
+
async put() {},
|
|
7
|
+
async delete() {},
|
|
8
|
+
async expireTag() {},
|
|
9
|
+
key: () => null
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { noopCacheAdapter };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=noop.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noop.mjs","names":[],"sources":["../../../src/next/cache/noop.ts"],"sourcesContent":["/**\n * `@medalsocial/booking/next/cache/noop` — a cache that never holds anything.\n *\n * Every read is a miss and every write is dropped, and its `key()` answers\n * `null`, so the booking seed takes its «no cache here» path (`bypass`) and\n * reads Medal through the data cache. For tests, draft mode, and a site that\n * wants every request live.\n */\n\nimport 'server-only';\n\nimport type { BookingCacheAdapter } from '../options';\n\nexport function noopCacheAdapter(): BookingCacheAdapter {\n return {\n async get() {\n return undefined;\n },\n async put() {},\n async delete() {},\n async expireTag() {},\n key: () => null,\n };\n}\n"],"mappings":";;AAaA,SAAgB,mBAAwC;CACtD,OAAO;EACL,MAAM,MAAM,CAEZ;EACA,MAAM,MAAM,CAAC;EACb,MAAM,SAAS,CAAC;EAChB,MAAM,YAAY,CAAC;EACnB,WAAW;CACb;AACF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BookingCacheAdapter } from "../options.mjs";
|
|
2
|
+
import "server-only";
|
|
3
|
+
//#region src/next/cache/workers.d.ts
|
|
4
|
+
/** The slice of the Worker context this adapter reads. */
|
|
5
|
+
export interface WorkerContext {
|
|
6
|
+
ctx?: {
|
|
7
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
8
|
+
};
|
|
9
|
+
env?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/** A value, or a function that reads it when it is needed. */
|
|
12
|
+
type Lazy<T> = T | (() => T);
|
|
13
|
+
export interface WorkersCacheAdapterOptions {
|
|
14
|
+
/** The site's own origin (`https://example.com`). Without one nothing is cached. */
|
|
15
|
+
origin: Lazy<string | undefined>;
|
|
16
|
+
/** The deployed version for keys. Default: `env.CF_VERSION_METADATA.id`. */
|
|
17
|
+
version?: Lazy<string | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* The Worker context for this request. Default: the one OpenNext publishes
|
|
20
|
+
* on `globalThis` (what its `getCloudflareContext()` returns). Pass
|
|
21
|
+
* `() => getCloudflareContext()` to be explicit.
|
|
22
|
+
*/
|
|
23
|
+
context?: () => WorkerContext | undefined;
|
|
24
|
+
/** The cache. Default `caches.default`. */
|
|
25
|
+
cache?: () => Cache | undefined;
|
|
26
|
+
}
|
|
27
|
+
export declare function workersCacheAdapter(options: WorkersCacheAdapterOptions): BookingCacheAdapter;
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=workers.d.mts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
//#region src/next/cache/workers.ts
|
|
3
|
+
/** Where `@opennextjs/cloudflare` keeps the request's `{ env, ctx, cf }`. */
|
|
4
|
+
const OPENNEXT_CONTEXT = Symbol.for("__cloudflare-context__");
|
|
5
|
+
function defaultContext() {
|
|
6
|
+
return globalThis[OPENNEXT_CONTEXT];
|
|
7
|
+
}
|
|
8
|
+
function defaultCache() {
|
|
9
|
+
return globalThis.caches?.default;
|
|
10
|
+
}
|
|
11
|
+
function read(value) {
|
|
12
|
+
return typeof value === "function" ? value() : value;
|
|
13
|
+
}
|
|
14
|
+
function workersCacheAdapter(options) {
|
|
15
|
+
const cacheOf = options.cache ?? defaultCache;
|
|
16
|
+
function context() {
|
|
17
|
+
try {
|
|
18
|
+
return (options.context ?? defaultContext)();
|
|
19
|
+
} catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function originOf() {
|
|
24
|
+
const origin = read(options.origin);
|
|
25
|
+
if (!origin) return null;
|
|
26
|
+
try {
|
|
27
|
+
return new URL(origin).origin;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** The cache, or a refusal: every operation goes through here. */
|
|
33
|
+
function cache() {
|
|
34
|
+
const found = cacheOf();
|
|
35
|
+
if (!found) throw new Error("The Workers Cache API is not available here");
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
key(key) {
|
|
40
|
+
if (!cacheOf()) return null;
|
|
41
|
+
const origin = originOf();
|
|
42
|
+
return origin === null ? null : `${origin}${key}`;
|
|
43
|
+
},
|
|
44
|
+
version() {
|
|
45
|
+
if (options.version !== void 0) return read(options.version) || void 0;
|
|
46
|
+
const metadata = context()?.env?.CF_VERSION_METADATA;
|
|
47
|
+
return typeof metadata?.id === "string" && metadata.id !== "" ? metadata.id : void 0;
|
|
48
|
+
},
|
|
49
|
+
async get(key) {
|
|
50
|
+
const hit = await cache().match(key);
|
|
51
|
+
return hit ? await hit.json() : void 0;
|
|
52
|
+
},
|
|
53
|
+
async put(key, value, ttlSeconds) {
|
|
54
|
+
await cache().put(key, new Response(JSON.stringify(value), { headers: {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
"Cache-Control": `public, max-age=${ttlSeconds}`
|
|
57
|
+
} }));
|
|
58
|
+
},
|
|
59
|
+
async delete(keys) {
|
|
60
|
+
const found = cache();
|
|
61
|
+
await Promise.all(keys.map((key) => found.delete(key)));
|
|
62
|
+
},
|
|
63
|
+
defer(work) {
|
|
64
|
+
const ctx = context()?.ctx;
|
|
65
|
+
if (typeof ctx?.waitUntil !== "function") return false;
|
|
66
|
+
ctx.waitUntil(work);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { workersCacheAdapter };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=workers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workers.mjs","names":[],"sources":["../../../src/next/cache/workers.ts"],"sourcesContent":["/**\n * `@medalsocial/booking/next/cache/workers` — the Cloudflare Workers Cache API.\n *\n * `caches.default`: per colo, a few milliseconds to read, no global purge —\n * changing the key is the purge. Use it for `cache.edge` on Workers (OpenNext).\n *\n * KEYS are `<origin><package key>`, so with the package's edge keys:\n * `<origin>/__medal-edge/booking-seed/v1/<env>[-<version>]/…`. On the site's\n * own origin because Cache API entries belong to the zone, and with the\n * deployed version in the path (from the `CF_VERSION_METADATA` binding unless\n * `version` is passed) so a deploy starts cold instead of reading entries an\n * older build wrote.\n *\n * `defer` hands work to the Worker's `ctx.waitUntil`, which keeps the isolate\n * alive for it after the response has gone; a floating promise on Workers is\n * cancelled when the response goes out.\n *\n * Off Workers (no `caches.default`, `next dev`, `next start`) and with no\n * origin configured, `key()` answers `null` and the package reads live, the\n * way it would with the `noop` adapter.\n */\n\nimport 'server-only';\n\nimport type { BookingCacheAdapter } from '../options';\n\n/** The slice of the Worker context this adapter reads. */\nexport interface WorkerContext {\n ctx?: { waitUntil?: (promise: Promise<unknown>) => void };\n env?: Record<string, unknown>;\n}\n\n/** A value, or a function that reads it when it is needed. */\ntype Lazy<T> = T | (() => T);\n\nexport interface WorkersCacheAdapterOptions {\n /** The site's own origin (`https://example.com`). Without one nothing is cached. */\n origin: Lazy<string | undefined>;\n /** The deployed version for keys. Default: `env.CF_VERSION_METADATA.id`. */\n version?: Lazy<string | undefined>;\n /**\n * The Worker context for this request. Default: the one OpenNext publishes\n * on `globalThis` (what its `getCloudflareContext()` returns). Pass\n * `() => getCloudflareContext()` to be explicit.\n */\n context?: () => WorkerContext | undefined;\n /** The cache. Default `caches.default`. */\n cache?: () => Cache | undefined;\n}\n\n/** Where `@opennextjs/cloudflare` keeps the request's `{ env, ctx, cf }`. */\nconst OPENNEXT_CONTEXT = Symbol.for('__cloudflare-context__');\n\nfunction defaultContext(): WorkerContext | undefined {\n return (globalThis as Record<symbol, WorkerContext | undefined>)[OPENNEXT_CONTEXT];\n}\n\nfunction defaultCache(): Cache | undefined {\n const storage = (globalThis as { caches?: CacheStorage & { default?: Cache } }).caches;\n return storage?.default;\n}\n\nfunction read<T>(value: Lazy<T>): T {\n return typeof value === 'function' ? (value as () => T)() : value;\n}\n\nexport function workersCacheAdapter(options: WorkersCacheAdapterOptions): BookingCacheAdapter {\n const cacheOf = options.cache ?? defaultCache;\n\n function context(): WorkerContext | undefined {\n try {\n return (options.context ?? defaultContext)();\n } catch {\n // Not on Workers, or outside a request.\n return undefined;\n }\n }\n\n function originOf(): string | null {\n const origin = read(options.origin);\n if (!origin) return null;\n try {\n return new URL(origin).origin;\n } catch {\n return null;\n }\n }\n\n /** The cache, or a refusal: every operation goes through here. */\n function cache(): Cache {\n const found = cacheOf();\n if (!found) throw new Error('The Workers Cache API is not available here');\n return found;\n }\n\n return {\n key(key) {\n if (!cacheOf()) return null;\n const origin = originOf();\n return origin === null ? null : `${origin}${key}`;\n },\n\n version() {\n if (options.version !== undefined) return read(options.version) || undefined;\n const metadata = context()?.env?.CF_VERSION_METADATA as { id?: unknown } | undefined;\n return typeof metadata?.id === 'string' && metadata.id !== '' ? metadata.id : undefined;\n },\n\n async get<T>(key: string) {\n const hit = await cache().match(key);\n return hit ? ((await hit.json()) as T) : undefined;\n },\n\n async put(key, value, ttlSeconds) {\n await cache().put(\n key,\n new Response(JSON.stringify(value), {\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': `public, max-age=${ttlSeconds}`,\n },\n })\n );\n },\n\n async delete(keys) {\n const found = cache();\n await Promise.all(keys.map((key) => found.delete(key)));\n },\n\n defer(work) {\n const ctx = context()?.ctx;\n if (typeof ctx?.waitUntil !== 'function') return false;\n ctx.waitUntil(work);\n return true;\n },\n };\n}\n"],"mappings":";;;AAmDA,MAAM,mBAAmB,OAAO,IAAI,wBAAwB;AAE5D,SAAS,iBAA4C;CACnD,OAAQ,WAAyD;AACnE;AAEA,SAAS,eAAkC;CAEzC,OADiB,WAA+D,QAChE;AAClB;AAEA,SAAS,KAAQ,OAAmB;CAClC,OAAO,OAAO,UAAU,aAAc,MAAkB,IAAI;AAC9D;AAEA,SAAgB,oBAAoB,SAA0D;CAC5F,MAAM,UAAU,QAAQ,SAAS;CAEjC,SAAS,UAAqC;EAC5C,IAAI;GACF,QAAQ,QAAQ,WAAW,eAAA,CAAgB;EAC7C,QAAQ;GAEN;EACF;CACF;CAEA,SAAS,WAA0B;EACjC,MAAM,SAAS,KAAK,QAAQ,MAAM;EAClC,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI;GACF,OAAO,IAAI,IAAI,MAAM,CAAC,CAAC;EACzB,QAAQ;GACN,OAAO;EACT;CACF;;CAGA,SAAS,QAAe;EACtB,MAAM,QAAQ,QAAQ;EACtB,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,6CAA6C;EACzE,OAAO;CACT;CAEA,OAAO;EACL,IAAI,KAAK;GACP,IAAI,CAAC,QAAQ,GAAG,OAAO;GACvB,MAAM,SAAS,SAAS;GACxB,OAAO,WAAW,OAAO,OAAO,GAAG,SAAS;EAC9C;EAEA,UAAU;GACR,IAAI,QAAQ,YAAY,KAAA,GAAW,OAAO,KAAK,QAAQ,OAAO,KAAK,KAAA;GACnE,MAAM,WAAW,QAAQ,CAAC,EAAE,KAAK;GACjC,OAAO,OAAO,UAAU,OAAO,YAAY,SAAS,OAAO,KAAK,SAAS,KAAK,KAAA;EAChF;EAEA,MAAM,IAAO,KAAa;GACxB,MAAM,MAAM,MAAM,MAAM,CAAC,CAAC,MAAM,GAAG;GACnC,OAAO,MAAQ,MAAM,IAAI,KAAK,IAAW,KAAA;EAC3C;EAEA,MAAM,IAAI,KAAK,OAAO,YAAY;GAChC,MAAM,MAAM,CAAC,CAAC,IACZ,KACA,IAAI,SAAS,KAAK,UAAU,KAAK,GAAG,EAClC,SAAS;IACP,gBAAgB;IAChB,iBAAiB,mBAAmB;GACtC,EACF,CAAC,CACH;EACF;EAEA,MAAM,OAAO,MAAM;GACjB,MAAM,QAAQ,MAAM;GACpB,MAAM,QAAQ,IAAI,KAAK,KAAK,QAAQ,MAAM,OAAO,GAAG,CAAC,CAAC;EACxD;EAEA,MAAM,MAAM;GACV,MAAM,MAAM,QAAQ,CAAC,EAAE;GACvB,IAAI,OAAO,KAAK,cAAc,YAAY,OAAO;GACjD,IAAI,UAAU,IAAI;GAClB,OAAO;EACT;CACF;AACF"}
|