@aglyn/plugins-bookings 1.0.0-beta.228 → 1.0.0-beta.230
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/package.json +11 -11
- package/src/lib/components/book-meeting-action.js +3 -2
- package/src/lib/components/book-meeting-action.js.map +1 -1
- package/src/lib/components/booking.js +4 -0
- package/src/lib/components/booking.js.map +1 -1
- package/src/lib/components/bookings-console-page.js +62 -4
- package/src/lib/components/bookings-console-page.js.map +1 -1
- package/src/lib/model/bookings.d.ts +22 -0
- package/src/lib/model/bookings.js +11 -0
- package/src/lib/model/bookings.js.map +1 -1
- package/src/lib/server/booking-service-ai-capability.d.ts +50 -0
- package/src/lib/server/booking-service-ai-capability.js +203 -0
- package/src/lib/server/booking-service-ai-capability.js.map +1 -0
- package/src/lib/server/booking-service-drafts.d.ts +107 -0
- package/src/lib/server/booking-service-drafts.js +320 -0
- package/src/lib/server/booking-service-drafts.js.map +1 -0
- package/src/lib/server.js +15 -4
- package/src/lib/server.js.map +1 -1
- package/src/lib/utils/use-booking-purchase-event.js +5 -0
- package/src/lib/utils/use-booking-purchase-event.js.map +1 -1
|
@@ -0,0 +1,203 @@
|
|
|
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 { registerPluginAiCapability } from "@aglyn/aglyn/plugin-manager/plugin-ai-capabilities";
|
|
18
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
19
|
+
import { BOOKING_FIELD_ASKS } from "../model/booking-contact-fields.js";
|
|
20
|
+
import { BOOKING_PRICE_DISPLAYS } from "../model/booking-price.js";
|
|
21
|
+
import { BOOKING_MAX_DURATION_MINUTES, BOOKING_MIN_DURATION_MINUTES } from "../model/bookings.js";
|
|
22
|
+
import { BOOKING_SERVICE_DESCRIPTION_MAX, BOOKING_SERVICE_DRAFT_RESOURCE, BOOKING_SERVICE_NAME_MAX, BOOKING_SERVICE_PRICE_MAX_USD } from "./booking-service-drafts.js";
|
|
23
|
+
/**
|
|
24
|
+
* WHAT AN AI BUILD CAN MAKE IN BOOKINGS (AGL-3616): a bookable service, as a
|
|
25
|
+
* draft.
|
|
26
|
+
*
|
|
27
|
+
* The capability the AI plugin's build planner offers for "a way to book
|
|
28
|
+
* me". Its item is written by this plugin's `booking-service` draft writer,
|
|
29
|
+
* so every rule is the writer's: the plan's `bookings` feature and service
|
|
30
|
+
* allowance, the member's role, the shape a service takes. The service is
|
|
31
|
+
* born a draft and takes no bookings until a person activates it on the
|
|
32
|
+
* Bookings page.
|
|
33
|
+
*
|
|
34
|
+
* The arguments are flat, as the contract requires: one set of opening hours
|
|
35
|
+
* on the weekdays named, which `draftContent` turns into the weekly windows
|
|
36
|
+
* the service stores. A page that places the Booking block depends on the
|
|
37
|
+
* service; when the service cannot be made, the page is built without the
|
|
38
|
+
* block (`degrade: 'omit'`).
|
|
39
|
+
*/ /** The operation's name in a build plan. */ export const BOOKING_SERVICE_AI_OP = 'booking-service';
|
|
40
|
+
/** Weekday names as the planner writes them, in the model's order (0 = Sunday). */ export const BOOKING_SERVICE_AI_DAYS = [
|
|
41
|
+
'sun',
|
|
42
|
+
'mon',
|
|
43
|
+
'tue',
|
|
44
|
+
'wed',
|
|
45
|
+
'thu',
|
|
46
|
+
'fri',
|
|
47
|
+
'sat'
|
|
48
|
+
];
|
|
49
|
+
/** "09:30" → 570; anything else → `null`. */ function minutesOf(value) {
|
|
50
|
+
const match = typeof value === 'string' ? value.trim().match(/^(\d{1,2}):(\d{2})$/) : null;
|
|
51
|
+
if (!match) return null;
|
|
52
|
+
const hours = Number(match[1]);
|
|
53
|
+
const minutes = Number(match[2]);
|
|
54
|
+
if (hours > 24 || minutes > 59 || hours === 24 && minutes > 0) return null;
|
|
55
|
+
return hours * 60 + minutes;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The writer's content for an item. Hours that cannot be read are handed on
|
|
59
|
+
* as a window the writer refuses, so the item fails with the writer's own
|
|
60
|
+
* sentence rather than being stored with hours nobody asked for.
|
|
61
|
+
*/ export function bookingServiceDraftContentFromArgs(args) {
|
|
62
|
+
const opens = minutesOf(args['opensAt']);
|
|
63
|
+
const closes = minutesOf(args['closesAt']);
|
|
64
|
+
const window = opens === null || closes === null ? {
|
|
65
|
+
start: -1,
|
|
66
|
+
end: -1
|
|
67
|
+
} : {
|
|
68
|
+
start: opens,
|
|
69
|
+
end: closes
|
|
70
|
+
};
|
|
71
|
+
const days = Array.isArray(args['days']) ? args['days'] : [];
|
|
72
|
+
const windows = {};
|
|
73
|
+
for (const day of days){
|
|
74
|
+
const weekday = BOOKING_SERVICE_AI_DAYS.indexOf(day);
|
|
75
|
+
if (weekday >= 0) windows[weekday] = [
|
|
76
|
+
window
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
const copied = [
|
|
80
|
+
'name',
|
|
81
|
+
'durationMinutes',
|
|
82
|
+
'description',
|
|
83
|
+
'timezone',
|
|
84
|
+
'priceDisplay',
|
|
85
|
+
'priceUsd',
|
|
86
|
+
'askPhone',
|
|
87
|
+
'askAddress'
|
|
88
|
+
];
|
|
89
|
+
return _extends({}, Object.fromEntries(copied.filter((key)=>args[key] !== undefined).map((key)=>[
|
|
90
|
+
key,
|
|
91
|
+
args[key]
|
|
92
|
+
])), {
|
|
93
|
+
windows
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export const bookingServiceAiCapability = {
|
|
97
|
+
op: BOOKING_SERVICE_AI_OP,
|
|
98
|
+
noun: 'booking service',
|
|
99
|
+
where: 'Bookings → Services',
|
|
100
|
+
intents: [
|
|
101
|
+
'a service visitors can book online, with its length, weekly hours and price — set up as a draft to activate',
|
|
102
|
+
'a way for visitors to book an appointment, consultation or estimate visit'
|
|
103
|
+
],
|
|
104
|
+
argsSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
name: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'What the service is called on the booking widget, e.g. "Free estimate visit".',
|
|
110
|
+
maxLength: BOOKING_SERVICE_NAME_MAX
|
|
111
|
+
},
|
|
112
|
+
durationMinutes: {
|
|
113
|
+
type: 'integer',
|
|
114
|
+
description: 'How long one booking takes, in minutes.',
|
|
115
|
+
minimum: BOOKING_MIN_DURATION_MINUTES,
|
|
116
|
+
maximum: BOOKING_MAX_DURATION_MINUTES
|
|
117
|
+
},
|
|
118
|
+
description: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
description: 'One or two sentences on what the visitor is booking.',
|
|
121
|
+
maxLength: BOOKING_SERVICE_DESCRIPTION_MAX
|
|
122
|
+
},
|
|
123
|
+
days: {
|
|
124
|
+
type: 'array',
|
|
125
|
+
description: 'The weekdays it can be booked on.',
|
|
126
|
+
items: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
enum: BOOKING_SERVICE_AI_DAYS
|
|
129
|
+
},
|
|
130
|
+
maxItems: 7
|
|
131
|
+
},
|
|
132
|
+
opensAt: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description: 'When bookable hours start on those days, 24-hour "HH:MM", e.g. "09:00".',
|
|
135
|
+
maxLength: 5
|
|
136
|
+
},
|
|
137
|
+
closesAt: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'When bookable hours end on those days, 24-hour "HH:MM", e.g. "17:00".',
|
|
140
|
+
maxLength: 5
|
|
141
|
+
},
|
|
142
|
+
timezone: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'The IANA time zone the hours are in, e.g. "America/Chicago".',
|
|
145
|
+
maxLength: 64
|
|
146
|
+
},
|
|
147
|
+
priceDisplay: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'How the price is stated: "fixed" charges priceUsd when booked; "varies", "estimate" ' + '(a free estimate) and "contact" show a label and book with no charge. Use "fixed" only ' + 'when the request states the price; otherwise "contact" unless it says otherwise.',
|
|
150
|
+
enum: BOOKING_PRICE_DISPLAYS
|
|
151
|
+
},
|
|
152
|
+
priceUsd: {
|
|
153
|
+
type: 'integer',
|
|
154
|
+
description: 'The price in whole dollars, only with priceDisplay "fixed" and only when the request states it.',
|
|
155
|
+
minimum: 0,
|
|
156
|
+
maximum: BOOKING_SERVICE_PRICE_MAX_USD
|
|
157
|
+
},
|
|
158
|
+
askPhone: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'Whether the booking form asks for a phone number.',
|
|
161
|
+
enum: BOOKING_FIELD_ASKS
|
|
162
|
+
},
|
|
163
|
+
askAddress: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
description: 'Whether the booking form asks for the address the job is at, for work done on site.',
|
|
166
|
+
enum: BOOKING_FIELD_ASKS
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
required: [
|
|
170
|
+
'name',
|
|
171
|
+
'durationMinutes',
|
|
172
|
+
'days',
|
|
173
|
+
'opensAt',
|
|
174
|
+
'closesAt',
|
|
175
|
+
'timezone'
|
|
176
|
+
],
|
|
177
|
+
additionalProperties: false
|
|
178
|
+
},
|
|
179
|
+
maxPerPlan: 5,
|
|
180
|
+
// Free includes no services (`servicesPerHost: 0`) and no bookings.
|
|
181
|
+
freeAllowed: false,
|
|
182
|
+
feature: 'bookings',
|
|
183
|
+
quota: 'servicesPerHost',
|
|
184
|
+
draftResource: BOOKING_SERVICE_DRAFT_RESOURCE,
|
|
185
|
+
// Written without a model: the planner already filled the arguments.
|
|
186
|
+
estimateCredits: ()=>0,
|
|
187
|
+
degrade: 'omit',
|
|
188
|
+
// A page that books this service places the Booking block.
|
|
189
|
+
pageBlocks: [
|
|
190
|
+
'booking'
|
|
191
|
+
],
|
|
192
|
+
draftContent: (item)=>bookingServiceDraftContentFromArgs(item.args)
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Registers the capability; the console surface calls it beside the writer,
|
|
196
|
+
* since only the console runs AI jobs (AGL-3026). Idempotent.
|
|
197
|
+
*/ export function registerBookingServiceAiCapability() {
|
|
198
|
+
registerPluginAiCapability(bookingServiceAiCapability, {
|
|
199
|
+
pluginId: BUNDLE_ID
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
//# sourceMappingURL=booking-service-ai-capability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/server/booking-service-ai-capability.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 registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { BOOKING_FIELD_ASKS } from '../model/booking-contact-fields'\nimport { BOOKING_PRICE_DISPLAYS } from '../model/booking-price'\nimport { BOOKING_MAX_DURATION_MINUTES, BOOKING_MIN_DURATION_MINUTES } from '../model/bookings'\nimport {\n BOOKING_SERVICE_DESCRIPTION_MAX,\n BOOKING_SERVICE_DRAFT_RESOURCE,\n BOOKING_SERVICE_NAME_MAX,\n BOOKING_SERVICE_PRICE_MAX_USD,\n} from './booking-service-drafts'\n\n/**\n * WHAT AN AI BUILD CAN MAKE IN BOOKINGS (AGL-3616): a bookable service, as a\n * draft.\n *\n * The capability the AI plugin's build planner offers for \"a way to book\n * me\". Its item is written by this plugin's `booking-service` draft writer,\n * so every rule is the writer's: the plan's `bookings` feature and service\n * allowance, the member's role, the shape a service takes. The service is\n * born a draft and takes no bookings until a person activates it on the\n * Bookings page.\n *\n * The arguments are flat, as the contract requires: one set of opening hours\n * on the weekdays named, which `draftContent` turns into the weekly windows\n * the service stores. A page that places the Booking block depends on the\n * service; when the service cannot be made, the page is built without the\n * block (`degrade: 'omit'`).\n */\n\n/** The operation's name in a build plan. */\nexport const BOOKING_SERVICE_AI_OP = 'booking-service'\n\n/** Weekday names as the planner writes them, in the model's order (0 = Sunday). */\nexport const BOOKING_SERVICE_AI_DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] as const\n\n/** \"09:30\" → 570; anything else → `null`. */\nfunction minutesOf(value: unknown): number | null {\n const match = typeof value === 'string' ? value.trim().match(/^(\\d{1,2}):(\\d{2})$/) : null\n if (!match) return null\n const hours = Number(match[1])\n const minutes = Number(match[2])\n if (hours > 24 || minutes > 59 || (hours === 24 && minutes > 0)) return null\n return hours * 60 + minutes\n}\n\n/**\n * The writer's content for an item. Hours that cannot be read are handed on\n * as a window the writer refuses, so the item fails with the writer's own\n * sentence rather than being stored with hours nobody asked for.\n */\nexport function bookingServiceDraftContentFromArgs(\n args: PluginAiCapabilityArgs,\n): Readonly<Record<string, unknown>> {\n const opens = minutesOf(args['opensAt'])\n const closes = minutesOf(args['closesAt'])\n const window = opens === null || closes === null ? { start: -1, end: -1 } : { start: opens, end: closes }\n const days = Array.isArray(args['days']) ? (args['days'] as readonly string[]) : []\n const windows: Record<number, Array<{ start: number; end: number }>> = {}\n for (const day of days) {\n const weekday = (BOOKING_SERVICE_AI_DAYS as readonly string[]).indexOf(day)\n if (weekday >= 0) windows[weekday] = [window]\n }\n const copied = ['name', 'durationMinutes', 'description', 'timezone', 'priceDisplay', 'priceUsd', 'askPhone', 'askAddress']\n return {\n ...Object.fromEntries(copied.filter((key) => args[key] !== undefined).map((key) => [key, args[key]])),\n windows,\n }\n}\n\nexport const bookingServiceAiCapability: PluginAiCapability = {\n op: BOOKING_SERVICE_AI_OP,\n noun: 'booking service',\n where: 'Bookings → Services',\n intents: [\n 'a service visitors can book online, with its length, weekly hours and price — set up as a draft to activate',\n 'a way for visitors to book an appointment, consultation or estimate visit',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'What the service is called on the booking widget, e.g. \"Free estimate visit\".',\n maxLength: BOOKING_SERVICE_NAME_MAX,\n },\n durationMinutes: {\n type: 'integer',\n description: 'How long one booking takes, in minutes.',\n minimum: BOOKING_MIN_DURATION_MINUTES,\n maximum: BOOKING_MAX_DURATION_MINUTES,\n },\n description: {\n type: 'string',\n description: 'One or two sentences on what the visitor is booking.',\n maxLength: BOOKING_SERVICE_DESCRIPTION_MAX,\n },\n days: {\n type: 'array',\n description: 'The weekdays it can be booked on.',\n items: { type: 'string', enum: BOOKING_SERVICE_AI_DAYS },\n maxItems: 7,\n },\n opensAt: {\n type: 'string',\n description: 'When bookable hours start on those days, 24-hour \"HH:MM\", e.g. \"09:00\".',\n maxLength: 5,\n },\n closesAt: {\n type: 'string',\n description: 'When bookable hours end on those days, 24-hour \"HH:MM\", e.g. \"17:00\".',\n maxLength: 5,\n },\n timezone: {\n type: 'string',\n description: 'The IANA time zone the hours are in, e.g. \"America/Chicago\".',\n maxLength: 64,\n },\n priceDisplay: {\n type: 'string',\n description:\n 'How the price is stated: \"fixed\" charges priceUsd when booked; \"varies\", \"estimate\" ' +\n '(a free estimate) and \"contact\" show a label and book with no charge. Use \"fixed\" only ' +\n 'when the request states the price; otherwise \"contact\" unless it says otherwise.',\n enum: BOOKING_PRICE_DISPLAYS,\n },\n priceUsd: {\n type: 'integer',\n description: 'The price in whole dollars, only with priceDisplay \"fixed\" and only when the request states it.',\n minimum: 0,\n maximum: BOOKING_SERVICE_PRICE_MAX_USD,\n },\n askPhone: {\n type: 'string',\n description: 'Whether the booking form asks for a phone number.',\n enum: BOOKING_FIELD_ASKS,\n },\n askAddress: {\n type: 'string',\n description: 'Whether the booking form asks for the address the job is at, for work done on site.',\n enum: BOOKING_FIELD_ASKS,\n },\n },\n required: ['name', 'durationMinutes', 'days', 'opensAt', 'closesAt', 'timezone'],\n additionalProperties: false,\n },\n maxPerPlan: 5,\n // Free includes no services (`servicesPerHost: 0`) and no bookings.\n freeAllowed: false,\n feature: 'bookings',\n quota: 'servicesPerHost',\n draftResource: BOOKING_SERVICE_DRAFT_RESOURCE,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n // A page that books this service places the Booking block.\n pageBlocks: ['booking'],\n draftContent: (item) => bookingServiceDraftContentFromArgs(item.args),\n}\n\n/**\n * Registers the capability; the console surface calls it beside the writer,\n * since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerBookingServiceAiCapability(): void {\n registerPluginAiCapability(bookingServiceAiCapability, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","BOOKING_FIELD_ASKS","BOOKING_PRICE_DISPLAYS","BOOKING_MAX_DURATION_MINUTES","BOOKING_MIN_DURATION_MINUTES","BOOKING_SERVICE_DESCRIPTION_MAX","BOOKING_SERVICE_DRAFT_RESOURCE","BOOKING_SERVICE_NAME_MAX","BOOKING_SERVICE_PRICE_MAX_USD","BOOKING_SERVICE_AI_OP","BOOKING_SERVICE_AI_DAYS","minutesOf","value","match","trim","hours","Number","minutes","bookingServiceDraftContentFromArgs","args","opens","closes","window","start","end","days","Array","isArray","windows","day","weekday","indexOf","copied","Object","fromEntries","filter","key","undefined","map","bookingServiceAiCapability","op","noun","where","intents","argsSchema","type","properties","name","description","maxLength","durationMinutes","minimum","maximum","items","enum","maxItems","opensAt","closesAt","timezone","priceDisplay","priceUsd","askPhone","askAddress","required","additionalProperties","maxPerPlan","freeAllowed","feature","quota","draftResource","estimateCredits","degrade","pageBlocks","draftContent","item","registerBookingServiceAiCapability","pluginId"],"mappings":"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"}
|
|
@@ -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
|
+
*/
|
|
17
|
+
import { type PluginDraftCheck, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
|
|
18
|
+
import { type BookingFieldAsk } from '../model/booking-contact-fields';
|
|
19
|
+
import { type BookingPriceDisplay } from '../model/booking-price';
|
|
20
|
+
import { type HostBookingService } from '../model/bookings';
|
|
21
|
+
/**
|
|
22
|
+
* A BOOKING SERVICE ANOTHER PLUGIN ASKS FOR (AGL-3616).
|
|
23
|
+
*
|
|
24
|
+
* The draft writer this plugin registers for the `booking-service` resource
|
|
25
|
+
* on the core's resource-drafts seam. A plugin that sets a site up from a
|
|
26
|
+
* brief asks for the writer by name and gets this plugin's rules:
|
|
27
|
+
*
|
|
28
|
+
* - THE DOCUMENT is a `hosts/{hostId}/services/{id}` service in the shape
|
|
29
|
+
* the console's service dialog saves, born `status: 'draft'`. A draft is
|
|
30
|
+
* offered nowhere — not in the Booking block's list, not by its slots, not
|
|
31
|
+
* by the booking route — until a person activates it on the Bookings page.
|
|
32
|
+
* - THE ROOM is the `servicesPerHost` allowance, counted the way the
|
|
33
|
+
* resources route counts it and inside the transaction that creates, so
|
|
34
|
+
* two writes at once cannot both take the last slot. Free includes none.
|
|
35
|
+
* - THE PLAN is the `bookings` feature, and THE ROLE is a member who may
|
|
36
|
+
* write the site's content — the resources route's two gates, refused in
|
|
37
|
+
* its words.
|
|
38
|
+
* - THE PRICE is a label (`contact`, `varies`, `estimate`) unless the
|
|
39
|
+
* content states a fixed price, so nothing the caller did not say is
|
|
40
|
+
* charged. No price stated and no label asked for is `contact`.
|
|
41
|
+
*
|
|
42
|
+
* Nothing is published or charged. The writer touches the one new document.
|
|
43
|
+
*/
|
|
44
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
45
|
+
/** The resource name this writer is registered under. */
|
|
46
|
+
export declare const BOOKING_SERVICE_DRAFT_RESOURCE = "booking-service";
|
|
47
|
+
/** The longest name the console's dialog keeps. */
|
|
48
|
+
export declare const BOOKING_SERVICE_NAME_MAX = 80;
|
|
49
|
+
/** The longest description the console's dialog keeps. */
|
|
50
|
+
export declare const BOOKING_SERVICE_DESCRIPTION_MAX = 500;
|
|
51
|
+
/** The most open intervals one weekday holds. */
|
|
52
|
+
export declare const BOOKING_SERVICE_WINDOWS_PER_DAY_MAX = 12;
|
|
53
|
+
/** The highest fixed price a draft may state, in whole dollars. */
|
|
54
|
+
export declare const BOOKING_SERVICE_PRICE_MAX_USD = 100000;
|
|
55
|
+
/** The resources route's refusal for a member who may not write the site. */
|
|
56
|
+
export declare const BOOKING_SERVICE_ROLE_REFUSAL = "Editing requires the editor role";
|
|
57
|
+
/** The resources route's refusal for a plan without the feature. */
|
|
58
|
+
export declare const BOOKING_SERVICE_PLAN_REFUSAL = "This feature is not included in your plan \u2014 see Billing";
|
|
59
|
+
/** The resources route's refusal at the plan's allowance. */
|
|
60
|
+
export declare function bookingServiceLimitRefusal(limit: number): string;
|
|
61
|
+
/**
|
|
62
|
+
* What a caller sends as `content`. `windows` is keyed by weekday
|
|
63
|
+
* (0 = Sunday … 6 = Saturday), each a list of `{ start, end }` in minutes
|
|
64
|
+
* since midnight in `timezone`. `priceUsd` is read only with
|
|
65
|
+
* `priceDisplay: 'fixed'`, which needs it.
|
|
66
|
+
*/
|
|
67
|
+
export interface BookingServiceDraftContent {
|
|
68
|
+
name: string;
|
|
69
|
+
durationMinutes: number;
|
|
70
|
+
description?: string;
|
|
71
|
+
windows: Partial<Record<number, Array<{
|
|
72
|
+
start: number;
|
|
73
|
+
end: number;
|
|
74
|
+
}>>>;
|
|
75
|
+
timezone: string;
|
|
76
|
+
priceDisplay?: BookingPriceDisplay;
|
|
77
|
+
priceUsd?: number;
|
|
78
|
+
askPhone?: BookingFieldAsk;
|
|
79
|
+
askAddress?: BookingFieldAsk;
|
|
80
|
+
crmFollowUpTask?: boolean;
|
|
81
|
+
crmMeetingActivity?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** The service document's editable fields, as the writer stores them. */
|
|
84
|
+
export type BookingServiceDraftFields = Required<Pick<HostBookingService, 'name' | 'durationMinutes' | 'windows' | 'timezone' | 'priceDisplay' | 'priceUsd' | 'askPhone' | 'askAddress' | 'crmFollowUpTask' | 'crmMeetingActivity'>> & Pick<HostBookingService, 'description'>;
|
|
85
|
+
export type BookingServiceContentRead = {
|
|
86
|
+
ok: true;
|
|
87
|
+
value: BookingServiceDraftFields;
|
|
88
|
+
} | {
|
|
89
|
+
ok: false;
|
|
90
|
+
problems: string[];
|
|
91
|
+
};
|
|
92
|
+
/** The content as the writer stores it, or every problem that stops it. */
|
|
93
|
+
export declare function readBookingServiceContent(content: Readonly<Record<string, unknown>>): BookingServiceContentRead;
|
|
94
|
+
/** Whether content is a service this plugin would store, with what it says about it. Pure. */
|
|
95
|
+
export declare function checkBookingServiceContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck;
|
|
96
|
+
export interface BookingServiceDraftWriterDeps {
|
|
97
|
+
/** The Admin SDK handle; specs hand in a double. */
|
|
98
|
+
firestore?: () => Firestore;
|
|
99
|
+
}
|
|
100
|
+
export declare function createBookingServiceDraftWriter(deps?: BookingServiceDraftWriterDeps): PluginResourceDraftWriter;
|
|
101
|
+
export declare const bookingServiceDraftWriter: PluginResourceDraftWriter;
|
|
102
|
+
/**
|
|
103
|
+
* Registers the writer; the console surface calls it, since only the console
|
|
104
|
+
* runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
|
|
105
|
+
*/
|
|
106
|
+
export declare function registerBookingServiceDraftWriter(): void;
|
|
107
|
+
export {};
|
|
@@ -0,0 +1,320 @@
|
|
|
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 { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
|
|
18
|
+
import { checkEntitlement, checkQuota } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
19
|
+
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
20
|
+
import { isValidTimeZone } from "@aglyn/shared-util-timestamp/zoned-time";
|
|
21
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
22
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
23
|
+
import { BOOKING_FIELD_ASKS } from "../model/booking-contact-fields.js";
|
|
24
|
+
import { BOOKING_PRICE_DISPLAYS, bookingPriceText } from "../model/booking-price.js";
|
|
25
|
+
import { BOOKING_MAX_DURATION_MINUTES, BOOKING_MIN_DURATION_MINUTES } from "../model/bookings.js";
|
|
26
|
+
/** The resource name this writer is registered under. */ export const BOOKING_SERVICE_DRAFT_RESOURCE = 'booking-service';
|
|
27
|
+
/** The longest name the console's dialog keeps. */ export const BOOKING_SERVICE_NAME_MAX = 80;
|
|
28
|
+
/** The longest description the console's dialog keeps. */ export const BOOKING_SERVICE_DESCRIPTION_MAX = 500;
|
|
29
|
+
/** The most open intervals one weekday holds. */ export const BOOKING_SERVICE_WINDOWS_PER_DAY_MAX = 12;
|
|
30
|
+
/** The highest fixed price a draft may state, in whole dollars. */ export const BOOKING_SERVICE_PRICE_MAX_USD = 100000;
|
|
31
|
+
/** The resources route's refusal for a member who may not write the site. */ export const BOOKING_SERVICE_ROLE_REFUSAL = 'Editing requires the editor role';
|
|
32
|
+
/** The resources route's refusal for a plan without the feature. */ export const BOOKING_SERVICE_PLAN_REFUSAL = 'This feature is not included in your plan — see Billing';
|
|
33
|
+
/** The resources route's refusal at the plan's allowance. */ export function bookingServiceLimitRefusal(limit) {
|
|
34
|
+
return `Your plan includes ${limit} ${limit === 1 ? 'service' : 'services'} — ` + 'upgrade in Billing for more';
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
/** Text on one line: controls and runs of space folded to one space. */ function line(value) {
|
|
40
|
+
return typeof value === 'string' ? value.replace(RegExp("[\\u0000-\\u001F\\u007F-\\u009F\\s]+", "gu"), ' ').trim() : '';
|
|
41
|
+
}
|
|
42
|
+
/** The weekly windows, or what is wrong with them. */ function readWindows(value, problems) {
|
|
43
|
+
const windows = {};
|
|
44
|
+
if (!isRecord(value)) {
|
|
45
|
+
problems.push('The service needs weekly hours: at least one open window on one weekday');
|
|
46
|
+
return windows;
|
|
47
|
+
}
|
|
48
|
+
for (const [key, raw] of Object.entries(value)){
|
|
49
|
+
const weekday = Number(key);
|
|
50
|
+
if (!/^[0-6]$/.test(key) || !Number.isInteger(weekday)) {
|
|
51
|
+
problems.push(`"${key}" is not a weekday: weekdays are 0 (Sunday) to 6 (Saturday)`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(raw)) {
|
|
55
|
+
problems.push(`The hours for weekday ${key} are not a list of windows`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!raw.length) continue;
|
|
59
|
+
if (raw.length > BOOKING_SERVICE_WINDOWS_PER_DAY_MAX) {
|
|
60
|
+
problems.push(`Weekday ${key} has more than ${BOOKING_SERVICE_WINDOWS_PER_DAY_MAX} windows`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const day = [];
|
|
64
|
+
for (const window of raw){
|
|
65
|
+
const start = isRecord(window) ? window['start'] : undefined;
|
|
66
|
+
const end = isRecord(window) ? window['end'] : undefined;
|
|
67
|
+
if (typeof start !== 'number' || typeof end !== 'number' || !Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end > 24 * 60 || end <= start) {
|
|
68
|
+
problems.push(`A window on weekday ${key} is not a start before its end, in whole minutes from 0 to 1440`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
day.push({
|
|
72
|
+
start,
|
|
73
|
+
end
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
day.sort((a, b)=>a.start - b.start);
|
|
77
|
+
if (day.some((window, index)=>index > 0 && window.start < day[index - 1].end)) {
|
|
78
|
+
problems.push(`Two windows on weekday ${key} overlap`);
|
|
79
|
+
}
|
|
80
|
+
if (day.length) windows[weekday] = day;
|
|
81
|
+
}
|
|
82
|
+
if (!Object.keys(windows).length && !problems.length) {
|
|
83
|
+
problems.push('The service needs weekly hours: at least one open window on one weekday');
|
|
84
|
+
}
|
|
85
|
+
return windows;
|
|
86
|
+
}
|
|
87
|
+
function readAsk(label, value, problems) {
|
|
88
|
+
if (value === undefined) return 'off';
|
|
89
|
+
if (BOOKING_FIELD_ASKS.includes(value)) return value;
|
|
90
|
+
problems.push(`Asking for ${label} is off, optional or required`);
|
|
91
|
+
return 'off';
|
|
92
|
+
}
|
|
93
|
+
function readSwitch(label, value, fallback, problems) {
|
|
94
|
+
if (value === undefined) return fallback;
|
|
95
|
+
if (typeof value === 'boolean') return value;
|
|
96
|
+
problems.push(`${label} is on or off`);
|
|
97
|
+
return fallback;
|
|
98
|
+
}
|
|
99
|
+
/** The content as the writer stores it, or every problem that stops it. */ export function readBookingServiceContent(content) {
|
|
100
|
+
const problems = [];
|
|
101
|
+
const name = line(content['name']);
|
|
102
|
+
if (!name) problems.push('The service needs a name');
|
|
103
|
+
else if (name.length > BOOKING_SERVICE_NAME_MAX) {
|
|
104
|
+
problems.push(`The name is longer than ${BOOKING_SERVICE_NAME_MAX} characters`);
|
|
105
|
+
}
|
|
106
|
+
const duration = content['durationMinutes'];
|
|
107
|
+
if (typeof duration !== 'number' || !Number.isInteger(duration) || duration < BOOKING_MIN_DURATION_MINUTES || duration > BOOKING_MAX_DURATION_MINUTES) {
|
|
108
|
+
problems.push(`The length is whole minutes from ${BOOKING_MIN_DURATION_MINUTES} to ${BOOKING_MAX_DURATION_MINUTES}`);
|
|
109
|
+
}
|
|
110
|
+
const rawDescription = content['description'];
|
|
111
|
+
const description = typeof rawDescription === 'string' ? rawDescription.trim() : '';
|
|
112
|
+
if (rawDescription !== undefined && typeof rawDescription !== 'string') {
|
|
113
|
+
problems.push('The description is text');
|
|
114
|
+
} else if (description.length > BOOKING_SERVICE_DESCRIPTION_MAX) {
|
|
115
|
+
problems.push(`The description is longer than ${BOOKING_SERVICE_DESCRIPTION_MAX} characters`);
|
|
116
|
+
}
|
|
117
|
+
const timezone = line(content['timezone']);
|
|
118
|
+
if (!isValidTimeZone(timezone)) {
|
|
119
|
+
problems.push('The time zone is an IANA zone, such as America/Chicago');
|
|
120
|
+
}
|
|
121
|
+
const windows = readWindows(content['windows'], problems);
|
|
122
|
+
// No price stated and no label asked for is "Contact for price": a draft
|
|
123
|
+
// never charges what the caller did not say.
|
|
124
|
+
const rawPrice = content['priceUsd'];
|
|
125
|
+
const askedDisplay = content['priceDisplay'];
|
|
126
|
+
let priceDisplay = 'contact';
|
|
127
|
+
let priceUsd = 0;
|
|
128
|
+
if (askedDisplay !== undefined) {
|
|
129
|
+
if (BOOKING_PRICE_DISPLAYS.includes(askedDisplay)) {
|
|
130
|
+
priceDisplay = askedDisplay;
|
|
131
|
+
} else {
|
|
132
|
+
problems.push('The price is stated as fixed, varies, estimate or contact');
|
|
133
|
+
}
|
|
134
|
+
} else if (rawPrice !== undefined) {
|
|
135
|
+
priceDisplay = 'fixed';
|
|
136
|
+
}
|
|
137
|
+
if (priceDisplay === 'fixed') {
|
|
138
|
+
if (typeof rawPrice !== 'number' || !Number.isInteger(rawPrice) || rawPrice < 0 || rawPrice > BOOKING_SERVICE_PRICE_MAX_USD) {
|
|
139
|
+
problems.push(`A fixed price is whole dollars from 0 to ${BOOKING_SERVICE_PRICE_MAX_USD}; ` + 'state it, or say the price varies, is a free estimate, or is on request');
|
|
140
|
+
} else {
|
|
141
|
+
priceUsd = rawPrice;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const askPhone = readAsk('a phone number', content['askPhone'], problems);
|
|
145
|
+
const askAddress = readAsk('an address', content['askAddress'], problems);
|
|
146
|
+
// The model's defaults: a booking files a meeting, and owes no follow-up.
|
|
147
|
+
const crmMeetingActivity = readSwitch('Filing a meeting', content['crmMeetingActivity'], true, problems);
|
|
148
|
+
const crmFollowUpTask = readSwitch('A follow-up task', content['crmFollowUpTask'], false, problems);
|
|
149
|
+
if (problems.length) return {
|
|
150
|
+
ok: false,
|
|
151
|
+
problems: [
|
|
152
|
+
...new Set(problems)
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
value: _extends({
|
|
158
|
+
name,
|
|
159
|
+
durationMinutes: duration
|
|
160
|
+
}, description ? {
|
|
161
|
+
description
|
|
162
|
+
} : {}, {
|
|
163
|
+
windows,
|
|
164
|
+
timezone,
|
|
165
|
+
priceDisplay,
|
|
166
|
+
priceUsd,
|
|
167
|
+
askPhone,
|
|
168
|
+
askAddress,
|
|
169
|
+
crmMeetingActivity,
|
|
170
|
+
crmFollowUpTask
|
|
171
|
+
})
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** What a caller is told about a service, written or about to be. */ function factsOf(value) {
|
|
175
|
+
var _value_priceDisplay, _value_timezone, _value_windows;
|
|
176
|
+
return {
|
|
177
|
+
status: 'draft',
|
|
178
|
+
durationMinutes: value.durationMinutes,
|
|
179
|
+
priceDisplay: (_value_priceDisplay = value.priceDisplay) != null ? _value_priceDisplay : 'fixed',
|
|
180
|
+
priceText: bookingPriceText(value),
|
|
181
|
+
timezone: (_value_timezone = value.timezone) != null ? _value_timezone : 'UTC',
|
|
182
|
+
openWeekdays: Object.keys((_value_windows = value.windows) != null ? _value_windows : {}).map(Number).sort((a, b)=>a - b)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Whether content is a service this plugin would store, with what it says about it. Pure. */ export function checkBookingServiceContent(content) {
|
|
186
|
+
const read = readBookingServiceContent(content);
|
|
187
|
+
if (read.ok === false) return read;
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
facts: factsOf(read.value)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function recordOf(service) {
|
|
194
|
+
var _service_data, _data_name;
|
|
195
|
+
const data = (_service_data = service.data()) != null ? _service_data : {};
|
|
196
|
+
return {
|
|
197
|
+
id: service.id,
|
|
198
|
+
name: String((_data_name = data.name) != null ? _data_name : ''),
|
|
199
|
+
versionId: null,
|
|
200
|
+
facts: _extends({}, factsOf(data), {
|
|
201
|
+
status: data.status === 'draft' ? 'draft' : 'active'
|
|
202
|
+
})
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function roleRefusal(host, uid) {
|
|
206
|
+
var _host_get;
|
|
207
|
+
const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
|
|
208
|
+
return hostRoleCanWrite(role) ? null : {
|
|
209
|
+
status: 403,
|
|
210
|
+
error: BOOKING_SERVICE_ROLE_REFUSAL
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function planRefusal(org) {
|
|
214
|
+
return checkEntitlement(org, 'bookings') ? null : {
|
|
215
|
+
status: 403,
|
|
216
|
+
error: BOOKING_SERVICE_PLAN_REFUSAL
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The allowance, counted as `/api/hosts/resources` counts a service create:
|
|
221
|
+
* every document in the collection.
|
|
222
|
+
*/ function roomRefusal(org, used) {
|
|
223
|
+
const quota = checkQuota(org, 'servicesPerHost', used);
|
|
224
|
+
return quota.allowed ? null : {
|
|
225
|
+
status: 403,
|
|
226
|
+
error: bookingServiceLimitRefusal(quota.limit)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export function createBookingServiceDraftWriter(deps = {}) {
|
|
230
|
+
var _deps_firestore;
|
|
231
|
+
const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore();
|
|
232
|
+
return {
|
|
233
|
+
refusal: async (context)=>{
|
|
234
|
+
var _roleRefusal;
|
|
235
|
+
const hostRef = firestore().collection('hosts').doc(context.hostId);
|
|
236
|
+
const host = await hostRef.get();
|
|
237
|
+
if (!host.exists) return {
|
|
238
|
+
status: 404,
|
|
239
|
+
error: 'Unknown site'
|
|
240
|
+
};
|
|
241
|
+
const early = (_roleRefusal = roleRefusal(host, context.uid)) != null ? _roleRefusal : planRefusal(context.org);
|
|
242
|
+
if (early) return early;
|
|
243
|
+
const used = (await hostRef.collection('services').count().get()).data().count;
|
|
244
|
+
return roomRefusal(context.org, Number(used) || 0);
|
|
245
|
+
},
|
|
246
|
+
check: (content)=>checkBookingServiceContent(content),
|
|
247
|
+
read: async ({ hostId, id })=>{
|
|
248
|
+
const service = await firestore().collection('hosts').doc(hostId).collection('services').doc(id).get();
|
|
249
|
+
return service.exists ? recordOf(service) : null;
|
|
250
|
+
},
|
|
251
|
+
write: async (request)=>{
|
|
252
|
+
// The request's name is the one asked for; the content's stands in.
|
|
253
|
+
const asked = line(request.name);
|
|
254
|
+
const read = readBookingServiceContent(asked ? _extends({}, request.content, {
|
|
255
|
+
name: asked
|
|
256
|
+
}) : request.content);
|
|
257
|
+
if (read.ok === false) return {
|
|
258
|
+
ok: false,
|
|
259
|
+
status: 400,
|
|
260
|
+
error: read.problems[0]
|
|
261
|
+
};
|
|
262
|
+
const db = firestore();
|
|
263
|
+
const hostRef = db.collection('hosts').doc(request.hostId);
|
|
264
|
+
const servicesRef = hostRef.collection('services');
|
|
265
|
+
const serviceRef = servicesRef.doc(request.id);
|
|
266
|
+
return db.runTransaction(async (tx)=>{
|
|
267
|
+
var _roleRefusal;
|
|
268
|
+
// Every read before any write, which Firestore requires.
|
|
269
|
+
const [host, existing] = await Promise.all([
|
|
270
|
+
tx.get(hostRef),
|
|
271
|
+
tx.get(serviceRef)
|
|
272
|
+
]);
|
|
273
|
+
if (!host.exists) return {
|
|
274
|
+
ok: false,
|
|
275
|
+
status: 404,
|
|
276
|
+
error: 'Unknown site'
|
|
277
|
+
};
|
|
278
|
+
// Asked again under the same id: the draft it already wrote.
|
|
279
|
+
if (existing.exists) return _extends({
|
|
280
|
+
ok: true,
|
|
281
|
+
replayed: true
|
|
282
|
+
}, recordOf(existing));
|
|
283
|
+
const early = (_roleRefusal = roleRefusal(host, request.uid)) != null ? _roleRefusal : planRefusal(request.org);
|
|
284
|
+
if (early) return _extends({
|
|
285
|
+
ok: false
|
|
286
|
+
}, early);
|
|
287
|
+
const used = (await tx.get(servicesRef.count())).data().count;
|
|
288
|
+
const room = roomRefusal(request.org, Number(used) || 0);
|
|
289
|
+
if (room) return _extends({
|
|
290
|
+
ok: false
|
|
291
|
+
}, room);
|
|
292
|
+
tx.create(serviceRef, _extends({}, read.value, {
|
|
293
|
+
status: 'draft',
|
|
294
|
+
createdAt: request.now,
|
|
295
|
+
updatedAt: request.now,
|
|
296
|
+
createdBy: request.uid
|
|
297
|
+
}));
|
|
298
|
+
return {
|
|
299
|
+
ok: true,
|
|
300
|
+
replayed: false,
|
|
301
|
+
id: request.id,
|
|
302
|
+
name: read.value.name,
|
|
303
|
+
versionId: null,
|
|
304
|
+
facts: factsOf(read.value)
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
export const bookingServiceDraftWriter = createBookingServiceDraftWriter();
|
|
311
|
+
/**
|
|
312
|
+
* Registers the writer; the console surface calls it, since only the console
|
|
313
|
+
* runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
|
|
314
|
+
*/ export function registerBookingServiceDraftWriter() {
|
|
315
|
+
registerPluginResourceDraftWriter(BOOKING_SERVICE_DRAFT_RESOURCE, bookingServiceDraftWriter, {
|
|
316
|
+
pluginId: BUNDLE_ID
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
//# sourceMappingURL=booking-service-drafts.js.map
|