@voyantjs/products 0.52.1 → 0.52.3
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/action-ledger-drift.d.ts +29 -0
- package/dist/action-ledger-drift.d.ts.map +1 -0
- package/dist/action-ledger-drift.js +335 -0
- package/dist/action-ledger.d.ts +104 -0
- package/dist/action-ledger.d.ts.map +1 -0
- package/dist/action-ledger.js +100 -0
- package/dist/booking-extension.d.ts +3 -3
- 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/events.d.ts +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/route-env.d.ts +22 -0
- package/dist/route-env.d.ts.map +1 -0
- package/dist/route-env.js +1 -0
- package/dist/routes-associations.d.ts +164 -0
- package/dist/routes-associations.d.ts.map +1 -0
- package/dist/routes-associations.js +100 -0
- package/dist/routes-catalog.d.ts +436 -0
- package/dist/routes-catalog.d.ts.map +1 -0
- package/dist/routes-catalog.js +104 -0
- package/dist/routes-configuration.d.ts +773 -0
- package/dist/routes-configuration.d.ts.map +1 -0
- package/dist/routes-configuration.js +364 -0
- package/dist/routes-core.d.ts +302 -0
- package/dist/routes-core.d.ts.map +1 -0
- package/dist/routes-core.js +79 -0
- package/dist/routes-itinerary.d.ts +614 -0
- package/dist/routes-itinerary.d.ts.map +1 -0
- package/dist/routes-itinerary.js +309 -0
- package/dist/routes-maintenance.d.ts +32 -0
- package/dist/routes-maintenance.d.ts.map +1 -0
- package/dist/routes-maintenance.js +14 -0
- package/dist/routes-media.d.ts +634 -0
- package/dist/routes-media.d.ts.map +1 -0
- package/dist/routes-media.js +245 -0
- package/dist/routes-merchandising.d.ts +1108 -0
- package/dist/routes-merchandising.d.ts.map +1 -0
- package/dist/routes-merchandising.js +376 -0
- package/dist/routes-options.d.ts +363 -0
- package/dist/routes-options.d.ts.map +1 -0
- package/dist/routes-options.js +173 -0
- package/dist/routes-public.d.ts +4 -4
- package/dist/routes-translations.d.ts +477 -0
- package/dist/routes-translations.d.ts.map +1 -0
- package/dist/routes-translations.js +258 -0
- package/dist/routes.d.ts +417 -355
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +21 -1133
- package/dist/schema-core.d.ts +3 -3
- package/dist/schema-itinerary.d.ts +1 -1
- package/dist/schema-settings.d.ts +4 -4
- package/dist/service-catalog-plane.d.ts.map +1 -1
- package/dist/service-catalog-plane.js +48 -1
- package/dist/service-catalog.d.ts +2 -2
- package/dist/service-content-owned.d.ts.map +1 -1
- package/dist/service-content-owned.js +98 -4
- package/dist/service-public.d.ts +4 -4
- package/dist/service.d.ts +225 -97
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +91 -0
- package/dist/tasks/brochures.d.ts +1 -1
- package/dist/validation-catalog.d.ts +10 -10
- package/dist/validation-config.d.ts +17 -17
- package/dist/validation-content.d.ts +26 -26
- package/dist/validation-core.d.ts +46 -46
- package/dist/validation-core.d.ts.map +1 -1
- package/dist/validation-core.js +17 -1
- package/dist/validation-public.d.ts +25 -25
- package/dist/validation-shared.d.ts +11 -11
- package/package.json +13 -7
package/dist/schema-core.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare const products: import("drizzle-orm/pg-core").PgTableWithColumns<
|
|
|
41
41
|
tableName: "products";
|
|
42
42
|
dataType: "string";
|
|
43
43
|
columnType: "PgEnumColumn";
|
|
44
|
-
data: "
|
|
44
|
+
data: "draft" | "active" | "archived";
|
|
45
45
|
driverParam: string;
|
|
46
46
|
notNull: true;
|
|
47
47
|
hasDefault: true;
|
|
@@ -528,7 +528,7 @@ export declare const productOptions: import("drizzle-orm/pg-core").PgTableWithCo
|
|
|
528
528
|
tableName: "product_options";
|
|
529
529
|
dataType: "string";
|
|
530
530
|
columnType: "PgEnumColumn";
|
|
531
|
-
data: "
|
|
531
|
+
data: "draft" | "active" | "archived";
|
|
532
532
|
driverParam: string;
|
|
533
533
|
notNull: true;
|
|
534
534
|
hasDefault: true;
|
|
@@ -741,7 +741,7 @@ export declare const optionUnits: import("drizzle-orm/pg-core").PgTableWithColum
|
|
|
741
741
|
tableName: "option_units";
|
|
742
742
|
dataType: "string";
|
|
743
743
|
columnType: "PgEnumColumn";
|
|
744
|
-
data: "
|
|
744
|
+
data: "person" | "group" | "room" | "vehicle" | "service" | "other";
|
|
745
745
|
driverParam: string;
|
|
746
746
|
notNull: true;
|
|
747
747
|
hasDefault: true;
|
|
@@ -331,7 +331,7 @@ export declare const productDayServices: import("drizzle-orm/pg-core").PgTableWi
|
|
|
331
331
|
tableName: "product_day_services";
|
|
332
332
|
dataType: "string";
|
|
333
333
|
columnType: "PgEnumColumn";
|
|
334
|
-
data: "other" | "
|
|
334
|
+
data: "other" | "transfer" | "accommodation" | "experience" | "guide" | "meal";
|
|
335
335
|
driverParam: string;
|
|
336
336
|
notNull: true;
|
|
337
337
|
hasDefault: false;
|
|
@@ -218,7 +218,7 @@ export declare const productTicketSettings: import("drizzle-orm/pg-core").PgTabl
|
|
|
218
218
|
tableName: "product_ticket_settings";
|
|
219
219
|
dataType: "string";
|
|
220
220
|
columnType: "PgEnumColumn";
|
|
221
|
-
data: "
|
|
221
|
+
data: "none" | "voucher" | "ticket" | "pdf" | "qr_code" | "barcode" | "email" | "mobile";
|
|
222
222
|
driverParam: string;
|
|
223
223
|
notNull: true;
|
|
224
224
|
hasDefault: true;
|
|
@@ -521,7 +521,7 @@ export declare const productCapabilities: import("drizzle-orm/pg-core").PgTableW
|
|
|
521
521
|
tableName: "product_capabilities";
|
|
522
522
|
dataType: "string";
|
|
523
523
|
columnType: "PgEnumColumn";
|
|
524
|
-
data: "
|
|
524
|
+
data: "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "accommodation" | "transport";
|
|
525
525
|
driverParam: string;
|
|
526
526
|
notNull: true;
|
|
527
527
|
hasDefault: false;
|
|
@@ -647,7 +647,7 @@ export declare const productDeliveryFormats: import("drizzle-orm/pg-core").PgTab
|
|
|
647
647
|
tableName: "product_delivery_formats";
|
|
648
648
|
dataType: "string";
|
|
649
649
|
columnType: "PgEnumColumn";
|
|
650
|
-
data: "
|
|
650
|
+
data: "none" | "voucher" | "ticket" | "pdf" | "qr_code" | "barcode" | "email" | "mobile";
|
|
651
651
|
driverParam: string;
|
|
652
652
|
notNull: true;
|
|
653
653
|
hasDefault: false;
|
|
@@ -1035,7 +1035,7 @@ export declare const productLocations: import("drizzle-orm/pg-core").PgTableWith
|
|
|
1035
1035
|
tableName: "product_locations";
|
|
1036
1036
|
dataType: "string";
|
|
1037
1037
|
columnType: "PgEnumColumn";
|
|
1038
|
-
data: "
|
|
1038
|
+
data: "end" | "other" | "start" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
1039
1039
|
driverParam: string;
|
|
1040
1040
|
notNull: true;
|
|
1041
1041
|
hasDefault: true;
|
|
@@ -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":"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"}
|
|
@@ -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 })
|
|
@@ -57,7 +57,7 @@ export declare const catalogProductsService: {
|
|
|
57
57
|
}[];
|
|
58
58
|
locations: {
|
|
59
59
|
id: string;
|
|
60
|
-
locationType: "
|
|
60
|
+
locationType: "end" | "other" | "start" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
61
61
|
title: string;
|
|
62
62
|
address: string | null;
|
|
63
63
|
city: string | null;
|
|
@@ -168,7 +168,7 @@ export declare const catalogProductsService: {
|
|
|
168
168
|
}[];
|
|
169
169
|
locations: {
|
|
170
170
|
id: string;
|
|
171
|
-
locationType: "
|
|
171
|
+
locationType: "end" | "other" | "start" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
172
172
|
title: string;
|
|
173
173
|
address: string | null;
|
|
174
174
|
city: string | null;
|
|
@@ -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;
|
package/dist/service-public.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export declare const publicProductsService: {
|
|
|
51
51
|
}[];
|
|
52
52
|
locations: {
|
|
53
53
|
id: string;
|
|
54
|
-
locationType: "
|
|
54
|
+
locationType: "end" | "other" | "start" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
55
55
|
title: string;
|
|
56
56
|
address: string | null;
|
|
57
57
|
city: string | null;
|
|
@@ -162,7 +162,7 @@ export declare const publicProductsService: {
|
|
|
162
162
|
}[];
|
|
163
163
|
locations: {
|
|
164
164
|
id: string;
|
|
165
|
-
locationType: "
|
|
165
|
+
locationType: "end" | "other" | "start" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
166
166
|
title: string;
|
|
167
167
|
address: string | null;
|
|
168
168
|
city: string | null;
|
|
@@ -241,7 +241,7 @@ export declare const publicProductsService: {
|
|
|
241
241
|
}[];
|
|
242
242
|
locations: {
|
|
243
243
|
id: string;
|
|
244
|
-
locationType: "
|
|
244
|
+
locationType: "end" | "other" | "start" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
245
245
|
title: string;
|
|
246
246
|
address: string | null;
|
|
247
247
|
city: string | null;
|
|
@@ -314,7 +314,7 @@ export declare const publicProductsService: {
|
|
|
314
314
|
}[];
|
|
315
315
|
locations: {
|
|
316
316
|
id: string;
|
|
317
|
-
locationType: "
|
|
317
|
+
locationType: "end" | "other" | "start" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
318
318
|
title: string;
|
|
319
319
|
address: string | null;
|
|
320
320
|
city: string | null;
|