@voyant-travel/inventory 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,14 @@
1
1
  import type { KVStore } from "@voyant-travel/utils/cache";
2
2
  export declare function productDocKey(productId: string, variant: string): string;
3
- export declare function productSlugMapKey(slug: string): string;
3
+ export declare function productSlugMapKey(slug: string, variant: string): string;
4
4
  /** Stable variant id from the detail query (currently just the locale). */
5
5
  export declare function productDocVariant(query: {
6
6
  languageTag?: string | null;
7
7
  }): string;
8
+ export interface ProductSlugResolution {
9
+ productId: string;
10
+ languageTag: string | null;
11
+ }
8
12
  /**
9
13
  * Read-through document fetch. `null` compute results (missing/inactive
10
14
  * product) are never cached — a 404 must not mask a product that
@@ -15,7 +19,7 @@ export declare function readThroughProductDoc<T>(kv: KVStore | undefined, key: s
15
19
  fromReadModel: boolean;
16
20
  }>;
17
21
  /** Resolve a slug to a product id through the KV mapping. */
18
- export declare function readThroughSlugMapping(kv: KVStore | undefined, slug: string, resolve: () => Promise<string | null>): Promise<string | null>;
22
+ export declare function readThroughSlugMapping(kv: KVStore | undefined, slug: string, variant: string, resolve: () => Promise<ProductSlugResolution | null>): Promise<ProductSlugResolution | null>;
19
23
  /**
20
24
  * Drop every cached document variant for a product. Uses KV `list` by
21
25
  * prefix (optional on the KVStore contract — silently a no-op without
@@ -1 +1 @@
1
- {"version":3,"file":"read-model.d.ts","sourceRoot":"","sources":["../src/read-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AA2BzD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,MAAM,CAEhF;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAC3C,EAAE,EAAE,OAAO,GAAG,SAAS,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,GAC/B,OAAO,CAAC;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAAC,CAkBrD;AAED,6DAA6D;AAC7D,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,OAAO,GAAG,SAAS,EACvB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GACpC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkBxB;AAED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAAC,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9F"}
1
+ {"version":3,"file":"read-model.d.ts","sourceRoot":"","sources":["../src/read-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AA2BzD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,MAAM,CAEhF;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAC3C,EAAE,EAAE,OAAO,GAAG,SAAS,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,GAC/B,OAAO,CAAC;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAAC,CAkBrD;AAED,6DAA6D;AAC7D,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,OAAO,GAAG,SAAS,EACvB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,GACnD,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAmBvC;AAED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAAC,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9F"}
@@ -10,8 +10,8 @@
10
10
  * `productsReadModelInvalidation` in routes.ts), and a generous TTL
11
11
  * bounds staleness for anything invalidation misses.
12
12
  *
13
- * Slug lookups resolve through a short-lived slug→id mapping so the
14
- * id-keyed document is shared between `/:id` and `/slug/:slug`. The
13
+ * Slug lookups resolve through a short-lived slug→product+locale mapping
14
+ * so the id-keyed document is shared between `/:id` and `/slug/:slug`. The
15
15
  * mapping is deliberately NOT invalidated on mutation: it's cheap to
16
16
  * refill, and its short TTL bounds the only affected case (a renamed
17
17
  * slug serving the old document) to a few minutes.
@@ -24,8 +24,8 @@ const SLUG_MAP_TTL_SECONDS = 5 * 60;
24
24
  export function productDocKey(productId, variant) {
25
25
  return `${RM_PREFIX}:${productId}:${variant}`;
26
26
  }
27
- export function productSlugMapKey(slug) {
28
- return `rm:v1:product-slug:${slug}`;
27
+ export function productSlugMapKey(slug, variant) {
28
+ return `rm:v1:product-slug:${variant}:${slug}`;
29
29
  }
30
30
  /** Stable variant id from the detail query (currently just the locale). */
31
31
  export function productDocVariant(query) {
@@ -59,10 +59,11 @@ export async function readThroughProductDoc(kv, key, compute) {
59
59
  return { data, fromReadModel: false };
60
60
  }
61
61
  /** Resolve a slug to a product id through the KV mapping. */
62
- export async function readThroughSlugMapping(kv, slug, resolve) {
62
+ export async function readThroughSlugMapping(kv, slug, variant, resolve) {
63
+ const key = productSlugMapKey(slug, variant);
63
64
  if (kv) {
64
65
  try {
65
- const hit = await kv.get(productSlugMapKey(slug), { type: "text" });
66
+ const hit = await kv.get(key, { type: "json" });
66
67
  if (hit)
67
68
  return hit;
68
69
  }
@@ -70,16 +71,16 @@ export async function readThroughSlugMapping(kv, slug, resolve) {
70
71
  // fall through
71
72
  }
72
73
  }
73
- const id = await resolve();
74
- if (id && kv) {
74
+ const resolution = await resolve();
75
+ if (resolution && kv) {
75
76
  try {
76
- await kv.put(productSlugMapKey(slug), id, { expirationTtl: SLUG_MAP_TTL_SECONDS });
77
+ await kv.put(key, JSON.stringify(resolution), { expirationTtl: SLUG_MAP_TTL_SECONDS });
77
78
  }
78
79
  catch {
79
80
  // best-effort
80
81
  }
81
82
  }
82
- return id;
83
+ return resolution;
83
84
  }
84
85
  /**
85
86
  * Drop every cached document variant for a product. Uses KV `list` by
@@ -1 +1 @@
1
- {"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAqBjE,KAAK,GAAG,GAAG;IACT,QAAQ,EAAE;QACR,mEAAmE;QACnE,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB,CAAA;IACD,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AA2DD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAkF5B,CAAA;AAEJ,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAA"}
1
+ {"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAqBjE,KAAK,GAAG,GAAG;IACT,QAAQ,EAAE;QACR,mEAAmE;QACnE,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB,CAAA;IACD,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AA2DD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgG5B,CAAA;AAEJ,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAA"}
@@ -66,14 +66,26 @@ export const publicProductRoutes = new Hono()
66
66
  const slug = c.req.param("slug");
67
67
  // Resolve slug → id through the short-lived KV mapping so both detail
68
68
  // routes share one id-keyed document per variant.
69
- const productId = await readThroughSlugMapping(kv, slug, async () => {
69
+ const requestedVariant = productDocVariant(query);
70
+ const resolution = await readThroughSlugMapping(kv, slug, requestedVariant, async () => {
70
71
  const row = await publicProductsService.getCatalogProductBySlug(c.get("db"), slug, query);
71
- return row ? (row.id ?? null) : null;
72
+ const productId = row ? (row.id ?? null) : null;
73
+ if (!productId)
74
+ return null;
75
+ return {
76
+ productId,
77
+ languageTag: row.contentLanguageTag ??
78
+ query.languageTag ??
79
+ null,
80
+ };
72
81
  });
73
- if (!productId) {
82
+ if (!resolution) {
74
83
  return c.json({ error: "Catalog product not found" }, 404);
75
84
  }
76
- const { data } = await readThroughProductDoc(kv, productDocKey(productId, productDocVariant(query)), () => publicProductsService.getCatalogProductById(c.get("db"), productId, query));
85
+ const detailQuery = resolution.languageTag
86
+ ? { ...query, languageTag: resolution.languageTag }
87
+ : query;
88
+ const { data } = await readThroughProductDoc(kv, productDocKey(resolution.productId, productDocVariant(detailQuery)), () => publicProductsService.getCatalogProductById(c.get("db"), resolution.productId, detailQuery));
77
89
  if (!data) {
78
90
  return c.json({ error: "Catalog product not found" }, 404);
79
91
  }
@@ -7,6 +7,7 @@ type HydrateCatalogProductOptions = {
7
7
  languageTag?: string | null;
8
8
  fallbackLanguageTags?: string[];
9
9
  };
10
+ export declare const DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS: readonly ["en", "ro"];
10
11
  export declare const catalogProductsService: {
11
12
  hydrateProducts(db: PostgresJsDatabase, productRows: CatalogProductRow[], options?: HydrateCatalogProductOptions): Promise<({
12
13
  description: string | null;
@@ -1 +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"}
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;AAED,eAAO,MAAM,6CAA6C,uBAAwB,CAAA;AA2alF,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;qCAoFI,kBAAkB,aACX,MAAM,UACV,OAAO,CAAC,IAAI,CAAC,8BAA8B,EAAE,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAa1F,CAAA"}
@@ -1,6 +1,7 @@
1
1
  // agent-quality: file-size exception -- owner: inventory; existing service module stays co-located until a dedicated split preserves behavior and tests.
2
2
  import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
3
3
  import { destinations, destinationTranslations, productCapabilities, productCategories, productCategoryProducts, productDestinations, productFaqs, productFeatures, productLocations, productMedia, products, productTagProducts, productTags, productTranslations, productTypes, productVisibilitySettings, } from "./schema.js";
4
+ export const DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS = ["en", "ro"];
4
5
  function normalizeDate(value) {
5
6
  if (!value) {
6
7
  return null;
@@ -456,7 +457,8 @@ export const catalogProductsService = {
456
457
  const localizedProducts = (await this.hydrateProducts(db, rows, {
457
458
  includeContent: true,
458
459
  languageTag: query.languageTag,
459
- fallbackLanguageTags: query.fallbackLanguageTags ?? (query.languageTag ? ["en", "ro"] : []),
460
+ fallbackLanguageTags: query.fallbackLanguageTags ??
461
+ (query.languageTag ? [...DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS] : []),
460
462
  }));
461
463
  const rowById = new Map(rows.map((row) => [row.id, row]));
462
464
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAgBjE,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,6BAA6B,EAC7B,qCAAqC,EACrC,yBAAyB,EAC1B,MAAM,wBAAwB,CAAA;AAgI/B,eAAO,MAAM,qBAAqB;4BAE1B,kBAAkB,SACf,6BAA6B,GAAG;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA6H/D,kBAAkB,MAClB,MAAM,UACH;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCA2BlC,kBAAkB,QAChB,MAAM,UACL,qCAAqC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAqCxC,kBAAkB,aACX,MAAM,UACV;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;;;;;;;;;;;8BAMR,kBAAkB,SAAS,8BAA8B;;;;;;;;;;;;;wBAkD/D,kBAAkB,SAAS,yBAAyB;;;;;;;;;gCA+B5C,kBAAkB,SAAS,iCAAiC;;;;;;;;;;;;;;;;;;;CAqG/F,CAAA"}
1
+ {"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAmBjE,OAAO,KAAK,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,6BAA6B,EAC7B,qCAAqC,EACrC,yBAAyB,EAC1B,MAAM,wBAAwB,CAAA;AA0I/B,eAAO,MAAM,qBAAqB;4BAE1B,kBAAkB,SACf,6BAA6B,GAAG;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA6H/D,kBAAkB,MAClB,MAAM,UACH;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCA2BlC,kBAAkB,QAChB,MAAM,UACL,qCAAqC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAoDxC,kBAAkB,aACX,MAAM,UACV;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;;;;;;;;;;;8BAMR,kBAAkB,SAAS,8BAA8B;;;;;;;;;;;;;wBAkD/D,kBAAkB,SAAS,yBAAyB;;;;;;;;;gCA+B5C,kBAAkB,SAAS,iCAAiC;;;;;;;;;;;;;;;;;;;CAqG/F,CAAA"}
@@ -1,7 +1,7 @@
1
1
  // agent-quality: file-size exception -- owner: inventory; existing service module stays co-located until a dedicated split preserves behavior and tests.
2
2
  import { and, asc, desc, eq, ilike, inArray, notInArray, or, sql } from "drizzle-orm";
3
3
  import { destinations, destinationTranslations, productCategories, productCategoryProducts, productDestinations, productLocations, products, productTagProducts, productTags, productTranslations, productVisibilitySettings, } from "./schema.js";
4
- import { catalogProductsService } from "./service-catalog.js";
4
+ import { catalogProductsService, DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS, } from "./service-catalog.js";
5
5
  function impossibleCondition() {
6
6
  return sql `1 = 0`;
7
7
  }
@@ -9,6 +9,11 @@ function normalizeLanguageTag(value) {
9
9
  const normalized = value?.trim().toLowerCase();
10
10
  return normalized || null;
11
11
  }
12
+ function normalizeLanguageTagList(values) {
13
+ return Array.from(new Set(values
14
+ .map((value) => normalizeLanguageTag(value))
15
+ .filter((value) => Boolean(value))));
16
+ }
12
17
  async function listProductIdsForCategory(db, categoryId) {
13
18
  const rows = await db
14
19
  .select({ productId: productCategoryProducts.productId })
@@ -198,6 +203,12 @@ export const publicProductsService = {
198
203
  async getCatalogProductBySlug(db, slug, query = {}) {
199
204
  const normalizedSlug = slug.trim().toLowerCase();
200
205
  const normalizedLanguageTag = normalizeLanguageTag(query.languageTag);
206
+ const candidateLanguageTags = normalizedLanguageTag
207
+ ? normalizeLanguageTagList([
208
+ normalizedLanguageTag,
209
+ ...DEFAULT_CATALOG_SEARCH_FALLBACK_LANGUAGE_TAGS,
210
+ ])
211
+ : [];
201
212
  const conditions = [
202
213
  // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
203
214
  sql `lower(${productTranslations.slug}) = ${normalizedSlug}`,
@@ -206,9 +217,9 @@ export const publicProductsService = {
206
217
  eq(products.visibility, "public"),
207
218
  ];
208
219
  if (normalizedLanguageTag) {
209
- conditions.push(eq(productTranslations.languageTag, normalizedLanguageTag));
220
+ conditions.push(inArray(productTranslations.languageTag, candidateLanguageTags));
210
221
  }
211
- const [row] = await db
222
+ const rows = await db
212
223
  .select({
213
224
  productId: products.id,
214
225
  languageTag: productTranslations.languageTag,
@@ -217,12 +228,17 @@ export const publicProductsService = {
217
228
  .innerJoin(products, eq(products.id, productTranslations.productId))
218
229
  .where(and(...conditions))
219
230
  .orderBy(desc(productTranslations.updatedAt))
220
- .limit(1);
231
+ .limit(candidateLanguageTags.length || 1);
232
+ const row = candidateLanguageTags.length > 0
233
+ ? (candidateLanguageTags
234
+ .map((languageTag) => rows.find((item) => normalizeLanguageTag(item.languageTag) === languageTag))
235
+ .find(Boolean) ?? null)
236
+ : (rows[0] ?? null);
221
237
  if (!row) {
222
238
  return null;
223
239
  }
224
240
  return this.getCatalogProductById(db, row.productId, {
225
- languageTag: normalizedLanguageTag ?? row.languageTag,
241
+ languageTag: row.languageTag,
226
242
  });
227
243
  },
228
244
  async getCatalogProductBrochure(db, productId, query = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/inventory",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -164,15 +164,15 @@
164
164
  "zod": "^4.3.6",
165
165
  "@voyant-travel/action-ledger": "^0.105.3",
166
166
  "@voyant-travel/catalog": "^0.124.1",
167
+ "@voyant-travel/extras-contracts": "^0.104.2",
167
168
  "@voyant-travel/core": "^0.110.0",
168
169
  "@voyant-travel/db": "^0.108.4",
169
- "@voyant-travel/extras-contracts": "^0.104.2",
170
170
  "@voyant-travel/hono": "^0.112.2",
171
- "@voyant-travel/products-contracts": "^0.105.7",
171
+ "@voyant-travel/commerce": "^0.8.1",
172
172
  "@voyant-travel/storage": "^0.105.0",
173
173
  "@voyant-travel/utils": "^0.105.2",
174
- "@voyant-travel/operations": "^0.1.7",
175
- "@voyant-travel/commerce": "^0.8.1"
174
+ "@voyant-travel/products-contracts": "^0.105.7",
175
+ "@voyant-travel/operations": "^0.1.7"
176
176
  },
177
177
  "devDependencies": {
178
178
  "@types/sanitize-html": "^2.16.1",