@voyantjs/products 0.52.1 → 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.
- package/dist/catalog-policy.d.ts.map +1 -1
- package/dist/catalog-policy.js +18 -0
- package/dist/content-shape.d.ts +2 -0
- package/dist/content-shape.d.ts.map +1 -1
- package/dist/content-shape.js +2 -0
- package/dist/service-catalog-plane.d.ts.map +1 -1
- package/dist/service-catalog-plane.js +48 -1
- package/dist/service-content-owned.d.ts.map +1 -1
- package/dist/service-content-owned.js +98 -4
- package/dist/validation-core.d.ts +26 -26
- package/dist/validation-core.d.ts.map +1 -1
- package/dist/validation-core.js +17 -1
- package/package.json +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,EAwiB3C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,2CAA0C,CAAA;AAE3E,OAAO,EAAE,oBAAoB,EAAE,CAAA"}
|
package/dist/catalog-policy.js
CHANGED
|
@@ -377,6 +377,24 @@ const PRODUCT_FIELD_POLICY = [
|
|
|
377
377
|
overrideFriction: "none",
|
|
378
378
|
sourceFreshness: "sync",
|
|
379
379
|
},
|
|
380
|
+
{
|
|
381
|
+
path: "availableDeparturesCount",
|
|
382
|
+
class: "merchandisable",
|
|
383
|
+
merge: "source-only",
|
|
384
|
+
// High-drift because the count moves whenever availability_slots are
|
|
385
|
+
// added/removed or close-outs change; reindex-on-entry keeps it warm
|
|
386
|
+
// enough for the operator catalog table without being load-bearing
|
|
387
|
+
// for downstream booking flows.
|
|
388
|
+
drift: "high",
|
|
389
|
+
reindex: "facet-affecting",
|
|
390
|
+
snapshot: "never",
|
|
391
|
+
query: "indexed-column",
|
|
392
|
+
localized: false,
|
|
393
|
+
visibility: ["staff", "customer", "partner"],
|
|
394
|
+
editRole: "none",
|
|
395
|
+
overrideFriction: "none",
|
|
396
|
+
sourceFreshness: "sync",
|
|
397
|
+
},
|
|
380
398
|
{
|
|
381
399
|
path: "durationDays",
|
|
382
400
|
class: "structural",
|
package/dist/content-shape.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export declare const productDaySchema: z.ZodObject<{
|
|
|
77
77
|
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
78
78
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
79
79
|
location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
80
|
+
hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
80
81
|
services: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
81
82
|
}, z.core.$strip>;
|
|
82
83
|
export declare const productPolicySchema: z.ZodObject<{
|
|
@@ -150,6 +151,7 @@ export declare const productContentSchema: z.ZodObject<{
|
|
|
150
151
|
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
151
152
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
152
153
|
location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
154
|
+
hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
153
155
|
services: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
154
156
|
}, z.core.$strip>>>;
|
|
155
157
|
media: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-shape.d.ts","sourceRoot":"","sources":["../src/content-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAE1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;GAGG;AACH,eAAO,MAAM,+BAA+B,gBAAgB,CAAA;AAE5D;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;iBAe/B,CAAA;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;iBAKjC,CAAA;AAEF,eAAO,MAAM,uBAAuB;;;;;;iBAMlC,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAM9B,CAAA;AAEF,eAAO,MAAM,gBAAgB
|
|
1
|
+
{"version":3,"file":"content-shape.d.ts","sourceRoot":"","sources":["../src/content-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAE1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;GAGG;AACH,eAAO,MAAM,+BAA+B,gBAAgB,CAAA;AAE5D;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;iBAe/B,CAAA;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;iBAKjC,CAAA;AAEF,eAAO,MAAM,uBAAuB;;;;;;iBAMlC,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAM9B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;iBAQ3B,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;iBAK9B,CAAA;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;iBAejC,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO/B,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACrE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACrE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACzD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAE/D;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,GACf;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAY7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,aAAa,CAAC,cAAc,CAAC,EACvC,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,gBAAgB,CAAM,GACzD,cAAc,CAchB"}
|
package/dist/content-shape.js
CHANGED
|
@@ -67,6 +67,8 @@ export const productDaySchema = z.object({
|
|
|
67
67
|
title: z.string().nullable().optional(),
|
|
68
68
|
description: z.string().nullable().optional(),
|
|
69
69
|
location: z.string().nullable().optional(),
|
|
70
|
+
/** Day-level hero image (catalog detail sheet thumbnail). */
|
|
71
|
+
hero_image_url: z.string().nullable().optional(),
|
|
70
72
|
services: z.array(z.string()).optional().default([]),
|
|
71
73
|
});
|
|
72
74
|
export const productPolicySchema = z.object({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAElB,KAAK,UAAU,EAChB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAIhD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAgB3C;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,QAAQ,CAAC,YAAY,EACjC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACpC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CA0C9B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,QAAQ,CAAC,YAAY,EAClC,QAAQ,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACrC,UAAU,CAKZ;AAED,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,gBAAgB,EAAE,MAAM,CAAA;IACxB,qCAAqC;IACrC,KAAK,EAAE,aAAa,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAS9B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,aAAa,CAAC,OAAO,QAAQ,CAAC,YAAY,CAAC,EACjD,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,EAAE,CAAC,CAkBzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,qBAAqB,GAAG;IAAE,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GAC/D,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,CASzD;AAMD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,0BAA0B;IACzC,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,OAAO,CACL,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACzC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,iBAAiB,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,GAC9D,mBAAmB,CAOrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAC/B,GAAG,eAAe,CAAC,OAAO,QAAQ,CAAC,YAAY,CAAC,CAWhD;
|
|
1
|
+
{"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAElB,KAAK,UAAU,EAChB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAIhD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAgB3C;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,QAAQ,CAAC,YAAY,EACjC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACpC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CA0C9B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,QAAQ,CAAC,YAAY,EAClC,QAAQ,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACrC,UAAU,CAKZ;AAED,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,gBAAgB,EAAE,MAAM,CAAA;IACxB,qCAAqC;IACrC,KAAK,EAAE,aAAa,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAS9B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,aAAa,CAAC,OAAO,QAAQ,CAAC,YAAY,CAAC,EACjD,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,EAAE,CAAC,CAkBzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,qBAAqB,GAAG;IAAE,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GAC/D,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,CASzD;AAMD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,0BAA0B;IACzC,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,OAAO,CACL,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACzC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,iBAAiB,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,GAC9D,mBAAmB,CAOrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAC/B,GAAG,eAAe,CAAC,OAAO,QAAQ,CAAC,YAAY,CAAC,CAWhD;AAsBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE;IACP,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,aAAa,CAAC,0BAA0B,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAC/B,GACA,eAAe,CA2BjB;AAED;;;;GAIG;AACH,wBAAgB,8CAA8C,IAAI,0BAA0B,CAyE3F;AA4ED;;GAEG;AACH,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,aAAa,EACb,UAAU,GACX,CAAA"}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* in their own service-catalog-plane.ts files).
|
|
20
20
|
*/
|
|
21
21
|
import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, resolveEntityView, } from "@voyantjs/catalog";
|
|
22
|
-
import { asc, eq } from "drizzle-orm";
|
|
22
|
+
import { asc, eq, sql } from "drizzle-orm";
|
|
23
23
|
import { productCatalogPolicy } from "./catalog-policy.js";
|
|
24
24
|
import { products } from "./schema-core.js";
|
|
25
25
|
import { productDays, productItineraries, productMedia } from "./schema-itinerary.js";
|
|
@@ -214,6 +214,12 @@ function isPublicStorefrontProduct(row) {
|
|
|
214
214
|
return row.status === "active" && row.activated === true && row.visibility === "public";
|
|
215
215
|
}
|
|
216
216
|
function shouldEmitForSlice(row, slice) {
|
|
217
|
+
// The catalog is a "bookable now" surface — draft and archived
|
|
218
|
+
// products don't belong there, regardless of audience. Operators
|
|
219
|
+
// browsing /catalog get the same active-only set the storefront sees,
|
|
220
|
+
// just with staff-visible attribute columns.
|
|
221
|
+
if (row.status !== "active")
|
|
222
|
+
return false;
|
|
217
223
|
if (slice.audience === "customer" ||
|
|
218
224
|
slice.audience === "partner" ||
|
|
219
225
|
slice.audience === "supplier") {
|
|
@@ -320,6 +326,7 @@ export function createProductStorefrontCardProjectionExtension() {
|
|
|
320
326
|
const durationDays = defaultItinerary
|
|
321
327
|
? await estimateItineraryDurationDays(db, defaultItinerary.id)
|
|
322
328
|
: null;
|
|
329
|
+
const availableDeparturesCount = await countAvailableDepartures(db, productId);
|
|
323
330
|
const out = new Map([
|
|
324
331
|
["slug", translation?.slug ?? null],
|
|
325
332
|
["shortDescription", translation?.shortDescription ?? null],
|
|
@@ -327,6 +334,7 @@ export function createProductStorefrontCardProjectionExtension() {
|
|
|
327
334
|
["thumbnailUrl", primaryMedia?.url ?? null],
|
|
328
335
|
["coverMediaUrl", primaryMedia?.url ?? null],
|
|
329
336
|
["durationDays", durationDays],
|
|
337
|
+
["availableDeparturesCount", availableDeparturesCount],
|
|
330
338
|
["latitude", coordinateLocation?.latitude ?? null],
|
|
331
339
|
["longitude", coordinateLocation?.longitude ?? null],
|
|
332
340
|
]);
|
|
@@ -344,6 +352,45 @@ function pickTranslation(rows, locale) {
|
|
|
344
352
|
rows[0] ??
|
|
345
353
|
null);
|
|
346
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Counts future, open availability slots for a product — surfaces in the
|
|
357
|
+
* catalog index as `availableDeparturesCount` so the operator catalog
|
|
358
|
+
* table can show booking-ready stock at a glance without a separate
|
|
359
|
+
* round-trip. Owned-products only; sourced products carry departures via
|
|
360
|
+
* the upstream feed and don't write to `availability_slots`.
|
|
361
|
+
*
|
|
362
|
+
* Cross-package boundary: queries `availability_slots` by raw table name
|
|
363
|
+
* via `sql` so the products module doesn't take a hard dependency on the
|
|
364
|
+
* `@voyantjs/availability` schema.
|
|
365
|
+
*/
|
|
366
|
+
async function countAvailableDepartures(db, productId) {
|
|
367
|
+
try {
|
|
368
|
+
const result = await db.execute(sql `
|
|
369
|
+
SELECT COUNT(*)::int AS count
|
|
370
|
+
FROM availability_slots
|
|
371
|
+
WHERE product_id = ${productId}
|
|
372
|
+
AND status = 'open'
|
|
373
|
+
AND starts_at >= NOW()
|
|
374
|
+
`);
|
|
375
|
+
// postgres-js + neon-serverless both return `{ count }` on the first row;
|
|
376
|
+
// shape-defensive read in case a driver wraps it differently.
|
|
377
|
+
const rows = Array.isArray(result) ? result : (result.rows ?? []);
|
|
378
|
+
const row = rows[0];
|
|
379
|
+
const value = row?.count;
|
|
380
|
+
if (typeof value === "number")
|
|
381
|
+
return value;
|
|
382
|
+
if (typeof value === "string") {
|
|
383
|
+
const n = Number(value);
|
|
384
|
+
return Number.isFinite(n) ? n : 0;
|
|
385
|
+
}
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// availability_slots may not exist in slim test fixtures; treat as 0
|
|
390
|
+
// so reindex doesn't fail.
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
347
394
|
async function estimateItineraryDurationDays(db, itineraryId) {
|
|
348
395
|
const rows = await db
|
|
349
396
|
.select({ dayNumber: productDays.dayNumber })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-content-owned.d.ts","sourceRoot":"","sources":["../src/service-content-owned.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAE/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGhD,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,oBAAoB,CAAA;AAW3B,MAAM,WAAW,+BAA+B;IAC9C;;;;;OAKG;IACH,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,8BAA8B;IAC7C,qDAAqD;IACrD,OAAO,EAAE,cAAc,CAAA;IACvB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,SAAS,EAAE,sBAAsB,CAAA;CAClC;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,8BAA8B,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"service-content-owned.d.ts","sourceRoot":"","sources":["../src/service-content-owned.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAE/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGhD,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,oBAAoB,CAAA;AAW3B,MAAM,WAAW,+BAA+B;IAC9C;;;;;OAKG;IACH,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,8BAA8B;IAC7C,qDAAqD;IACrD,OAAO,EAAE,cAAc,CAAA;IACvB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,SAAS,EAAE,sBAAsB,CAAA;CAClC;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,8BAA8B,GAAG,IAAI,CAAC,CAwIhD"}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* same way.
|
|
32
32
|
*/
|
|
33
33
|
import { pickBestCachedLocale } from "@voyantjs/catalog";
|
|
34
|
-
import { and, asc, eq, inArray } from "drizzle-orm";
|
|
34
|
+
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
35
35
|
import { productContentSchema, validateProductContent, } from "./content-shape.js";
|
|
36
36
|
import { productDays, productItineraries, productMedia, productOptions, productOptionTranslations, products, productTranslations, } from "./schema.js";
|
|
37
37
|
/**
|
|
@@ -130,6 +130,9 @@ export async function buildOwnedProductContent(db, entityId, options) {
|
|
|
130
130
|
title: d.title ?? null,
|
|
131
131
|
description: d.description ?? null,
|
|
132
132
|
location: d.location ?? null,
|
|
133
|
+
// Per-day hero — prefer the cover, fall back to the first sorted
|
|
134
|
+
// image attached to this day in `product_media`.
|
|
135
|
+
hero_image_url: pickDayHeroImage(mediaRows, d.id),
|
|
133
136
|
services: [],
|
|
134
137
|
})),
|
|
135
138
|
media: mediaRows
|
|
@@ -141,9 +144,11 @@ export async function buildOwnedProductContent(db, entityId, options) {
|
|
|
141
144
|
alt: m.altText ?? null,
|
|
142
145
|
})),
|
|
143
146
|
policies: [],
|
|
144
|
-
// Owned products
|
|
145
|
-
//
|
|
146
|
-
departures
|
|
147
|
+
// Owned products derive departures from `availability_slots`. Pull
|
|
148
|
+
// future-or-current slots only so the catalog sheet doesn't drown
|
|
149
|
+
// operators in expired departures; ordering is chronological so the
|
|
150
|
+
// UI can group consecutive months without sorting client-side.
|
|
151
|
+
departures: await readOwnedProductDepartures(db, entityId, productRow.sellCurrency),
|
|
147
152
|
});
|
|
148
153
|
const validation = validateProductContent(content);
|
|
149
154
|
if (!validation.valid) {
|
|
@@ -155,6 +160,95 @@ export async function buildOwnedProductContent(db, entityId, options) {
|
|
|
155
160
|
matchKind: productMatchKind,
|
|
156
161
|
};
|
|
157
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Pick the best image to surface on an itinerary day card. Filters
|
|
165
|
+
* media rows to entries with a matching `dayId`, ignores brochures and
|
|
166
|
+
* non-images, then prefers `isCover === true` before falling back to
|
|
167
|
+
* the first sorted entry. `media-rows` is the same list the parent
|
|
168
|
+
* projection already pulled; no extra round-trip.
|
|
169
|
+
*/
|
|
170
|
+
function pickDayHeroImage(mediaRows, dayId) {
|
|
171
|
+
const dayImages = mediaRows.filter((m) => m.dayId === dayId && m.mediaType === "image" && !m.isBrochure);
|
|
172
|
+
if (dayImages.length === 0)
|
|
173
|
+
return null;
|
|
174
|
+
const cover = dayImages.find((m) => m.isCover);
|
|
175
|
+
return (cover ?? dayImages[0])?.url ?? null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Map future availability_slots → ProductDeparture[]. Raw SQL keeps the
|
|
179
|
+
* products module from depending on `@voyantjs/availability` (cross-
|
|
180
|
+
* module schema coupling is avoided per the workspace's separation).
|
|
181
|
+
*/
|
|
182
|
+
async function readOwnedProductDepartures(db, productId, sellCurrency) {
|
|
183
|
+
try {
|
|
184
|
+
const result = await db.execute(sql `
|
|
185
|
+
SELECT
|
|
186
|
+
id,
|
|
187
|
+
starts_at,
|
|
188
|
+
ends_at,
|
|
189
|
+
status,
|
|
190
|
+
initial_pax,
|
|
191
|
+
remaining_pax
|
|
192
|
+
FROM availability_slots
|
|
193
|
+
WHERE product_id = ${productId}
|
|
194
|
+
AND starts_at >= NOW()
|
|
195
|
+
ORDER BY starts_at ASC
|
|
196
|
+
LIMIT 365
|
|
197
|
+
`);
|
|
198
|
+
const rows = Array.isArray(result) ? result : (result.rows ?? []);
|
|
199
|
+
return rows
|
|
200
|
+
.map((raw) => {
|
|
201
|
+
const row = raw;
|
|
202
|
+
const id = typeof row.id === "string" ? row.id : null;
|
|
203
|
+
const startsAt = isoOrNull(row.starts_at);
|
|
204
|
+
if (!id || !startsAt)
|
|
205
|
+
return null;
|
|
206
|
+
const status = typeof row.status === "string" ? row.status : null;
|
|
207
|
+
const capacity = numberOrNull(row.initial_pax);
|
|
208
|
+
const remaining = numberOrNull(row.remaining_pax);
|
|
209
|
+
return {
|
|
210
|
+
id,
|
|
211
|
+
starts_at: startsAt,
|
|
212
|
+
ends_at: isoOrNull(row.ends_at),
|
|
213
|
+
status,
|
|
214
|
+
capacity,
|
|
215
|
+
remaining,
|
|
216
|
+
// Lowest price hint is for display only; the live engine
|
|
217
|
+
// resolves the actual quote. Fall back to the product's base
|
|
218
|
+
// sell_amount via the content shape's parent currency.
|
|
219
|
+
lowest_price_cents: null,
|
|
220
|
+
currency: sellCurrency,
|
|
221
|
+
note: null,
|
|
222
|
+
};
|
|
223
|
+
})
|
|
224
|
+
.filter((row) => row !== null);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// availability_slots may be absent in trimmed test fixtures —
|
|
228
|
+
// empty list is a safe default (matches "on-request" behavior).
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function isoOrNull(value) {
|
|
233
|
+
if (value instanceof Date) {
|
|
234
|
+
const ms = value.getTime();
|
|
235
|
+
return Number.isFinite(ms) ? value.toISOString() : null;
|
|
236
|
+
}
|
|
237
|
+
if (typeof value === "string" && value.length > 0) {
|
|
238
|
+
const d = new Date(value);
|
|
239
|
+
return Number.isFinite(d.getTime()) ? d.toISOString() : null;
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
function numberOrNull(value) {
|
|
244
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
245
|
+
return value;
|
|
246
|
+
if (typeof value === "string") {
|
|
247
|
+
const n = Number(value);
|
|
248
|
+
return Number.isFinite(n) ? n : null;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
158
252
|
function pickBestProductTranslation(rows, preferred) {
|
|
159
253
|
if (rows.length === 0)
|
|
160
254
|
return null;
|
|
@@ -58,33 +58,8 @@ export declare const insertProductSchema: z.ZodObject<{
|
|
|
58
58
|
}, z.core.$strip>;
|
|
59
59
|
export declare const updateProductSchema: z.ZodObject<{
|
|
60
60
|
name: z.ZodOptional<z.ZodString>;
|
|
61
|
-
status: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
|
|
62
|
-
active: "active";
|
|
63
|
-
draft: "draft";
|
|
64
|
-
archived: "archived";
|
|
65
|
-
}>>>;
|
|
66
61
|
description: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
67
|
-
bookingMode: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
|
|
68
|
-
date: "date";
|
|
69
|
-
other: "other";
|
|
70
|
-
date_time: "date_time";
|
|
71
|
-
open: "open";
|
|
72
|
-
stay: "stay";
|
|
73
|
-
transfer: "transfer";
|
|
74
|
-
itinerary: "itinerary";
|
|
75
|
-
}>>>;
|
|
76
|
-
capacityMode: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
|
|
77
|
-
free_sale: "free_sale";
|
|
78
|
-
limited: "limited";
|
|
79
|
-
on_request: "on_request";
|
|
80
|
-
}>>>;
|
|
81
62
|
timezone: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
82
|
-
visibility: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
|
|
83
|
-
public: "public";
|
|
84
|
-
private: "private";
|
|
85
|
-
hidden: "hidden";
|
|
86
|
-
}>>>;
|
|
87
|
-
activated: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
88
63
|
reservationTimeoutMinutes: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodNumber>>>;
|
|
89
64
|
sellCurrency: z.ZodOptional<z.ZodString>;
|
|
90
65
|
facilityId: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
|
@@ -108,7 +83,32 @@ export declare const updateProductSchema: z.ZodObject<{
|
|
|
108
83
|
balanceDueDaysBeforeDeparture: z.ZodNumber;
|
|
109
84
|
balanceDueMinDaysFromNow: z.ZodNumber;
|
|
110
85
|
}, z.core.$strip>>>>;
|
|
111
|
-
|
|
86
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
87
|
+
active: "active";
|
|
88
|
+
draft: "draft";
|
|
89
|
+
archived: "archived";
|
|
90
|
+
}>>;
|
|
91
|
+
bookingMode: z.ZodOptional<z.ZodEnum<{
|
|
92
|
+
date: "date";
|
|
93
|
+
other: "other";
|
|
94
|
+
date_time: "date_time";
|
|
95
|
+
open: "open";
|
|
96
|
+
stay: "stay";
|
|
97
|
+
transfer: "transfer";
|
|
98
|
+
itinerary: "itinerary";
|
|
99
|
+
}>>;
|
|
100
|
+
capacityMode: z.ZodOptional<z.ZodEnum<{
|
|
101
|
+
free_sale: "free_sale";
|
|
102
|
+
limited: "limited";
|
|
103
|
+
on_request: "on_request";
|
|
104
|
+
}>>;
|
|
105
|
+
visibility: z.ZodOptional<z.ZodEnum<{
|
|
106
|
+
public: "public";
|
|
107
|
+
private: "private";
|
|
108
|
+
hidden: "hidden";
|
|
109
|
+
}>>;
|
|
110
|
+
activated: z.ZodOptional<z.ZodBoolean>;
|
|
111
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
112
112
|
sellAmountCents: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
|
|
113
113
|
costAmountCents: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
|
|
114
114
|
marginPercent: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation-core.d.ts","sourceRoot":"","sources":["../src/validation-core.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,CAAC,EACF,MAAM,wBAAwB,CAAA;AA2C/B,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAiD,CAAA;
|
|
1
|
+
{"version":3,"file":"validation-core.d.ts","sourceRoot":"","sources":["../src/validation-core.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,CAAC,EACF,MAAM,wBAAwB,CAAA;AA2C/B,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAiD,CAAA;AAOjF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAUD,CAAA;AAC/B,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO9B,CAAA;AACF,eAAO,MAAM,0BAA0B;;;;;;;;EAQrC,CAAA;AAEF,eAAO,MAAM,wBAAwB;;;EAA0B,CAAA;AAE/D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsBjC,CAAA;AAEF,eAAO,MAAM,4BAA4B;;;iBAGvC,CAAA;AACF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAY/D,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;iBAA0B,CAAA;AAChE,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;iBAAoC,CAAA;AAC1E,eAAO,MAAM,4BAA4B;;;;;;;;;iBAKvC,CAAA;AACF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAC3E,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAwB3E,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACpC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACxC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CACzC,CAAA;AAED,KAAK,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AA2BzD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;iBAIjC,CAAA;AAMF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;iBAYjC,CAAA;AAEF;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,cAAc,GACrB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,cAAc,EAAE,CAAA;CAAE,CAGxD;AACD,eAAO,MAAM,yBAAyB;;;;;;;;;;;;iBAKpC,CAAA;AACF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACrE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAErE,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAE/D,eAAO,MAAM,uBAAuB;;iBAElC,CAAA"}
|
package/dist/validation-core.js
CHANGED
|
@@ -37,7 +37,23 @@ const productPricingFields = {
|
|
|
37
37
|
marginPercent: z.number().int().optional().nullable(),
|
|
38
38
|
};
|
|
39
39
|
export const insertProductSchema = productCoreSchema.extend(productPricingFields);
|
|
40
|
-
|
|
40
|
+
// `productCoreSchema` carries `.default(...)` on enum-ish fields so
|
|
41
|
+
// insert can omit them. zod's `.partial()` does NOT strip those
|
|
42
|
+
// defaults — every PATCH would synthesize `status: "draft"`,
|
|
43
|
+
// `visibility: "private"`, etc., and overwrite the row. Re-declare the
|
|
44
|
+
// defaulted fields without defaults before partial-ising so PATCH only
|
|
45
|
+
// touches the keys the client actually sent.
|
|
46
|
+
export const updateProductSchema = productCoreSchema
|
|
47
|
+
.extend({
|
|
48
|
+
status: productStatusSchema,
|
|
49
|
+
bookingMode: productBookingModeSchema,
|
|
50
|
+
capacityMode: productCapacityModeSchema,
|
|
51
|
+
visibility: productVisibilitySchema,
|
|
52
|
+
activated: z.boolean(),
|
|
53
|
+
tags: z.array(z.string()),
|
|
54
|
+
})
|
|
55
|
+
.partial()
|
|
56
|
+
.extend(productPricingFields);
|
|
41
57
|
export const selectProductSchema = productCoreSchema.extend({
|
|
42
58
|
id: typeIdSchema("products"),
|
|
43
59
|
sellAmountCents: z.number().int().nullable(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/products",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -125,12 +125,12 @@
|
|
|
125
125
|
"hono": "^4.12.10",
|
|
126
126
|
"pdf-lib": "^1.17.1",
|
|
127
127
|
"zod": "^4.3.6",
|
|
128
|
-
"@voyantjs/core": "0.52.
|
|
129
|
-
"@voyantjs/db": "0.52.
|
|
130
|
-
"@voyantjs/hono": "0.52.
|
|
131
|
-
"@voyantjs/utils": "0.52.
|
|
132
|
-
"@voyantjs/catalog": "0.52.
|
|
133
|
-
"@voyantjs/storage": "0.52.
|
|
128
|
+
"@voyantjs/core": "0.52.2",
|
|
129
|
+
"@voyantjs/db": "0.52.2",
|
|
130
|
+
"@voyantjs/hono": "0.52.2",
|
|
131
|
+
"@voyantjs/utils": "0.52.2",
|
|
132
|
+
"@voyantjs/catalog": "0.52.2",
|
|
133
|
+
"@voyantjs/storage": "0.52.2"
|
|
134
134
|
},
|
|
135
135
|
"devDependencies": {
|
|
136
136
|
"typescript": "^6.0.2",
|