@open-mercato/core 0.10.1-develop.7954.1.48da4a7d13 → 0.10.1-develop.7972.1.03f813eaaf

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.
Files changed (88) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/logout.js +3 -3
  3. package/dist/modules/auth/api/logout.js.map +2 -2
  4. package/dist/modules/auth/commands/roles.js +42 -31
  5. package/dist/modules/auth/commands/roles.js.map +2 -2
  6. package/dist/modules/communication_channels/setup.js +49 -45
  7. package/dist/modules/communication_channels/setup.js.map +2 -2
  8. package/dist/modules/customer_accounts/services/domainMappingService.js +55 -29
  9. package/dist/modules/customer_accounts/services/domainMappingService.js.map +2 -2
  10. package/dist/modules/customers/components/detail/ActivitiesSection.js +8 -3
  11. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  12. package/dist/modules/customers/components/detail/ActivityCard.js +3 -3
  13. package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
  14. package/dist/modules/customers/components/detail/ActivityHistorySection.js +19 -7
  15. package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
  16. package/dist/modules/customers/components/detail/ActivityTimeline.js +5 -5
  17. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
  18. package/dist/modules/directory/subscribers/seedOrganizationDefaults.js +67 -0
  19. package/dist/modules/directory/subscribers/seedOrganizationDefaults.js.map +7 -0
  20. package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js +1 -1
  21. package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js.map +2 -2
  22. package/dist/modules/ecommerce/api/storefront/categories/route.js +1 -1
  23. package/dist/modules/ecommerce/api/storefront/categories/route.js.map +2 -2
  24. package/dist/modules/ecommerce/api/storefront/context/route.js +1 -1
  25. package/dist/modules/ecommerce/api/storefront/context/route.js.map +2 -2
  26. package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js +1 -1
  27. package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js.map +2 -2
  28. package/dist/modules/ecommerce/api/storefront/products/route.js +1 -1
  29. package/dist/modules/ecommerce/api/storefront/products/route.js.map +2 -2
  30. package/dist/modules/ecommerce/api/storefront/search/suggest/route.js +1 -1
  31. package/dist/modules/ecommerce/api/storefront/search/suggest/route.js.map +2 -2
  32. package/dist/modules/ecommerce/lib/cacheInvalidation.js +3 -0
  33. package/dist/modules/ecommerce/lib/cacheInvalidation.js.map +2 -2
  34. package/dist/modules/ecommerce/lib/cacheKeys.js +4 -0
  35. package/dist/modules/ecommerce/lib/cacheKeys.js.map +2 -2
  36. package/dist/modules/ecommerce/lib/storeContext.js +36 -12
  37. package/dist/modules/ecommerce/lib/storeContext.js.map +2 -2
  38. package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js +14 -0
  39. package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js.map +7 -0
  40. package/dist/modules/integrations/setup.js +34 -24
  41. package/dist/modules/integrations/setup.js.map +2 -2
  42. package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -1
  43. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  44. package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js +3 -3
  45. package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js.map +2 -2
  46. package/dist/modules/messages/components/message-detail/panels/body-panel.js +2 -2
  47. package/dist/modules/messages/components/message-detail/panels/body-panel.js.map +2 -2
  48. package/dist/modules/payment_gateways/setup.js +3 -0
  49. package/dist/modules/payment_gateways/setup.js.map +2 -2
  50. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +30 -11
  51. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +3 -3
  52. package/dist/modules/warranty_claims/setup.js +34 -30
  53. package/dist/modules/warranty_claims/setup.js.map +2 -2
  54. package/dist/modules/workflows/setup.js +7 -0
  55. package/dist/modules/workflows/setup.js.map +2 -2
  56. package/package.json +7 -7
  57. package/src/modules/auth/api/logout.ts +3 -3
  58. package/src/modules/auth/commands/roles.ts +43 -32
  59. package/src/modules/communication_channels/setup.ts +59 -53
  60. package/src/modules/customer_accounts/services/domainMappingService.ts +57 -29
  61. package/src/modules/customers/components/detail/ActivitiesSection.tsx +8 -3
  62. package/src/modules/customers/components/detail/ActivityCard.tsx +3 -3
  63. package/src/modules/customers/components/detail/ActivityHistorySection.tsx +20 -8
  64. package/src/modules/customers/components/detail/ActivityTimeline.tsx +7 -7
  65. package/src/modules/customers/i18n/de.json +1 -0
  66. package/src/modules/customers/i18n/en.json +1 -0
  67. package/src/modules/customers/i18n/es.json +1 -0
  68. package/src/modules/customers/i18n/ko.json +1 -0
  69. package/src/modules/customers/i18n/pl.json +1 -0
  70. package/src/modules/directory/subscribers/seedOrganizationDefaults.ts +84 -0
  71. package/src/modules/ecommerce/api/storefront/categories/[slug]/route.ts +1 -1
  72. package/src/modules/ecommerce/api/storefront/categories/route.ts +1 -1
  73. package/src/modules/ecommerce/api/storefront/context/route.ts +1 -1
  74. package/src/modules/ecommerce/api/storefront/products/[idOrHandle]/route.ts +1 -1
  75. package/src/modules/ecommerce/api/storefront/products/route.ts +1 -1
  76. package/src/modules/ecommerce/api/storefront/search/suggest/route.ts +1 -1
  77. package/src/modules/ecommerce/lib/cacheInvalidation.ts +4 -0
  78. package/src/modules/ecommerce/lib/cacheKeys.ts +4 -0
  79. package/src/modules/ecommerce/lib/storeContext.ts +56 -10
  80. package/src/modules/ecommerce/subscribers/sales-channel-cache-invalidation.ts +12 -0
  81. package/src/modules/integrations/setup.ts +41 -24
  82. package/src/modules/messages/components/MessagesInboxPageClient.tsx +1 -1
  83. package/src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx +3 -3
  84. package/src/modules/messages/components/message-detail/panels/body-panel.tsx +2 -2
  85. package/src/modules/payment_gateways/setup.ts +4 -0
  86. package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +55 -28
  87. package/src/modules/warranty_claims/setup.ts +40 -30
  88. package/src/modules/workflows/setup.ts +8 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/ecommerce/api/storefront/categories/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontCategoryTreeQuerySchema } from '../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport { cachedGetStorefrontCategoryTree } from '../../../lib/storefrontCategoryCache'\nimport { parseStorefrontCategoryTreeQuery } from '../../../lib/storefrontQuery'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../storefrontRouteSupport'\nimport { storefrontCategoryTreeResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/categories',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=300, stale-while-revalidate=60'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontCategoryTreeQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'categories')\n if (rateLimited) return rateLimited\n const body = await cachedGetStorefrontCategoryTree(container, context, query)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront category tree failed',\n code: 'ecommerce.storefront_categories_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontCategoriesTag = 'Ecommerce'\n\nconst storefrontCategoriesGetDoc: OpenApiMethodDoc = {\n summary: 'List the storefront category tree for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the visible category tree below `parentId` (the roots when omitted), `depth` levels deep (all levels when omitted). `productCount` is descendant-inclusive and counts only products inside the buyer\\'s effective assortment; categories the buyer cannot browse are absent, and empty categories are absent unless `includeEmpty=true`. Past 10 000 products in the assortment the counts are not computed: every `productCount` is null and no category is dropped as empty. A `parentId` that is unknown or not visible to the buyer yields an empty tree, identical to a nonexistent one. A nested node beyond `depth` has empty `children` and `hasChildren: true`. Anonymous responses are `public, max-age=300, stale-while-revalidate=60`; authenticated responses are `private, no-store`. Cached server-side for 300 s per assortment scope. Rate limited per IP and store.',\n tags: [storefrontCategoriesTag],\n query: ecommerceStorefrontCategoryTreeQuerySchema,\n responses: [\n { status: 200, description: 'The category tree within the buyer\\'s assortment.', schema: storefrontCategoryTreeResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontCategoriesTag,\n summary: 'Public storefront category tree',\n methods: {\n GET: storefrontCategoriesGetDoc,\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,kDAAkD;AAC3D,SAAS,kCAAkC;AAE3C,SAAS,uCAAuC;AAChD,SAAS,wCAAwC;AACjD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4CAA4C;AAE9C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,iCAAiC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC5E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,YAAY;AAC1F,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,gCAAgC,WAAW,SAAS,KAAK;AAC5E,aAAO,aAAa,KAAK,MAAM,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IACxG,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,0BAA0B;AAEhC,MAAM,6BAA+C;AAAA,EACnD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,uBAAuB;AAAA,EAC9B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oDAAqD,QAAQ,qCAAqC;AAAA,EAChI;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,oDAAoD,QAAQ,sBAAsB;AAAA,EAChH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontCategoryTreeQuerySchema } from '../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport { cachedGetStorefrontCategoryTree } from '../../../lib/storefrontCategoryCache'\nimport { parseStorefrontCategoryTreeQuery } from '../../../lib/storefrontQuery'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../storefrontRouteSupport'\nimport { storefrontCategoryTreeResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/categories',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=300, stale-while-revalidate=60'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontCategoryTreeQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'categories')\n if (rateLimited) return rateLimited\n const body = await cachedGetStorefrontCategoryTree(container, context, query)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront category tree failed',\n code: 'ecommerce.storefront_categories_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontCategoriesTag = 'Ecommerce'\n\nconst storefrontCategoriesGetDoc: OpenApiMethodDoc = {\n summary: 'List the storefront category tree for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the visible category tree below `parentId` (the roots when omitted), `depth` levels deep (all levels when omitted). `productCount` is descendant-inclusive and counts only products inside the buyer\\'s effective assortment; categories the buyer cannot browse are absent, and empty categories are absent unless `includeEmpty=true`. Past 10 000 products in the assortment the counts are not computed: every `productCount` is null and no category is dropped as empty. A `parentId` that is unknown or not visible to the buyer yields an empty tree, identical to a nonexistent one. A nested node beyond `depth` has empty `children` and `hasChildren: true`. Anonymous responses are `public, max-age=300, stale-while-revalidate=60`; authenticated responses are `private, no-store`. Cached server-side for 300 s per assortment scope. Rate limited per IP and store.',\n tags: [storefrontCategoriesTag],\n query: ecommerceStorefrontCategoryTreeQuerySchema,\n responses: [\n { status: 200, description: 'The category tree within the buyer\\'s assortment.', schema: storefrontCategoryTreeResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontCategoriesTag,\n summary: 'Public storefront category tree',\n methods: {\n GET: storefrontCategoriesGetDoc,\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,kDAAkD;AAC3D,SAAS,kCAAkC;AAE3C,SAAS,uCAAuC;AAChD,SAAS,wCAAwC;AACjD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4CAA4C;AAE9C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,iCAAiC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC5E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,YAAY;AAC1F,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,gCAAgC,WAAW,SAAS,KAAK;AAC5E,aAAO,aAAa,KAAK,MAAM,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IACxG,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,0BAA0B;AAEhC,MAAM,6BAA+C;AAAA,EACnD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,uBAAuB;AAAA,EAC9B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oDAAqD,QAAQ,qCAAqC;AAAA,EAChI;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,2DAA2D,QAAQ,sBAAsB;AAAA,EACvH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
6
6
  "names": []
7
7
  }
@@ -142,7 +142,7 @@ const storefrontContextGetDoc = {
142
142
  { status: 404, description: "No store serves this host or path", schema: storefrontErrorSchema },
143
143
  { status: 410, description: "Store archived", schema: storefrontErrorSchema },
144
144
  { status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
145
- { status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
145
+ { status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
146
146
  ]
147
147
  };
148
148
  const openApi = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/ecommerce/api/storefront/context/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { CustomerEntity } from '@open-mercato/core/modules/customers/data/entities'\nimport { ecommercePriceDisplayModeSchema, ecommerceStorefrontContextQuerySchema } from '../../../data/validators'\nimport { parseStorefrontContextQuery } from '../../../lib/storefrontQuery'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport type { StoreContext } from '../../../lib/types'\nimport {\n runInStoreCacheTenant,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n} from '../storefrontRouteSupport'\n\nexport const metadata = {\n path: '/ecommerce/storefront/context',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=60'\n\nexport type StorefrontBuyerNames = {\n displayName: string | null\n companyName: string | null\n}\n\nexport type StorefrontContextResponse = {\n store: {\n id: string\n code: string\n name: string\n slug: string\n status: 'active'\n defaultLocale: string\n supportedLocales: string[]\n defaultCurrencyCode: string\n settings: StoreContext['store']['settings']\n }\n effectiveLocale: string\n requestedLocale: string | null\n supportedLocales: string[]\n currencyCode: string\n buyer: {\n isAuthenticated: boolean\n taxMode: StoreContext['buyer']['taxMode']\n displayName: string | null\n companyName: string | null\n allowPurchaseOnAccount: boolean\n }\n}\n\nexport function projectStorefrontContext(context: StoreContext, names: StorefrontBuyerNames): StorefrontContextResponse {\n const { store, buyer } = context\n return {\n store: {\n id: store.id,\n code: store.code,\n name: store.name,\n slug: store.slug,\n status: store.status,\n defaultLocale: store.defaultLocale,\n supportedLocales: [...store.supportedLocales],\n defaultCurrencyCode: store.defaultCurrencyCode,\n settings: store.settings,\n },\n effectiveLocale: context.effectiveLocale,\n requestedLocale: context.requestedLocale,\n supportedLocales: [...store.supportedLocales],\n currencyCode: context.currencyCode,\n buyer: {\n isAuthenticated: buyer.isAuthenticated,\n taxMode: buyer.taxMode,\n displayName: buyer.isAuthenticated ? names.displayName : null,\n companyName: buyer.isAuthenticated ? names.companyName : null,\n allowPurchaseOnAccount: buyer.allowPurchaseOnAccount,\n },\n }\n}\n\nfunction nonEmptyOrNull(value: string | null | undefined): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value : null\n}\n\nasync function loadBuyerNames(em: EntityManager, context: StoreContext): Promise<StorefrontBuyerNames> {\n const { buyer } = context\n if (!buyer.isAuthenticated || !buyer.customerUserId) return { displayName: null, companyName: null }\n const scope = { tenantId: context.tenantId, organizationId: context.organizationId }\n const user = await findOneWithDecryption(\n em,\n CustomerUser,\n { id: buyer.customerUserId, ...scope, deletedAt: null },\n undefined,\n scope,\n )\n const company = buyer.companyId\n ? await findOneWithDecryption(\n em,\n CustomerEntity,\n { id: buyer.companyId, ...scope, deletedAt: null },\n undefined,\n scope,\n )\n : null\n return {\n displayName: nonEmptyOrNull(user?.displayName),\n companyName: nonEmptyOrNull(company?.displayName),\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontContextQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'context')\n if (rateLimited) return rateLimited\n const em = container.resolve('em') as EntityManager\n const names = await loadBuyerNames(em, context)\n const body = projectStorefrontContext(context, names)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront context resolution failed',\n code: 'ecommerce.storefront_context_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontContextTag = 'Ecommerce'\n\nexport const storefrontContextResponseSchema = z.object({\n store: z.object({\n id: z.string().uuid(),\n code: z.string(),\n name: z.string(),\n slug: z.string(),\n status: z.literal('active'),\n defaultLocale: z.string(),\n supportedLocales: z.array(z.string()),\n defaultCurrencyCode: z.string(),\n settings: z.record(z.string(), z.unknown()),\n }),\n effectiveLocale: z.string(),\n requestedLocale: z.string().nullable(),\n supportedLocales: z.array(z.string()),\n currencyCode: z.string(),\n buyer: z.object({\n isAuthenticated: z.boolean(),\n taxMode: ecommercePriceDisplayModeSchema,\n displayName: z.string().nullable(),\n companyName: z.string().nullable(),\n allowPurchaseOnAccount: z.boolean(),\n }),\n})\n\nconst storefrontContextBadRequestSchema = z.union([storefrontInvalidQueryErrorSchema, storefrontErrorSchema])\n\nconst storefrontContextGetDoc: OpenApiMethodDoc = {\n summary: 'Resolve the storefront context for the current host',\n description:\n 'Public, unauthenticated. Resolves the store from the Host header (or `storeSlug` when OM_ECOMMERCE_DEV_STORE_SLUG=true) and the optional portal session, and returns a safe projection of the store and buyer context. Anonymous responses are `public, max-age=60`; authenticated responses are `private, no-store`. Rate limited per IP and store.',\n tags: [storefrontContextTag],\n query: ecommerceStorefrontContextQuerySchema,\n responses: [\n { status: 200, description: 'Storefront context resolved.', schema: storefrontContextResponseSchema },\n ],\n errors: [\n {\n status: 400,\n description: 'Unknown, repeated or malformed query parameters, or storeSlug used while the development flag is off',\n schema: storefrontContextBadRequestSchema,\n },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontContextTag,\n summary: 'Public storefront context',\n methods: {\n GET: storefrontContextGetDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { CustomerEntity } from '@open-mercato/core/modules/customers/data/entities'\nimport { ecommercePriceDisplayModeSchema, ecommerceStorefrontContextQuerySchema } from '../../../data/validators'\nimport { parseStorefrontContextQuery } from '../../../lib/storefrontQuery'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport type { StoreContext } from '../../../lib/types'\nimport {\n runInStoreCacheTenant,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n} from '../storefrontRouteSupport'\n\nexport const metadata = {\n path: '/ecommerce/storefront/context',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=60'\n\nexport type StorefrontBuyerNames = {\n displayName: string | null\n companyName: string | null\n}\n\nexport type StorefrontContextResponse = {\n store: {\n id: string\n code: string\n name: string\n slug: string\n status: 'active'\n defaultLocale: string\n supportedLocales: string[]\n defaultCurrencyCode: string\n settings: StoreContext['store']['settings']\n }\n effectiveLocale: string\n requestedLocale: string | null\n supportedLocales: string[]\n currencyCode: string\n buyer: {\n isAuthenticated: boolean\n taxMode: StoreContext['buyer']['taxMode']\n displayName: string | null\n companyName: string | null\n allowPurchaseOnAccount: boolean\n }\n}\n\nexport function projectStorefrontContext(context: StoreContext, names: StorefrontBuyerNames): StorefrontContextResponse {\n const { store, buyer } = context\n return {\n store: {\n id: store.id,\n code: store.code,\n name: store.name,\n slug: store.slug,\n status: store.status,\n defaultLocale: store.defaultLocale,\n supportedLocales: [...store.supportedLocales],\n defaultCurrencyCode: store.defaultCurrencyCode,\n settings: store.settings,\n },\n effectiveLocale: context.effectiveLocale,\n requestedLocale: context.requestedLocale,\n supportedLocales: [...store.supportedLocales],\n currencyCode: context.currencyCode,\n buyer: {\n isAuthenticated: buyer.isAuthenticated,\n taxMode: buyer.taxMode,\n displayName: buyer.isAuthenticated ? names.displayName : null,\n companyName: buyer.isAuthenticated ? names.companyName : null,\n allowPurchaseOnAccount: buyer.allowPurchaseOnAccount,\n },\n }\n}\n\nfunction nonEmptyOrNull(value: string | null | undefined): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value : null\n}\n\nasync function loadBuyerNames(em: EntityManager, context: StoreContext): Promise<StorefrontBuyerNames> {\n const { buyer } = context\n if (!buyer.isAuthenticated || !buyer.customerUserId) return { displayName: null, companyName: null }\n const scope = { tenantId: context.tenantId, organizationId: context.organizationId }\n const user = await findOneWithDecryption(\n em,\n CustomerUser,\n { id: buyer.customerUserId, ...scope, deletedAt: null },\n undefined,\n scope,\n )\n const company = buyer.companyId\n ? await findOneWithDecryption(\n em,\n CustomerEntity,\n { id: buyer.companyId, ...scope, deletedAt: null },\n undefined,\n scope,\n )\n : null\n return {\n displayName: nonEmptyOrNull(user?.displayName),\n companyName: nonEmptyOrNull(company?.displayName),\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontContextQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'context')\n if (rateLimited) return rateLimited\n const em = container.resolve('em') as EntityManager\n const names = await loadBuyerNames(em, context)\n const body = projectStorefrontContext(context, names)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront context resolution failed',\n code: 'ecommerce.storefront_context_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontContextTag = 'Ecommerce'\n\nexport const storefrontContextResponseSchema = z.object({\n store: z.object({\n id: z.string().uuid(),\n code: z.string(),\n name: z.string(),\n slug: z.string(),\n status: z.literal('active'),\n defaultLocale: z.string(),\n supportedLocales: z.array(z.string()),\n defaultCurrencyCode: z.string(),\n settings: z.record(z.string(), z.unknown()),\n }),\n effectiveLocale: z.string(),\n requestedLocale: z.string().nullable(),\n supportedLocales: z.array(z.string()),\n currencyCode: z.string(),\n buyer: z.object({\n isAuthenticated: z.boolean(),\n taxMode: ecommercePriceDisplayModeSchema,\n displayName: z.string().nullable(),\n companyName: z.string().nullable(),\n allowPurchaseOnAccount: z.boolean(),\n }),\n})\n\nconst storefrontContextBadRequestSchema = z.union([storefrontInvalidQueryErrorSchema, storefrontErrorSchema])\n\nconst storefrontContextGetDoc: OpenApiMethodDoc = {\n summary: 'Resolve the storefront context for the current host',\n description:\n 'Public, unauthenticated. Resolves the store from the Host header (or `storeSlug` when OM_ECOMMERCE_DEV_STORE_SLUG=true) and the optional portal session, and returns a safe projection of the store and buyer context. Anonymous responses are `public, max-age=60`; authenticated responses are `private, no-store`. Rate limited per IP and store.',\n tags: [storefrontContextTag],\n query: ecommerceStorefrontContextQuerySchema,\n responses: [\n { status: 200, description: 'Storefront context resolved.', schema: storefrontContextResponseSchema },\n ],\n errors: [\n {\n status: 400,\n description: 'Unknown, repeated or malformed query parameters, or storeSlug used while the development flag is off',\n schema: storefrontContextBadRequestSchema,\n },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontContextTag,\n summary: 'Public storefront context',\n methods: {\n GET: storefrontContextGetDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -68,7 +68,7 @@ const storefrontProductGetDoc = {
68
68
  { status: 404, description: "No store serves this host or path, or the product is not visible to this buyer", schema: storefrontErrorSchema },
69
69
  { status: 410, description: "Store archived", schema: storefrontErrorSchema },
70
70
  { status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
71
- { status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
71
+ { status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
72
72
  ]
73
73
  };
74
74
  const openApi = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/ecommerce/api/storefront/products/%5BidOrHandle%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n ecommerceStorefrontProductDetailQuerySchema,\n ecommerceStorefrontProductParamsSchema,\n} from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { cachedGetStorefrontProductDetail } from '../../../../lib/storefrontProductCache'\nimport { parseStorefrontProductDetailQuery } from '../../../../lib/storefrontQuery'\nimport {\n storefrontErrorResponse,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontProductDetailResponseSchema } from '../openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/products/[idOrHandle]',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=60'\nconst PRODUCT_NOT_FOUND = { error: 'product_not_found' } as const\n\ntype ProductRouteParams = { idOrHandle?: string }\n\ntype ProductRouteContext = { params?: ProductRouteParams | Promise<ProductRouteParams> }\n\nexport async function GET(req: Request, routeContext: ProductRouteContext = {}) {\n try {\n const query = parseStorefrontProductDetailQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'productDetail')\n if (rateLimited) return rateLimited\n const params = ecommerceStorefrontProductParamsSchema.safeParse((await routeContext.params) ?? {})\n if (!params.success) return storefrontErrorResponse(404, { ...PRODUCT_NOT_FOUND })\n const detail = await cachedGetStorefrontProductDetail(container, context, params.data.idOrHandle, {\n variantId: query.variantId ?? null,\n locale: query.locale ?? null,\n })\n if (!detail) return storefrontErrorResponse(404, { ...PRODUCT_NOT_FOUND })\n return NextResponse.json(detail, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront product detail failed',\n code: 'ecommerce.storefront_product_detail_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontProductTag = 'Ecommerce'\n\nconst storefrontProductGetDoc: OpenApiMethodDoc = {\n summary: 'Get one storefront product for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the product by UUID or handle with buyer-resolved prices per variant. A product that does not exist, is inactive, deleted, of another tenant or outside the buyer\\'s effective assortment answers the same 404 `{ \"error\": \"product_not_found\" }`. `variantId` preselects a variant; top-level `priceTiers` are the selected variant\\'s tiers. Anonymous responses are `public, max-age=60`; authenticated responses are `private, no-store`. Cached server-side for 60 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontProductTag],\n pathParams: ecommerceStorefrontProductParamsSchema,\n query: ecommerceStorefrontProductDetailQuerySchema,\n responses: [\n { status: 200, description: 'The product as this buyer sees it.', schema: storefrontProductDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path, or the product is not visible to this buyer', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontProductTag,\n summary: 'Public storefront product detail',\n methods: {\n GET: storefrontProductGetDoc,\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kCAAkC;AAE3C,SAAS,wCAAwC;AACjD,SAAS,yCAAyC;AAClD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6CAA6C;AAE/C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAChC,MAAM,oBAAoB,EAAE,OAAO,oBAAoB;AAMvD,eAAsB,IAAI,KAAc,eAAoC,CAAC,GAAG;AAC9E,MAAI;AACF,UAAM,QAAQ,kCAAkC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC7E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,eAAe;AAC7F,UAAI,YAAa,QAAO;AACxB,YAAM,SAAS,uCAAuC,UAAW,MAAM,aAAa,UAAW,CAAC,CAAC;AACjG,UAAI,CAAC,OAAO,QAAS,QAAO,wBAAwB,KAAK,EAAE,GAAG,kBAAkB,CAAC;AACjF,YAAM,SAAS,MAAM,iCAAiC,WAAW,SAAS,OAAO,KAAK,YAAY;AAAA,QAChG,WAAW,MAAM,aAAa;AAAA,QAC9B,QAAQ,MAAM,UAAU;AAAA,MAC1B,CAAC;AACD,UAAI,CAAC,OAAQ,QAAO,wBAAwB,KAAK,EAAE,GAAG,kBAAkB,CAAC;AACzE,aAAO,aAAa,KAAK,QAAQ,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IAC1G,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,uBAAuB;AAE7B,MAAM,0BAA4C;AAAA,EAChD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,oBAAoB;AAAA,EAC3B,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,sCAAsC,QAAQ,sCAAsC;AAAA,EAClH;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,kFAAkF,QAAQ,sBAAsB;AAAA,IAC5I,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,oDAAoD,QAAQ,sBAAsB;AAAA,EAChH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n ecommerceStorefrontProductDetailQuerySchema,\n ecommerceStorefrontProductParamsSchema,\n} from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { cachedGetStorefrontProductDetail } from '../../../../lib/storefrontProductCache'\nimport { parseStorefrontProductDetailQuery } from '../../../../lib/storefrontQuery'\nimport {\n storefrontErrorResponse,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontProductDetailResponseSchema } from '../openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/products/[idOrHandle]',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=60'\nconst PRODUCT_NOT_FOUND = { error: 'product_not_found' } as const\n\ntype ProductRouteParams = { idOrHandle?: string }\n\ntype ProductRouteContext = { params?: ProductRouteParams | Promise<ProductRouteParams> }\n\nexport async function GET(req: Request, routeContext: ProductRouteContext = {}) {\n try {\n const query = parseStorefrontProductDetailQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'productDetail')\n if (rateLimited) return rateLimited\n const params = ecommerceStorefrontProductParamsSchema.safeParse((await routeContext.params) ?? {})\n if (!params.success) return storefrontErrorResponse(404, { ...PRODUCT_NOT_FOUND })\n const detail = await cachedGetStorefrontProductDetail(container, context, params.data.idOrHandle, {\n variantId: query.variantId ?? null,\n locale: query.locale ?? null,\n })\n if (!detail) return storefrontErrorResponse(404, { ...PRODUCT_NOT_FOUND })\n return NextResponse.json(detail, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront product detail failed',\n code: 'ecommerce.storefront_product_detail_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontProductTag = 'Ecommerce'\n\nconst storefrontProductGetDoc: OpenApiMethodDoc = {\n summary: 'Get one storefront product for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the product by UUID or handle with buyer-resolved prices per variant. A product that does not exist, is inactive, deleted, of another tenant or outside the buyer\\'s effective assortment answers the same 404 `{ \"error\": \"product_not_found\" }`. `variantId` preselects a variant; top-level `priceTiers` are the selected variant\\'s tiers. Anonymous responses are `public, max-age=60`; authenticated responses are `private, no-store`. Cached server-side for 60 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontProductTag],\n pathParams: ecommerceStorefrontProductParamsSchema,\n query: ecommerceStorefrontProductDetailQuerySchema,\n responses: [\n { status: 200, description: 'The product as this buyer sees it.', schema: storefrontProductDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path, or the product is not visible to this buyer', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontProductTag,\n summary: 'Public storefront product detail',\n methods: {\n GET: storefrontProductGetDoc,\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kCAAkC;AAE3C,SAAS,wCAAwC;AACjD,SAAS,yCAAyC;AAClD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6CAA6C;AAE/C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAChC,MAAM,oBAAoB,EAAE,OAAO,oBAAoB;AAMvD,eAAsB,IAAI,KAAc,eAAoC,CAAC,GAAG;AAC9E,MAAI;AACF,UAAM,QAAQ,kCAAkC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC7E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,eAAe;AAC7F,UAAI,YAAa,QAAO;AACxB,YAAM,SAAS,uCAAuC,UAAW,MAAM,aAAa,UAAW,CAAC,CAAC;AACjG,UAAI,CAAC,OAAO,QAAS,QAAO,wBAAwB,KAAK,EAAE,GAAG,kBAAkB,CAAC;AACjF,YAAM,SAAS,MAAM,iCAAiC,WAAW,SAAS,OAAO,KAAK,YAAY;AAAA,QAChG,WAAW,MAAM,aAAa;AAAA,QAC9B,QAAQ,MAAM,UAAU;AAAA,MAC1B,CAAC;AACD,UAAI,CAAC,OAAQ,QAAO,wBAAwB,KAAK,EAAE,GAAG,kBAAkB,CAAC;AACzE,aAAO,aAAa,KAAK,QAAQ,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IAC1G,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,uBAAuB;AAE7B,MAAM,0BAA4C;AAAA,EAChD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,oBAAoB;AAAA,EAC3B,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,sCAAsC,QAAQ,sCAAsC;AAAA,EAClH;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,kFAAkF,QAAQ,sBAAsB;AAAA,IAC5I,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,2DAA2D,QAAQ,sBAAsB;AAAA,EACvH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
6
6
  "names": []
7
7
  }
@@ -59,7 +59,7 @@ const storefrontProductsGetDoc = {
59
59
  { status: 404, description: "No store serves this host or path", schema: storefrontErrorSchema },
60
60
  { status: 410, description: "Store archived", schema: storefrontErrorSchema },
61
61
  { status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
62
- { status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
62
+ { status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
63
63
  ]
64
64
  };
65
65
  const openApi = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/ecommerce/api/storefront/products/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontProductListQuerySchema } from '../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport { cachedListStorefrontProducts } from '../../../lib/storefrontProductCache'\nimport { parseStorefrontProductListQuery } from '../../../lib/storefrontQuery'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../storefrontRouteSupport'\nimport { storefrontProductListResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/products',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontProductListQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'products')\n if (rateLimited) return rateLimited\n const body = await cachedListStorefrontProducts(container, context, query)\n const headers = storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL)\n if (body.sortApproximate) headers['X-Sort-Approximate'] = 'true'\n if (body.sortUnavailable) headers['X-Sort-Unavailable'] = 'true'\n return NextResponse.json(body, { headers })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront product listing failed',\n code: 'ecommerce.storefront_products_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontProductsTag = 'Ecommerce'\n\nconst storefrontProductsGetDoc: OpenApiMethodDoc = {\n summary: 'List storefront products for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then lists the products of the buyer\\'s effective assortment with buyer-resolved prices. Unknown, repeated or malformed parameters are rejected with 400. Options use bracket notation (`options[color]=red,blue`). `availability=` is page-scoped: `total` counts the pre-availability set and a page may be short. Facets count each dimension against every other active filter (cross-exclusion); category, tag, option and product-type counts are shared per assortment scope (and empty past 10 000 products in the search universe), `priceRange` spans the buyer\\'s own prices over the filtered set without the price filter, and `availability` counts the returned page only. `X-Sort-Approximate: true` marks a price order past the sort cap; `X-Sort-Unavailable: true` marks a declined price sort. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. Cached server-side for 30 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontProductsTag],\n query: ecommerceStorefrontProductListQuerySchema,\n responses: [\n { status: 200, description: 'Products of the buyer\\'s effective assortment.', schema: storefrontProductListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontProductsTag,\n summary: 'Public storefront product listing',\n methods: {\n GET: storefrontProductsGetDoc,\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,iDAAiD;AAC1D,SAAS,kCAAkC;AAE3C,SAAS,oCAAoC;AAC7C,SAAS,uCAAuC;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,gCAAgC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC3E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,UAAU;AACxF,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,6BAA6B,WAAW,SAAS,KAAK;AACzE,YAAM,UAAU,yBAAyB,SAAS,uBAAuB;AACzE,UAAI,KAAK,gBAAiB,SAAQ,oBAAoB,IAAI;AAC1D,UAAI,KAAK,gBAAiB,SAAQ,oBAAoB,IAAI;AAC1D,aAAO,aAAa,KAAK,MAAM,EAAE,QAAQ,CAAC;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,wBAAwB;AAE9B,MAAM,2BAA6C;AAAA,EACjD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,qBAAqB;AAAA,EAC5B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,iDAAkD,QAAQ,oCAAoC;AAAA,EAC5H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,oDAAoD,QAAQ,sBAAsB;AAAA,EAChH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontProductListQuerySchema } from '../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport { cachedListStorefrontProducts } from '../../../lib/storefrontProductCache'\nimport { parseStorefrontProductListQuery } from '../../../lib/storefrontQuery'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../storefrontRouteSupport'\nimport { storefrontProductListResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/products',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontProductListQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'products')\n if (rateLimited) return rateLimited\n const body = await cachedListStorefrontProducts(container, context, query)\n const headers = storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL)\n if (body.sortApproximate) headers['X-Sort-Approximate'] = 'true'\n if (body.sortUnavailable) headers['X-Sort-Unavailable'] = 'true'\n return NextResponse.json(body, { headers })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront product listing failed',\n code: 'ecommerce.storefront_products_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontProductsTag = 'Ecommerce'\n\nconst storefrontProductsGetDoc: OpenApiMethodDoc = {\n summary: 'List storefront products for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then lists the products of the buyer\\'s effective assortment with buyer-resolved prices. Unknown, repeated or malformed parameters are rejected with 400. Options use bracket notation (`options[color]=red,blue`). `availability=` is page-scoped: `total` counts the pre-availability set and a page may be short. Facets count each dimension against every other active filter (cross-exclusion); category, tag, option and product-type counts are shared per assortment scope (and empty past 10 000 products in the search universe), `priceRange` spans the buyer\\'s own prices over the filtered set without the price filter, and `availability` counts the returned page only. `X-Sort-Approximate: true` marks a price order past the sort cap; `X-Sort-Unavailable: true` marks a declined price sort. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. Cached server-side for 30 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontProductsTag],\n query: ecommerceStorefrontProductListQuerySchema,\n responses: [\n { status: 200, description: 'Products of the buyer\\'s effective assortment.', schema: storefrontProductListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontProductsTag,\n summary: 'Public storefront product listing',\n methods: {\n GET: storefrontProductsGetDoc,\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,iDAAiD;AAC1D,SAAS,kCAAkC;AAE3C,SAAS,oCAAoC;AAC7C,SAAS,uCAAuC;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,gCAAgC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC3E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,UAAU;AACxF,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,6BAA6B,WAAW,SAAS,KAAK;AACzE,YAAM,UAAU,yBAAyB,SAAS,uBAAuB;AACzE,UAAI,KAAK,gBAAiB,SAAQ,oBAAoB,IAAI;AAC1D,UAAI,KAAK,gBAAiB,SAAQ,oBAAoB,IAAI;AAC1D,aAAO,aAAa,KAAK,MAAM,EAAE,QAAQ,CAAC;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,wBAAwB;AAE9B,MAAM,2BAA6C;AAAA,EACjD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,qBAAqB;AAAA,EAC5B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,iDAAkD,QAAQ,oCAAoC;AAAA,EAC5H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,2DAA2D,QAAQ,sBAAsB;AAAA,EACvH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
6
6
  "names": []
7
7
  }
@@ -56,7 +56,7 @@ const storefrontSearchSuggestGetDoc = {
56
56
  { status: 404, description: "No store serves this host or path", schema: storefrontErrorSchema },
57
57
  { status: 410, description: "Store archived", schema: storefrontErrorSchema },
58
58
  { status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
59
- { status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
59
+ { status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
60
60
  ]
61
61
  };
62
62
  const openApi = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/ecommerce/api/storefront/search/suggest/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontSearchSuggestQuerySchema } from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { parseStorefrontSearchSuggestQuery } from '../../../../lib/storefrontQuery'\nimport { cachedSuggestStorefrontSearch } from '../../../../lib/storefrontSearchCache'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontSearchSuggestResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/search/suggest',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontSearchSuggestQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'searchSuggest')\n if (rateLimited) return rateLimited\n const body = await cachedSuggestStorefrontSearch(container, context, query)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront search suggest failed',\n code: 'ecommerce.storefront_search_suggest_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontSearchTag = 'Ecommerce'\n\nconst storefrontSearchSuggestGetDoc: OpenApiMethodDoc = {\n summary: 'Typeahead suggestions for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns up to `limit` products of the buyer\\'s effective assortment matching `q`, with the buyer\\'s formatted price, and up to `limit` visible categories whose localized name matches `q`. Products are ranked by the `tokens` or `pgvector` search strategy with the assortment scope inside the ranking query, or by the escaped `ILIKE` match when neither is available; the payload is identical either way. A `q` shorter than 2 characters returns empty arrays, never an error. Products whose availability policy hides them when out of stock are omitted. No facets are computed. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. Cached server-side for 30 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontSearchTag],\n query: ecommerceStorefrontSearchSuggestQuerySchema,\n responses: [\n { status: 200, description: 'Products and categories matching the query.', schema: storefrontSearchSuggestResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing `q`, or unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontSearchTag,\n summary: 'Public storefront search suggestions',\n methods: {\n GET: storefrontSearchSuggestGetDoc,\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,mDAAmD;AAC5D,SAAS,kCAAkC;AAE3C,SAAS,yCAAyC;AAClD,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6CAA6C;AAE/C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,kCAAkC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC7E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,eAAe;AAC7F,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,8BAA8B,WAAW,SAAS,KAAK;AAC1E,aAAO,aAAa,KAAK,MAAM,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IACxG,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,sBAAsB;AAE5B,MAAM,gCAAkD;AAAA,EACtD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,mBAAmB;AAAA,EAC1B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,+CAA+C,QAAQ,sCAAsC;AAAA,EAC3H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mEAAmE,QAAQ,kCAAkC;AAAA,IACzI,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,oDAAoD,QAAQ,sBAAsB;AAAA,EAChH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontSearchSuggestQuerySchema } from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { parseStorefrontSearchSuggestQuery } from '../../../../lib/storefrontQuery'\nimport { cachedSuggestStorefrontSearch } from '../../../../lib/storefrontSearchCache'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontSearchSuggestResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/search/suggest',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontSearchSuggestQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'searchSuggest')\n if (rateLimited) return rateLimited\n const body = await cachedSuggestStorefrontSearch(container, context, query)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront search suggest failed',\n code: 'ecommerce.storefront_search_suggest_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontSearchTag = 'Ecommerce'\n\nconst storefrontSearchSuggestGetDoc: OpenApiMethodDoc = {\n summary: 'Typeahead suggestions for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns up to `limit` products of the buyer\\'s effective assortment matching `q`, with the buyer\\'s formatted price, and up to `limit` visible categories whose localized name matches `q`. Products are ranked by the `tokens` or `pgvector` search strategy with the assortment scope inside the ranking query, or by the escaped `ILIKE` match when neither is available; the payload is identical either way. A `q` shorter than 2 characters returns empty arrays, never an error. Products whose availability policy hides them when out of stock are omitted. No facets are computed. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. Cached server-side for 30 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontSearchTag],\n query: ecommerceStorefrontSearchSuggestQuerySchema,\n responses: [\n { status: 200, description: 'Products and categories matching the query.', schema: storefrontSearchSuggestResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing `q`, or unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontSearchTag,\n summary: 'Public storefront search suggestions',\n methods: {\n GET: storefrontSearchSuggestGetDoc,\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,mDAAmD;AAC5D,SAAS,kCAAkC;AAE3C,SAAS,yCAAyC;AAClD,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6CAA6C;AAE/C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,kCAAkC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC7E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,eAAe;AAC7F,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,8BAA8B,WAAW,SAAS,KAAK;AAC1E,aAAO,aAAa,KAAK,MAAM,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IACxG,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,sBAAsB;AAE5B,MAAM,gCAAkD;AAAA,EACtD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,mBAAmB;AAAA,EAC1B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,+CAA+C,QAAQ,sCAAsC;AAAA,EAC3H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mEAAmE,QAAQ,kCAAkC;AAAA,IACzI,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,2DAA2D,QAAQ,sBAAsB;AAAA,EACvH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
6
6
  "names": []
7
7
  }
@@ -10,6 +10,7 @@ import {
10
10
  ecommerceCustomerUserTag,
11
11
  ecommerceDomainMappingTag,
12
12
  ecommerceDomainTag,
13
+ ecommerceSalesChannelTag,
13
14
  ecommerceStoreTag,
14
15
  ecommerceUngroupedBuyerTag,
15
16
  invalidateEcommerceCacheTags,
@@ -46,6 +47,7 @@ const storeDomainBindingEventTags = (payload) => [
46
47
  )
47
48
  ];
48
49
  const storeChannelBindingEventTags = (payload) => tagsFrom([readPayloadString(payload, "storeId"), readPayloadString(payload, "previousStoreId")], ecommerceStoreTag);
50
+ const salesChannelEventTags = (payload) => tagsFrom([readPayloadString(payload, "id")], ecommerceSalesChannelTag);
49
51
  const domainMappingEventTags = (payload) => [
50
52
  ...tagsFrom(
51
53
  [readPayloadString(payload, "id"), readPayloadString(payload, "replacedDomainId")],
@@ -179,6 +181,7 @@ export {
179
181
  invalidateForPriceEvent,
180
182
  invalidateForProductEvent,
181
183
  invalidateForVariantEvent,
184
+ salesChannelEventTags,
182
185
  storeChannelBindingEventTags,
183
186
  storeDomainBindingEventTags,
184
187
  storeEventTags
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ecommerce/lib/cacheInvalidation.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport {\n catalogCategoryTag,\n catalogPriceTag,\n catalogProductTag,\n catalogProductsTag,\n ecommerceCustomerGroupTag,\n ecommerceCustomerTag,\n ecommerceCustomerUserTag,\n ecommerceDomainMappingTag,\n ecommerceDomainTag,\n ecommerceStoreTag,\n ecommerceUngroupedBuyerTag,\n invalidateEcommerceCacheTags,\n storefrontAvailabilityTag,\n} from './cacheKeys'\nimport {\n asPayloadRecord,\n eventAction,\n readEventScope,\n readPayloadString,\n tryResolveService,\n type EcommerceSubscriberContext,\n} from './subscriberSupport'\n\n/**\n * Event \u2192 cache-tag mapping for the storefront caches (SPEC-029 \u00A78 table). Every eviction goes\n * through `invalidateEcommerceCacheTags`, which clears the tenant-agnostic resolution cache and the\n * emitting tenant's buyer/storefront caches.\n */\n\nconst logger = createLogger('ecommerce').child({ component: 'cache-invalidation' })\n\ntype TagDeriver = (payload: Record<string, unknown>, action: string | null, tenantId: string | null) => string[]\n\nconst STORE_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted', 'branding_updated'])\nconst CUSTOMER_GROUP_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])\nconst CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated'])\nconst CATALOG_CRUD_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])\nconst CUSTOMER_USER_CACHE_ACTIONS: ReadonlySet<string> = new Set(['updated', 'deleted'])\n\nfunction tagsFrom(values: Array<string | null>, toTag: (value: string) => string): string[] {\n return values.filter((value): value is string => value !== null).map(toTag)\n}\n\nexport const storeEventTags: TagDeriver = (payload, action) => {\n if (action && !STORE_CACHE_ACTIONS.has(action)) return []\n return tagsFrom([readPayloadString(payload, 'id')], ecommerceStoreTag)\n}\n\nexport const storeDomainBindingEventTags: TagDeriver = (payload) => [\n ...tagsFrom(\n [readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')],\n ecommerceStoreTag,\n ),\n ...tagsFrom(\n [readPayloadString(payload, 'domainMappingId'), readPayloadString(payload, 'previousDomainMappingId')],\n ecommerceDomainMappingTag,\n ),\n]\n\nexport const storeChannelBindingEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')], ecommerceStoreTag)\n\nexport const domainMappingEventTags: TagDeriver = (payload) => [\n ...tagsFrom(\n [readPayloadString(payload, 'id'), readPayloadString(payload, 'replacedDomainId')],\n ecommerceDomainMappingTag,\n ),\n ...tagsFrom(\n [readPayloadString(payload, 'hostname'), readPayloadString(payload, 'replacedHostname')],\n ecommerceDomainTag,\n ),\n]\n\n/**\n * `customer_accounts.user.updated|deleted`: the user's buyer contexts, so a deactivated or deleted\n * portal user stops resolving to their cached identity (and contract prices) before the TTL ends.\n */\nexport const customerUserEventTags: TagDeriver = (payload, action) => {\n if (action && !CUSTOMER_USER_CACHE_ACTIONS.has(action)) return []\n return tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerUserTag)\n}\n\nexport const customerGroupMembershipEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'customerId')], ecommerceCustomerTag)\n\n/**\n * `customer_groups.group.*`: the group's buyer contexts. The payload carries no `isDefault`, so a\n * create or update also evicts the tenant's ungrouped buyer contexts, which would fall back to the\n * group if it became the tenant default; a replaced default is announced as its own update.\n */\nexport const customerGroupEventTags: TagDeriver = (payload, action, tenantId) => {\n if (action && !CUSTOMER_GROUP_CACHE_ACTIONS.has(action)) return []\n const tags = action === 'created' ? [] : tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerGroupTag)\n if (tenantId && (!action || CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS.has(action))) {\n tags.push(ecommerceUngroupedBuyerTag(tenantId))\n }\n return tags\n}\n\nexport const customerGroupTermsEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'groupId')], ecommerceCustomerGroupTag)\n\nexport async function invalidateForEvent(\n payload: unknown,\n ctx: EcommerceSubscriberContext,\n deriveTags: TagDeriver,\n): Promise<number> {\n const record = asPayloadRecord(payload)\n const { tenantId } = readEventScope(record, ctx)\n const tags = deriveTags(record, eventAction(ctx.eventName), tenantId)\n if (tags.length === 0) return 0\n return invalidateEcommerceCacheTags(ctx, { tenantId, tags })\n}\n\ntype CatalogLookupDatabase = {\n catalog_product_variant_prices: {\n id: string\n tenant_id: string\n organization_id: string\n customer_id: string | null\n product_id: string | null\n variant_id: string | null\n }\n catalog_product_variants: {\n id: string\n tenant_id: string\n organization_id: string\n product_id: string\n }\n availability_policies: {\n id: string\n tenant_id: string\n organization_id: string\n product_id: string | null\n }\n}\n\ntype PriceOwner = { customerId: string | null; productId: string | null }\n\ntype LookupScope = { tenantId: string; organizationId: string | null }\n\nasync function readPriceOwner(em: EntityManager, priceId: string, scope: LookupScope): Promise<PriceOwner | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('catalog_product_variant_prices as price')\n .leftJoin('catalog_product_variants as variant', 'variant.id', 'price.variant_id')\n .select(['price.customer_id as customerId', 'price.product_id as productId', 'variant.product_id as variantProductId'])\n .where('price.id', '=', priceId)\n .where('price.tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('price.organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n if (!row) return null\n return { customerId: row.customerId ?? null, productId: row.productId ?? row.variantProductId ?? null }\n}\n\nasync function readVariantProductId(em: EntityManager, variantId: string, scope: LookupScope): Promise<string | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('catalog_product_variants')\n .select('product_id')\n .where('id', '=', variantId)\n .where('tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n return row?.product_id ?? null\n}\n\nasync function readPolicyProductId(em: EntityManager, policyId: string, scope: LookupScope): Promise<string | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('availability_policies')\n .select('product_id')\n .where('id', '=', policyId)\n .where('tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n return row?.product_id ?? null\n}\n\n/**\n * Reads the row an id-only catalog/availability payload points at. A missing `em`, a missing row\n * and a failed lookup all answer `undefined`, so the caller falls back to its broader tag.\n */\nasync function lookupForEvent<T>(\n ctx: EcommerceSubscriberContext,\n id: string,\n scope: LookupScope,\n lookup: (em: EntityManager, id: string, scope: LookupScope) => Promise<T | null>,\n failureCode: string,\n): Promise<T | undefined> {\n const em = tryResolveService<EntityManager>(ctx, 'em')\n if (!em) return undefined\n try {\n return (await lookup(em, id, scope)) ?? undefined\n } catch (error) {\n logger.warn('Storefront cache lookup failed; falling back to broader tags', { id, failureCode, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: failureCode,\n attributes: { id },\n })\n return undefined\n }\n}\n\ntype ScopedEvent = { id: string; action: string | null; scope: LookupScope }\n\nfunction readScopedEvent(payload: unknown, ctx: EcommerceSubscriberContext): ScopedEvent | null {\n const record = asPayloadRecord(payload)\n const id = readPayloadString(record, 'id')\n const scope = readEventScope(record, ctx)\n if (!id || !scope.tenantId) return null\n return { id, action: eventAction(ctx.eventName), scope: { tenantId: scope.tenantId, organizationId: scope.organizationId } }\n}\n\nfunction isCatalogCrudAction(action: string | null): boolean {\n return action === null || CATALOG_CRUD_ACTIONS.has(action)\n}\n\n/**\n * `catalog.price.*` (\u00A78 buyer context, Storefront Public API \u00A79): evicts the product's\n * `catalog-price` tag and every listing of the tenant, plus the owning customer's buyer context\n * when the row is customer-specific (`customerOverlayId`). The payload carries ids only, so the row\n * is read back; a deleted row is gone, so a delete evicts the listings only and the product detail\n * and buyer context fall back to their TTLs (catalog declares no price-kind events either).\n */\nexport async function invalidateForPriceEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const owner =\n event.action === 'deleted'\n ? undefined\n : await lookupForEvent(ctx, event.id, event.scope, readPriceOwner, 'ecommerce.price_cache_invalidation_failed')\n const tags = [catalogProductsTag(event.scope.tenantId)]\n if (owner?.productId) tags.push(catalogPriceTag(owner.productId))\n if (owner?.customerId) tags.push(ecommerceCustomerTag(owner.customerId))\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n\n/** `catalog.product.created|updated|deleted`: the product's detail entries and every listing of the tenant. */\nexport async function invalidateForProductEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n return invalidateEcommerceCacheTags(ctx, {\n tenantId: event.scope.tenantId,\n tags: [catalogProductTag(event.id), catalogProductsTag(event.scope.tenantId)],\n })\n}\n\n/**\n * `catalog.variant.*`: the variant's product (read back \u2014 the payload carries ids only) and every\n * listing of the tenant. A deleted variant row is gone, so its product detail falls back to its TTL.\n */\nexport async function invalidateForVariantEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const productId =\n event.action === 'deleted'\n ? undefined\n : await lookupForEvent(ctx, event.id, event.scope, readVariantProductId, 'ecommerce.variant_cache_invalidation_failed')\n const tags = [catalogProductsTag(event.scope.tenantId)]\n if (productId) tags.push(catalogProductTag(productId))\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n\n/** `catalog.category.*`: details showing the category and every listing (category names, filters, assortment). */\nexport async function invalidateForCategoryEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n return invalidateEcommerceCacheTags(ctx, {\n tenantId: event.scope.tenantId,\n tags: [catalogCategoryTag(event.id), catalogProductsTag(event.scope.tenantId)],\n })\n}\n\n/**\n * `availability.policy.*`: a policy tied to a product evicts that product and every listing; a\n * store- or tenant-wide policy (or one that cannot be read back) evicts every storefront product\n * entry of the tenant. Policies are soft-deleted, so a delete still reads back its product.\n */\nexport async function invalidateForAvailabilityPolicyEvent(\n payload: unknown,\n ctx: EcommerceSubscriberContext,\n): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const productId = await lookupForEvent(\n ctx,\n event.id,\n event.scope,\n readPolicyProductId,\n 'ecommerce.availability_policy_cache_invalidation_failed',\n )\n const tags = productId\n ? [catalogProductTag(productId), catalogProductsTag(event.scope.tenantId)]\n : [storefrontAvailabilityTag(event.scope.tenantId)]\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport {\n catalogCategoryTag,\n catalogPriceTag,\n catalogProductTag,\n catalogProductsTag,\n ecommerceCustomerGroupTag,\n ecommerceCustomerTag,\n ecommerceCustomerUserTag,\n ecommerceDomainMappingTag,\n ecommerceDomainTag,\n ecommerceSalesChannelTag,\n ecommerceStoreTag,\n ecommerceUngroupedBuyerTag,\n invalidateEcommerceCacheTags,\n storefrontAvailabilityTag,\n} from './cacheKeys'\nimport {\n asPayloadRecord,\n eventAction,\n readEventScope,\n readPayloadString,\n tryResolveService,\n type EcommerceSubscriberContext,\n} from './subscriberSupport'\n\n/**\n * Event \u2192 cache-tag mapping for the storefront caches (SPEC-029 \u00A78 table). Every eviction goes\n * through `invalidateEcommerceCacheTags`, which clears the tenant-agnostic resolution cache and the\n * emitting tenant's buyer/storefront caches.\n */\n\nconst logger = createLogger('ecommerce').child({ component: 'cache-invalidation' })\n\ntype TagDeriver = (payload: Record<string, unknown>, action: string | null, tenantId: string | null) => string[]\n\nconst STORE_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted', 'branding_updated'])\nconst CUSTOMER_GROUP_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])\nconst CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated'])\nconst CATALOG_CRUD_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])\nconst CUSTOMER_USER_CACHE_ACTIONS: ReadonlySet<string> = new Set(['updated', 'deleted'])\n\nfunction tagsFrom(values: Array<string | null>, toTag: (value: string) => string): string[] {\n return values.filter((value): value is string => value !== null).map(toTag)\n}\n\nexport const storeEventTags: TagDeriver = (payload, action) => {\n if (action && !STORE_CACHE_ACTIONS.has(action)) return []\n return tagsFrom([readPayloadString(payload, 'id')], ecommerceStoreTag)\n}\n\nexport const storeDomainBindingEventTags: TagDeriver = (payload) => [\n ...tagsFrom(\n [readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')],\n ecommerceStoreTag,\n ),\n ...tagsFrom(\n [readPayloadString(payload, 'domainMappingId'), readPayloadString(payload, 'previousDomainMappingId')],\n ecommerceDomainMappingTag,\n ),\n]\n\nexport const storeChannelBindingEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')], ecommerceStoreTag)\n\nexport const salesChannelEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'id')], ecommerceSalesChannelTag)\n\nexport const domainMappingEventTags: TagDeriver = (payload) => [\n ...tagsFrom(\n [readPayloadString(payload, 'id'), readPayloadString(payload, 'replacedDomainId')],\n ecommerceDomainMappingTag,\n ),\n ...tagsFrom(\n [readPayloadString(payload, 'hostname'), readPayloadString(payload, 'replacedHostname')],\n ecommerceDomainTag,\n ),\n]\n\n/**\n * `customer_accounts.user.updated|deleted`: the user's buyer contexts, so a deactivated or deleted\n * portal user stops resolving to their cached identity (and contract prices) before the TTL ends.\n */\nexport const customerUserEventTags: TagDeriver = (payload, action) => {\n if (action && !CUSTOMER_USER_CACHE_ACTIONS.has(action)) return []\n return tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerUserTag)\n}\n\nexport const customerGroupMembershipEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'customerId')], ecommerceCustomerTag)\n\n/**\n * `customer_groups.group.*`: the group's buyer contexts. The payload carries no `isDefault`, so a\n * create or update also evicts the tenant's ungrouped buyer contexts, which would fall back to the\n * group if it became the tenant default; a replaced default is announced as its own update.\n */\nexport const customerGroupEventTags: TagDeriver = (payload, action, tenantId) => {\n if (action && !CUSTOMER_GROUP_CACHE_ACTIONS.has(action)) return []\n const tags = action === 'created' ? [] : tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerGroupTag)\n if (tenantId && (!action || CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS.has(action))) {\n tags.push(ecommerceUngroupedBuyerTag(tenantId))\n }\n return tags\n}\n\nexport const customerGroupTermsEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'groupId')], ecommerceCustomerGroupTag)\n\nexport async function invalidateForEvent(\n payload: unknown,\n ctx: EcommerceSubscriberContext,\n deriveTags: TagDeriver,\n): Promise<number> {\n const record = asPayloadRecord(payload)\n const { tenantId } = readEventScope(record, ctx)\n const tags = deriveTags(record, eventAction(ctx.eventName), tenantId)\n if (tags.length === 0) return 0\n return invalidateEcommerceCacheTags(ctx, { tenantId, tags })\n}\n\ntype CatalogLookupDatabase = {\n catalog_product_variant_prices: {\n id: string\n tenant_id: string\n organization_id: string\n customer_id: string | null\n product_id: string | null\n variant_id: string | null\n }\n catalog_product_variants: {\n id: string\n tenant_id: string\n organization_id: string\n product_id: string\n }\n availability_policies: {\n id: string\n tenant_id: string\n organization_id: string\n product_id: string | null\n }\n}\n\ntype PriceOwner = { customerId: string | null; productId: string | null }\n\ntype LookupScope = { tenantId: string; organizationId: string | null }\n\nasync function readPriceOwner(em: EntityManager, priceId: string, scope: LookupScope): Promise<PriceOwner | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('catalog_product_variant_prices as price')\n .leftJoin('catalog_product_variants as variant', 'variant.id', 'price.variant_id')\n .select(['price.customer_id as customerId', 'price.product_id as productId', 'variant.product_id as variantProductId'])\n .where('price.id', '=', priceId)\n .where('price.tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('price.organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n if (!row) return null\n return { customerId: row.customerId ?? null, productId: row.productId ?? row.variantProductId ?? null }\n}\n\nasync function readVariantProductId(em: EntityManager, variantId: string, scope: LookupScope): Promise<string | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('catalog_product_variants')\n .select('product_id')\n .where('id', '=', variantId)\n .where('tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n return row?.product_id ?? null\n}\n\nasync function readPolicyProductId(em: EntityManager, policyId: string, scope: LookupScope): Promise<string | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('availability_policies')\n .select('product_id')\n .where('id', '=', policyId)\n .where('tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n return row?.product_id ?? null\n}\n\n/**\n * Reads the row an id-only catalog/availability payload points at. A missing `em`, a missing row\n * and a failed lookup all answer `undefined`, so the caller falls back to its broader tag.\n */\nasync function lookupForEvent<T>(\n ctx: EcommerceSubscriberContext,\n id: string,\n scope: LookupScope,\n lookup: (em: EntityManager, id: string, scope: LookupScope) => Promise<T | null>,\n failureCode: string,\n): Promise<T | undefined> {\n const em = tryResolveService<EntityManager>(ctx, 'em')\n if (!em) return undefined\n try {\n return (await lookup(em, id, scope)) ?? undefined\n } catch (error) {\n logger.warn('Storefront cache lookup failed; falling back to broader tags', { id, failureCode, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: failureCode,\n attributes: { id },\n })\n return undefined\n }\n}\n\ntype ScopedEvent = { id: string; action: string | null; scope: LookupScope }\n\nfunction readScopedEvent(payload: unknown, ctx: EcommerceSubscriberContext): ScopedEvent | null {\n const record = asPayloadRecord(payload)\n const id = readPayloadString(record, 'id')\n const scope = readEventScope(record, ctx)\n if (!id || !scope.tenantId) return null\n return { id, action: eventAction(ctx.eventName), scope: { tenantId: scope.tenantId, organizationId: scope.organizationId } }\n}\n\nfunction isCatalogCrudAction(action: string | null): boolean {\n return action === null || CATALOG_CRUD_ACTIONS.has(action)\n}\n\n/**\n * `catalog.price.*` (\u00A78 buyer context, Storefront Public API \u00A79): evicts the product's\n * `catalog-price` tag and every listing of the tenant, plus the owning customer's buyer context\n * when the row is customer-specific (`customerOverlayId`). The payload carries ids only, so the row\n * is read back; a deleted row is gone, so a delete evicts the listings only and the product detail\n * and buyer context fall back to their TTLs (catalog declares no price-kind events either).\n */\nexport async function invalidateForPriceEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const owner =\n event.action === 'deleted'\n ? undefined\n : await lookupForEvent(ctx, event.id, event.scope, readPriceOwner, 'ecommerce.price_cache_invalidation_failed')\n const tags = [catalogProductsTag(event.scope.tenantId)]\n if (owner?.productId) tags.push(catalogPriceTag(owner.productId))\n if (owner?.customerId) tags.push(ecommerceCustomerTag(owner.customerId))\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n\n/** `catalog.product.created|updated|deleted`: the product's detail entries and every listing of the tenant. */\nexport async function invalidateForProductEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n return invalidateEcommerceCacheTags(ctx, {\n tenantId: event.scope.tenantId,\n tags: [catalogProductTag(event.id), catalogProductsTag(event.scope.tenantId)],\n })\n}\n\n/**\n * `catalog.variant.*`: the variant's product (read back \u2014 the payload carries ids only) and every\n * listing of the tenant. A deleted variant row is gone, so its product detail falls back to its TTL.\n */\nexport async function invalidateForVariantEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const productId =\n event.action === 'deleted'\n ? undefined\n : await lookupForEvent(ctx, event.id, event.scope, readVariantProductId, 'ecommerce.variant_cache_invalidation_failed')\n const tags = [catalogProductsTag(event.scope.tenantId)]\n if (productId) tags.push(catalogProductTag(productId))\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n\n/** `catalog.category.*`: details showing the category and every listing (category names, filters, assortment). */\nexport async function invalidateForCategoryEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n return invalidateEcommerceCacheTags(ctx, {\n tenantId: event.scope.tenantId,\n tags: [catalogCategoryTag(event.id), catalogProductsTag(event.scope.tenantId)],\n })\n}\n\n/**\n * `availability.policy.*`: a policy tied to a product evicts that product and every listing; a\n * store- or tenant-wide policy (or one that cannot be read back) evicts every storefront product\n * entry of the tenant. Policies are soft-deleted, so a delete still reads back its product.\n */\nexport async function invalidateForAvailabilityPolicyEvent(\n payload: unknown,\n ctx: EcommerceSubscriberContext,\n): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const productId = await lookupForEvent(\n ctx,\n event.id,\n event.scope,\n readPolicyProductId,\n 'ecommerce.availability_policy_cache_invalidation_failed',\n )\n const tags = productId\n ? [catalogProductTag(productId), catalogProductsTag(event.scope.tenantId)]\n : [storefrontAvailabilityTag(event.scope.tenantId)]\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -69,6 +69,9 @@ function ecommerceDomainMappingTag(domainMappingId) {
69
69
  function ecommerceDomainTag(hostname) {
70
70
  return `ecommerce-domain:${hostname}`;
71
71
  }
72
+ function ecommerceSalesChannelTag(salesChannelId) {
73
+ return `ecommerce-sales-channel:${salesChannelId}`;
74
+ }
72
75
  function ecommerceCustomerTag(customerId) {
73
76
  return `customer:${customerId}`;
74
77
  }
@@ -226,6 +229,7 @@ export {
226
229
  ecommerceDomainMappingTag,
227
230
  ecommerceDomainTag,
228
231
  ecommerceResolutionCache,
232
+ ecommerceSalesChannelTag,
229
233
  ecommerceStoreTag,
230
234
  ecommerceUngroupedBuyerTag,
231
235
  invalidateEcommerceCacheTags,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ecommerce/lib/cacheKeys.ts"],
4
- "sourcesContent": ["import { createHash } from 'node:crypto'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { BuyerContext, StoreContext } from './types'\n\n/**\n * The ONLY file in the ecommerce module allowed to resolve the DI `cache` (SPEC-029 \u00A76.1\n * \"Enforcement\", D7). Every storefront key is built by `buildStorefrontCacheKey`; callers pass\n * `parts`, never a raw key. `__tests__/no-raw-cache-access.test.ts` fails on any other access.\n */\n\nexport type StorefrontCacheScope = 'digest' | 'assortment' | 'priceScope' | 'store'\n\nexport type StorefrontCacheKeyOptions = { scope?: StorefrontCacheScope }\n\nexport type StorefrontCacheSetOptions = {\n ttlMs: number\n tags?: string[]\n scope?: StorefrontCacheScope\n}\n\nexport type StorefrontCache = {\n get<T>(parts: string[], options?: StorefrontCacheKeyOptions): Promise<T | null>\n set<T>(parts: string[], value: T, options: StorefrontCacheSetOptions): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport type EcommerceResolutionCacheSetOptions = { ttlMs: number; tags?: string[] }\n\n/**\n * Store-resolution cache (host/slug \u2192 store + default channel binding, \u00A78.1 steps 3\u20135). It runs\n * BEFORE a `StoreContext` exists, so it is tenant-agnostic and MUST NEVER carry buyer data.\n */\nexport type EcommerceResolutionCache = {\n get<T>(parts: string[]): Promise<T | null>\n set<T>(parts: string[], value: T, options: EcommerceResolutionCacheSetOptions): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\n/**\n * Buyer-context cache (\u00A78, step 6): one entry per (store, customerUserId | anonymous), holding the\n * locale-independent buyer layer only. Tagged per store, per portal user, per identity in\n * `customerIds` and per contributing group so user, membership, terms, group and price events can\n * evict it.\n */\nexport type BuyerContextCache = {\n get(customerUserId: string | null): Promise<BuyerContext | null>\n set(buyer: BuyerContext): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport type BuyerContextCacheScope = { tenantId: string; storeId: string }\n\nexport type CacheContainer = { resolve: (name: string) => unknown }\n\nexport const BUYER_CONTEXT_TTL_MS = 60_000\n\nconst STOREFRONT_KEY_NAMESPACE = 'ecommerce:storefront'\nconst RESOLUTION_KEY_NAMESPACE = 'ecommerce:resolution'\nconst BUYER_KEY_NAMESPACE = 'ecommerce:buyer'\nconst ANONYMOUS_SEGMENT = 'anonymous'\nconst NULL_SEGMENT = '-'\n\nconst SCOPE_SEGMENTS: Record<StorefrontCacheScope, string> = {\n digest: 'digest',\n assortment: 'assortment',\n priceScope: 'price-scope',\n store: 'store',\n}\n\nconst logger = createLogger('ecommerce').child({ component: 'storefront-cache' })\n\nfunction encodeSegment(value: string): string {\n return encodeURIComponent(value)\n}\n\nfunction encodeParts(parts: string[]): string {\n if (parts.length === 0) {\n throw new Error('[internal] ecommerce cache keys require at least one part')\n }\n return parts.map(encodeSegment).join(':')\n}\n\nfunction scopeSegments(ctx: StoreContext, scope: StorefrontCacheScope): string[] {\n const storeId = ctx.store.id\n switch (scope) {\n case 'digest':\n return [storeId, ctx.digest]\n case 'assortment':\n return [storeId, ctx.effectiveLocale, ctx.buyer.assortmentScopeHash]\n case 'priceScope':\n return [storeId, ctx.buyer.priceScopeKey, ctx.buyer.customerOverlayId ?? NULL_SEGMENT]\n case 'store':\n return [storeId, ctx.effectiveLocale]\n }\n}\n\nexport function buildStorefrontCacheKey(\n ctx: StoreContext,\n parts: string[],\n options?: StorefrontCacheKeyOptions,\n): string {\n const scope = options?.scope ?? 'digest'\n const prefix = [SCOPE_SEGMENTS[scope], ...scopeSegments(ctx, scope).map(encodeSegment)].join(':')\n return `${STOREFRONT_KEY_NAMESPACE}:${prefix}:${encodeParts(parts)}`\n}\n\nfunction canonicalCacheValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalCacheValue)\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .filter((entry) => entry[1] !== undefined)\n .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))\n .map(([key, entry]) => [key, canonicalCacheValue(entry)]),\n )\n }\n return value\n}\n\n/** A fixed-length key part for a structured value: object keys sorted, `undefined` entries dropped, sha256-hashed. */\nexport function storefrontCacheValueHash(value: unknown): string {\n return createHash('sha256').update(JSON.stringify(canonicalCacheValue(value))).digest('hex')\n}\n\nexport function buildEcommerceResolutionCacheKey(parts: string[]): string {\n return `${RESOLUTION_KEY_NAMESPACE}:${encodeParts(parts)}`\n}\n\nexport function buildBuyerContextCacheKey(storeId: string, customerUserId: string | null): string {\n return `${BUYER_KEY_NAMESPACE}:${encodeParts([storeId, customerUserId ?? ANONYMOUS_SEGMENT])}`\n}\n\nexport function ecommerceStoreTag(storeId: string): string {\n return `ecommerce-store:${storeId}`\n}\n\nexport function ecommerceDomainMappingTag(domainMappingId: string): string {\n return `ecommerce-domain-mapping:${domainMappingId}`\n}\n\nexport function ecommerceDomainTag(hostname: string): string {\n return `ecommerce-domain:${hostname}`\n}\n\nexport function ecommerceCustomerTag(customerId: string): string {\n return `customer:${customerId}`\n}\n\n/** One portal user's buyer contexts: evicted when the user is updated (e.g. deactivated) or deleted. */\nexport function ecommerceCustomerUserTag(customerUserId: string): string {\n return `customer-user:${customerUserId}`\n}\n\nexport function ecommerceCustomerGroupTag(customerGroupId: string): string {\n return `customer-group:${customerGroupId}`\n}\n\nexport function catalogProductTag(productId: string): string {\n return `catalog-product:${productId}`\n}\n\n/** Every storefront listing of a tenant: evicted by any product, variant, category or price change. */\nexport function catalogProductsTag(tenantId: string): string {\n return `catalog-products:${tenantId}`\n}\n\nexport function catalogCategoryTag(categoryId: string): string {\n return `catalog-category:${categoryId}`\n}\n\nexport function catalogPriceTag(productId: string): string {\n return `catalog-price:${productId}`\n}\n\n/** Every storefront product entry of a tenant: evicted by an availability policy not tied to one product. */\nexport function storefrontAvailabilityTag(tenantId: string): string {\n return `availability:${tenantId}`\n}\n\n/**\n * Buyer contexts of a tenant that resolved to no customer group (anonymous or unassigned buyers\n * while the tenant has no default group): evicted when a group is created or updated, since that\n * group may have become the default they fall back to.\n */\nexport function ecommerceUngroupedBuyerTag(tenantId: string): string {\n return `customer-group-none:${tenantId}`\n}\n\nexport function buyerContextTags(\n buyer: Pick<BuyerContext, 'customerIds' | 'customerGroupIds'> & { customerUserId?: string | null },\n tenantId?: string,\n): string[] {\n return [\n ...(buyer.customerUserId ? [ecommerceCustomerUserTag(buyer.customerUserId)] : []),\n ...buyer.customerIds.map(ecommerceCustomerTag),\n ...buyer.customerGroupIds.map(ecommerceCustomerGroupTag),\n ...(tenantId && buyer.customerGroupIds.length === 0 ? [ecommerceUngroupedBuyerTag(tenantId)] : []),\n ]\n}\n\nfunction resolveCacheService(container: CacheContainer | null | undefined): CacheStrategy | null {\n if (!container?.resolve) return null\n try {\n const candidate = container.resolve('cache') as CacheStrategy | null | undefined\n if (\n candidate &&\n typeof candidate.get === 'function' &&\n typeof candidate.set === 'function' &&\n typeof candidate.deleteByTags === 'function'\n ) {\n return candidate\n }\n } catch {\n return null\n }\n return null\n}\n\nfunction reportCacheFailure(operation: string, error: unknown): void {\n logger.warn('Ecommerce cache operation failed; degrading to a cache miss', { operation, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: 'ecommerce.cache_operation_failed',\n attributes: { operation },\n })\n}\n\ntype TenantScopedCacheOptions = {\n cache: CacheStrategy | null\n tenantId: string | null\n baseTags: string[]\n}\n\nfunction createTenantScopedAccess({ cache, tenantId, baseTags }: TenantScopedCacheOptions) {\n return {\n async get<T>(key: string): Promise<T | null> {\n if (!cache) return null\n try {\n const value = await runWithCacheTenant(tenantId, () => cache.get(key))\n return (value ?? null) as T | null\n } catch (error) {\n reportCacheFailure('get', error)\n return null\n }\n },\n async set<T>(key: string, value: T, ttlMs: number, tags: string[] | undefined): Promise<void> {\n if (!cache) return\n const mergedTags = Array.from(new Set([...baseTags, ...(tags ?? [])]))\n try {\n await runWithCacheTenant(tenantId, () => cache.set(key, value, { ttl: ttlMs, tags: mergedTags }))\n } catch (error) {\n reportCacheFailure('set', error)\n }\n },\n async deleteByTags(tags: string[]): Promise<number> {\n if (!cache || tags.length === 0) return 0\n try {\n return await runWithCacheTenant(tenantId, () => cache.deleteByTags(tags))\n } catch (error) {\n reportCacheFailure('deleteByTags', error)\n return 0\n }\n },\n }\n}\n\nexport function storefrontCache(container: CacheContainer | null | undefined, ctx: StoreContext): StorefrontCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: ctx.tenantId,\n baseTags: [ecommerceStoreTag(ctx.store.id)],\n })\n return {\n get: (parts, options) => access.get(buildStorefrontCacheKey(ctx, parts, options)),\n set: (parts, value, options) =>\n access.set(buildStorefrontCacheKey(ctx, parts, { scope: options.scope }), value, options.ttlMs, options.tags),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n\nexport function ecommerceResolutionCache(container: CacheContainer | null | undefined): EcommerceResolutionCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: null,\n baseTags: [],\n })\n return {\n get: (parts) => access.get(buildEcommerceResolutionCacheKey(parts)),\n set: (parts, value, options) =>\n access.set(buildEcommerceResolutionCacheKey(parts), value, options.ttlMs, options.tags),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n\nexport type EcommerceCacheInvalidation = { tenantId: string | null; tags: string[] }\n\n/**\n * Event-driven invalidation (\u00A78): evicts `tags` from the tenant-agnostic resolution cache and, when\n * the tenant is known, from that tenant's storefront and buyer-context caches. Tags are tenant\n * scoped by the cache layer, so each scope is cleared inside its own tenant.\n */\nexport async function invalidateEcommerceCacheTags(\n container: CacheContainer | null | undefined,\n invalidation: EcommerceCacheInvalidation,\n): Promise<number> {\n const tags = Array.from(new Set(invalidation.tags.filter((tag) => tag.length > 0)))\n if (tags.length === 0) return 0\n const cache = resolveCacheService(container)\n if (!cache) return 0\n const tenantIds = invalidation.tenantId ? [null, invalidation.tenantId] : [null]\n let deleted = 0\n for (const tenantId of tenantIds) {\n deleted += await createTenantScopedAccess({ cache, tenantId, baseTags: [] }).deleteByTags(tags)\n }\n return deleted\n}\n\nexport function buyerContextCache(\n container: CacheContainer | null | undefined,\n scope: BuyerContextCacheScope,\n): BuyerContextCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: scope.tenantId,\n baseTags: [ecommerceStoreTag(scope.storeId)],\n })\n return {\n get: (customerUserId) => access.get<BuyerContext>(buildBuyerContextCacheKey(scope.storeId, customerUserId)),\n set: (buyer) =>\n access.set(\n buildBuyerContextCacheKey(scope.storeId, buyer.customerUserId),\n buyer,\n BUYER_CONTEXT_TTL_MS,\n buyerContextTags(buyer, scope.tenantId),\n ),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,kBAAkB;AAC3B,SAAS,0BAA8C;AACvD,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAqD7B,MAAM,uBAAuB;AAEpC,MAAM,2BAA2B;AACjC,MAAM,2BAA2B;AACjC,MAAM,sBAAsB;AAC5B,MAAM,oBAAoB;AAC1B,MAAM,eAAe;AAErB,MAAM,iBAAuD;AAAA,EAC3D,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,OAAO;AACT;AAEA,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAEhF,SAAS,cAAc,OAAuB;AAC5C,SAAO,mBAAmB,KAAK;AACjC;AAEA,SAAS,YAAY,OAAyB;AAC5C,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,MAAM,2DAA2D;AAAA,EAC7E;AACA,SAAO,MAAM,IAAI,aAAa,EAAE,KAAK,GAAG;AAC1C;AAEA,SAAS,cAAc,KAAmB,OAAuC;AAC/E,QAAM,UAAU,IAAI,MAAM;AAC1B,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,aAAO,CAAC,SAAS,IAAI,MAAM;AAAA,IAC7B,KAAK;AACH,aAAO,CAAC,SAAS,IAAI,iBAAiB,IAAI,MAAM,mBAAmB;AAAA,IACrE,KAAK;AACH,aAAO,CAAC,SAAS,IAAI,MAAM,eAAe,IAAI,MAAM,qBAAqB,YAAY;AAAA,IACvF,KAAK;AACH,aAAO,CAAC,SAAS,IAAI,eAAe;AAAA,EACxC;AACF;AAEO,SAAS,wBACd,KACA,OACA,SACQ;AACR,QAAM,QAAQ,SAAS,SAAS;AAChC,QAAM,SAAS,CAAC,eAAe,KAAK,GAAG,GAAG,cAAc,KAAK,KAAK,EAAE,IAAI,aAAa,CAAC,EAAE,KAAK,GAAG;AAChG,SAAO,GAAG,wBAAwB,IAAI,MAAM,IAAI,YAAY,KAAK,CAAC;AACpE;AAEA,SAAS,oBAAoB,OAAyB;AACpD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,mBAAmB;AAC9D,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,WAAO,OAAO;AAAA,MACZ,OAAO,QAAQ,KAAgC,EAC5C,OAAO,CAAC,UAAU,MAAM,CAAC,MAAM,MAAS,EACxC,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,MAAO,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAE,EACpE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,oBAAoB,KAAK,CAAC,CAAC;AAAA,IAC5D;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,yBAAyB,OAAwB;AAC/D,SAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,UAAU,oBAAoB,KAAK,CAAC,CAAC,EAAE,OAAO,KAAK;AAC7F;AAEO,SAAS,iCAAiC,OAAyB;AACxE,SAAO,GAAG,wBAAwB,IAAI,YAAY,KAAK,CAAC;AAC1D;AAEO,SAAS,0BAA0B,SAAiB,gBAAuC;AAChG,SAAO,GAAG,mBAAmB,IAAI,YAAY,CAAC,SAAS,kBAAkB,iBAAiB,CAAC,CAAC;AAC9F;AAEO,SAAS,kBAAkB,SAAyB;AACzD,SAAO,mBAAmB,OAAO;AACnC;AAEO,SAAS,0BAA0B,iBAAiC;AACzE,SAAO,4BAA4B,eAAe;AACpD;AAEO,SAAS,mBAAmB,UAA0B;AAC3D,SAAO,oBAAoB,QAAQ;AACrC;AAEO,SAAS,qBAAqB,YAA4B;AAC/D,SAAO,YAAY,UAAU;AAC/B;AAGO,SAAS,yBAAyB,gBAAgC;AACvE,SAAO,iBAAiB,cAAc;AACxC;AAEO,SAAS,0BAA0B,iBAAiC;AACzE,SAAO,kBAAkB,eAAe;AAC1C;AAEO,SAAS,kBAAkB,WAA2B;AAC3D,SAAO,mBAAmB,SAAS;AACrC;AAGO,SAAS,mBAAmB,UAA0B;AAC3D,SAAO,oBAAoB,QAAQ;AACrC;AAEO,SAAS,mBAAmB,YAA4B;AAC7D,SAAO,oBAAoB,UAAU;AACvC;AAEO,SAAS,gBAAgB,WAA2B;AACzD,SAAO,iBAAiB,SAAS;AACnC;AAGO,SAAS,0BAA0B,UAA0B;AAClE,SAAO,gBAAgB,QAAQ;AACjC;AAOO,SAAS,2BAA2B,UAA0B;AACnE,SAAO,uBAAuB,QAAQ;AACxC;AAEO,SAAS,iBACd,OACA,UACU;AACV,SAAO;AAAA,IACL,GAAI,MAAM,iBAAiB,CAAC,yBAAyB,MAAM,cAAc,CAAC,IAAI,CAAC;AAAA,IAC/E,GAAG,MAAM,YAAY,IAAI,oBAAoB;AAAA,IAC7C,GAAG,MAAM,iBAAiB,IAAI,yBAAyB;AAAA,IACvD,GAAI,YAAY,MAAM,iBAAiB,WAAW,IAAI,CAAC,2BAA2B,QAAQ,CAAC,IAAI,CAAC;AAAA,EAClG;AACF;AAEA,SAAS,oBAAoB,WAAoE;AAC/F,MAAI,CAAC,WAAW,QAAS,QAAO;AAChC,MAAI;AACF,UAAM,YAAY,UAAU,QAAQ,OAAO;AAC3C,QACE,aACA,OAAO,UAAU,QAAQ,cACzB,OAAO,UAAU,QAAQ,cACzB,OAAO,UAAU,iBAAiB,YAClC;AACA,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,WAAmB,OAAsB;AACnE,SAAO,KAAK,+DAA+D,EAAE,WAAW,KAAK,MAAM,CAAC;AACpG,sBAAoB,GAAG,YAAY,OAAO;AAAA,IACxC,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,YAAY,EAAE,UAAU;AAAA,EAC1B,CAAC;AACH;AAQA,SAAS,yBAAyB,EAAE,OAAO,UAAU,SAAS,GAA6B;AACzF,SAAO;AAAA,IACL,MAAM,IAAO,KAAgC;AAC3C,UAAI,CAAC,MAAO,QAAO;AACnB,UAAI;AACF,cAAM,QAAQ,MAAM,mBAAmB,UAAU,MAAM,MAAM,IAAI,GAAG,CAAC;AACrE,eAAQ,SAAS;AAAA,MACnB,SAAS,OAAO;AACd,2BAAmB,OAAO,KAAK;AAC/B,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,MAAM,IAAO,KAAa,OAAU,OAAe,MAA2C;AAC5F,UAAI,CAAC,MAAO;AACZ,YAAM,aAAa,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAG,UAAU,GAAI,QAAQ,CAAC,CAAE,CAAC,CAAC;AACrE,UAAI;AACF,cAAM,mBAAmB,UAAU,MAAM,MAAM,IAAI,KAAK,OAAO,EAAE,KAAK,OAAO,MAAM,WAAW,CAAC,CAAC;AAAA,MAClG,SAAS,OAAO;AACd,2BAAmB,OAAO,KAAK;AAAA,MACjC;AAAA,IACF;AAAA,IACA,MAAM,aAAa,MAAiC;AAClD,UAAI,CAAC,SAAS,KAAK,WAAW,EAAG,QAAO;AACxC,UAAI;AACF,eAAO,MAAM,mBAAmB,UAAU,MAAM,MAAM,aAAa,IAAI,CAAC;AAAA,MAC1E,SAAS,OAAO;AACd,2BAAmB,gBAAgB,KAAK;AACxC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,gBAAgB,WAA8C,KAAoC;AAChH,QAAM,SAAS,yBAAyB;AAAA,IACtC,OAAO,oBAAoB,SAAS;AAAA,IACpC,UAAU,IAAI;AAAA,IACd,UAAU,CAAC,kBAAkB,IAAI,MAAM,EAAE,CAAC;AAAA,EAC5C,CAAC;AACD,SAAO;AAAA,IACL,KAAK,CAAC,OAAO,YAAY,OAAO,IAAI,wBAAwB,KAAK,OAAO,OAAO,CAAC;AAAA,IAChF,KAAK,CAAC,OAAO,OAAO,YAClB,OAAO,IAAI,wBAAwB,KAAK,OAAO,EAAE,OAAO,QAAQ,MAAM,CAAC,GAAG,OAAO,QAAQ,OAAO,QAAQ,IAAI;AAAA,IAC9G,cAAc,CAAC,SAAS,OAAO,aAAa,IAAI;AAAA,EAClD;AACF;AAEO,SAAS,yBAAyB,WAAwE;AAC/G,QAAM,SAAS,yBAAyB;AAAA,IACtC,OAAO,oBAAoB,SAAS;AAAA,IACpC,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb,CAAC;AACD,SAAO;AAAA,IACL,KAAK,CAAC,UAAU,OAAO,IAAI,iCAAiC,KAAK,CAAC;AAAA,IAClE,KAAK,CAAC,OAAO,OAAO,YAClB,OAAO,IAAI,iCAAiC,KAAK,GAAG,OAAO,QAAQ,OAAO,QAAQ,IAAI;AAAA,IACxF,cAAc,CAAC,SAAS,OAAO,aAAa,IAAI;AAAA,EAClD;AACF;AASA,eAAsB,6BACpB,WACA,cACiB;AACjB,QAAM,OAAO,MAAM,KAAK,IAAI,IAAI,aAAa,KAAK,OAAO,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC;AAClF,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,QAAQ,oBAAoB,SAAS;AAC3C,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,YAAY,aAAa,WAAW,CAAC,MAAM,aAAa,QAAQ,IAAI,CAAC,IAAI;AAC/E,MAAI,UAAU;AACd,aAAW,YAAY,WAAW;AAChC,eAAW,MAAM,yBAAyB,EAAE,OAAO,UAAU,UAAU,CAAC,EAAE,CAAC,EAAE,aAAa,IAAI;AAAA,EAChG;AACA,SAAO;AACT;AAEO,SAAS,kBACd,WACA,OACmB;AACnB,QAAM,SAAS,yBAAyB;AAAA,IACtC,OAAO,oBAAoB,SAAS;AAAA,IACpC,UAAU,MAAM;AAAA,IAChB,UAAU,CAAC,kBAAkB,MAAM,OAAO,CAAC;AAAA,EAC7C,CAAC;AACD,SAAO;AAAA,IACL,KAAK,CAAC,mBAAmB,OAAO,IAAkB,0BAA0B,MAAM,SAAS,cAAc,CAAC;AAAA,IAC1G,KAAK,CAAC,UACJ,OAAO;AAAA,MACL,0BAA0B,MAAM,SAAS,MAAM,cAAc;AAAA,MAC7D;AAAA,MACA;AAAA,MACA,iBAAiB,OAAO,MAAM,QAAQ;AAAA,IACxC;AAAA,IACF,cAAc,CAAC,SAAS,OAAO,aAAa,IAAI;AAAA,EAClD;AACF;",
4
+ "sourcesContent": ["import { createHash } from 'node:crypto'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { BuyerContext, StoreContext } from './types'\n\n/**\n * The ONLY file in the ecommerce module allowed to resolve the DI `cache` (SPEC-029 \u00A76.1\n * \"Enforcement\", D7). Every storefront key is built by `buildStorefrontCacheKey`; callers pass\n * `parts`, never a raw key. `__tests__/no-raw-cache-access.test.ts` fails on any other access.\n */\n\nexport type StorefrontCacheScope = 'digest' | 'assortment' | 'priceScope' | 'store'\n\nexport type StorefrontCacheKeyOptions = { scope?: StorefrontCacheScope }\n\nexport type StorefrontCacheSetOptions = {\n ttlMs: number\n tags?: string[]\n scope?: StorefrontCacheScope\n}\n\nexport type StorefrontCache = {\n get<T>(parts: string[], options?: StorefrontCacheKeyOptions): Promise<T | null>\n set<T>(parts: string[], value: T, options: StorefrontCacheSetOptions): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport type EcommerceResolutionCacheSetOptions = { ttlMs: number; tags?: string[] }\n\n/**\n * Store-resolution cache (host/slug \u2192 store + default channel binding, \u00A78.1 steps 3\u20135). It runs\n * BEFORE a `StoreContext` exists, so it is tenant-agnostic and MUST NEVER carry buyer data.\n */\nexport type EcommerceResolutionCache = {\n get<T>(parts: string[]): Promise<T | null>\n set<T>(parts: string[], value: T, options: EcommerceResolutionCacheSetOptions): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\n/**\n * Buyer-context cache (\u00A78, step 6): one entry per (store, customerUserId | anonymous), holding the\n * locale-independent buyer layer only. Tagged per store, per portal user, per identity in\n * `customerIds` and per contributing group so user, membership, terms, group and price events can\n * evict it.\n */\nexport type BuyerContextCache = {\n get(customerUserId: string | null): Promise<BuyerContext | null>\n set(buyer: BuyerContext): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport type BuyerContextCacheScope = { tenantId: string; storeId: string }\n\nexport type CacheContainer = { resolve: (name: string) => unknown }\n\nexport const BUYER_CONTEXT_TTL_MS = 60_000\n\nconst STOREFRONT_KEY_NAMESPACE = 'ecommerce:storefront'\nconst RESOLUTION_KEY_NAMESPACE = 'ecommerce:resolution'\nconst BUYER_KEY_NAMESPACE = 'ecommerce:buyer'\nconst ANONYMOUS_SEGMENT = 'anonymous'\nconst NULL_SEGMENT = '-'\n\nconst SCOPE_SEGMENTS: Record<StorefrontCacheScope, string> = {\n digest: 'digest',\n assortment: 'assortment',\n priceScope: 'price-scope',\n store: 'store',\n}\n\nconst logger = createLogger('ecommerce').child({ component: 'storefront-cache' })\n\nfunction encodeSegment(value: string): string {\n return encodeURIComponent(value)\n}\n\nfunction encodeParts(parts: string[]): string {\n if (parts.length === 0) {\n throw new Error('[internal] ecommerce cache keys require at least one part')\n }\n return parts.map(encodeSegment).join(':')\n}\n\nfunction scopeSegments(ctx: StoreContext, scope: StorefrontCacheScope): string[] {\n const storeId = ctx.store.id\n switch (scope) {\n case 'digest':\n return [storeId, ctx.digest]\n case 'assortment':\n return [storeId, ctx.effectiveLocale, ctx.buyer.assortmentScopeHash]\n case 'priceScope':\n return [storeId, ctx.buyer.priceScopeKey, ctx.buyer.customerOverlayId ?? NULL_SEGMENT]\n case 'store':\n return [storeId, ctx.effectiveLocale]\n }\n}\n\nexport function buildStorefrontCacheKey(\n ctx: StoreContext,\n parts: string[],\n options?: StorefrontCacheKeyOptions,\n): string {\n const scope = options?.scope ?? 'digest'\n const prefix = [SCOPE_SEGMENTS[scope], ...scopeSegments(ctx, scope).map(encodeSegment)].join(':')\n return `${STOREFRONT_KEY_NAMESPACE}:${prefix}:${encodeParts(parts)}`\n}\n\nfunction canonicalCacheValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalCacheValue)\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .filter((entry) => entry[1] !== undefined)\n .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))\n .map(([key, entry]) => [key, canonicalCacheValue(entry)]),\n )\n }\n return value\n}\n\n/** A fixed-length key part for a structured value: object keys sorted, `undefined` entries dropped, sha256-hashed. */\nexport function storefrontCacheValueHash(value: unknown): string {\n return createHash('sha256').update(JSON.stringify(canonicalCacheValue(value))).digest('hex')\n}\n\nexport function buildEcommerceResolutionCacheKey(parts: string[]): string {\n return `${RESOLUTION_KEY_NAMESPACE}:${encodeParts(parts)}`\n}\n\nexport function buildBuyerContextCacheKey(storeId: string, customerUserId: string | null): string {\n return `${BUYER_KEY_NAMESPACE}:${encodeParts([storeId, customerUserId ?? ANONYMOUS_SEGMENT])}`\n}\n\nexport function ecommerceStoreTag(storeId: string): string {\n return `ecommerce-store:${storeId}`\n}\n\nexport function ecommerceDomainMappingTag(domainMappingId: string): string {\n return `ecommerce-domain-mapping:${domainMappingId}`\n}\n\nexport function ecommerceDomainTag(hostname: string): string {\n return `ecommerce-domain:${hostname}`\n}\n\nexport function ecommerceSalesChannelTag(salesChannelId: string): string {\n return `ecommerce-sales-channel:${salesChannelId}`\n}\n\nexport function ecommerceCustomerTag(customerId: string): string {\n return `customer:${customerId}`\n}\n\n/** One portal user's buyer contexts: evicted when the user is updated (e.g. deactivated) or deleted. */\nexport function ecommerceCustomerUserTag(customerUserId: string): string {\n return `customer-user:${customerUserId}`\n}\n\nexport function ecommerceCustomerGroupTag(customerGroupId: string): string {\n return `customer-group:${customerGroupId}`\n}\n\nexport function catalogProductTag(productId: string): string {\n return `catalog-product:${productId}`\n}\n\n/** Every storefront listing of a tenant: evicted by any product, variant, category or price change. */\nexport function catalogProductsTag(tenantId: string): string {\n return `catalog-products:${tenantId}`\n}\n\nexport function catalogCategoryTag(categoryId: string): string {\n return `catalog-category:${categoryId}`\n}\n\nexport function catalogPriceTag(productId: string): string {\n return `catalog-price:${productId}`\n}\n\n/** Every storefront product entry of a tenant: evicted by an availability policy not tied to one product. */\nexport function storefrontAvailabilityTag(tenantId: string): string {\n return `availability:${tenantId}`\n}\n\n/**\n * Buyer contexts of a tenant that resolved to no customer group (anonymous or unassigned buyers\n * while the tenant has no default group): evicted when a group is created or updated, since that\n * group may have become the default they fall back to.\n */\nexport function ecommerceUngroupedBuyerTag(tenantId: string): string {\n return `customer-group-none:${tenantId}`\n}\n\nexport function buyerContextTags(\n buyer: Pick<BuyerContext, 'customerIds' | 'customerGroupIds'> & { customerUserId?: string | null },\n tenantId?: string,\n): string[] {\n return [\n ...(buyer.customerUserId ? [ecommerceCustomerUserTag(buyer.customerUserId)] : []),\n ...buyer.customerIds.map(ecommerceCustomerTag),\n ...buyer.customerGroupIds.map(ecommerceCustomerGroupTag),\n ...(tenantId && buyer.customerGroupIds.length === 0 ? [ecommerceUngroupedBuyerTag(tenantId)] : []),\n ]\n}\n\nfunction resolveCacheService(container: CacheContainer | null | undefined): CacheStrategy | null {\n if (!container?.resolve) return null\n try {\n const candidate = container.resolve('cache') as CacheStrategy | null | undefined\n if (\n candidate &&\n typeof candidate.get === 'function' &&\n typeof candidate.set === 'function' &&\n typeof candidate.deleteByTags === 'function'\n ) {\n return candidate\n }\n } catch {\n return null\n }\n return null\n}\n\nfunction reportCacheFailure(operation: string, error: unknown): void {\n logger.warn('Ecommerce cache operation failed; degrading to a cache miss', { operation, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: 'ecommerce.cache_operation_failed',\n attributes: { operation },\n })\n}\n\ntype TenantScopedCacheOptions = {\n cache: CacheStrategy | null\n tenantId: string | null\n baseTags: string[]\n}\n\nfunction createTenantScopedAccess({ cache, tenantId, baseTags }: TenantScopedCacheOptions) {\n return {\n async get<T>(key: string): Promise<T | null> {\n if (!cache) return null\n try {\n const value = await runWithCacheTenant(tenantId, () => cache.get(key))\n return (value ?? null) as T | null\n } catch (error) {\n reportCacheFailure('get', error)\n return null\n }\n },\n async set<T>(key: string, value: T, ttlMs: number, tags: string[] | undefined): Promise<void> {\n if (!cache) return\n const mergedTags = Array.from(new Set([...baseTags, ...(tags ?? [])]))\n try {\n await runWithCacheTenant(tenantId, () => cache.set(key, value, { ttl: ttlMs, tags: mergedTags }))\n } catch (error) {\n reportCacheFailure('set', error)\n }\n },\n async deleteByTags(tags: string[]): Promise<number> {\n if (!cache || tags.length === 0) return 0\n try {\n return await runWithCacheTenant(tenantId, () => cache.deleteByTags(tags))\n } catch (error) {\n reportCacheFailure('deleteByTags', error)\n return 0\n }\n },\n }\n}\n\nexport function storefrontCache(container: CacheContainer | null | undefined, ctx: StoreContext): StorefrontCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: ctx.tenantId,\n baseTags: [ecommerceStoreTag(ctx.store.id)],\n })\n return {\n get: (parts, options) => access.get(buildStorefrontCacheKey(ctx, parts, options)),\n set: (parts, value, options) =>\n access.set(buildStorefrontCacheKey(ctx, parts, { scope: options.scope }), value, options.ttlMs, options.tags),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n\nexport function ecommerceResolutionCache(container: CacheContainer | null | undefined): EcommerceResolutionCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: null,\n baseTags: [],\n })\n return {\n get: (parts) => access.get(buildEcommerceResolutionCacheKey(parts)),\n set: (parts, value, options) =>\n access.set(buildEcommerceResolutionCacheKey(parts), value, options.ttlMs, options.tags),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n\nexport type EcommerceCacheInvalidation = { tenantId: string | null; tags: string[] }\n\n/**\n * Event-driven invalidation (\u00A78): evicts `tags` from the tenant-agnostic resolution cache and, when\n * the tenant is known, from that tenant's storefront and buyer-context caches. Tags are tenant\n * scoped by the cache layer, so each scope is cleared inside its own tenant.\n */\nexport async function invalidateEcommerceCacheTags(\n container: CacheContainer | null | undefined,\n invalidation: EcommerceCacheInvalidation,\n): Promise<number> {\n const tags = Array.from(new Set(invalidation.tags.filter((tag) => tag.length > 0)))\n if (tags.length === 0) return 0\n const cache = resolveCacheService(container)\n if (!cache) return 0\n const tenantIds = invalidation.tenantId ? [null, invalidation.tenantId] : [null]\n let deleted = 0\n for (const tenantId of tenantIds) {\n deleted += await createTenantScopedAccess({ cache, tenantId, baseTags: [] }).deleteByTags(tags)\n }\n return deleted\n}\n\nexport function buyerContextCache(\n container: CacheContainer | null | undefined,\n scope: BuyerContextCacheScope,\n): BuyerContextCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: scope.tenantId,\n baseTags: [ecommerceStoreTag(scope.storeId)],\n })\n return {\n get: (customerUserId) => access.get<BuyerContext>(buildBuyerContextCacheKey(scope.storeId, customerUserId)),\n set: (buyer) =>\n access.set(\n buildBuyerContextCacheKey(scope.storeId, buyer.customerUserId),\n buyer,\n BUYER_CONTEXT_TTL_MS,\n buyerContextTags(buyer, scope.tenantId),\n ),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }