@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;AAE/F,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAA;AACtF,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAA;AAMlF,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,eAAe,EAAE,MAAM,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,gBAAgB,EAAE,CAAA;CAC9B;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,kBAAkB,EAAE,CAAA;CACjC;AAED,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,8BAA8B,EAAE,CAAC,CAyC3C;AAED,wBAAsB,mCAAmC,CACvD,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,2BAA2B,GACjC,OAAO,CAAC,gBAAgB,CAAC,CA8C3B;AAED,wBAAsB,mCAAmC,CACvD,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,MAAM;;;UAeb;AAqBD,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,yBAAyB,EAChC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,0BAA0B,CAAC,CA+GrC;AAED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,yBAAyB,EAChC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAkDrC;AAED,MAAM,WAAW,4CAA4C;IAC3D;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,4CAA4C,CAChE,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,4CAAiD,GACtD,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,kBAAkB,EAAE,CAAA;CAAE,CAAC,CA6D/D"}
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}::text[])
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
- slot_items AS (
132
- SELECT bi.booking_id, bi.option_id
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: group.ref_type,
180
- refId: group.ref_id,
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}::text[], ${resourceIds}::text[]) AS row(traveler_id, resource_id)
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 rows = await db.execute(query);
476
- return Array.isArray(rows) ? rows : [];
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}::text[])
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}::text[])
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}::text[])
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}::text[]) AS u(id)
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}::text[])`);
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 rows = await db.execute(query);
650
- return Array.isArray(rows) ? rows : [];
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}::text[])
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.0",
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.0",
48
- "@voyantjs/db": "0.52.0",
49
- "@voyantjs/hono": "0.52.0"
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.0",
53
+ "@voyantjs/products": "0.52.2",
54
54
  "@voyantjs/voyant-typescript-config": "0.1.0"
55
55
  },
56
56
  "files": [