@aglyn/plugins-bookings 1.0.0-beta.231 → 1.0.0-beta.232
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/README.md +3 -1
- package/package.json +11 -11
- package/src/lib/components/bookings-console-page.js +30 -4
- package/src/lib/components/bookings-console-page.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +3 -0
- package/src/lib/declarations.console-server.js +20 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/model/booking-events.d.ts +52 -0
- package/src/lib/model/booking-events.js +53 -0
- package/src/lib/model/booking-events.js.map +1 -0
- package/src/lib/model/booking-manage.d.ts +7 -0
- package/src/lib/model/booking-manage.js +14 -0
- package/src/lib/model/booking-manage.js.map +1 -1
- package/src/lib/model/booking-view.d.ts +63 -0
- package/src/lib/model/booking-view.js +70 -0
- package/src/lib/model/booking-view.js.map +1 -0
- package/src/lib/server/api-v1/bookings.d.ts +33 -0
- package/src/lib/server/api-v1/bookings.js +103 -0
- package/src/lib/server/api-v1/bookings.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +22 -0
- package/src/lib/server/api-v1/openapi.js +107 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/billing-webhook.js +17 -0
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/booking-events.d.ts +40 -0
- package/src/lib/server/booking-events.js +50 -0
- package/src/lib/server/booking-events.js.map +1 -0
- package/src/lib/server/booking-manage.d.ts +16 -0
- package/src/lib/server/booking-manage.js +76 -2
- package/src/lib/server/booking-manage.js.map +1 -1
- package/src/lib/server/refund.js +17 -1
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server.js +22 -3
- package/src/lib/server.js.map +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { definePluginDomainEvent } from "@aglyn/aglyn/plugin-manager/plugin-domain-events";
|
|
17
|
+
export const BOOKING_CREATED_EVENT = definePluginDomainEvent('booking.created');
|
|
18
|
+
export const BOOKING_RESCHEDULED_EVENT = definePluginDomainEvent('booking.rescheduled');
|
|
19
|
+
export const BOOKING_CANCELED_EVENT = definePluginDomainEvent('booking.canceled');
|
|
20
|
+
export const BOOKING_EVENT_DECLARATIONS = [
|
|
21
|
+
{
|
|
22
|
+
event: BOOKING_CREATED_EVENT,
|
|
23
|
+
label: 'Booking confirmed',
|
|
24
|
+
description: 'A booking was confirmed: a free one when it was made, a paid one when its payment landed.',
|
|
25
|
+
payloadKeys: [
|
|
26
|
+
'booking'
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
event: BOOKING_RESCHEDULED_EVENT,
|
|
31
|
+
label: 'Booking rescheduled',
|
|
32
|
+
description: 'The site’s team moved a booking to another time, once per move.',
|
|
33
|
+
payloadKeys: [
|
|
34
|
+
'booking'
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
event: BOOKING_CANCELED_EVENT,
|
|
39
|
+
label: 'Booking canceled',
|
|
40
|
+
description: 'A booking was canceled: a free one by the site’s team, a paid one by a full refund.',
|
|
41
|
+
payloadKeys: [
|
|
42
|
+
'booking'
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* The key naming one occurrence, so the same fact raised twice is one event:
|
|
48
|
+
* a booking is confirmed once, canceled once, and moved once to each time.
|
|
49
|
+
*/ export function bookingEventKey(event, bookingId, startsAtMs) {
|
|
50
|
+
return event === 'booking.rescheduled' ? `${bookingId}@${Number(startsAtMs != null ? startsAtMs : 0)}` : bookingId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=booking-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-events.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginDomainEvent,\n type PluginDomainEvent,\n type PluginDomainEventDeclaration,\n} from '@aglyn/aglyn/plugin-manager/plugin-domain-events'\nimport type { BookingView } from './booking-view'\n\n/**\n * The booking events the bookings plugin raises (AGL-3643), for other\n * plugins — the merchant's Zapier connections first — through core's\n * domain-event outbox: written when the fact is, delivered at least once,\n * retried with backoff. A subscriber names the event and never imports this\n * plugin.\n *\n * Each payload is `{ booking }` in the public API's shape\n * (`GET /v1/sites/{siteId}/bookings/{bookingId}`), as it stood once the fact\n * was written.\n *\n * - `booking.created`: a booking is confirmed — a free one when it is made,\n * a paid one when its payment lands. A checkout the guest never finished\n * raises nothing.\n * - `booking.rescheduled`: the site's team moved it to another time; one\n * event per move.\n * - `booking.canceled`: it was canceled — a free one by the site's team, a\n * paid one by the refund that gave all of the money back.\n */\nexport interface BookingEventPayload {\n booking: BookingView\n}\n\nexport const BOOKING_CREATED_EVENT = definePluginDomainEvent<BookingEventPayload>('booking.created')\nexport const BOOKING_RESCHEDULED_EVENT = definePluginDomainEvent<BookingEventPayload>('booking.rescheduled')\nexport const BOOKING_CANCELED_EVENT = definePluginDomainEvent<BookingEventPayload>('booking.canceled')\n\nexport type BookingEventName = 'booking.created' | 'booking.rescheduled' | 'booking.canceled'\n\nexport const BOOKING_EVENT_DECLARATIONS: ReadonlyArray<PluginDomainEventDeclaration & { event: PluginDomainEvent<unknown> }> = [\n {\n event: BOOKING_CREATED_EVENT,\n label: 'Booking confirmed',\n description: 'A booking was confirmed: a free one when it was made, a paid one when its payment landed.',\n payloadKeys: ['booking'],\n },\n {\n event: BOOKING_RESCHEDULED_EVENT,\n label: 'Booking rescheduled',\n description: 'The site’s team moved a booking to another time, once per move.',\n payloadKeys: ['booking'],\n },\n {\n event: BOOKING_CANCELED_EVENT,\n label: 'Booking canceled',\n description: 'A booking was canceled: a free one by the site’s team, a paid one by a full refund.',\n payloadKeys: ['booking'],\n },\n]\n\n/**\n * The key naming one occurrence, so the same fact raised twice is one event:\n * a booking is confirmed once, canceled once, and moved once to each time.\n */\nexport function bookingEventKey(event: BookingEventName, bookingId: string, startsAtMs?: number): string {\n return event === 'booking.rescheduled' ? `${bookingId}@${Number(startsAtMs ?? 0)}` : bookingId\n}\n"],"names":["definePluginDomainEvent","BOOKING_CREATED_EVENT","BOOKING_RESCHEDULED_EVENT","BOOKING_CANCELED_EVENT","BOOKING_EVENT_DECLARATIONS","event","label","description","payloadKeys","bookingEventKey","bookingId","startsAtMs","Number"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,uBAAuB,QAGlB,mDAAkD;AA0BzD,OAAO,MAAMC,wBAAwBD,wBAA6C,mBAAkB;AACpG,OAAO,MAAME,4BAA4BF,wBAA6C,uBAAsB;AAC5G,OAAO,MAAMG,yBAAyBH,wBAA6C,oBAAmB;AAItG,OAAO,MAAMI,6BAAkH;IAC7H;QACEC,OAAOJ;QACPK,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAU;IAC1B;IACA;QACEH,OAAOH;QACPI,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAU;IAC1B;IACA;QACEH,OAAOF;QACPG,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAU;IAC1B;CACD,CAAA;AAED;;;CAGC,GACD,OAAO,SAASC,gBAAgBJ,KAAuB,EAAEK,SAAiB,EAAEC,UAAmB;IAC7F,OAAON,UAAU,wBAAwB,GAAGK,UAAU,CAAC,EAAEE,OAAOD,qBAAAA,aAAc,IAAI,GAAGD;AACvF"}
|
|
@@ -62,5 +62,12 @@ export declare function bookingActions(booking: ManagedBooking, nowMs: number):
|
|
|
62
62
|
export declare function checkInRefusal(booking: ManagedBooking, checkedIn: boolean, nowMs: number): string | null;
|
|
63
63
|
/** Why a booking cannot move to `startsAtMs`, or null. The slot itself is checked apart. */
|
|
64
64
|
export declare function rescheduleRefusal(booking: ManagedBooking, startsAtMs: number, nowMs: number): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Why a booking cannot be canceled by the cancel route, or null (AGL-3643).
|
|
67
|
+
* A canceled booking is not refused: canceling it again changes nothing. A
|
|
68
|
+
* booking with money still paid on it cancels through the refund instead, so
|
|
69
|
+
* a guest is never left canceled and charged.
|
|
70
|
+
*/
|
|
71
|
+
export declare function cancelRefusal(booking: ManagedBooking, nowMs: number): string | null;
|
|
65
72
|
/** The booking's length, kept when it moves: what the guest booked, not what the service says today. */
|
|
66
73
|
export declare function bookingDurationMs(booking: ManagedBooking, fallbackMinutes: number): number;
|
|
@@ -81,6 +81,20 @@ export const BOOKING_STATE_LABELS = {
|
|
|
81
81
|
if (!Number.isFinite(startsAtMs) || startsAtMs <= nowMs) return 'Pick a time that has not passed';
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Why a booking cannot be canceled by the cancel route, or null (AGL-3643).
|
|
86
|
+
* A canceled booking is not refused: canceling it again changes nothing. A
|
|
87
|
+
* booking with money still paid on it cancels through the refund instead, so
|
|
88
|
+
* a guest is never left canceled and charged.
|
|
89
|
+
*/ export function cancelRefusal(booking, nowMs) {
|
|
90
|
+
var _booking_checkedInAtMs;
|
|
91
|
+
if (bookingState(booking, nowMs) === 'canceled') return null;
|
|
92
|
+
if (Number((_booking_checkedInAtMs = booking.checkedInAtMs) != null ? _booking_checkedInAtMs : 0) > 0) return 'This guest is already checked in';
|
|
93
|
+
if (bookingOutstandingCents(booking) > 0) {
|
|
94
|
+
return 'This booking was paid. Cancel it with a refund so the guest gets their money back';
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
84
98
|
/** The booking's length, kept when it moves: what the guest booked, not what the service says today. */ export function bookingDurationMs(booking, fallbackMinutes) {
|
|
85
99
|
var _booking_endsAtMs, _booking_startsAtMs;
|
|
86
100
|
const stored = Number((_booking_endsAtMs = booking.endsAtMs) != null ? _booking_endsAtMs : 0) - Number((_booking_startsAtMs = booking.startsAtMs) != null ? _booking_startsAtMs : 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-manage.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * What a site's team may do to a booking once it exists (AGL-3621): check\n * the guest in, move it, cancel it. One pure rule set, read by the routes\n * that do it (`bookings/check-in`, `bookings/reschedule`) under their own\n * transaction, and the rules a client mirrors to decide what to OFFER (the\n * native Aglyn app's bookings screens port them, cased by this file's spec),\n * so a screen can only offer what the route would then do.\n *\n * No I/O and no React, so client and server code alike may import it.\n */\n\n/** `hosts/{hostId}/bookings/{id}`, the fields these rules read. */\nexport interface ManagedBooking {\n status?: string\n startsAtMs?: number\n endsAtMs?: number\n expiresAtMs?: number\n paidAmountCents?: number\n refundedCents?: number\n checkedInAtMs?: number\n}\n\nexport type BookingState = 'confirmed' | 'pendingPayment' | 'expired' | 'canceled'\n\n/**\n * The booking's state as a reader should treat it. A payment hold whose\n * fifteen minutes ran out released its slot (AGL-170) even while the\n * document still says `pendingPayment`.\n */\nexport function bookingState(booking: ManagedBooking, nowMs: number): BookingState {\n if (booking.status === 'canceled') return 'canceled'\n if (booking.status === 'pendingPayment') {\n return Number(booking.expiresAtMs ?? 0) < nowMs ? 'expired' : 'pendingPayment'\n }\n return 'confirmed'\n}\n\nexport const BOOKING_STATE_LABELS: Readonly<Record<BookingState, string>> = {\n confirmed: 'Confirmed',\n pendingPayment: 'Awaiting payment',\n expired: 'Payment not finished',\n canceled: 'Canceled',\n}\n\n/** The paid amount not yet refunded, in cents. */\nexport function bookingOutstandingCents(booking: ManagedBooking): number {\n const paid = Math.max(0, Math.round(Number(booking.paidAmountCents ?? 0)))\n const refunded = Math.max(0, Math.round(Number(booking.refundedCents ?? 0)))\n return Math.max(0, paid - refunded)\n}\n\nexport interface BookingActions {\n checkIn: boolean\n undoCheckIn: boolean\n reschedule: boolean\n /** Cancel; with `refundCents` above zero the cancel goes through the refund route. */\n cancel: boolean\n refundCents: number\n}\n\n/**\n * What may be done to a booking now. A guest is checked in only on a\n * confirmed booking; a booking moves only while it is confirmed and has not\n * ended; and nothing is done to a canceled one.\n */\nexport function bookingActions(booking: ManagedBooking, nowMs: number): BookingActions {\n const state = bookingState(booking, nowMs)\n const confirmed = state === 'confirmed'\n const checkedIn = Number(booking.checkedInAtMs ?? 0) > 0\n const ended = Number(booking.endsAtMs ?? 0) <= nowMs\n return {\n checkIn: confirmed && !checkedIn,\n undoCheckIn: confirmed && checkedIn,\n reschedule: confirmed && !checkedIn && !ended,\n cancel: state !== 'canceled' && !checkedIn,\n refundCents: bookingOutstandingCents(booking),\n }\n}\n\n/** Why a check-in (or its undo) cannot be written, or null. The route's own refusal. */\nexport function checkInRefusal(booking: ManagedBooking, checkedIn: boolean, nowMs: number): string | null {\n const state = bookingState(booking, nowMs)\n if (state === 'canceled') return 'This booking was canceled'\n if (state !== 'confirmed') return 'This booking is not paid yet'\n return checkedIn || Number(booking.checkedInAtMs ?? 0) > 0 ? null : 'This guest is not checked in'\n}\n\n/** Why a booking cannot move to `startsAtMs`, or null. The slot itself is checked apart. */\nexport function rescheduleRefusal(booking: ManagedBooking, startsAtMs: number, nowMs: number): string | null {\n const state = bookingState(booking, nowMs)\n if (state === 'canceled') return 'This booking was canceled'\n if (state !== 'confirmed') return 'This booking is not paid yet'\n if (Number(booking.checkedInAtMs ?? 0) > 0) return 'This guest is already checked in'\n if (Number(booking.endsAtMs ?? 0) <= nowMs) return 'This booking has already ended'\n if (!Number.isFinite(startsAtMs) || startsAtMs <= nowMs) return 'Pick a time that has not passed'\n return null\n}\n\n/** The booking's length, kept when it moves: what the guest booked, not what the service says today. */\nexport function bookingDurationMs(booking: ManagedBooking, fallbackMinutes: number): number {\n const stored = Number(booking.endsAtMs ?? 0) - Number(booking.startsAtMs ?? 0)\n return stored > 0 ? stored : Math.max(5, Math.round(fallbackMinutes || 30)) * 60_000\n}\n"],"names":["bookingState","booking","nowMs","status","Number","expiresAtMs","BOOKING_STATE_LABELS","confirmed","pendingPayment","expired","canceled","bookingOutstandingCents","paid","Math","max","round","paidAmountCents","refunded","refundedCents","bookingActions","state","checkedIn","checkedInAtMs","ended","endsAtMs","checkIn","undoCheckIn","reschedule","cancel","refundCents","checkInRefusal","rescheduleRefusal","startsAtMs","isFinite","bookingDurationMs","fallbackMinutes","stored"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GAED,iEAAiE,GAajE;;;;CAIC,GACD,OAAO,SAASA,aAAaC,OAAuB,EAAEC,KAAa;IACjE,IAAID,QAAQE,MAAM,KAAK,YAAY,OAAO;IAC1C,IAAIF,QAAQE,MAAM,KAAK,kBAAkB;YACzBF;QAAd,OAAOG,QAAOH,uBAAAA,QAAQI,WAAW,YAAnBJ,uBAAuB,KAAKC,QAAQ,YAAY;IAChE;IACA,OAAO;AACT;AAEA,OAAO,MAAMI,uBAA+D;IAC1EC,WAAW;IACXC,gBAAgB;IAChBC,SAAS;IACTC,UAAU;AACZ,EAAC;AAED,gDAAgD,GAChD,OAAO,SAASC,wBAAwBV,OAAuB;QAClBA,0BACIA;IAD/C,MAAMW,OAAOC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACX,QAAOH,2BAAAA,QAAQe,eAAe,YAAvBf,2BAA2B;IACtE,MAAMgB,WAAWJ,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACX,QAAOH,yBAAAA,QAAQiB,aAAa,YAArBjB,yBAAyB;IACxE,OAAOY,KAAKC,GAAG,CAAC,GAAGF,OAAOK;AAC5B;AAWA;;;;CAIC,GACD,OAAO,SAASE,eAAelB,OAAuB,EAAEC,KAAa;QAG1CD,wBACJA;IAHrB,MAAMmB,QAAQpB,aAAaC,SAASC;IACpC,MAAMK,YAAYa,UAAU;IAC5B,MAAMC,YAAYjB,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK;IACvD,MAAMsB,QAAQnB,QAAOH,oBAAAA,QAAQuB,QAAQ,YAAhBvB,oBAAoB,MAAMC;IAC/C,OAAO;QACLuB,SAASlB,aAAa,CAACc;QACvBK,aAAanB,aAAac;QAC1BM,YAAYpB,aAAa,CAACc,aAAa,CAACE;QACxCK,QAAQR,UAAU,cAAc,CAACC;QACjCQ,aAAalB,wBAAwBV;IACvC;AACF;AAEA,sFAAsF,GACtF,OAAO,SAAS6B,eAAe7B,OAAuB,EAAEoB,SAAkB,EAAEnB,KAAa;QAI5DD;IAH3B,MAAMmB,QAAQpB,aAAaC,SAASC;IACpC,IAAIkB,UAAU,YAAY,OAAO;IACjC,IAAIA,UAAU,aAAa,OAAO;IAClC,OAAOC,aAAajB,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK,IAAI,OAAO;AACtE;AAEA,0FAA0F,GAC1F,OAAO,SAAS8B,kBAAkB9B,OAAuB,EAAE+B,UAAkB,EAAE9B,KAAa;QAI/ED,wBACAA;IAJX,MAAMmB,QAAQpB,aAAaC,SAASC;IACpC,IAAIkB,UAAU,YAAY,OAAO;IACjC,IAAIA,UAAU,aAAa,OAAO;IAClC,IAAIhB,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK,GAAG,OAAO;IACnD,IAAIG,QAAOH,oBAAAA,QAAQuB,QAAQ,YAAhBvB,oBAAoB,MAAMC,OAAO,OAAO;IACnD,IAAI,CAACE,OAAO6B,QAAQ,CAACD,eAAeA,cAAc9B,OAAO,OAAO;IAChE,OAAO;AACT;AAEA,sGAAsG,GACtG,OAAO,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-manage.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * What a site's team may do to a booking once it exists (AGL-3621): check\n * the guest in, move it, cancel it. One pure rule set, read by the routes\n * that do it (`bookings/check-in`, `bookings/reschedule`) under their own\n * transaction, and the rules a client mirrors to decide what to OFFER (the\n * native Aglyn app's bookings screens port them, cased by this file's spec),\n * so a screen can only offer what the route would then do.\n *\n * No I/O and no React, so client and server code alike may import it.\n */\n\n/** `hosts/{hostId}/bookings/{id}`, the fields these rules read. */\nexport interface ManagedBooking {\n status?: string\n startsAtMs?: number\n endsAtMs?: number\n expiresAtMs?: number\n paidAmountCents?: number\n refundedCents?: number\n checkedInAtMs?: number\n}\n\nexport type BookingState = 'confirmed' | 'pendingPayment' | 'expired' | 'canceled'\n\n/**\n * The booking's state as a reader should treat it. A payment hold whose\n * fifteen minutes ran out released its slot (AGL-170) even while the\n * document still says `pendingPayment`.\n */\nexport function bookingState(booking: ManagedBooking, nowMs: number): BookingState {\n if (booking.status === 'canceled') return 'canceled'\n if (booking.status === 'pendingPayment') {\n return Number(booking.expiresAtMs ?? 0) < nowMs ? 'expired' : 'pendingPayment'\n }\n return 'confirmed'\n}\n\nexport const BOOKING_STATE_LABELS: Readonly<Record<BookingState, string>> = {\n confirmed: 'Confirmed',\n pendingPayment: 'Awaiting payment',\n expired: 'Payment not finished',\n canceled: 'Canceled',\n}\n\n/** The paid amount not yet refunded, in cents. */\nexport function bookingOutstandingCents(booking: ManagedBooking): number {\n const paid = Math.max(0, Math.round(Number(booking.paidAmountCents ?? 0)))\n const refunded = Math.max(0, Math.round(Number(booking.refundedCents ?? 0)))\n return Math.max(0, paid - refunded)\n}\n\nexport interface BookingActions {\n checkIn: boolean\n undoCheckIn: boolean\n reschedule: boolean\n /** Cancel; with `refundCents` above zero the cancel goes through the refund route. */\n cancel: boolean\n refundCents: number\n}\n\n/**\n * What may be done to a booking now. A guest is checked in only on a\n * confirmed booking; a booking moves only while it is confirmed and has not\n * ended; and nothing is done to a canceled one.\n */\nexport function bookingActions(booking: ManagedBooking, nowMs: number): BookingActions {\n const state = bookingState(booking, nowMs)\n const confirmed = state === 'confirmed'\n const checkedIn = Number(booking.checkedInAtMs ?? 0) > 0\n const ended = Number(booking.endsAtMs ?? 0) <= nowMs\n return {\n checkIn: confirmed && !checkedIn,\n undoCheckIn: confirmed && checkedIn,\n reschedule: confirmed && !checkedIn && !ended,\n cancel: state !== 'canceled' && !checkedIn,\n refundCents: bookingOutstandingCents(booking),\n }\n}\n\n/** Why a check-in (or its undo) cannot be written, or null. The route's own refusal. */\nexport function checkInRefusal(booking: ManagedBooking, checkedIn: boolean, nowMs: number): string | null {\n const state = bookingState(booking, nowMs)\n if (state === 'canceled') return 'This booking was canceled'\n if (state !== 'confirmed') return 'This booking is not paid yet'\n return checkedIn || Number(booking.checkedInAtMs ?? 0) > 0 ? null : 'This guest is not checked in'\n}\n\n/** Why a booking cannot move to `startsAtMs`, or null. The slot itself is checked apart. */\nexport function rescheduleRefusal(booking: ManagedBooking, startsAtMs: number, nowMs: number): string | null {\n const state = bookingState(booking, nowMs)\n if (state === 'canceled') return 'This booking was canceled'\n if (state !== 'confirmed') return 'This booking is not paid yet'\n if (Number(booking.checkedInAtMs ?? 0) > 0) return 'This guest is already checked in'\n if (Number(booking.endsAtMs ?? 0) <= nowMs) return 'This booking has already ended'\n if (!Number.isFinite(startsAtMs) || startsAtMs <= nowMs) return 'Pick a time that has not passed'\n return null\n}\n\n/**\n * Why a booking cannot be canceled by the cancel route, or null (AGL-3643).\n * A canceled booking is not refused: canceling it again changes nothing. A\n * booking with money still paid on it cancels through the refund instead, so\n * a guest is never left canceled and charged.\n */\nexport function cancelRefusal(booking: ManagedBooking, nowMs: number): string | null {\n if (bookingState(booking, nowMs) === 'canceled') return null\n if (Number(booking.checkedInAtMs ?? 0) > 0) return 'This guest is already checked in'\n if (bookingOutstandingCents(booking) > 0) {\n return 'This booking was paid. Cancel it with a refund so the guest gets their money back'\n }\n return null\n}\n\n/** The booking's length, kept when it moves: what the guest booked, not what the service says today. */\nexport function bookingDurationMs(booking: ManagedBooking, fallbackMinutes: number): number {\n const stored = Number(booking.endsAtMs ?? 0) - Number(booking.startsAtMs ?? 0)\n return stored > 0 ? stored : Math.max(5, Math.round(fallbackMinutes || 30)) * 60_000\n}\n"],"names":["bookingState","booking","nowMs","status","Number","expiresAtMs","BOOKING_STATE_LABELS","confirmed","pendingPayment","expired","canceled","bookingOutstandingCents","paid","Math","max","round","paidAmountCents","refunded","refundedCents","bookingActions","state","checkedIn","checkedInAtMs","ended","endsAtMs","checkIn","undoCheckIn","reschedule","cancel","refundCents","checkInRefusal","rescheduleRefusal","startsAtMs","isFinite","cancelRefusal","bookingDurationMs","fallbackMinutes","stored"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GAED,iEAAiE,GAajE;;;;CAIC,GACD,OAAO,SAASA,aAAaC,OAAuB,EAAEC,KAAa;IACjE,IAAID,QAAQE,MAAM,KAAK,YAAY,OAAO;IAC1C,IAAIF,QAAQE,MAAM,KAAK,kBAAkB;YACzBF;QAAd,OAAOG,QAAOH,uBAAAA,QAAQI,WAAW,YAAnBJ,uBAAuB,KAAKC,QAAQ,YAAY;IAChE;IACA,OAAO;AACT;AAEA,OAAO,MAAMI,uBAA+D;IAC1EC,WAAW;IACXC,gBAAgB;IAChBC,SAAS;IACTC,UAAU;AACZ,EAAC;AAED,gDAAgD,GAChD,OAAO,SAASC,wBAAwBV,OAAuB;QAClBA,0BACIA;IAD/C,MAAMW,OAAOC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACX,QAAOH,2BAAAA,QAAQe,eAAe,YAAvBf,2BAA2B;IACtE,MAAMgB,WAAWJ,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACX,QAAOH,yBAAAA,QAAQiB,aAAa,YAArBjB,yBAAyB;IACxE,OAAOY,KAAKC,GAAG,CAAC,GAAGF,OAAOK;AAC5B;AAWA;;;;CAIC,GACD,OAAO,SAASE,eAAelB,OAAuB,EAAEC,KAAa;QAG1CD,wBACJA;IAHrB,MAAMmB,QAAQpB,aAAaC,SAASC;IACpC,MAAMK,YAAYa,UAAU;IAC5B,MAAMC,YAAYjB,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK;IACvD,MAAMsB,QAAQnB,QAAOH,oBAAAA,QAAQuB,QAAQ,YAAhBvB,oBAAoB,MAAMC;IAC/C,OAAO;QACLuB,SAASlB,aAAa,CAACc;QACvBK,aAAanB,aAAac;QAC1BM,YAAYpB,aAAa,CAACc,aAAa,CAACE;QACxCK,QAAQR,UAAU,cAAc,CAACC;QACjCQ,aAAalB,wBAAwBV;IACvC;AACF;AAEA,sFAAsF,GACtF,OAAO,SAAS6B,eAAe7B,OAAuB,EAAEoB,SAAkB,EAAEnB,KAAa;QAI5DD;IAH3B,MAAMmB,QAAQpB,aAAaC,SAASC;IACpC,IAAIkB,UAAU,YAAY,OAAO;IACjC,IAAIA,UAAU,aAAa,OAAO;IAClC,OAAOC,aAAajB,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK,IAAI,OAAO;AACtE;AAEA,0FAA0F,GAC1F,OAAO,SAAS8B,kBAAkB9B,OAAuB,EAAE+B,UAAkB,EAAE9B,KAAa;QAI/ED,wBACAA;IAJX,MAAMmB,QAAQpB,aAAaC,SAASC;IACpC,IAAIkB,UAAU,YAAY,OAAO;IACjC,IAAIA,UAAU,aAAa,OAAO;IAClC,IAAIhB,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK,GAAG,OAAO;IACnD,IAAIG,QAAOH,oBAAAA,QAAQuB,QAAQ,YAAhBvB,oBAAoB,MAAMC,OAAO,OAAO;IACnD,IAAI,CAACE,OAAO6B,QAAQ,CAACD,eAAeA,cAAc9B,OAAO,OAAO;IAChE,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAASgC,cAAcjC,OAAuB,EAAEC,KAAa;QAEvDD;IADX,IAAID,aAAaC,SAASC,WAAW,YAAY,OAAO;IACxD,IAAIE,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK,GAAG,OAAO;IACnD,IAAIU,wBAAwBV,WAAW,GAAG;QACxC,OAAO;IACT;IACA,OAAO;AACT;AAEA,sGAAsG,GACtG,OAAO,SAASkC,kBAAkBlC,OAAuB,EAAEmC,eAAuB;QAC1DnC,mBAAgCA;IAAtD,MAAMoC,SAASjC,QAAOH,oBAAAA,QAAQuB,QAAQ,YAAhBvB,oBAAoB,KAAKG,QAAOH,sBAAAA,QAAQ+B,UAAU,YAAlB/B,sBAAsB;IAC5E,OAAOoC,SAAS,IAAIA,SAASxB,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACqB,mBAAmB,OAAO;AAChF"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type BookingState } from './booking-manage';
|
|
18
|
+
/**
|
|
19
|
+
* A booking as the customer REST API publishes it (AGL-3643):
|
|
20
|
+
* `GET /v1/sites/{siteId}/bookings/{bookingId}`, and the `booking` every
|
|
21
|
+
* booking event carries, so a subscriber reads one documented object rather
|
|
22
|
+
* than the plugin's storage.
|
|
23
|
+
*
|
|
24
|
+
* Money is integer cents, in US dollars — a booking is charged in USD. Times
|
|
25
|
+
* are ISO 8601 in UTC, with the zone the booking was made in beside them.
|
|
26
|
+
* Never Stripe's handles, the CRM reference or the reminder stamps: those are
|
|
27
|
+
* how the platform runs a booking, not what one is.
|
|
28
|
+
*
|
|
29
|
+
* Pure: no I/O, so the API handler, the event raisers and a spec share it.
|
|
30
|
+
*/
|
|
31
|
+
export interface BookingView {
|
|
32
|
+
id: string;
|
|
33
|
+
object: 'booking';
|
|
34
|
+
serviceId: string | null;
|
|
35
|
+
serviceName: string | null;
|
|
36
|
+
/**
|
|
37
|
+
* `confirmed`, `pendingPayment` (a paid booking whose checkout is still
|
|
38
|
+
* open), `expired` (that checkout ran out and the slot was released) or
|
|
39
|
+
* `canceled`.
|
|
40
|
+
*/
|
|
41
|
+
status: BookingState;
|
|
42
|
+
name: string | null;
|
|
43
|
+
email: string | null;
|
|
44
|
+
phone: string | null;
|
|
45
|
+
address: Record<string, unknown> | string | null;
|
|
46
|
+
startsAt: string | null;
|
|
47
|
+
endsAt: string | null;
|
|
48
|
+
timeZone: string | null;
|
|
49
|
+
currency: 'usd';
|
|
50
|
+
/** What the guest paid, tax included; 0 for a free booking. */
|
|
51
|
+
paidCents: number;
|
|
52
|
+
taxCents: number;
|
|
53
|
+
refundedCents: number;
|
|
54
|
+
checkedIn: boolean;
|
|
55
|
+
checkedInAt: string | null;
|
|
56
|
+
/** Where a moved booking started before its latest move, else `null`. */
|
|
57
|
+
rescheduledFrom: string | null;
|
|
58
|
+
created: string | null;
|
|
59
|
+
}
|
|
60
|
+
/** Epoch milliseconds, a Firestore Timestamp or a Date, as ISO 8601. */
|
|
61
|
+
export declare function bookingInstant(value: unknown): string | null;
|
|
62
|
+
/** One stored booking as the API publishes it. `nowMs` decides an expired hold. */
|
|
63
|
+
export declare function bookingViewFromData(id: string, data: Record<string, unknown>, nowMs?: number): BookingView;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { bookingState } from "./booking-manage.js";
|
|
18
|
+
const text = (value)=>{
|
|
19
|
+
const out = typeof value === 'string' ? value.trim() : '';
|
|
20
|
+
return out ? out : null;
|
|
21
|
+
};
|
|
22
|
+
const cents = (value)=>{
|
|
23
|
+
const out = Math.round(Number(value != null ? value : 0));
|
|
24
|
+
return Number.isFinite(out) && out > 0 ? out : 0;
|
|
25
|
+
};
|
|
26
|
+
/** Epoch milliseconds, a Firestore Timestamp or a Date, as ISO 8601. */ export function bookingInstant(value) {
|
|
27
|
+
if (value == null) return null;
|
|
28
|
+
if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? new Date(value).toISOString() : null;
|
|
29
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
30
|
+
const stamp = value;
|
|
31
|
+
if (typeof stamp.toMillis === 'function') return bookingInstant(stamp.toMillis());
|
|
32
|
+
if (typeof stamp.toDate === 'function') return bookingInstant(stamp.toDate());
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function addressOf(value) {
|
|
36
|
+
if (typeof value === 'string') return text(value);
|
|
37
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return _extends({}, value);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** One stored booking as the API publishes it. `nowMs` decides an expired hold. */ export function bookingViewFromData(id, data, nowMs = Date.now()) {
|
|
41
|
+
var _data_checkedInAtMs, _data_expiresAtMs, _data_startsAtMs, _data_endsAtMs, _data_rescheduledFromMs;
|
|
42
|
+
const checkedInAtMs = Number((_data_checkedInAtMs = data['checkedInAtMs']) != null ? _data_checkedInAtMs : 0);
|
|
43
|
+
return {
|
|
44
|
+
id,
|
|
45
|
+
object: 'booking',
|
|
46
|
+
serviceId: text(data['serviceId']),
|
|
47
|
+
serviceName: text(data['serviceName']),
|
|
48
|
+
status: bookingState({
|
|
49
|
+
status: typeof data['status'] === 'string' ? data['status'] : undefined,
|
|
50
|
+
expiresAtMs: Number((_data_expiresAtMs = data['expiresAtMs']) != null ? _data_expiresAtMs : 0) || undefined
|
|
51
|
+
}, nowMs),
|
|
52
|
+
name: text(data['name']),
|
|
53
|
+
email: text(data['email']),
|
|
54
|
+
phone: text(data['phone']),
|
|
55
|
+
address: addressOf(data['address']),
|
|
56
|
+
startsAt: bookingInstant(Number((_data_startsAtMs = data['startsAtMs']) != null ? _data_startsAtMs : 0)),
|
|
57
|
+
endsAt: bookingInstant(Number((_data_endsAtMs = data['endsAtMs']) != null ? _data_endsAtMs : 0)),
|
|
58
|
+
timeZone: text(data['timezone']),
|
|
59
|
+
currency: 'usd',
|
|
60
|
+
paidCents: cents(data['paidAmountCents']),
|
|
61
|
+
taxCents: cents(data['taxCents']),
|
|
62
|
+
refundedCents: cents(data['refundedCents']),
|
|
63
|
+
checkedIn: checkedInAtMs > 0,
|
|
64
|
+
checkedInAt: checkedInAtMs > 0 ? bookingInstant(checkedInAtMs) : null,
|
|
65
|
+
rescheduledFrom: bookingInstant(Number((_data_rescheduledFromMs = data['rescheduledFromMs']) != null ? _data_rescheduledFromMs : 0)),
|
|
66
|
+
created: bookingInstant(data['createdAt'])
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=booking-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-view.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { bookingState, type BookingState } from './booking-manage'\n\n/**\n * A booking as the customer REST API publishes it (AGL-3643):\n * `GET /v1/sites/{siteId}/bookings/{bookingId}`, and the `booking` every\n * booking event carries, so a subscriber reads one documented object rather\n * than the plugin's storage.\n *\n * Money is integer cents, in US dollars — a booking is charged in USD. Times\n * are ISO 8601 in UTC, with the zone the booking was made in beside them.\n * Never Stripe's handles, the CRM reference or the reminder stamps: those are\n * how the platform runs a booking, not what one is.\n *\n * Pure: no I/O, so the API handler, the event raisers and a spec share it.\n */\nexport interface BookingView {\n id: string\n object: 'booking'\n serviceId: string | null\n serviceName: string | null\n /**\n * `confirmed`, `pendingPayment` (a paid booking whose checkout is still\n * open), `expired` (that checkout ran out and the slot was released) or\n * `canceled`.\n */\n status: BookingState\n name: string | null\n email: string | null\n phone: string | null\n address: Record<string, unknown> | string | null\n startsAt: string | null\n endsAt: string | null\n timeZone: string | null\n currency: 'usd'\n /** What the guest paid, tax included; 0 for a free booking. */\n paidCents: number\n taxCents: number\n refundedCents: number\n checkedIn: boolean\n checkedInAt: string | null\n /** Where a moved booking started before its latest move, else `null`. */\n rescheduledFrom: string | null\n created: string | null\n}\n\nconst text = (value: unknown): string | null => {\n const out = typeof value === 'string' ? value.trim() : ''\n return out ? out : null\n}\n\nconst cents = (value: unknown): number => {\n const out = Math.round(Number(value ?? 0))\n return Number.isFinite(out) && out > 0 ? out : 0\n}\n\n/** Epoch milliseconds, a Firestore Timestamp or a Date, as ISO 8601. */\nexport function bookingInstant(value: unknown): string | null {\n if (value == null) return null\n if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? new Date(value).toISOString() : null\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString()\n const stamp = value as { toMillis?: () => number; toDate?: () => Date }\n if (typeof stamp.toMillis === 'function') return bookingInstant(stamp.toMillis())\n if (typeof stamp.toDate === 'function') return bookingInstant(stamp.toDate())\n return null\n}\n\nfunction addressOf(value: unknown): BookingView['address'] {\n if (typeof value === 'string') return text(value)\n if (value && typeof value === 'object' && !Array.isArray(value)) return { ...(value as Record<string, unknown>) }\n return null\n}\n\n/** One stored booking as the API publishes it. `nowMs` decides an expired hold. */\nexport function bookingViewFromData(id: string, data: Record<string, unknown>, nowMs = Date.now()): BookingView {\n const checkedInAtMs = Number(data['checkedInAtMs'] ?? 0)\n return {\n id,\n object: 'booking',\n serviceId: text(data['serviceId']),\n serviceName: text(data['serviceName']),\n status: bookingState(\n {\n status: typeof data['status'] === 'string' ? data['status'] : undefined,\n expiresAtMs: Number(data['expiresAtMs'] ?? 0) || undefined,\n },\n nowMs,\n ),\n name: text(data['name']),\n email: text(data['email']),\n phone: text(data['phone']),\n address: addressOf(data['address']),\n startsAt: bookingInstant(Number(data['startsAtMs'] ?? 0)),\n endsAt: bookingInstant(Number(data['endsAtMs'] ?? 0)),\n timeZone: text(data['timezone']),\n currency: 'usd',\n paidCents: cents(data['paidAmountCents']),\n taxCents: cents(data['taxCents']),\n refundedCents: cents(data['refundedCents']),\n checkedIn: checkedInAtMs > 0,\n checkedInAt: checkedInAtMs > 0 ? bookingInstant(checkedInAtMs) : null,\n rescheduledFrom: bookingInstant(Number(data['rescheduledFromMs'] ?? 0)),\n created: bookingInstant(data['createdAt']),\n }\n}\n"],"names":["bookingState","text","value","out","trim","cents","Math","round","Number","isFinite","bookingInstant","Date","toISOString","isNaN","getTime","stamp","toMillis","toDate","addressOf","Array","isArray","bookingViewFromData","id","data","nowMs","now","checkedInAtMs","object","serviceId","serviceName","status","undefined","expiresAtMs","name","email","phone","address","startsAt","endsAt","timeZone","currency","paidCents","taxCents","refundedCents","checkedIn","checkedInAt","rescheduledFrom","created"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,YAAY,QAA2B,sBAAkB;AA6ClE,MAAMC,OAAO,CAACC;IACZ,MAAMC,MAAM,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACvD,OAAOD,MAAMA,MAAM;AACrB;AAEA,MAAME,QAAQ,CAACH;IACb,MAAMC,MAAMG,KAAKC,KAAK,CAACC,OAAON,gBAAAA,QAAS;IACvC,OAAOM,OAAOC,QAAQ,CAACN,QAAQA,MAAM,IAAIA,MAAM;AACjD;AAEA,sEAAsE,GACtE,OAAO,SAASO,eAAeR,KAAc;IAC3C,IAAIA,SAAS,MAAM,OAAO;IAC1B,IAAI,OAAOA,UAAU,UAAU,OAAOM,OAAOC,QAAQ,CAACP,UAAUA,QAAQ,IAAI,IAAIS,KAAKT,OAAOU,WAAW,KAAK;IAC5G,IAAIV,iBAAiBS,MAAM,OAAOH,OAAOK,KAAK,CAACX,MAAMY,OAAO,MAAM,OAAOZ,MAAMU,WAAW;IAC1F,MAAMG,QAAQb;IACd,IAAI,OAAOa,MAAMC,QAAQ,KAAK,YAAY,OAAON,eAAeK,MAAMC,QAAQ;IAC9E,IAAI,OAAOD,MAAME,MAAM,KAAK,YAAY,OAAOP,eAAeK,MAAME,MAAM;IAC1E,OAAO;AACT;AAEA,SAASC,UAAUhB,KAAc;IAC/B,IAAI,OAAOA,UAAU,UAAU,OAAOD,KAAKC;IAC3C,IAAIA,SAAS,OAAOA,UAAU,YAAY,CAACiB,MAAMC,OAAO,CAAClB,QAAQ,OAAO,aAAMA;IAC9E,OAAO;AACT;AAEA,iFAAiF,GACjF,OAAO,SAASmB,oBAAoBC,EAAU,EAAEC,IAA6B,EAAEC,QAAQb,KAAKc,GAAG,EAAE;QAClEF,qBASHA,mBAQQA,kBACFA,gBAQSA;IA1BzC,MAAMG,gBAAgBlB,QAAOe,sBAAAA,IAAI,CAAC,gBAAgB,YAArBA,sBAAyB;IACtD,OAAO;QACLD;QACAK,QAAQ;QACRC,WAAW3B,KAAKsB,IAAI,CAAC,YAAY;QACjCM,aAAa5B,KAAKsB,IAAI,CAAC,cAAc;QACrCO,QAAQ9B,aACN;YACE8B,QAAQ,OAAOP,IAAI,CAAC,SAAS,KAAK,WAAWA,IAAI,CAAC,SAAS,GAAGQ;YAC9DC,aAAaxB,QAAOe,oBAAAA,IAAI,CAAC,cAAc,YAAnBA,oBAAuB,MAAMQ;QACnD,GACAP;QAEFS,MAAMhC,KAAKsB,IAAI,CAAC,OAAO;QACvBW,OAAOjC,KAAKsB,IAAI,CAAC,QAAQ;QACzBY,OAAOlC,KAAKsB,IAAI,CAAC,QAAQ;QACzBa,SAASlB,UAAUK,IAAI,CAAC,UAAU;QAClCc,UAAU3B,eAAeF,QAAOe,mBAAAA,IAAI,CAAC,aAAa,YAAlBA,mBAAsB;QACtDe,QAAQ5B,eAAeF,QAAOe,iBAAAA,IAAI,CAAC,WAAW,YAAhBA,iBAAoB;QAClDgB,UAAUtC,KAAKsB,IAAI,CAAC,WAAW;QAC/BiB,UAAU;QACVC,WAAWpC,MAAMkB,IAAI,CAAC,kBAAkB;QACxCmB,UAAUrC,MAAMkB,IAAI,CAAC,WAAW;QAChCoB,eAAetC,MAAMkB,IAAI,CAAC,gBAAgB;QAC1CqB,WAAWlB,gBAAgB;QAC3BmB,aAAanB,gBAAgB,IAAIhB,eAAegB,iBAAiB;QACjEoB,iBAAiBpC,eAAeF,QAAOe,0BAAAA,IAAI,CAAC,oBAAoB,YAAzBA,0BAA6B;QACpEwB,SAASrC,eAAea,IAAI,CAAC,YAAY;IAC3C;AACF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type ApiV1Context } from '@aglyn/tenant-data-admin/server/api-v1-kit';
|
|
18
|
+
/**
|
|
19
|
+
* A site's bookings on the customer REST API,
|
|
20
|
+
* `/v1/sites/{siteId}/bookings/…` (AGL-3643). Registered from this plugin's
|
|
21
|
+
* console server declarations; the console's `/v1` router owns the pipeline
|
|
22
|
+
* in front — the key, the plan's API access, the request quota, the rate
|
|
23
|
+
* limit — and refuses a site the key's organization does not own before it
|
|
24
|
+
* hands a request here.
|
|
25
|
+
*
|
|
26
|
+
* Read-only: `bookings:read`, and the `bookings` plan feature. The scope is
|
|
27
|
+
* asked first and the plan second, the order every commerce resource asks
|
|
28
|
+
* them in (AGL-900), so a key without the scope is told about the scope
|
|
29
|
+
* whatever plan its organization is on.
|
|
30
|
+
*/
|
|
31
|
+
/** The statuses a booking is stored with, and so the ones `?status=` takes. */
|
|
32
|
+
export declare const BOOKING_LIST_STATUSES: readonly ["confirmed", "pendingPayment", "canceled"];
|
|
33
|
+
export declare function handleBookings(request: Request, ctx: ApiV1Context, segments: string[], url: URL): Promise<Response>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { checkEntitlement } from "@aglyn/aglyn/server";
|
|
18
|
+
import { ApiErrors, apiJson, listResponse } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { paginate, requireScope } from "@aglyn/tenant-data-admin/server/api-v1-kit";
|
|
20
|
+
import { bookingViewFromData } from "../../model/booking-view.js";
|
|
21
|
+
/**
|
|
22
|
+
* A site's bookings on the customer REST API,
|
|
23
|
+
* `/v1/sites/{siteId}/bookings/…` (AGL-3643). Registered from this plugin's
|
|
24
|
+
* console server declarations; the console's `/v1` router owns the pipeline
|
|
25
|
+
* in front — the key, the plan's API access, the request quota, the rate
|
|
26
|
+
* limit — and refuses a site the key's organization does not own before it
|
|
27
|
+
* hands a request here.
|
|
28
|
+
*
|
|
29
|
+
* Read-only: `bookings:read`, and the `bookings` plan feature. The scope is
|
|
30
|
+
* asked first and the plan second, the order every commerce resource asks
|
|
31
|
+
* them in (AGL-900), so a key without the scope is told about the scope
|
|
32
|
+
* whatever plan its organization is on.
|
|
33
|
+
*/ /** The statuses a booking is stored with, and so the ones `?status=` takes. */ export const BOOKING_LIST_STATUSES = [
|
|
34
|
+
'confirmed',
|
|
35
|
+
'pendingPayment',
|
|
36
|
+
'canceled'
|
|
37
|
+
];
|
|
38
|
+
function requireBookings(ctx) {
|
|
39
|
+
return checkEntitlement(ctx.org, 'bookings') ? null : ApiErrors.planRequired({
|
|
40
|
+
message: 'Bookings are not included in this organization’s plan',
|
|
41
|
+
code: 'bookings',
|
|
42
|
+
headers: ctx.headers
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export async function handleBookings(request, ctx, segments, url) {
|
|
46
|
+
const [, hostId, , bookingId, extra] = segments;
|
|
47
|
+
const denied = requireScope(ctx, 'bookings:read');
|
|
48
|
+
if (denied) return denied;
|
|
49
|
+
const unentitled = requireBookings(ctx);
|
|
50
|
+
if (unentitled) return unentitled;
|
|
51
|
+
if (request.method !== 'GET') {
|
|
52
|
+
return ApiErrors.methodNotAllowed({
|
|
53
|
+
headers: _extends({}, ctx.headers, {
|
|
54
|
+
Allow: 'GET'
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (extra !== undefined) {
|
|
59
|
+
return ApiErrors.notFound({
|
|
60
|
+
message: 'Unknown endpoint',
|
|
61
|
+
headers: ctx.headers
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const collection = ctx.firestore.collection('hosts').doc(hostId).collection('bookings');
|
|
65
|
+
const nowMs = Date.now();
|
|
66
|
+
if (bookingId) {
|
|
67
|
+
var _snapshot_data;
|
|
68
|
+
const snapshot = await collection.doc(bookingId).get();
|
|
69
|
+
if (!snapshot.exists) {
|
|
70
|
+
return ApiErrors.notFound({
|
|
71
|
+
message: 'No such booking',
|
|
72
|
+
headers: ctx.headers
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return apiJson(bookingViewFromData(snapshot.id, (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {}, nowMs), {
|
|
76
|
+
headers: ctx.headers
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
let query = collection;
|
|
80
|
+
const status = url.searchParams.get('status');
|
|
81
|
+
if (status !== null) {
|
|
82
|
+
if (!BOOKING_LIST_STATUSES.includes(status)) {
|
|
83
|
+
return ApiErrors.badRequest({
|
|
84
|
+
message: 'Unknown booking status',
|
|
85
|
+
code: 'validation_failed',
|
|
86
|
+
fields: {
|
|
87
|
+
status: `Must be one of: ${BOOKING_LIST_STATUSES.join(', ')}`
|
|
88
|
+
},
|
|
89
|
+
headers: ctx.headers
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
query = query.where('status', '==', status);
|
|
93
|
+
}
|
|
94
|
+
const serviceId = url.searchParams.get('serviceId');
|
|
95
|
+
if (serviceId) query = query.where('serviceId', '==', serviceId);
|
|
96
|
+
const { docs, nextCursor } = await paginate(query, url);
|
|
97
|
+
return listResponse(docs.map((doc)=>{
|
|
98
|
+
var _doc_data;
|
|
99
|
+
return bookingViewFromData(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {}, nowMs);
|
|
100
|
+
}), nextCursor, ctx.headers);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=bookings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/bookings/src/lib/server/api-v1/bookings.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport { ApiErrors, apiJson, listResponse } from '@aglyn/tenant-data-admin'\nimport {\n type ApiV1Context,\n paginate,\n requireScope,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport { bookingViewFromData } from '../../model/booking-view'\n\n/**\n * A site's bookings on the customer REST API,\n * `/v1/sites/{siteId}/bookings/…` (AGL-3643). Registered from this plugin's\n * console server declarations; the console's `/v1` router owns the pipeline\n * in front — the key, the plan's API access, the request quota, the rate\n * limit — and refuses a site the key's organization does not own before it\n * hands a request here.\n *\n * Read-only: `bookings:read`, and the `bookings` plan feature. The scope is\n * asked first and the plan second, the order every commerce resource asks\n * them in (AGL-900), so a key without the scope is told about the scope\n * whatever plan its organization is on.\n */\n\n/** The statuses a booking is stored with, and so the ones `?status=` takes. */\nexport const BOOKING_LIST_STATUSES = ['confirmed', 'pendingPayment', 'canceled'] as const\n\nfunction requireBookings(ctx: ApiV1Context): Response | null {\n return checkEntitlement(ctx.org, 'bookings')\n ? null\n : ApiErrors.planRequired({\n message: 'Bookings are not included in this organization’s plan',\n code: 'bookings',\n headers: ctx.headers,\n })\n}\n\nexport async function handleBookings(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const [, hostId, , bookingId, extra] = segments\n const denied = requireScope(ctx, 'bookings:read')\n if (denied) return denied\n const unentitled = requireBookings(ctx)\n if (unentitled) return unentitled\n if (request.method !== 'GET') {\n return ApiErrors.methodNotAllowed({ headers: { ...ctx.headers, Allow: 'GET' } })\n }\n if (extra !== undefined) {\n return ApiErrors.notFound({ message: 'Unknown endpoint', headers: ctx.headers })\n }\n const collection = ctx.firestore.collection('hosts').doc(hostId).collection('bookings')\n const nowMs = Date.now()\n\n if (bookingId) {\n const snapshot = await collection.doc(bookingId).get()\n if (!snapshot.exists) {\n return ApiErrors.notFound({ message: 'No such booking', headers: ctx.headers })\n }\n return apiJson(bookingViewFromData(snapshot.id, snapshot.data() ?? {}, nowMs), { headers: ctx.headers })\n }\n\n let query: FirebaseFirestore.Query = collection\n const status = url.searchParams.get('status')\n if (status !== null) {\n if (!(BOOKING_LIST_STATUSES as readonly string[]).includes(status)) {\n return ApiErrors.badRequest({\n message: 'Unknown booking status',\n code: 'validation_failed',\n fields: { status: `Must be one of: ${BOOKING_LIST_STATUSES.join(', ')}` },\n headers: ctx.headers,\n })\n }\n query = query.where('status', '==', status)\n }\n const serviceId = url.searchParams.get('serviceId')\n if (serviceId) query = query.where('serviceId', '==', serviceId)\n const { docs, nextCursor } = await paginate(query, url)\n return listResponse(\n docs.map((doc) => bookingViewFromData(doc.id, doc.data() ?? {}, nowMs)),\n nextCursor,\n ctx.headers,\n )\n}\n"],"names":["checkEntitlement","ApiErrors","apiJson","listResponse","paginate","requireScope","bookingViewFromData","BOOKING_LIST_STATUSES","requireBookings","ctx","org","planRequired","message","code","headers","handleBookings","request","segments","url","hostId","bookingId","extra","denied","unentitled","method","methodNotAllowed","Allow","undefined","notFound","collection","firestore","doc","nowMs","Date","now","snapshot","get","exists","id","data","query","status","searchParams","includes","badRequest","fields","join","where","serviceId","docs","nextCursor","map"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,sBAAqB;AACtD,SAASC,SAAS,EAAEC,OAAO,EAAEC,YAAY,QAAQ,2BAA0B;AAC3E,SAEEC,QAAQ,EACRC,YAAY,QACP,6CAA4C;AACnD,SAASC,mBAAmB,QAAQ,8BAA0B;AAE9D;;;;;;;;;;;;CAYC,GAED,6EAA6E,GAC7E,OAAO,MAAMC,wBAAwB;IAAC;IAAa;IAAkB;CAAW,CAAS;AAEzF,SAASC,gBAAgBC,GAAiB;IACxC,OAAOT,iBAAiBS,IAAIC,GAAG,EAAE,cAC7B,OACAT,UAAUU,YAAY,CAAC;QACrBC,SAAS;QACTC,MAAM;QACNC,SAASL,IAAIK,OAAO;IACtB;AACN;AAEA,OAAO,eAAeC,eACpBC,OAAgB,EAChBP,GAAiB,EACjBQ,QAAkB,EAClBC,GAAQ;IAER,MAAM,GAAGC,UAAUC,WAAWC,MAAM,GAAGJ;IACvC,MAAMK,SAASjB,aAAaI,KAAK;IACjC,IAAIa,QAAQ,OAAOA;IACnB,MAAMC,aAAaf,gBAAgBC;IACnC,IAAIc,YAAY,OAAOA;IACvB,IAAIP,QAAQQ,MAAM,KAAK,OAAO;QAC5B,OAAOvB,UAAUwB,gBAAgB,CAAC;YAAEX,SAAS,aAAKL,IAAIK,OAAO;gBAAEY,OAAO;;QAAQ;IAChF;IACA,IAAIL,UAAUM,WAAW;QACvB,OAAO1B,UAAU2B,QAAQ,CAAC;YAAEhB,SAAS;YAAoBE,SAASL,IAAIK,OAAO;QAAC;IAChF;IACA,MAAMe,aAAapB,IAAIqB,SAAS,CAACD,UAAU,CAAC,SAASE,GAAG,CAACZ,QAAQU,UAAU,CAAC;IAC5E,MAAMG,QAAQC,KAAKC,GAAG;IAEtB,IAAId,WAAW;YAKmCe;QAJhD,MAAMA,WAAW,MAAMN,WAAWE,GAAG,CAACX,WAAWgB,GAAG;QACpD,IAAI,CAACD,SAASE,MAAM,EAAE;YACpB,OAAOpC,UAAU2B,QAAQ,CAAC;gBAAEhB,SAAS;gBAAmBE,SAASL,IAAIK,OAAO;YAAC;QAC/E;QACA,OAAOZ,QAAQI,oBAAoB6B,SAASG,EAAE,GAAEH,iBAAAA,SAASI,IAAI,cAAbJ,iBAAmB,CAAC,GAAGH,QAAQ;YAAElB,SAASL,IAAIK,OAAO;QAAC;IACxG;IAEA,IAAI0B,QAAiCX;IACrC,MAAMY,SAASvB,IAAIwB,YAAY,CAACN,GAAG,CAAC;IACpC,IAAIK,WAAW,MAAM;QACnB,IAAI,CAAC,AAAClC,sBAA4CoC,QAAQ,CAACF,SAAS;YAClE,OAAOxC,UAAU2C,UAAU,CAAC;gBAC1BhC,SAAS;gBACTC,MAAM;gBACNgC,QAAQ;oBAAEJ,QAAQ,CAAC,gBAAgB,EAAElC,sBAAsBuC,IAAI,CAAC,OAAO;gBAAC;gBACxEhC,SAASL,IAAIK,OAAO;YACtB;QACF;QACA0B,QAAQA,MAAMO,KAAK,CAAC,UAAU,MAAMN;IACtC;IACA,MAAMO,YAAY9B,IAAIwB,YAAY,CAACN,GAAG,CAAC;IACvC,IAAIY,WAAWR,QAAQA,MAAMO,KAAK,CAAC,aAAa,MAAMC;IACtD,MAAM,EAAEC,IAAI,EAAEC,UAAU,EAAE,GAAG,MAAM9C,SAASoC,OAAOtB;IACnD,OAAOf,aACL8C,KAAKE,GAAG,CAAC,CAACpB;YAAoCA;eAA5BzB,oBAAoByB,IAAIO,EAAE,GAAEP,YAAAA,IAAIQ,IAAI,cAARR,YAAc,CAAC,GAAGC;QAChEkB,YACAzC,IAAIK,OAAO;AAEf"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type ApiV1ResourceDescription } from '@aglyn/tenant-data-admin/server/api-v1-description';
|
|
18
|
+
/**
|
|
19
|
+
* A site's bookings as the customer API's OpenAPI document describes them
|
|
20
|
+
* (AGL-3643), in the shape `apps/docs/api/resources/bookings.md` documents.
|
|
21
|
+
*/
|
|
22
|
+
export declare const BOOKINGS_API_V1_DESCRIPTION: ApiV1ResourceDescription;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { booleanField as bool, integerField as int, isoField as iso, nullableField as nullable, objectKindField as OBJECT_FIELD, queryParam as q, stringField as str } from "@aglyn/tenant-data-admin/server/api-v1-description";
|
|
17
|
+
/**
|
|
18
|
+
* A site's bookings as the customer API's OpenAPI document describes them
|
|
19
|
+
* (AGL-3643), in the shape `apps/docs/api/resources/bookings.md` documents.
|
|
20
|
+
*/ export const BOOKINGS_API_V1_DESCRIPTION = {
|
|
21
|
+
tag: 'Bookings',
|
|
22
|
+
description: 'A site’s bookings. Read-only over the API.',
|
|
23
|
+
schemaName: 'Booking',
|
|
24
|
+
required: [
|
|
25
|
+
'id',
|
|
26
|
+
'object',
|
|
27
|
+
'status'
|
|
28
|
+
],
|
|
29
|
+
fields: {
|
|
30
|
+
id: str('Booking id.'),
|
|
31
|
+
object: OBJECT_FIELD('booking'),
|
|
32
|
+
serviceId: nullable(str('The booked service.')),
|
|
33
|
+
serviceName: nullable(str('The service’s name when it was booked.')),
|
|
34
|
+
status: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
enum: [
|
|
37
|
+
'confirmed',
|
|
38
|
+
'pendingPayment',
|
|
39
|
+
'expired',
|
|
40
|
+
'canceled'
|
|
41
|
+
],
|
|
42
|
+
description: '`pendingPayment` is a paid booking whose checkout is still open; `expired` is one whose checkout ran out, and its slot was released.'
|
|
43
|
+
},
|
|
44
|
+
name: nullable(str('The guest’s name.')),
|
|
45
|
+
email: nullable(str('The guest’s email address.')),
|
|
46
|
+
phone: nullable(str('The guest’s phone number, when the service asked for one.')),
|
|
47
|
+
address: {
|
|
48
|
+
type: [
|
|
49
|
+
'object',
|
|
50
|
+
'string',
|
|
51
|
+
'null'
|
|
52
|
+
],
|
|
53
|
+
description: 'The address the service is at, when the service asked for one: the guest’s address parts, or the line they typed.',
|
|
54
|
+
additionalProperties: true
|
|
55
|
+
},
|
|
56
|
+
startsAt: nullable(iso('When the booking starts, UTC.')),
|
|
57
|
+
endsAt: nullable(iso('When the booking ends, UTC.')),
|
|
58
|
+
timeZone: nullable(str('The IANA time zone the booking was made in.')),
|
|
59
|
+
currency: str('Always `usd`.'),
|
|
60
|
+
paidCents: int('What the guest paid, tax included; 0 for a free booking.'),
|
|
61
|
+
taxCents: int('The tax inside `paidCents`.'),
|
|
62
|
+
refundedCents: int('Refunded so far.'),
|
|
63
|
+
checkedIn: bool('Whether the guest was checked in.'),
|
|
64
|
+
checkedInAt: nullable(iso('When the guest was checked in.')),
|
|
65
|
+
rescheduledFrom: nullable(iso('Where a moved booking started before its latest move.')),
|
|
66
|
+
created: nullable(iso('When the booking was made.'))
|
|
67
|
+
},
|
|
68
|
+
ops: [
|
|
69
|
+
{
|
|
70
|
+
path: '/v1/sites/{siteId}/bookings',
|
|
71
|
+
method: 'get',
|
|
72
|
+
operationId: 'listBookings',
|
|
73
|
+
summary: 'List bookings',
|
|
74
|
+
list: true,
|
|
75
|
+
returns: 'Booking',
|
|
76
|
+
filters: [
|
|
77
|
+
q('status', '`confirmed`, `pendingPayment` or `canceled`. An expired checkout is stored as `pendingPayment`.'),
|
|
78
|
+
q('serviceId', 'Bookings of one service.')
|
|
79
|
+
],
|
|
80
|
+
pathParams: [
|
|
81
|
+
{
|
|
82
|
+
name: 'siteId',
|
|
83
|
+
description: 'Site id.'
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: '/v1/sites/{siteId}/bookings/{bookingId}',
|
|
89
|
+
method: 'get',
|
|
90
|
+
operationId: 'getBooking',
|
|
91
|
+
summary: 'Retrieve a booking',
|
|
92
|
+
returns: 'Booking',
|
|
93
|
+
pathParams: [
|
|
94
|
+
{
|
|
95
|
+
name: 'siteId',
|
|
96
|
+
description: 'Site id.'
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'bookingId',
|
|
100
|
+
description: 'Booking id.'
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/bookings/src/lib/server/api-v1/openapi.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type ApiV1ResourceDescription,\n booleanField as bool,\n integerField as int,\n isoField as iso,\n nullableField as nullable,\n objectKindField as OBJECT_FIELD,\n queryParam as q,\n stringField as str,\n} from '@aglyn/tenant-data-admin/server/api-v1-description'\n\n/**\n * A site's bookings as the customer API's OpenAPI document describes them\n * (AGL-3643), in the shape `apps/docs/api/resources/bookings.md` documents.\n */\nexport const BOOKINGS_API_V1_DESCRIPTION: ApiV1ResourceDescription = {\n tag: 'Bookings',\n description: 'A site’s bookings. Read-only over the API.',\n schemaName: 'Booking',\n required: ['id', 'object', 'status'],\n fields: {\n id: str('Booking id.'),\n object: OBJECT_FIELD('booking'),\n serviceId: nullable(str('The booked service.')),\n serviceName: nullable(str('The service’s name when it was booked.')),\n status: {\n type: 'string',\n enum: ['confirmed', 'pendingPayment', 'expired', 'canceled'],\n description:\n '`pendingPayment` is a paid booking whose checkout is still open; `expired` is one whose checkout ran out, and its slot was released.',\n },\n name: nullable(str('The guest’s name.')),\n email: nullable(str('The guest’s email address.')),\n phone: nullable(str('The guest’s phone number, when the service asked for one.')),\n address: {\n type: ['object', 'string', 'null'],\n description: 'The address the service is at, when the service asked for one: the guest’s address parts, or the line they typed.',\n additionalProperties: true,\n },\n startsAt: nullable(iso('When the booking starts, UTC.')),\n endsAt: nullable(iso('When the booking ends, UTC.')),\n timeZone: nullable(str('The IANA time zone the booking was made in.')),\n currency: str('Always `usd`.'),\n paidCents: int('What the guest paid, tax included; 0 for a free booking.'),\n taxCents: int('The tax inside `paidCents`.'),\n refundedCents: int('Refunded so far.'),\n checkedIn: bool('Whether the guest was checked in.'),\n checkedInAt: nullable(iso('When the guest was checked in.')),\n rescheduledFrom: nullable(iso('Where a moved booking started before its latest move.')),\n created: nullable(iso('When the booking was made.')),\n },\n ops: [\n {\n path: '/v1/sites/{siteId}/bookings',\n method: 'get',\n operationId: 'listBookings',\n summary: 'List bookings',\n list: true,\n returns: 'Booking',\n filters: [\n q('status', '`confirmed`, `pendingPayment` or `canceled`. An expired checkout is stored as `pendingPayment`.'),\n q('serviceId', 'Bookings of one service.'),\n ],\n pathParams: [{ name: 'siteId', description: 'Site id.' }],\n },\n {\n path: '/v1/sites/{siteId}/bookings/{bookingId}',\n method: 'get',\n operationId: 'getBooking',\n summary: 'Retrieve a booking',\n returns: 'Booking',\n pathParams: [\n { name: 'siteId', description: 'Site id.' },\n { name: 'bookingId', description: 'Booking id.' },\n ],\n },\n ],\n}\n"],"names":["booleanField","bool","integerField","int","isoField","iso","nullableField","nullable","objectKindField","OBJECT_FIELD","queryParam","q","stringField","str","BOOKINGS_API_V1_DESCRIPTION","tag","description","schemaName","required","fields","id","object","serviceId","serviceName","status","type","enum","name","email","phone","address","additionalProperties","startsAt","endsAt","timeZone","currency","paidCents","taxCents","refundedCents","checkedIn","checkedInAt","rescheduledFrom","created","ops","path","method","operationId","summary","list","returns","filters","pathParams"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAEEA,gBAAgBC,IAAI,EACpBC,gBAAgBC,GAAG,EACnBC,YAAYC,GAAG,EACfC,iBAAiBC,QAAQ,EACzBC,mBAAmBC,YAAY,EAC/BC,cAAcC,CAAC,EACfC,eAAeC,GAAG,QACb,qDAAoD;AAE3D;;;CAGC,GACD,OAAO,MAAMC,8BAAwD;IACnEC,KAAK;IACLC,aAAa;IACbC,YAAY;IACZC,UAAU;QAAC;QAAM;QAAU;KAAS;IACpCC,QAAQ;QACNC,IAAIP,IAAI;QACRQ,QAAQZ,aAAa;QACrBa,WAAWf,SAASM,IAAI;QACxBU,aAAahB,SAASM,IAAI;QAC1BW,QAAQ;YACNC,MAAM;YACNC,MAAM;gBAAC;gBAAa;gBAAkB;gBAAW;aAAW;YAC5DV,aACE;QACJ;QACAW,MAAMpB,SAASM,IAAI;QACnBe,OAAOrB,SAASM,IAAI;QACpBgB,OAAOtB,SAASM,IAAI;QACpBiB,SAAS;YACPL,MAAM;gBAAC;gBAAU;gBAAU;aAAO;YAClCT,aAAa;YACbe,sBAAsB;QACxB;QACAC,UAAUzB,SAASF,IAAI;QACvB4B,QAAQ1B,SAASF,IAAI;QACrB6B,UAAU3B,SAASM,IAAI;QACvBsB,UAAUtB,IAAI;QACduB,WAAWjC,IAAI;QACfkC,UAAUlC,IAAI;QACdmC,eAAenC,IAAI;QACnBoC,WAAWtC,KAAK;QAChBuC,aAAajC,SAASF,IAAI;QAC1BoC,iBAAiBlC,SAASF,IAAI;QAC9BqC,SAASnC,SAASF,IAAI;IACxB;IACAsC,KAAK;QACH;YACEC,MAAM;YACNC,QAAQ;YACRC,aAAa;YACbC,SAAS;YACTC,MAAM;YACNC,SAAS;YACTC,SAAS;gBACPvC,EAAE,UAAU;gBACZA,EAAE,aAAa;aAChB;YACDwC,YAAY;gBAAC;oBAAExB,MAAM;oBAAUX,aAAa;gBAAW;aAAE;QAC3D;QACA;YACE4B,MAAM;YACNC,QAAQ;YACRC,aAAa;YACbC,SAAS;YACTE,SAAS;YACTE,YAAY;gBACV;oBAAExB,MAAM;oBAAUX,aAAa;gBAAW;gBAC1C;oBAAEW,MAAM;oBAAaX,aAAa;gBAAc;aACjD;QACH;KACD;AACH,EAAC"}
|
|
@@ -29,6 +29,7 @@ import { bookingPlatformNetCents, shouldSendBookingPlatformPurchase } from "../m
|
|
|
29
29
|
import { sendEmail } from "@aglyn/shared-util-email";
|
|
30
30
|
import { pluginTaxProfile } from "@aglyn/aglyn/plugin-manager/plugin-tax-profile";
|
|
31
31
|
import { fileBookingOnCrm } from "./booking-crm.js";
|
|
32
|
+
import { stageBookingEvent } from "./booking-events.js";
|
|
32
33
|
import { bookingTimeZoneFor } from "./booking-time-zone.js";
|
|
33
34
|
import { formatBookingWhen } from "../model/booking-time.js";
|
|
34
35
|
import { bookingContactLines, readBookingPhone } from "../model/booking-contact-fields.js";
|
|
@@ -339,6 +340,22 @@ export const bookingsBillingWebhookHandler = async ({ type, object })=>{
|
|
|
339
340
|
}), {
|
|
340
341
|
merge: true
|
|
341
342
|
});
|
|
343
|
+
// The booking is confirmed now, in this transaction (AGL-3643).
|
|
344
|
+
const nowMs = Date.now();
|
|
345
|
+
stageBookingEvent(transaction, firestore, {
|
|
346
|
+
event: 'booking.created',
|
|
347
|
+
hostId: String(hostId),
|
|
348
|
+
bookingId: String(bookingId),
|
|
349
|
+
booking: _extends({}, booking, {
|
|
350
|
+
status: 'confirmed',
|
|
351
|
+
paidAmountCents
|
|
352
|
+
}, taxCents > 0 ? {
|
|
353
|
+
taxCents
|
|
354
|
+
} : {}, {
|
|
355
|
+
expiresAtMs: null
|
|
356
|
+
}),
|
|
357
|
+
nowMs
|
|
358
|
+
});
|
|
342
359
|
return true;
|
|
343
360
|
});
|
|
344
361
|
if (!confirmedNow) return;
|