@voyantjs/availability 0.52.0 → 0.52.2
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-allocation-automation.d.ts","sourceRoot":"","sources":["../src/service-allocation-automation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAQ5B,OAAO,EACL,KAAK,kBAAkB,EAIxB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,yBAAyB,EAK/B,MAAM,yBAAyB,CAAA;AAChC,OAAO,KAAK,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"service-allocation-automation.d.ts","sourceRoot":"","sources":["../src/service-allocation-automation.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,16 @@ import { and, eq, sql } from "drizzle-orm";
|
|
|
2
2
|
import { planRoomAllocation, planVehicleSeatAllocation, } from "./auto-allocator.js";
|
|
3
3
|
import { allocationResources, availabilitySlots, productOptionResourceTemplates, } from "./schema.js";
|
|
4
4
|
import { AllocationServiceError, getSlotAllocationManifest, recordAllocationAudit, } from "./service-allocation.js";
|
|
5
|
+
/**
|
|
6
|
+
* Emit `ARRAY[$1, $2, …]::text[]` so Postgres doesn't try to cast a
|
|
7
|
+
* tuple to `text[]`. See `sqlTextArray` in service-allocation.ts and
|
|
8
|
+
* issue #952.
|
|
9
|
+
*/
|
|
10
|
+
function sqlTextArray(values) {
|
|
11
|
+
if (values.length === 0)
|
|
12
|
+
return sql `ARRAY[]::text[]`;
|
|
13
|
+
return sql `ARRAY[${sql.join(values.map((value) => sql `${value}`), sql.raw(", "))}]::text[]`;
|
|
14
|
+
}
|
|
5
15
|
export async function listProductOptionResourceTemplates(db, productId) {
|
|
6
16
|
const optionRows = await executeRows(db, sql `
|
|
7
17
|
SELECT id, name, code, description, status, is_default, sort_order
|
|
@@ -15,7 +25,7 @@ export async function listProductOptionResourceTemplates(db, productId) {
|
|
|
15
25
|
const templateRows = await executeRows(db, sql `
|
|
16
26
|
SELECT id, product_option_id, kind, ref_type, ref_id, capacity, name_pattern, layout, default_count, flags, created_at, updated_at
|
|
17
27
|
FROM product_option_resource_templates
|
|
18
|
-
WHERE product_option_id = ANY(${optionIds}
|
|
28
|
+
WHERE product_option_id = ANY(${sqlTextArray(optionIds)})
|
|
19
29
|
ORDER BY kind, created_at
|
|
20
30
|
`);
|
|
21
31
|
const byOption = new Map();
|
|
@@ -128,17 +138,17 @@ export async function autoMaterializeAllocationResources(db, slotId, input, opti
|
|
|
128
138
|
AND b.status IN ('draft', 'on_hold', 'confirmed', 'in_progress', 'completed')
|
|
129
139
|
AND ba.status IN ('held', 'confirmed', 'fulfilled')
|
|
130
140
|
),
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
-- Pax per option = sum of booking_item quantities for items belonging
|
|
142
|
+
-- to bookings on this slot. The previous formulation joined
|
|
143
|
+
-- booking_travelers to booking_id, which cross-multiplied items × travelers
|
|
144
|
+
-- whenever a booking had more than one item (e.g. Adult + Senior rows
|
|
145
|
+
-- on the same booking inflated pax_count to 4 instead of 2).
|
|
146
|
+
pax AS (
|
|
147
|
+
SELECT bi.option_id, SUM(bi.quantity)::int AS pax_count
|
|
133
148
|
FROM booking_items bi
|
|
134
149
|
JOIN slot_bookings sb ON sb.booking_id = bi.booking_id
|
|
135
150
|
WHERE bi.option_id IS NOT NULL
|
|
136
|
-
|
|
137
|
-
pax AS (
|
|
138
|
-
SELECT si.option_id, COUNT(bt.id)::int AS pax_count
|
|
139
|
-
FROM slot_items si
|
|
140
|
-
JOIN booking_travelers bt ON bt.booking_id = si.booking_id
|
|
141
|
-
GROUP BY si.option_id
|
|
151
|
+
GROUP BY bi.option_id
|
|
142
152
|
)
|
|
143
153
|
SELECT
|
|
144
154
|
pax.option_id,
|
|
@@ -171,13 +181,19 @@ export async function autoMaterializeAllocationResources(db, slotId, input, opti
|
|
|
171
181
|
const unitsNeeded = Math.max(1, Math.ceil(group.pax_count / Math.max(1, group.capacity)));
|
|
172
182
|
for (let index = 0; index < unitsNeeded; index++) {
|
|
173
183
|
sequence += 1;
|
|
184
|
+
// Default the resource's ref to its materializing option so the UI
|
|
185
|
+
// can badge each row with the option name (e.g. Standard double).
|
|
186
|
+
// Templates that explicitly set ref_type/ref_id (e.g. pointing at a
|
|
187
|
+
// hotel inventory row) keep their own values.
|
|
188
|
+
const resolvedRefType = group.ref_type ?? "option";
|
|
189
|
+
const resolvedRefId = group.ref_id ?? group.option_id;
|
|
174
190
|
const [row] = await db
|
|
175
191
|
.insert(allocationResources)
|
|
176
192
|
.values({
|
|
177
193
|
slotId,
|
|
178
194
|
kind,
|
|
179
|
-
refType:
|
|
180
|
-
refId:
|
|
195
|
+
refType: resolvedRefType,
|
|
196
|
+
refId: resolvedRefId,
|
|
181
197
|
label: renderNamePattern(group.name_pattern, {
|
|
182
198
|
sequence: String(sequence),
|
|
183
199
|
option: group.option_name ?? "",
|
|
@@ -231,7 +247,7 @@ export async function autoAllocateSlotResources(db, slotId, input, options = {})
|
|
|
231
247
|
SELECT
|
|
232
248
|
row.traveler_id,
|
|
233
249
|
jsonb_build_object(${kind}::text, row.resource_id::text)
|
|
234
|
-
FROM unnest(${travelerIds}
|
|
250
|
+
FROM unnest(${sqlTextArray(travelerIds)}, ${sqlTextArray(resourceIds)}) AS row(traveler_id, resource_id)
|
|
235
251
|
ON CONFLICT (traveler_id) DO UPDATE SET
|
|
236
252
|
allocations =
|
|
237
253
|
COALESCE(booking_traveler_travel_details.allocations, '{}'::jsonb)
|
|
@@ -472,6 +488,16 @@ function renderNamePattern(pattern, vars) {
|
|
|
472
488
|
return pattern.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? "");
|
|
473
489
|
}
|
|
474
490
|
async function executeRows(db, query) {
|
|
475
|
-
const
|
|
476
|
-
|
|
491
|
+
const result = await db.execute(query);
|
|
492
|
+
if (Array.isArray(result))
|
|
493
|
+
return result;
|
|
494
|
+
// node-postgres / neon-serverless drivers return `{ rows, rowCount, ... }`
|
|
495
|
+
// instead of a bare array — unwrap so this wrapper is driver-agnostic.
|
|
496
|
+
if (result &&
|
|
497
|
+
typeof result === "object" &&
|
|
498
|
+
"rows" in result &&
|
|
499
|
+
Array.isArray(result.rows)) {
|
|
500
|
+
return result.rows;
|
|
501
|
+
}
|
|
502
|
+
return [];
|
|
477
503
|
}
|
|
@@ -143,7 +143,7 @@ export async function getSlotsResourceAvailability(db, slotIds) {
|
|
|
143
143
|
AND b.status IN ('draft', 'on_hold', 'confirmed', 'in_progress', 'completed')
|
|
144
144
|
AND ba.status IN ('held', 'confirmed', 'fulfilled')
|
|
145
145
|
) usage ON true
|
|
146
|
-
WHERE ar.slot_id = ANY(${uniqueIds}
|
|
146
|
+
WHERE ar.slot_id = ANY(${sqlTextArray(uniqueIds)})
|
|
147
147
|
ORDER BY ar.slot_id, ar.kind, ar.sort_order, ar.created_at
|
|
148
148
|
`);
|
|
149
149
|
const out = new Map();
|
|
@@ -206,7 +206,7 @@ export async function validateSlotAllocationCapacity(db, slotId, planned) {
|
|
|
206
206
|
const resources = await executeRows(db, sql `
|
|
207
207
|
SELECT id, kind, capacity, slot_id
|
|
208
208
|
FROM allocation_resources
|
|
209
|
-
WHERE slot_id = ${slotId} AND id = ANY(${resourceIds}
|
|
209
|
+
WHERE slot_id = ${slotId} AND id = ANY(${sqlTextArray(resourceIds)})
|
|
210
210
|
FOR UPDATE
|
|
211
211
|
`);
|
|
212
212
|
const resourceById = new Map(resources.map((r) => [r.id, r]));
|
|
@@ -246,7 +246,7 @@ export async function validateSlotAllocationCapacity(db, slotId, planned) {
|
|
|
246
246
|
AND ba.availability_slot_id = ${slotId}
|
|
247
247
|
AND b.status IN ('draft', 'on_hold', 'confirmed', 'in_progress', 'completed')
|
|
248
248
|
AND ba.status IN ('held', 'confirmed', 'fulfilled')
|
|
249
|
-
AND btd.traveler_id <> ALL(${travelerIdsArr}
|
|
249
|
+
AND btd.traveler_id <> ALL(${sqlTextArray(travelerIdsArr)})
|
|
250
250
|
`);
|
|
251
251
|
const existingAssigned = existingRows[0]?.count ?? 0;
|
|
252
252
|
const total = existingAssigned + plan.travelerIds.size;
|
|
@@ -462,7 +462,7 @@ export async function pairSharingGroup(db, slotId, input, options = {}) {
|
|
|
462
462
|
await db.execute(sql `
|
|
463
463
|
INSERT INTO booking_traveler_travel_details (traveler_id, sharing_group_id)
|
|
464
464
|
SELECT id, ${sharingGroupId}
|
|
465
|
-
FROM unnest(${input.travelerIds}
|
|
465
|
+
FROM unnest(${sqlTextArray(input.travelerIds)}) AS u(id)
|
|
466
466
|
ON CONFLICT (traveler_id) DO UPDATE SET
|
|
467
467
|
sharing_group_id = EXCLUDED.sharing_group_id,
|
|
468
468
|
updated_at = now()
|
|
@@ -642,12 +642,34 @@ async function loadSharingGroupLabelMap(db, groupIds) {
|
|
|
642
642
|
label: sharingGroupLabels.label,
|
|
643
643
|
})
|
|
644
644
|
.from(sharingGroupLabels)
|
|
645
|
-
.where(sql `${sharingGroupLabels.groupId} = ANY(${uniqueIds}
|
|
645
|
+
.where(sql `${sharingGroupLabels.groupId} = ANY(${sqlTextArray(uniqueIds)})`);
|
|
646
646
|
return Object.fromEntries(rows.map((row) => [row.groupId, row.label]));
|
|
647
647
|
}
|
|
648
648
|
async function executeRows(db, query) {
|
|
649
|
-
const
|
|
650
|
-
|
|
649
|
+
const result = await db.execute(query);
|
|
650
|
+
if (Array.isArray(result))
|
|
651
|
+
return result;
|
|
652
|
+
// node-postgres / neon-serverless drivers return `{ rows, rowCount, ... }`
|
|
653
|
+
// instead of a bare array — unwrap so this wrapper is driver-agnostic.
|
|
654
|
+
if (result &&
|
|
655
|
+
typeof result === "object" &&
|
|
656
|
+
"rows" in result &&
|
|
657
|
+
Array.isArray(result.rows)) {
|
|
658
|
+
return result.rows;
|
|
659
|
+
}
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Emit a Postgres `ARRAY[$1, $2, …]::text[]` literal instead of the
|
|
664
|
+
* naive `${jsArray}::text[]` form. drizzle's `sql` template spreads
|
|
665
|
+
* JS arrays into a row constructor (`($1, $2)`) which Postgres
|
|
666
|
+
* refuses to cast to `text[]` — see issue #952. Empty input returns
|
|
667
|
+
* `ARRAY[]::text[]` which Postgres accepts.
|
|
668
|
+
*/
|
|
669
|
+
function sqlTextArray(values) {
|
|
670
|
+
if (values.length === 0)
|
|
671
|
+
return sql `ARRAY[]::text[]`;
|
|
672
|
+
return sql `ARRAY[${sql.join(values.map((value) => sql `${value}`), sql.raw(", "))}]::text[]`;
|
|
651
673
|
}
|
|
652
674
|
function serializeSlot(slot) {
|
|
653
675
|
return {
|
|
@@ -698,7 +720,7 @@ async function loadSlotTravelerRows(db, bookingIds) {
|
|
|
698
720
|
(btd.dietary_encrypted IS NOT NULL) AS has_dietary_requirements
|
|
699
721
|
FROM booking_travelers bt
|
|
700
722
|
LEFT JOIN booking_traveler_travel_details btd ON btd.traveler_id = bt.id
|
|
701
|
-
WHERE bt.booking_id = ANY(${bookingIds}
|
|
723
|
+
WHERE bt.booking_id = ANY(${sqlTextArray(bookingIds)})
|
|
702
724
|
ORDER BY bt.booking_id, bt.is_primary DESC, bt.created_at
|
|
703
725
|
`);
|
|
704
726
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/availability",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
"drizzle-orm": "^0.45.2",
|
|
45
45
|
"hono": "^4.12.10",
|
|
46
46
|
"zod": "^4.3.6",
|
|
47
|
-
"@voyantjs/core": "0.52.
|
|
48
|
-
"@voyantjs/db": "0.52.
|
|
49
|
-
"@voyantjs/hono": "0.52.
|
|
47
|
+
"@voyantjs/core": "0.52.2",
|
|
48
|
+
"@voyantjs/db": "0.52.2",
|
|
49
|
+
"@voyantjs/hono": "0.52.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"typescript": "^6.0.2",
|
|
53
|
-
"@voyantjs/products": "0.52.
|
|
53
|
+
"@voyantjs/products": "0.52.2",
|
|
54
54
|
"@voyantjs/voyant-typescript-config": "0.1.0"
|
|
55
55
|
},
|
|
56
56
|
"files": [
|