@voyant-travel/inventory 0.1.0
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/LICENSE +201 -0
- 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 +338 -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/authoring/builder.d.ts +37 -0
- package/dist/authoring/builder.d.ts.map +1 -0
- package/dist/authoring/builder.js +248 -0
- package/dist/authoring/clone-content.d.ts +38 -0
- package/dist/authoring/clone-content.d.ts.map +1 -0
- package/dist/authoring/clone-content.js +367 -0
- package/dist/authoring/clone-pricing.d.ts +9 -0
- package/dist/authoring/clone-pricing.d.ts.map +1 -0
- package/dist/authoring/clone-pricing.js +242 -0
- package/dist/authoring/clone.d.ts +45 -0
- package/dist/authoring/clone.d.ts.map +1 -0
- package/dist/authoring/clone.js +142 -0
- package/dist/authoring/errors.d.ts +21 -0
- package/dist/authoring/errors.d.ts.map +1 -0
- package/dist/authoring/errors.js +13 -0
- package/dist/authoring/extension.d.ts +248 -0
- package/dist/authoring/extension.d.ts.map +1 -0
- package/dist/authoring/extension.js +116 -0
- package/dist/authoring/index.d.ts +12 -0
- package/dist/authoring/index.d.ts.map +1 -0
- package/dist/authoring/index.js +11 -0
- package/dist/authoring/schema.d.ts +85 -0
- package/dist/authoring/schema.d.ts.map +1 -0
- package/dist/authoring/schema.js +16 -0
- package/dist/authoring/service.d.ts +28 -0
- package/dist/authoring/service.d.ts.map +1 -0
- package/dist/authoring/service.js +66 -0
- package/dist/authoring/spec.d.ts +524 -0
- package/dist/authoring/spec.d.ts.map +1 -0
- package/dist/authoring/spec.js +167 -0
- package/dist/authoring/validate.d.ts +17 -0
- package/dist/authoring/validate.d.ts.map +1 -0
- package/dist/authoring/validate.js +83 -0
- package/dist/authoring.d.ts +2 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +1 -0
- package/dist/booking-engine/handler-support.d.ts +91 -0
- package/dist/booking-engine/handler-support.d.ts.map +1 -0
- package/dist/booking-engine/handler-support.js +355 -0
- package/dist/booking-engine/handler.d.ts +404 -0
- package/dist/booking-engine/handler.d.ts.map +1 -0
- package/dist/booking-engine/handler.js +398 -0
- package/dist/booking-engine/index.d.ts +8 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +7 -0
- package/dist/booking-engine.d.ts +2 -0
- package/dist/booking-engine.d.ts.map +1 -0
- package/dist/booking-engine.js +1 -0
- package/dist/booking-extension.d.ts +278 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +161 -0
- package/dist/catalog-policy-departures.d.ts +52 -0
- package/dist/catalog-policy-departures.d.ts.map +1 -0
- package/dist/catalog-policy-departures.js +169 -0
- package/dist/catalog-policy-destinations.d.ts +43 -0
- package/dist/catalog-policy-destinations.d.ts.map +1 -0
- package/dist/catalog-policy-destinations.js +165 -0
- package/dist/catalog-policy-pricing.d.ts +55 -0
- package/dist/catalog-policy-pricing.d.ts.map +1 -0
- package/dist/catalog-policy-pricing.js +109 -0
- package/dist/catalog-policy-promotions.d.ts +52 -0
- package/dist/catalog-policy-promotions.d.ts.map +1 -0
- package/dist/catalog-policy-promotions.js +270 -0
- package/dist/catalog-policy-taxonomy.d.ts +51 -0
- package/dist/catalog-policy-taxonomy.d.ts.map +1 -0
- package/dist/catalog-policy-taxonomy.js +191 -0
- package/dist/catalog-policy.d.ts +33 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +733 -0
- package/dist/content-shape.d.ts +15 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +28 -0
- package/dist/draft-shape.d.ts +43 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +48 -0
- package/dist/events.d.ts +37 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +32 -0
- package/dist/extras/catalog-policy.d.ts +30 -0
- package/dist/extras/catalog-policy.d.ts.map +1 -0
- package/dist/extras/catalog-policy.js +319 -0
- package/dist/extras/content-shape.d.ts +5 -0
- package/dist/extras/content-shape.d.ts.map +1 -0
- package/dist/extras/content-shape.js +13 -0
- package/dist/extras/draft-shape.d.ts +34 -0
- package/dist/extras/draft-shape.d.ts.map +1 -0
- package/dist/extras/draft-shape.js +69 -0
- package/dist/extras/routes.d.ts +380 -0
- package/dist/extras/routes.d.ts.map +1 -0
- package/dist/extras/routes.js +59 -0
- package/dist/extras/schema-sourced-content.d.ts +254 -0
- package/dist/extras/schema-sourced-content.d.ts.map +1 -0
- package/dist/extras/schema-sourced-content.js +45 -0
- package/dist/extras/schema.d.ts +628 -0
- package/dist/extras/schema.d.ts.map +1 -0
- package/dist/extras/schema.js +87 -0
- package/dist/extras/service-catalog-plane.d.ts +77 -0
- package/dist/extras/service-catalog-plane.d.ts.map +1 -0
- package/dist/extras/service-catalog-plane.js +219 -0
- package/dist/extras/service-content-synthesizer.d.ts +41 -0
- package/dist/extras/service-content-synthesizer.d.ts.map +1 -0
- package/dist/extras/service-content-synthesizer.js +138 -0
- package/dist/extras/service-content.d.ts +48 -0
- package/dist/extras/service-content.d.ts.map +1 -0
- package/dist/extras/service-content.js +253 -0
- package/dist/extras/service.d.ts +185 -0
- package/dist/extras/service.d.ts.map +1 -0
- package/dist/extras/service.js +96 -0
- package/dist/extras/validation.d.ts +437 -0
- package/dist/extras/validation.d.ts.map +1 -0
- package/dist/extras/validation.js +149 -0
- package/dist/extras.d.ts +267 -0
- package/dist/extras.d.ts.map +1 -0
- package/dist/extras.js +19 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/interface.d.ts +5869 -0
- package/dist/interface.d.ts.map +1 -0
- package/dist/interface.js +54 -0
- package/dist/public-routes.d.ts +2 -0
- package/dist/public-routes.d.ts.map +1 -0
- package/dist/public-routes.js +1 -0
- package/dist/public-validation.d.ts +2 -0
- package/dist/public-validation.d.ts.map +1 -0
- package/dist/public-validation.js +1 -0
- package/dist/read-model.d.ts +25 -0
- package/dist/read-model.d.ts.map +1 -0
- package/dist/read-model.js +99 -0
- 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-content.d.ts +74 -0
- package/dist/routes-content.d.ts.map +1 -0
- package/dist/routes-content.js +117 -0
- package/dist/routes-core.d.ts +331 -0
- package/dist/routes-core.d.ts.map +1 -0
- package/dist/routes-core.js +95 -0
- package/dist/routes-itinerary.d.ts +759 -0
- package/dist/routes-itinerary.d.ts.map +1 -0
- package/dist/routes-itinerary.js +387 -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 +1120 -0
- package/dist/routes-merchandising.d.ts.map +1 -0
- package/dist/routes-merchandising.js +377 -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 +776 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +119 -0
- package/dist/routes-translations.d.ts +489 -0
- package/dist/routes-translations.d.ts.map +1 -0
- package/dist/routes-translations.js +258 -0
- package/dist/routes.d.ts +5097 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +64 -0
- package/dist/schema-core.d.ts +1238 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +157 -0
- package/dist/schema-itinerary.d.ts +1169 -0
- package/dist/schema-itinerary.d.ts.map +1 -0
- package/dist/schema-itinerary.js +130 -0
- package/dist/schema-relations.d.ts +117 -0
- package/dist/schema-relations.d.ts.map +1 -0
- package/dist/schema-relations.js +192 -0
- package/dist/schema-settings.d.ts +1800 -0
- package/dist/schema-settings.d.ts.map +1 -0
- package/dist/schema-settings.js +220 -0
- package/dist/schema-shared.d.ts +15 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +91 -0
- package/dist/schema-sourced-content.d.ts +262 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +69 -0
- package/dist/schema-taxonomy.d.ts +1363 -0
- package/dist/schema-taxonomy.d.ts.map +1 -0
- package/dist/schema-taxonomy.js +203 -0
- package/dist/schema.d.ts +10 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +9 -0
- package/dist/service-aggregates.d.ts +29 -0
- package/dist/service-aggregates.d.ts.map +1 -0
- package/dist/service-aggregates.js +56 -0
- package/dist/service-catalog-plane-destinations.d.ts +30 -0
- package/dist/service-catalog-plane-destinations.d.ts.map +1 -0
- package/dist/service-catalog-plane-destinations.js +143 -0
- package/dist/service-catalog-plane-taxonomy.d.ts +73 -0
- package/dist/service-catalog-plane-taxonomy.d.ts.map +1 -0
- package/dist/service-catalog-plane-taxonomy.js +242 -0
- package/dist/service-catalog-plane.d.ts +179 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +431 -0
- package/dist/service-catalog.d.ts +251 -0
- package/dist/service-catalog.d.ts.map +1 -0
- package/dist/service-catalog.js +517 -0
- package/dist/service-configuration.d.ts +261 -0
- package/dist/service-configuration.d.ts.map +1 -0
- package/dist/service-configuration.js +343 -0
- package/dist/service-content-owned.d.ts +68 -0
- package/dist/service-content-owned.d.ts.map +1 -0
- package/dist/service-content-owned.js +329 -0
- package/dist/service-content-synthesizer.d.ts +90 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +178 -0
- package/dist/service-content.d.ts +106 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +388 -0
- package/dist/service-core.d.ts +194 -0
- package/dist/service-core.d.ts.map +1 -0
- package/dist/service-core.js +213 -0
- package/dist/service-delivery-formats.d.ts +58 -0
- package/dist/service-delivery-formats.d.ts.map +1 -0
- package/dist/service-delivery-formats.js +107 -0
- package/dist/service-destinations.d.ts +223 -0
- package/dist/service-destinations.d.ts.map +1 -0
- package/dist/service-destinations.js +310 -0
- package/dist/service-itinerary-history.d.ts +457 -0
- package/dist/service-itinerary-history.d.ts.map +1 -0
- package/dist/service-itinerary-history.js +135 -0
- package/dist/service-itinerary.d.ts +1149 -0
- package/dist/service-itinerary.d.ts.map +1 -0
- package/dist/service-itinerary.js +419 -0
- package/dist/service-media.d.ts +272 -0
- package/dist/service-media.d.ts.map +1 -0
- package/dist/service-media.js +320 -0
- package/dist/service-merchandising.d.ts +184 -0
- package/dist/service-merchandising.d.ts.map +1 -0
- package/dist/service-merchandising.js +181 -0
- package/dist/service-option-translations.d.ts +268 -0
- package/dist/service-option-translations.d.ts.map +1 -0
- package/dist/service-option-translations.js +300 -0
- package/dist/service-options.d.ts +181 -0
- package/dist/service-options.d.ts.map +1 -0
- package/dist/service-options.js +179 -0
- package/dist/service-product-destinations.d.ts +37 -0
- package/dist/service-product-destinations.d.ts.map +1 -0
- package/dist/service-product-destinations.js +94 -0
- package/dist/service-public.d.ts +664 -0
- package/dist/service-public.d.ts.map +1 -0
- package/dist/service-public.js +374 -0
- package/dist/service-taxonomy.d.ts +197 -0
- package/dist/service-taxonomy.d.ts.map +1 -0
- package/dist/service-taxonomy.js +221 -0
- package/dist/service.d.ts +3929 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +28 -0
- package/dist/tasks/brochure-printers.d.ts +31 -0
- package/dist/tasks/brochure-printers.d.ts.map +1 -0
- package/dist/tasks/brochure-printers.js +149 -0
- package/dist/tasks/brochure-templates.d.ts +36 -0
- package/dist/tasks/brochure-templates.d.ts.map +1 -0
- package/dist/tasks/brochure-templates.js +110 -0
- package/dist/tasks/brochures.d.ts +43 -0
- package/dist/tasks/brochures.d.ts.map +1 -0
- package/dist/tasks/brochures.js +72 -0
- package/dist/tasks/generate-pdf.d.ts +8 -0
- package/dist/tasks/generate-pdf.d.ts.map +1 -0
- package/dist/tasks/generate-pdf.js +106 -0
- package/dist/tasks/index.d.ts +5 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/pdf-text.d.ts +2 -0
- package/dist/tasks/pdf-text.d.ts.map +1 -0
- package/dist/tasks/pdf-text.js +40 -0
- package/dist/tasks.d.ts +2 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +1 -0
- package/dist/validation-catalog.d.ts +2 -0
- package/dist/validation-catalog.d.ts.map +1 -0
- package/dist/validation-catalog.js +3 -0
- package/dist/validation-config.d.ts +2 -0
- package/dist/validation-config.d.ts.map +1 -0
- package/dist/validation-config.js +3 -0
- package/dist/validation-content.d.ts +2 -0
- package/dist/validation-content.d.ts.map +1 -0
- package/dist/validation-content.js +3 -0
- package/dist/validation-core.d.ts +2 -0
- package/dist/validation-core.d.ts.map +1 -0
- package/dist/validation-core.js +3 -0
- package/dist/validation-public.d.ts +2 -0
- package/dist/validation-public.d.ts.map +1 -0
- package/dist/validation-public.js +3 -0
- package/dist/validation-shared.d.ts +2 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +3 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +3 -0
- package/package.json +204 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// agent-quality: file-size exception -- owner: inventory; existing service module stays co-located until a dedicated split preserves behavior and tests.
|
|
2
|
+
/**
|
|
3
|
+
* Catalog-plane integration for the products service.
|
|
4
|
+
*
|
|
5
|
+
* Adds catalog-aware service methods alongside the existing `productsService`
|
|
6
|
+
* surface in `service.ts`. Routes opt in: the original `getProductById` /
|
|
7
|
+
* `listProducts` continue to return raw DB rows; the methods here return
|
|
8
|
+
* resolved CatalogEntry views with overlays + visibility filtering applied.
|
|
9
|
+
*
|
|
10
|
+
* Existing service code is untouched. Migration is per-route, gradual.
|
|
11
|
+
*
|
|
12
|
+
* Naming note: this file is `service-catalog-plane.ts` (not `service-catalog.ts`)
|
|
13
|
+
* because the existing `service-catalog.ts` handles the products module's own
|
|
14
|
+
* catalog management (categories, tags, types). The "catalog plane" is the
|
|
15
|
+
* cross-vertical projection / overlay / snapshot infrastructure from
|
|
16
|
+
* `@voyant-travel/catalog`.
|
|
17
|
+
*
|
|
18
|
+
* See `docs/architecture/catalog-architecture.md` §9.1 for the integration
|
|
19
|
+
* pattern this file establishes (replicated for cruises, accommodations, etc.
|
|
20
|
+
* in their own service-catalog-plane.ts files).
|
|
21
|
+
*/
|
|
22
|
+
import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, fetchOverlaysForEntities, resolveEntityView, resolveEntityViewWithOverlays, } from "@voyant-travel/catalog";
|
|
23
|
+
import { asc, eq, sql } from "drizzle-orm";
|
|
24
|
+
import { productCatalogPolicy } from "./catalog-policy.js";
|
|
25
|
+
import { products } from "./schema-core.js";
|
|
26
|
+
import { productDays, productItineraries, productMedia } from "./schema-itinerary.js";
|
|
27
|
+
import { productLocations, productTranslations } from "./schema-settings.js";
|
|
28
|
+
/**
|
|
29
|
+
* Lazy-initialized registry. Built once per process; the field-policy file
|
|
30
|
+
* is static so this is safe to memoize.
|
|
31
|
+
*/
|
|
32
|
+
let _registry;
|
|
33
|
+
function getProductsRegistry() {
|
|
34
|
+
if (!_registry) {
|
|
35
|
+
_registry = createFieldPolicyRegistry(productCatalogPolicy);
|
|
36
|
+
}
|
|
37
|
+
return _registry;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Maps a product row to a field-keyed projection consumable by the catalog
|
|
41
|
+
* resolver. Field paths match the policy registry declarations in
|
|
42
|
+
* `catalog-policy.ts`.
|
|
43
|
+
*
|
|
44
|
+
* Provenance fields (`source.kind`, `source.ref`, `seller.operator_id`) are
|
|
45
|
+
* synthesized: today's products module models operator-owned inventory
|
|
46
|
+
* exclusively, so `source.kind = "owned"` and `source.ref = undefined`.
|
|
47
|
+
* When sourced products land (e.g. via Voyant Connect), this helper picks
|
|
48
|
+
* up the provenance from a parallel provenance row instead.
|
|
49
|
+
*/
|
|
50
|
+
export function productRowToProjection(row, context) {
|
|
51
|
+
const projection = new Map([
|
|
52
|
+
// Provenance — synthesized for owned products.
|
|
53
|
+
["source.kind", "owned"],
|
|
54
|
+
["seller.operator_id", context.sellerOperatorId],
|
|
55
|
+
// Identity
|
|
56
|
+
["id", row.id],
|
|
57
|
+
["createdAt", row.createdAt],
|
|
58
|
+
["updatedAt", row.updatedAt],
|
|
59
|
+
// Merchandisable
|
|
60
|
+
["name", row.name],
|
|
61
|
+
["description", row.description],
|
|
62
|
+
["inclusionsHtml", row.inclusionsHtml],
|
|
63
|
+
["exclusionsHtml", row.exclusionsHtml],
|
|
64
|
+
["termsHtml", row.termsHtml],
|
|
65
|
+
["tags[]", row.tags],
|
|
66
|
+
// Structural
|
|
67
|
+
["status", row.status],
|
|
68
|
+
["bookingMode", row.bookingMode],
|
|
69
|
+
["capacityMode", row.capacityMode],
|
|
70
|
+
["visibility", row.visibility],
|
|
71
|
+
["activated", row.activated],
|
|
72
|
+
["productTypeId", row.productTypeId],
|
|
73
|
+
["facilityId", row.facilityId],
|
|
74
|
+
["supplierId", row.supplierId],
|
|
75
|
+
["pax", row.pax],
|
|
76
|
+
["startDate", row.startDate],
|
|
77
|
+
["endDate", row.endDate],
|
|
78
|
+
["startDateEpochDays", dateToEpochDays(row.startDate)],
|
|
79
|
+
["endDateEpochDays", dateToEpochDays(row.endDate)],
|
|
80
|
+
["timezone", row.timezone],
|
|
81
|
+
["reservationTimeoutMinutes", row.reservationTimeoutMinutes],
|
|
82
|
+
["termsShowOnContract", row.termsShowOnContract],
|
|
83
|
+
// Pricing (configured defaults — quote-time prices come from pricing module)
|
|
84
|
+
["sellAmountCents", row.sellAmountCents],
|
|
85
|
+
["sellCurrency", row.sellCurrency],
|
|
86
|
+
// Internal / staff-only
|
|
87
|
+
["costAmountCents", row.costAmountCents],
|
|
88
|
+
["marginPercent", row.marginPercent],
|
|
89
|
+
]);
|
|
90
|
+
return projection;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns the Provenance tuple for a product row. Owned products synthesize
|
|
94
|
+
* a `source.kind: "owned"` provenance with `static` freshness; sourced
|
|
95
|
+
* products (Voyant Connect / GDS / direct API) carry their actual source
|
|
96
|
+
* connection identity. Phase 1 ships only the owned form.
|
|
97
|
+
*/
|
|
98
|
+
export function productProvenance(_row, _context) {
|
|
99
|
+
return {
|
|
100
|
+
source_kind: "owned",
|
|
101
|
+
source_freshness: "static",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Catalog-aware product fetch. Returns the resolved view (source projection
|
|
106
|
+
* + active overlays + visibility filtering) instead of the raw DB row.
|
|
107
|
+
*
|
|
108
|
+
* The original `productsService.getProductById` continues to return raw
|
|
109
|
+
* rows — routes that haven't migrated to the catalog plane keep working.
|
|
110
|
+
*
|
|
111
|
+
* Returns `null` if no product with `id` exists.
|
|
112
|
+
*/
|
|
113
|
+
export async function getResolvedProductById(db, id, context) {
|
|
114
|
+
const rows = await db.select().from(products).where(eq(products.id, id)).limit(1);
|
|
115
|
+
const row = rows[0];
|
|
116
|
+
if (!row)
|
|
117
|
+
return null;
|
|
118
|
+
const projection = productRowToProjection(row, {
|
|
119
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
120
|
+
});
|
|
121
|
+
return resolveEntityView(db, getProductsRegistry(), "products", id, projection, context.scope);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Catalog-aware product list. Returns resolved views per row.
|
|
125
|
+
*
|
|
126
|
+
* Caller fetches the rows (typically via the existing `productsService.listProducts`
|
|
127
|
+
* with whatever filtering / pagination / sort the route applies) and passes
|
|
128
|
+
* them in. This keeps query construction in the existing service layer and
|
|
129
|
+
* adds the catalog overlay step on top.
|
|
130
|
+
*
|
|
131
|
+
* Overlays for the whole page are fetched in ONE query via
|
|
132
|
+
* `fetchOverlaysForEntities` and applied in-memory per product — the
|
|
133
|
+
* per-product output is byte-identical to calling `resolveEntityView`
|
|
134
|
+
* once per row, minus the N-1 sequential round trips.
|
|
135
|
+
*
|
|
136
|
+
* Real high-volume list paths (storefront browse, admin search) should
|
|
137
|
+
* still go through the search index instead — `IndexerService.search` is
|
|
138
|
+
* already wired for that purpose. Use this method for small admin-facing
|
|
139
|
+
* lists or detail-page composition where the index isn't on the read path.
|
|
140
|
+
*/
|
|
141
|
+
export async function listResolvedProducts(db, rows, context) {
|
|
142
|
+
const registry = getProductsRegistry();
|
|
143
|
+
const overlaysByEntity = await fetchOverlaysForEntities(db, "products", rows.map((row) => row.id));
|
|
144
|
+
return rows.map((row) => {
|
|
145
|
+
const projection = productRowToProjection(row, {
|
|
146
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
147
|
+
});
|
|
148
|
+
return resolveEntityViewWithOverlays(registry, projection, overlaysByEntity.get(row.id) ?? [], context.scope);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Build a `CaptureSnapshotInput` for a product to feed into the catalog
|
|
153
|
+
* plane's `captureSnapshot` / `captureSnapshotGraph` helpers at booking
|
|
154
|
+
* commit time. Fetches the product, resolves its view (overlays applied,
|
|
155
|
+
* visibility filter for the supplied scope), and returns the snapshot
|
|
156
|
+
* input shape.
|
|
157
|
+
*
|
|
158
|
+
* Returns `null` if the product doesn't exist.
|
|
159
|
+
*
|
|
160
|
+
* Composition: a single-product booking calls this once and passes the
|
|
161
|
+
* result to `captureSnapshot`. A composite booking (e.g. a tour-package
|
|
162
|
+
* booking with referenced accommodations + excursions) calls this and the
|
|
163
|
+
* other verticals' equivalents, collects the inputs, and passes them all
|
|
164
|
+
* to `captureSnapshotGraph` in one transaction.
|
|
165
|
+
*/
|
|
166
|
+
export async function buildProductSnapshotInput(db, productId, context) {
|
|
167
|
+
const view = await getResolvedProductById(db, productId, context);
|
|
168
|
+
if (!view)
|
|
169
|
+
return null;
|
|
170
|
+
return buildSnapshotInputFromView(view, {
|
|
171
|
+
entityModule: "products",
|
|
172
|
+
entityId: productId,
|
|
173
|
+
sourceKind: "owned",
|
|
174
|
+
pricingBasis: context.pricingBasis,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Compose the registry from the base product policy plus any contributing
|
|
179
|
+
* extensions' policies. Templates wire this when they enable child-entity
|
|
180
|
+
* registries.
|
|
181
|
+
*/
|
|
182
|
+
export function createProductsRegistry(...extensionPolicies) {
|
|
183
|
+
if (extensionPolicies.length === 0)
|
|
184
|
+
return getProductsRegistry();
|
|
185
|
+
const composed = [...productCatalogPolicy];
|
|
186
|
+
for (const policies of extensionPolicies) {
|
|
187
|
+
composed.push(...policies);
|
|
188
|
+
}
|
|
189
|
+
return createFieldPolicyRegistry(composed);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Construct a sync `DocumentEmitter` for products. The emitter takes a
|
|
193
|
+
* pre-fetched product row + a slice and returns the indexer document
|
|
194
|
+
* (filtered by visibility, with blob-only fields skipped).
|
|
195
|
+
*
|
|
196
|
+
* Bulk-reindex pipelines that already have rows in hand call this directly.
|
|
197
|
+
* Live reindex paths use `createProductDocumentBuilder` below, which fetches
|
|
198
|
+
* the row before emitting.
|
|
199
|
+
*
|
|
200
|
+
* Pass a custom `registry` when the deployment composes additional
|
|
201
|
+
* child-entity policies; otherwise the default products registry is used.
|
|
202
|
+
*/
|
|
203
|
+
export function createProductDocumentEmitter(context) {
|
|
204
|
+
const registry = context.registry ?? getProductsRegistry();
|
|
205
|
+
return {
|
|
206
|
+
vertical: "products",
|
|
207
|
+
emit(source, slice) {
|
|
208
|
+
const projection = productRowToProjection(source, {
|
|
209
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
210
|
+
});
|
|
211
|
+
return buildIndexerDocument(registry, projection, slice, source.id);
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function isPublicStorefrontProduct(row) {
|
|
216
|
+
return row.status === "active" && row.activated === true && row.visibility === "public";
|
|
217
|
+
}
|
|
218
|
+
function shouldEmitForSlice(row, slice) {
|
|
219
|
+
// The catalog is a "bookable now" surface — draft and archived
|
|
220
|
+
// products don't belong there, regardless of audience. Operators
|
|
221
|
+
// browsing /catalog get the same active-only set the storefront sees,
|
|
222
|
+
// just with staff-visible attribute columns.
|
|
223
|
+
if (row.status !== "active")
|
|
224
|
+
return false;
|
|
225
|
+
if (slice.audience === "customer" ||
|
|
226
|
+
slice.audience === "partner" ||
|
|
227
|
+
slice.audience === "supplier") {
|
|
228
|
+
return isPublicStorefrontProduct(row);
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Async `DocumentBuilder` for products — fetches the row by id, then emits.
|
|
234
|
+
* Plug this into `IndexerService.reindexEntity` for live reindex events.
|
|
235
|
+
*
|
|
236
|
+
* Returns `null` if the product no longer exists (e.g. it was deleted
|
|
237
|
+
* between the reindex enqueue and the worker picking it up). Callers can
|
|
238
|
+
* treat `null` as a delete signal.
|
|
239
|
+
*
|
|
240
|
+
* `extensions` denormalize child-entity fields onto the product doc. They
|
|
241
|
+
* run in parallel after the base row is fetched. An extension that throws
|
|
242
|
+
* fails the whole build — failures here would otherwise produce silently
|
|
243
|
+
* incomplete documents.
|
|
244
|
+
*
|
|
245
|
+
* Pass a custom `registry` (composed via `createProductsRegistry`) when
|
|
246
|
+
* extensions contribute fields beyond the base products policy.
|
|
247
|
+
*/
|
|
248
|
+
export function createProductDocumentBuilder(db, context) {
|
|
249
|
+
const registry = context.registry ?? getProductsRegistry();
|
|
250
|
+
const extensions = context.extensions ?? [];
|
|
251
|
+
return async (entityId, slice) => {
|
|
252
|
+
const rows = await db.select().from(products).where(eq(products.id, entityId)).limit(1);
|
|
253
|
+
const row = rows[0];
|
|
254
|
+
if (!row)
|
|
255
|
+
return null;
|
|
256
|
+
if (!shouldEmitForSlice(row, slice))
|
|
257
|
+
return null;
|
|
258
|
+
const baseProjection = productRowToProjection(row, {
|
|
259
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
260
|
+
});
|
|
261
|
+
if (extensions.length === 0) {
|
|
262
|
+
return buildIndexerDocument(registry, baseProjection, slice, entityId);
|
|
263
|
+
}
|
|
264
|
+
const extensionProjections = await Promise.all(extensions.map((ext) => ext.project(db, entityId, slice)));
|
|
265
|
+
const merged = new Map(baseProjection);
|
|
266
|
+
for (const projection of extensionProjections) {
|
|
267
|
+
for (const [path, value] of projection) {
|
|
268
|
+
merged.set(path, value);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return buildIndexerDocument(registry, merged, slice, entityId);
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Product-owned storefront-card projection. This extension keeps the
|
|
276
|
+
* customer catalog slice directly renderable by denormalizing localized
|
|
277
|
+
* routing, card media, duration, and map coordinates into the search doc.
|
|
278
|
+
*/
|
|
279
|
+
export function createProductStorefrontCardProjectionExtension() {
|
|
280
|
+
return {
|
|
281
|
+
name: "products:storefront-card",
|
|
282
|
+
async project(db, productId, slice) {
|
|
283
|
+
// Wave 1: everything keyed by productId alone runs concurrently —
|
|
284
|
+
// including the departures count, which used to trail sequentially.
|
|
285
|
+
const [translations, mediaRows, locationRows, itineraryRows, availableDeparturesCount] = await Promise.all([
|
|
286
|
+
db
|
|
287
|
+
.select({
|
|
288
|
+
languageTag: productTranslations.languageTag,
|
|
289
|
+
name: productTranslations.name,
|
|
290
|
+
slug: productTranslations.slug,
|
|
291
|
+
shortDescription: productTranslations.shortDescription,
|
|
292
|
+
inclusionsHtml: productTranslations.inclusionsHtml,
|
|
293
|
+
exclusionsHtml: productTranslations.exclusionsHtml,
|
|
294
|
+
termsHtml: productTranslations.termsHtml,
|
|
295
|
+
})
|
|
296
|
+
.from(productTranslations)
|
|
297
|
+
.where(eq(productTranslations.productId, productId))
|
|
298
|
+
.orderBy(asc(productTranslations.updatedAt)),
|
|
299
|
+
db
|
|
300
|
+
.select({
|
|
301
|
+
url: productMedia.url,
|
|
302
|
+
mediaType: productMedia.mediaType,
|
|
303
|
+
isCover: productMedia.isCover,
|
|
304
|
+
isBrochure: productMedia.isBrochure,
|
|
305
|
+
sortOrder: productMedia.sortOrder,
|
|
306
|
+
createdAt: productMedia.createdAt,
|
|
307
|
+
})
|
|
308
|
+
.from(productMedia)
|
|
309
|
+
.where(eq(productMedia.productId, productId))
|
|
310
|
+
.orderBy(asc(productMedia.sortOrder), asc(productMedia.createdAt)),
|
|
311
|
+
db
|
|
312
|
+
.select({
|
|
313
|
+
latitude: productLocations.latitude,
|
|
314
|
+
longitude: productLocations.longitude,
|
|
315
|
+
sortOrder: productLocations.sortOrder,
|
|
316
|
+
createdAt: productLocations.createdAt,
|
|
317
|
+
})
|
|
318
|
+
.from(productLocations)
|
|
319
|
+
.where(eq(productLocations.productId, productId))
|
|
320
|
+
.orderBy(asc(productLocations.sortOrder), asc(productLocations.createdAt)),
|
|
321
|
+
db
|
|
322
|
+
.select({ id: productItineraries.id, isDefault: productItineraries.isDefault })
|
|
323
|
+
.from(productItineraries)
|
|
324
|
+
.where(eq(productItineraries.productId, productId))
|
|
325
|
+
.orderBy(asc(productItineraries.sortOrder)),
|
|
326
|
+
countAvailableDepartures(db, productId),
|
|
327
|
+
]);
|
|
328
|
+
const translation = pickTranslation(translations, slice.locale);
|
|
329
|
+
const imageMediaRows = mediaRows.filter((m) => !m.isBrochure && m.mediaType === "image");
|
|
330
|
+
const cover = imageMediaRows.find((m) => m.isCover);
|
|
331
|
+
const primaryMedia = cover ?? imageMediaRows[0] ?? null;
|
|
332
|
+
const coordinateLocation = locationRows.find((l) => l.latitude != null && l.longitude != null) ?? null;
|
|
333
|
+
const defaultItinerary = itineraryRows.find((it) => it.isDefault) ?? itineraryRows[0];
|
|
334
|
+
// Wave 2: the duration estimate needs `defaultItinerary`, which is only
|
|
335
|
+
// known after the itinerary rows land — it cannot join wave 1.
|
|
336
|
+
const durationDays = defaultItinerary
|
|
337
|
+
? await estimateItineraryDurationDays(db, defaultItinerary.id)
|
|
338
|
+
: null;
|
|
339
|
+
const out = new Map([
|
|
340
|
+
["slug", translation?.slug ?? null],
|
|
341
|
+
["shortDescription", translation?.shortDescription ?? null],
|
|
342
|
+
["primaryMediaUrl", primaryMedia?.url ?? null],
|
|
343
|
+
["thumbnailUrl", primaryMedia?.url ?? null],
|
|
344
|
+
["coverMediaUrl", primaryMedia?.url ?? null],
|
|
345
|
+
["durationDays", durationDays],
|
|
346
|
+
["availableDeparturesCount", availableDeparturesCount],
|
|
347
|
+
["latitude", coordinateLocation?.latitude ?? null],
|
|
348
|
+
["longitude", coordinateLocation?.longitude ?? null],
|
|
349
|
+
]);
|
|
350
|
+
if (translation?.name) {
|
|
351
|
+
out.set("name", translation.name);
|
|
352
|
+
}
|
|
353
|
+
if (translation?.inclusionsHtml != null) {
|
|
354
|
+
out.set("inclusionsHtml", translation.inclusionsHtml);
|
|
355
|
+
}
|
|
356
|
+
if (translation?.exclusionsHtml != null) {
|
|
357
|
+
out.set("exclusionsHtml", translation.exclusionsHtml);
|
|
358
|
+
}
|
|
359
|
+
if (translation?.termsHtml != null) {
|
|
360
|
+
out.set("termsHtml", translation.termsHtml);
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function pickTranslation(rows, locale) {
|
|
367
|
+
return (rows.find((row) => row.languageTag === locale) ??
|
|
368
|
+
rows.find((row) => row.languageTag.toLowerCase() === locale.toLowerCase()) ??
|
|
369
|
+
rows.find((row) => row.languageTag.split("-")[0] === locale.split("-")[0]) ??
|
|
370
|
+
rows[0] ??
|
|
371
|
+
null);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Counts future, open availability slots for a product — surfaces in the
|
|
375
|
+
* catalog index as `availableDeparturesCount` so the operator catalog
|
|
376
|
+
* table can show booking-ready stock at a glance without a separate
|
|
377
|
+
* round-trip. Owned-products only; sourced products carry departures via
|
|
378
|
+
* the upstream feed and don't write to `availability_slots`.
|
|
379
|
+
*
|
|
380
|
+
* Cross-package boundary: queries `availability_slots` by raw table name
|
|
381
|
+
* via `sql` so the products module doesn't take a hard dependency on the
|
|
382
|
+
* `@voyant-travel/operations` schema.
|
|
383
|
+
*/
|
|
384
|
+
async function countAvailableDepartures(db, productId) {
|
|
385
|
+
try {
|
|
386
|
+
const result = await db.execute(sql `
|
|
387
|
+
SELECT COUNT(*)::int AS count
|
|
388
|
+
FROM availability_slots
|
|
389
|
+
WHERE product_id = ${productId}
|
|
390
|
+
AND status = 'open'
|
|
391
|
+
AND starts_at >= NOW()
|
|
392
|
+
`);
|
|
393
|
+
// postgres-js + neon-serverless both return `{ count }` on the first row;
|
|
394
|
+
// shape-defensive read in case a driver wraps it differently.
|
|
395
|
+
const rows = Array.isArray(result) ? result : (result.rows ?? []);
|
|
396
|
+
const row = rows[0];
|
|
397
|
+
const value = row?.count;
|
|
398
|
+
if (typeof value === "number")
|
|
399
|
+
return value;
|
|
400
|
+
if (typeof value === "string") {
|
|
401
|
+
const n = Number(value);
|
|
402
|
+
return Number.isFinite(n) ? n : 0;
|
|
403
|
+
}
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// availability_slots may not exist in slim test fixtures; treat as 0
|
|
408
|
+
// so reindex doesn't fail.
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function estimateItineraryDurationDays(db, itineraryId) {
|
|
413
|
+
const rows = await db
|
|
414
|
+
.select({ dayNumber: productDays.dayNumber })
|
|
415
|
+
.from(productDays)
|
|
416
|
+
.where(eq(productDays.itineraryId, itineraryId))
|
|
417
|
+
.orderBy(asc(productDays.dayNumber));
|
|
418
|
+
if (rows.length === 0)
|
|
419
|
+
return null;
|
|
420
|
+
const max = Math.max(...rows.map((row) => row.dayNumber));
|
|
421
|
+
return Number.isFinite(max) && max > 0 ? max : null;
|
|
422
|
+
}
|
|
423
|
+
function dateToEpochDays(value) {
|
|
424
|
+
if (!value)
|
|
425
|
+
return null;
|
|
426
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
427
|
+
const time = date.getTime();
|
|
428
|
+
if (Number.isNaN(time))
|
|
429
|
+
return null;
|
|
430
|
+
return Math.floor(time / (24 * 60 * 60 * 1000));
|
|
431
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import { products } from "./schema.js";
|
|
3
|
+
import type { CatalogSearchDocument, CatalogSearchDocumentListQuery } from "./validation-catalog.js";
|
|
4
|
+
type CatalogProductRow = typeof products.$inferSelect;
|
|
5
|
+
type HydrateCatalogProductOptions = {
|
|
6
|
+
includeContent?: boolean;
|
|
7
|
+
languageTag?: string | null;
|
|
8
|
+
fallbackLanguageTags?: string[];
|
|
9
|
+
};
|
|
10
|
+
export declare const catalogProductsService: {
|
|
11
|
+
hydrateProducts(db: PostgresJsDatabase, productRows: CatalogProductRow[], options?: HydrateCatalogProductOptions): Promise<({
|
|
12
|
+
description: string | null;
|
|
13
|
+
inclusionsHtml: null;
|
|
14
|
+
exclusionsHtml: null;
|
|
15
|
+
termsHtml: null;
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
contentLanguageTag: string | null;
|
|
19
|
+
slug: string | null;
|
|
20
|
+
shortDescription: string | null;
|
|
21
|
+
seoTitle: string | null;
|
|
22
|
+
seoDescription: string | null;
|
|
23
|
+
bookingMode: "date" | "date_time" | "open" | "stay" | "transfer" | "itinerary" | "other";
|
|
24
|
+
capacityMode: "free_sale" | "limited" | "on_request";
|
|
25
|
+
visibility: "public" | "private" | "hidden";
|
|
26
|
+
sellCurrency: string;
|
|
27
|
+
sellAmountCents: number | null;
|
|
28
|
+
startDate: string | null;
|
|
29
|
+
endDate: string | null;
|
|
30
|
+
pax: number | null;
|
|
31
|
+
contractTemplateId: string | null;
|
|
32
|
+
productType: {
|
|
33
|
+
id: string;
|
|
34
|
+
code: string;
|
|
35
|
+
name: string;
|
|
36
|
+
description: string | null;
|
|
37
|
+
} | null;
|
|
38
|
+
categories: {
|
|
39
|
+
id: string;
|
|
40
|
+
parentId: string | null;
|
|
41
|
+
name: string;
|
|
42
|
+
slug: string;
|
|
43
|
+
description: string | null;
|
|
44
|
+
sortOrder: number;
|
|
45
|
+
}[];
|
|
46
|
+
tags: {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
}[];
|
|
50
|
+
capabilities: string[];
|
|
51
|
+
destinations: {
|
|
52
|
+
id: string;
|
|
53
|
+
parentId: string | null;
|
|
54
|
+
slug: string;
|
|
55
|
+
canonicalPlaceId: string | null;
|
|
56
|
+
name: string;
|
|
57
|
+
description: string | null;
|
|
58
|
+
seoTitle: string | null;
|
|
59
|
+
seoDescription: string | null;
|
|
60
|
+
destinationType: string;
|
|
61
|
+
latitude: number | null;
|
|
62
|
+
longitude: number | null;
|
|
63
|
+
sortOrder: number;
|
|
64
|
+
}[];
|
|
65
|
+
locations: {
|
|
66
|
+
id: string;
|
|
67
|
+
locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
68
|
+
title: string;
|
|
69
|
+
address: string | null;
|
|
70
|
+
city: string | null;
|
|
71
|
+
countryCode: string | null;
|
|
72
|
+
latitude: number | null;
|
|
73
|
+
longitude: number | null;
|
|
74
|
+
sortOrder: number;
|
|
75
|
+
}[];
|
|
76
|
+
coverMedia: {
|
|
77
|
+
id: string;
|
|
78
|
+
mediaType: "image" | "video" | "document";
|
|
79
|
+
name: string;
|
|
80
|
+
url: string;
|
|
81
|
+
mimeType: string | null;
|
|
82
|
+
altText: string | null;
|
|
83
|
+
sortOrder: number;
|
|
84
|
+
isCover: boolean;
|
|
85
|
+
isBrochure: boolean;
|
|
86
|
+
isBrochureCurrent: boolean;
|
|
87
|
+
brochureVersion: number | null;
|
|
88
|
+
} | null;
|
|
89
|
+
isFeatured: boolean;
|
|
90
|
+
} | {
|
|
91
|
+
brochure: {
|
|
92
|
+
id: string;
|
|
93
|
+
mediaType: "image" | "video" | "document";
|
|
94
|
+
name: string;
|
|
95
|
+
url: string;
|
|
96
|
+
mimeType: string | null;
|
|
97
|
+
altText: string | null;
|
|
98
|
+
sortOrder: number;
|
|
99
|
+
isCover: boolean;
|
|
100
|
+
isBrochure: boolean;
|
|
101
|
+
isBrochureCurrent: boolean;
|
|
102
|
+
brochureVersion: number | null;
|
|
103
|
+
} | null;
|
|
104
|
+
media: {
|
|
105
|
+
id: string;
|
|
106
|
+
mediaType: "image" | "video" | "document";
|
|
107
|
+
name: string;
|
|
108
|
+
url: string;
|
|
109
|
+
mimeType: string | null;
|
|
110
|
+
altText: string | null;
|
|
111
|
+
sortOrder: number;
|
|
112
|
+
isCover: boolean;
|
|
113
|
+
isBrochure: boolean;
|
|
114
|
+
isBrochureCurrent: boolean;
|
|
115
|
+
brochureVersion: number | null;
|
|
116
|
+
}[];
|
|
117
|
+
features: {
|
|
118
|
+
id: string;
|
|
119
|
+
featureType: "other" | "inclusion" | "exclusion" | "highlight" | "important_information";
|
|
120
|
+
title: string;
|
|
121
|
+
description: string | null;
|
|
122
|
+
sortOrder: number;
|
|
123
|
+
}[];
|
|
124
|
+
faqs: {
|
|
125
|
+
id: string;
|
|
126
|
+
question: string;
|
|
127
|
+
answer: string;
|
|
128
|
+
sortOrder: number;
|
|
129
|
+
}[];
|
|
130
|
+
id: string;
|
|
131
|
+
name: string;
|
|
132
|
+
description: string | null;
|
|
133
|
+
inclusionsHtml: string | null;
|
|
134
|
+
exclusionsHtml: string | null;
|
|
135
|
+
termsHtml: string | null;
|
|
136
|
+
contentLanguageTag: string | null;
|
|
137
|
+
slug: string | null;
|
|
138
|
+
shortDescription: string | null;
|
|
139
|
+
seoTitle: string | null;
|
|
140
|
+
seoDescription: string | null;
|
|
141
|
+
bookingMode: "date" | "date_time" | "open" | "stay" | "transfer" | "itinerary" | "other";
|
|
142
|
+
capacityMode: "free_sale" | "limited" | "on_request";
|
|
143
|
+
visibility: "public" | "private" | "hidden";
|
|
144
|
+
sellCurrency: string;
|
|
145
|
+
sellAmountCents: number | null;
|
|
146
|
+
startDate: string | null;
|
|
147
|
+
endDate: string | null;
|
|
148
|
+
pax: number | null;
|
|
149
|
+
contractTemplateId: string | null;
|
|
150
|
+
productType: {
|
|
151
|
+
id: string;
|
|
152
|
+
code: string;
|
|
153
|
+
name: string;
|
|
154
|
+
description: string | null;
|
|
155
|
+
} | null;
|
|
156
|
+
categories: {
|
|
157
|
+
id: string;
|
|
158
|
+
parentId: string | null;
|
|
159
|
+
name: string;
|
|
160
|
+
slug: string;
|
|
161
|
+
description: string | null;
|
|
162
|
+
sortOrder: number;
|
|
163
|
+
}[];
|
|
164
|
+
tags: {
|
|
165
|
+
id: string;
|
|
166
|
+
name: string;
|
|
167
|
+
}[];
|
|
168
|
+
capabilities: string[];
|
|
169
|
+
destinations: {
|
|
170
|
+
id: string;
|
|
171
|
+
parentId: string | null;
|
|
172
|
+
slug: string;
|
|
173
|
+
canonicalPlaceId: string | null;
|
|
174
|
+
name: string;
|
|
175
|
+
description: string | null;
|
|
176
|
+
seoTitle: string | null;
|
|
177
|
+
seoDescription: string | null;
|
|
178
|
+
destinationType: string;
|
|
179
|
+
latitude: number | null;
|
|
180
|
+
longitude: number | null;
|
|
181
|
+
sortOrder: number;
|
|
182
|
+
}[];
|
|
183
|
+
locations: {
|
|
184
|
+
id: string;
|
|
185
|
+
locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
186
|
+
title: string;
|
|
187
|
+
address: string | null;
|
|
188
|
+
city: string | null;
|
|
189
|
+
countryCode: string | null;
|
|
190
|
+
latitude: number | null;
|
|
191
|
+
longitude: number | null;
|
|
192
|
+
sortOrder: number;
|
|
193
|
+
}[];
|
|
194
|
+
coverMedia: {
|
|
195
|
+
id: string;
|
|
196
|
+
mediaType: "image" | "video" | "document";
|
|
197
|
+
name: string;
|
|
198
|
+
url: string;
|
|
199
|
+
mimeType: string | null;
|
|
200
|
+
altText: string | null;
|
|
201
|
+
sortOrder: number;
|
|
202
|
+
isCover: boolean;
|
|
203
|
+
isBrochure: boolean;
|
|
204
|
+
isBrochureCurrent: boolean;
|
|
205
|
+
brochureVersion: number | null;
|
|
206
|
+
} | null;
|
|
207
|
+
isFeatured: boolean;
|
|
208
|
+
})[]>;
|
|
209
|
+
listSearchDocuments(db: PostgresJsDatabase, query: CatalogSearchDocumentListQuery): Promise<{
|
|
210
|
+
data: CatalogSearchDocument[];
|
|
211
|
+
total: number;
|
|
212
|
+
limit: number;
|
|
213
|
+
offset: number;
|
|
214
|
+
}>;
|
|
215
|
+
getSearchDocumentByProductId(db: PostgresJsDatabase, productId: string, query?: Partial<Omit<CatalogSearchDocumentListQuery, "productIds" | "limit" | "offset">>): Promise<{
|
|
216
|
+
id: string;
|
|
217
|
+
productId: string;
|
|
218
|
+
languageTag: string | null;
|
|
219
|
+
name: string;
|
|
220
|
+
slug: string | null;
|
|
221
|
+
shortDescription: string | null;
|
|
222
|
+
description: string | null;
|
|
223
|
+
seoTitle: string | null;
|
|
224
|
+
seoDescription: string | null;
|
|
225
|
+
sellCurrency: string;
|
|
226
|
+
sellAmountCents: number | null;
|
|
227
|
+
startDate: string | null;
|
|
228
|
+
endDate: string | null;
|
|
229
|
+
pax: number | null;
|
|
230
|
+
productTypeCode: string | null;
|
|
231
|
+
productTypeName: string | null;
|
|
232
|
+
categoryIds: string[];
|
|
233
|
+
categoryNames: string[];
|
|
234
|
+
categorySlugs: string[];
|
|
235
|
+
tagIds: string[];
|
|
236
|
+
tagNames: string[];
|
|
237
|
+
capabilities: string[];
|
|
238
|
+
destinationIds: string[];
|
|
239
|
+
destinationNames: string[];
|
|
240
|
+
destinationSlugs: string[];
|
|
241
|
+
locationTitles: string[];
|
|
242
|
+
locationCities: string[];
|
|
243
|
+
locationCountryCodes: string[];
|
|
244
|
+
coverMediaUrl: string | null;
|
|
245
|
+
isFeatured: boolean;
|
|
246
|
+
createdAt: string | null;
|
|
247
|
+
updatedAt: string | null;
|
|
248
|
+
} | null>;
|
|
249
|
+
};
|
|
250
|
+
export {};
|
|
251
|
+
//# sourceMappingURL=service-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-catalog.d.ts","sourceRoot":"","sources":["../src/service-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAWL,QAAQ,EAMT,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EACV,qBAAqB,EACrB,8BAA8B,EAE/B,MAAM,yBAAyB,CAAA;AAEhC,KAAK,iBAAiB,GAAG,OAAO,QAAQ,CAAC,YAAY,CAAA;AAErD,KAAK,4BAA4B,GAAG;IAClC,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAA;CAChC,CAAA;AA2aD,eAAO,MAAM,sBAAsB;wBAE3B,kBAAkB,eACT,iBAAiB,EAAE,YACvB,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAkIjC,kBAAkB,SACf,8BAA8B,GACpC,OAAO,CAAC;QACT,IAAI,EAAE,qBAAqB,EAAE,CAAA;QAC7B,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAC;qCAkFI,kBAAkB,aACX,MAAM,UACV,OAAO,CAAC,IAAI,CAAC,8BAA8B,EAAE,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAa1F,CAAA"}
|