@open-mercato/core 0.10.1-develop.7773.1.a41b0f6c03 → 0.10.1-develop.7775.1.2a1f553826

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 (33) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/cart/lib/promotionContext.js +3 -2
  3. package/dist/modules/cart/lib/promotionContext.js.map +2 -2
  4. package/dist/modules/customers/ai-tools.js.map +2 -2
  5. package/dist/modules/promotions/lib/promotions-service.js +5 -1
  6. package/dist/modules/promotions/lib/promotions-service.js.map +2 -2
  7. package/dist/modules/promotions/services/promotion-cache.js +35 -2
  8. package/dist/modules/promotions/services/promotion-cache.js.map +3 -3
  9. package/dist/modules/sales/api/documents/factory.js +1 -1
  10. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  11. package/dist/modules/sales/api/orders/route.js +5 -4
  12. package/dist/modules/sales/api/orders/route.js.map +2 -2
  13. package/dist/modules/sales/api/quotes/route.js +5 -4
  14. package/dist/modules/sales/api/quotes/route.js.map +2 -2
  15. package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -1
  16. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  17. package/dist/modules/sales/components/documents/SalesDocumentExtensionSpot.js +26 -0
  18. package/dist/modules/sales/components/documents/SalesDocumentExtensionSpot.js.map +7 -0
  19. package/dist/modules/sales/extension-points.js +26 -0
  20. package/dist/modules/sales/extension-points.js.map +2 -2
  21. package/package.json +7 -7
  22. package/src/modules/cart/lib/promotionContext.ts +3 -7
  23. package/src/modules/catalog/AGENTS.md +2 -2
  24. package/src/modules/customers/AGENTS.md +2 -2
  25. package/src/modules/customers/ai-tools.ts +6 -9
  26. package/src/modules/promotions/lib/promotions-service.ts +5 -3
  27. package/src/modules/promotions/services/promotion-cache.ts +41 -4
  28. package/src/modules/sales/api/documents/factory.ts +1 -1
  29. package/src/modules/sales/api/orders/route.ts +4 -3
  30. package/src/modules/sales/api/quotes/route.ts +4 -3
  31. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +19 -1
  32. package/src/modules/sales/components/documents/SalesDocumentExtensionSpot.tsx +44 -0
  33. package/src/modules/sales/extension-points.ts +26 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/promotions/services/promotion-cache.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n Benefit,\n Promotion,\n Rule,\n RuleGroup,\n type PromotionBenefitAppliesTo,\n type PromotionRuleGroupOperator,\n} from '../data/entities'\nimport type {\n NormalizedBenefit,\n NormalizedPromotion,\n NormalizedRule,\n NormalizedRuleGroup,\n} from '../lib/types'\n\n// SPEC-055 \u00A7Cache Build Contract + \u00A7Normalized Promotion Shape. This file is the ONLY\n// place in the module that builds `NormalizedPromotion[]` from the database \u2014 the pure\n// evaluation pipeline (`lib/evaluation-engine.ts` and friends) never touches the\n// EntityManager, only ever receives the plain objects this file produces.\n\n// ---------------------------------------------------------------------------\n// Cache keys\n// ---------------------------------------------------------------------------\n\n// Literal format pinned by SPEC-055 \u00A7Evaluation Engine Data Flow pseudocode\n// (`key: promotions:active:{tenantId}:{organizationId}`). Key and tag share the same\n// value intentionally (one tree cache entry per tenant/org, mirrors\n// `inbox_ops/lib/cache.ts`'s COUNTS/SETTINGS keys) so invalidation never needs a\n// separate tag registry.\nexport function buildPromotionTreeCacheKey(tenantId: string, organizationId: string): string {\n return `promotions:active:${tenantId}:${organizationId}`\n}\n\n// Not specified verbatim by the spec (only \"a separate, small counters key\" is\n// required, \u00A7Cache Build Contract item 4) \u2014 scoped per-promotion, not per-tenant, so\n// a `usage.registered`/`usage.reverted` event for one promotion only ever evicts that\n// promotion's own counter, never the whole tenant's budget-capped set and never the\n// (expensive-to-rebuild) main tree cache.\nexport function buildPromotionUsageCounterCacheKey(\n tenantId: string,\n organizationId: string,\n promotionId: string,\n): string {\n return `promotions:usage-counter:${tenantId}:${organizationId}:${promotionId}`\n}\n\nconst PROMOTION_TREE_CACHE_TTL_MS = 5 * 60 * 1000\nconst PROMOTION_USAGE_COUNTER_CACHE_TTL_MS = 5 * 60 * 1000\n\nasync function readPromotionCacheEntry<T>(\n cache: CacheStrategy,\n tenantId: string,\n key: string,\n): Promise<T | null> {\n try {\n const value = await runWithCacheTenant(tenantId, () => cache.get(key))\n return value === null || value === undefined ? null : (value as T)\n } catch {\n return null\n }\n}\n\nasync function writePromotionCacheEntry(\n cache: CacheStrategy,\n tenantId: string,\n key: string,\n value: unknown,\n tags: string[],\n ttlMs: number,\n): Promise<void> {\n try {\n await runWithCacheTenant(tenantId, () => cache.set(key, value, { ttl: ttlMs, tags }))\n } catch {\n // Best-effort: a cache write failure must not fail the read it is backing.\n }\n}\n\n// The subscriber handlers below (and `subscribers/*.ts`, which just wire an event id to\n// one of those handlers) call these two directly \u2014 they never build the tag string\n// themselves, so the key format stays single-sourced here.\nexport async function invalidatePromotionTreeCache(\n cache: CacheStrategy | null | undefined,\n tenantId: string,\n organizationId: string,\n): Promise<void> {\n if (!cache) return\n const tag = buildPromotionTreeCacheKey(tenantId, organizationId)\n await runWithCacheTenant(tenantId, () => cache.deleteByTags([tag]))\n}\n\nexport async function invalidatePromotionUsageCounterCache(\n cache: CacheStrategy | null | undefined,\n tenantId: string,\n organizationId: string,\n promotionId: string,\n): Promise<void> {\n if (!cache) return\n const tag = buildPromotionUsageCounterCacheKey(tenantId, organizationId, promotionId)\n await runWithCacheTenant(tenantId, () => cache.deleteByTags([tag]))\n}\n\n// ---------------------------------------------------------------------------\n// Row shapes \u2014 the already-fetched, plain-object input to the pure normalization\n// below. Field names mirror `data/entities.ts` exactly (camelCase entity properties).\n// ---------------------------------------------------------------------------\n\nexport type PromotionCacheRow = {\n id: string\n name: string\n order: number\n cumulative: boolean\n tags: string[]\n excludedTags: string[]\n excludeFlags: Record<string, boolean>\n eligibleCurrencies: string[]\n startsAt: Date | null\n endsAt: Date | null\n maxBudget: string | null\n budgetCurrency: string | null\n}\n\nexport type RuleGroupCacheRow = {\n id: string\n promotionId: string\n parentRuleGroupId: string | null\n operator: PromotionRuleGroupOperator\n sortOrder: number\n}\n\nexport type RuleCacheRow = {\n ruleGroupId: string\n promotionId: string\n ruleType: string\n config: Record<string, unknown>\n sortOrder: number\n}\n\nexport type BenefitCacheRow = {\n ruleGroupId: string\n promotionId: string\n benefitType: string\n appliesTo: PromotionBenefitAppliesTo\n config: Record<string, unknown>\n sortOrder: number\n}\n\n// ---------------------------------------------------------------------------\n// Deep freeze (\u00A7Normalized Promotion Shape: \"In non-production builds, objects are\n// deep-frozen to detect accidental mutation\")\n// ---------------------------------------------------------------------------\n\nfunction deepFreeze<T>(value: T): T {\n if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {\n Object.freeze(value)\n for (const key of Object.getOwnPropertyNames(value)) {\n deepFreeze((value as Record<string, unknown>)[key])\n }\n }\n return value\n}\n\nfunction freezeOutsideProduction<T>(value: T): T {\n return process.env.NODE_ENV === 'production' ? value : deepFreeze(value)\n}\n\n// ---------------------------------------------------------------------------\n// Pure normalization / tree assembly \u2014 no EntityManager, no cache, fully testable\n// without a database connection. Extracted per SPEC-055 Step 2.5's test requirement.\n// ---------------------------------------------------------------------------\n\nfunction buildNormalizedRuleGroup(\n groupId: string,\n groupsForPromotion: RuleGroupCacheRow[],\n rulesForPromotion: RuleCacheRow[],\n benefitsForPromotion: BenefitCacheRow[],\n): NormalizedRuleGroup {\n const group = groupsForPromotion.find((candidate) => candidate.id === groupId)\n if (!group) {\n // \u00A7Cache Build Contract item 3: \"Any missing eager-loaded relation is treated as a\n // critical bug.\" A child id referenced by `parentRuleGroupId` that was not fetched\n // in the same batch is exactly that \u2014 never silently produce an incomplete tree.\n throw new Error(`[internal] promotion cache build is missing rule group ${groupId}`)\n }\n\n const rules: NormalizedRule[] = rulesForPromotion\n .filter((rule) => rule.ruleGroupId === groupId)\n .slice()\n .sort((a, b) => a.sortOrder - b.sortOrder)\n .map((rule) => ({ type: rule.ruleType, config: rule.config }))\n\n const benefits: NormalizedBenefit[] = benefitsForPromotion\n .filter((benefit) => benefit.ruleGroupId === groupId)\n .slice()\n .sort((a, b) => a.sortOrder - b.sortOrder)\n .map((benefit) => ({ type: benefit.benefitType, appliesTo: benefit.appliesTo, config: benefit.config }))\n\n const children: NormalizedRuleGroup[] = groupsForPromotion\n .filter((candidate) => candidate.parentRuleGroupId === groupId)\n .slice()\n .sort((a, b) => a.sortOrder - b.sortOrder)\n .map((child) => buildNormalizedRuleGroup(child.id, groupsForPromotion, rulesForPromotion, benefitsForPromotion))\n\n return { operator: group.operator, rules, benefits, children }\n}\n\n/**\n * Pure tree assembly for a single promotion. Accepts the full fetched batch for every\n * rule-group/rule/benefit row (not pre-filtered per promotion) and narrows internally by\n * `promotionId` \u2014 the real service fetches these arrays once for a whole batch of active\n * promotions (\u00A7Cache Build Contract item 1: \"a single cache build performs all required\n * database reads\", not N+1 per promotion) and calls this function once per promotion.\n *\n * `totalDiscountGranted` is passed in already resolved \u2014 this function never reads\n * `promotion_usages` itself (\u00A7Cache Build Contract item 4: the counter lives in a\n * separate, small cache key the caller resolves independently).\n */\nexport function buildNormalizedPromotion(\n promotionRow: PromotionCacheRow,\n ruleGroupRows: RuleGroupCacheRow[],\n ruleRows: RuleCacheRow[],\n benefitRows: BenefitCacheRow[],\n totalDiscountGranted: string,\n): NormalizedPromotion {\n const groupsForPromotion = ruleGroupRows.filter((group) => group.promotionId === promotionRow.id)\n const rulesForPromotion = ruleRows.filter((rule) => rule.promotionId === promotionRow.id)\n const benefitsForPromotion = benefitRows.filter((benefit) => benefit.promotionId === promotionRow.id)\n\n const root = groupsForPromotion.find((group) => group.parentRuleGroupId === null)\n if (!root) {\n throw new Error(\n `[internal] promotion cache build is missing the root rule group for promotion ${promotionRow.id}`,\n )\n }\n\n // \u00A7Cache Build Contract item 3: a rule/benefit whose owning `ruleGroupId` was not\n // among the batch-fetched groups is exactly the \"missing eager-loaded relation\"\n // scenario \u2014 never silently drop it from the tree.\n const groupIdsForPromotion = new Set(groupsForPromotion.map((group) => group.id))\n for (const rule of rulesForPromotion) {\n if (!groupIdsForPromotion.has(rule.ruleGroupId)) {\n throw new Error(\n `[internal] promotion cache build is missing rule group ${rule.ruleGroupId} referenced by a rule for promotion ${promotionRow.id}`,\n )\n }\n }\n for (const benefit of benefitsForPromotion) {\n if (!groupIdsForPromotion.has(benefit.ruleGroupId)) {\n throw new Error(\n `[internal] promotion cache build is missing rule group ${benefit.ruleGroupId} referenced by a benefit for promotion ${promotionRow.id}`,\n )\n }\n }\n\n const normalized: NormalizedPromotion = {\n id: promotionRow.id,\n name: promotionRow.name,\n order: promotionRow.order,\n cumulative: promotionRow.cumulative,\n tags: [...promotionRow.tags],\n excludedTags: [...promotionRow.excludedTags],\n excludeFlags: { ...promotionRow.excludeFlags },\n eligibleCurrencies: [...promotionRow.eligibleCurrencies],\n startsAt: promotionRow.startsAt ? promotionRow.startsAt.toISOString() : null,\n endsAt: promotionRow.endsAt ? promotionRow.endsAt.toISOString() : null,\n maxBudget: promotionRow.maxBudget,\n budgetCurrency: promotionRow.budgetCurrency,\n totalDiscountGranted,\n rootGroup: buildNormalizedRuleGroup(root.id, groupsForPromotion, rulesForPromotion, benefitsForPromotion),\n }\n\n return freezeOutsideProduction(normalized)\n}\n\n// ---------------------------------------------------------------------------\n// Cache-backed service \u2014 the only place in this module that ever queries the\n// database for the evaluation engine's input (\u00A7Cache Build Contract item 2).\n// ---------------------------------------------------------------------------\n\nexport type PromotionCacheContainer = {\n resolve: <T = unknown>(name: string) => T\n}\n\nfunction resolvePromotionCacheStrategy(container: PromotionCacheContainer): CacheStrategy | null {\n try {\n return container.resolve<CacheStrategy>('cache') ?? null\n } catch {\n return null\n }\n}\n\nfunction resolvePromotionEm(container: PromotionCacheContainer): EntityManager {\n // Exact precedent: `commands/promotions.ts` resolves/forks the same way \u2014\n // `(ctx.container.resolve('em') as EntityManager).fork()` \u2014 before any read/write.\n return (container.resolve<EntityManager>('em')).fork()\n}\n\nexport class PromotionCacheService {\n constructor(private readonly container: PromotionCacheContainer) {}\n\n async getActivePromotions(tenantId: string, organizationId: string): Promise<NormalizedPromotion[]> {\n const cache = resolvePromotionCacheStrategy(this.container)\n const treeKey = buildPromotionTreeCacheKey(tenantId, organizationId)\n\n let promotions = cache ? await readPromotionCacheEntry<NormalizedPromotion[]>(cache, tenantId, treeKey) : null\n if (!promotions) {\n promotions = await this.buildActivePromotionTrees(tenantId, organizationId)\n if (cache) {\n await writePromotionCacheEntry(cache, tenantId, treeKey, promotions, [treeKey], PROMOTION_TREE_CACHE_TTL_MS)\n }\n }\n\n return Promise.all(\n promotions.map((promotion) => this.overlayUsageCounter(promotion, tenantId, organizationId)),\n )\n }\n\n // \u00A7Cache Build Contract item 4: \"read only for budget-capped promotions\" \u2014 an\n // uncapped promotion's `totalDiscountGranted` is never read back from\n // `promotion_usages`, it stays the inert '0' baked into the tree cache.\n private async overlayUsageCounter(\n promotion: NormalizedPromotion,\n tenantId: string,\n organizationId: string,\n ): Promise<NormalizedPromotion> {\n if (promotion.maxBudget === null) return promotion\n const totalDiscountGranted = await this.getUsageCounter(\n promotion.id,\n tenantId,\n organizationId,\n promotion.budgetCurrency,\n )\n return freezeOutsideProduction({ ...promotion, totalDiscountGranted })\n }\n\n private async getUsageCounter(\n promotionId: string,\n tenantId: string,\n organizationId: string,\n budgetCurrency: string | null,\n ): Promise<string> {\n const cache = resolvePromotionCacheStrategy(this.container)\n const key = buildPromotionUsageCounterCacheKey(tenantId, organizationId, promotionId)\n\n if (cache) {\n const cached = await readPromotionCacheEntry<string>(cache, tenantId, key)\n if (cached !== null) return cached\n }\n\n const total = await this.sumPromotionUsage(promotionId, tenantId, organizationId, budgetCurrency)\n\n if (cache) {\n await writePromotionCacheEntry(cache, tenantId, key, total, [key], PROMOTION_USAGE_COUNTER_CACHE_TTL_MS)\n }\n\n return total\n }\n\n // \u00A7Cache Build Contract item 1 / \u00A7Promotion Usage Lifecycle: `SUM(total_discount_amount)\n // WHERE reverted_at IS NULL AND currency = budget_currency` \u2014 the same filter\n // `registerUsage`'s own re-read-sum uses.\n private async sumPromotionUsage(\n promotionId: string,\n tenantId: string,\n organizationId: string,\n budgetCurrency: string | null,\n ): Promise<string> {\n if (!budgetCurrency) return '0'\n const em = resolvePromotionEm(this.container)\n const rows = (await em.getConnection().execute(\n `SELECT COALESCE(SUM(total_discount_amount), 0)::text AS total\n FROM promotion_usages\n WHERE promotion_id = ? AND organization_id = ? AND tenant_id = ? AND currency = ? AND reverted_at IS NULL`,\n [promotionId, organizationId, tenantId, budgetCurrency],\n )) as Array<{ total: string }>\n return rows[0]?.total ?? '0'\n }\n\n // \u00A7Cache Build Contract item 1: one query pass for the whole batch of active\n // promotions \u2014 every `RuleGroup`/`Rule`/`Benefit` row for them is fetched in three\n // fixed queries (run in parallel), never per-promotion.\n private async buildActivePromotionTrees(\n tenantId: string,\n organizationId: string,\n ): Promise<NormalizedPromotion[]> {\n const em = resolvePromotionEm(this.container)\n\n // \u00A7Cache Build Contract (rev R-H3b/P8): every `active = true` promotion, regardless\n // of `starts_at`/`ends_at` \u2014 date-window filtering happens only inside the evaluator\n // (`lib/evaluation-engine.ts`'s `isPromotionEligible`), never here.\n const promotionEntities = await em.find(\n Promotion,\n { tenantId, organizationId, active: true, deletedAt: null },\n // `lib/evaluation-engine.ts` documents that the cache is responsible for ordering\n // by `order ASC, id ASC` before evaluation ever sees the array.\n { orderBy: { order: 'asc', id: 'asc' } },\n )\n if (promotionEntities.length === 0) return []\n\n const promotionIds = promotionEntities.map((promotion) => promotion.id)\n\n const [ruleGroupEntities, ruleEntities, benefitEntities] = await Promise.all([\n em.find(RuleGroup, { tenantId, organizationId, promotionId: { $in: promotionIds } }),\n em.find(Rule, { tenantId, organizationId, promotionId: { $in: promotionIds } }),\n em.find(Benefit, { tenantId, organizationId, promotionId: { $in: promotionIds } }),\n ])\n\n const ruleGroupRows: RuleGroupCacheRow[] = ruleGroupEntities.map((group) => ({\n id: group.id,\n promotionId: group.promotionId,\n parentRuleGroupId: group.parentRuleGroupId ?? null,\n operator: group.operator,\n sortOrder: group.sortOrder,\n }))\n const ruleRows: RuleCacheRow[] = ruleEntities.map((rule) => ({\n ruleGroupId: rule.ruleGroupId,\n promotionId: rule.promotionId,\n ruleType: rule.ruleType,\n config: rule.config,\n sortOrder: rule.sortOrder,\n }))\n const benefitRows: BenefitCacheRow[] = benefitEntities.map((benefit) => ({\n ruleGroupId: benefit.ruleGroupId,\n promotionId: benefit.promotionId,\n benefitType: benefit.benefitType,\n appliesTo: benefit.appliesTo,\n config: benefit.config,\n sortOrder: benefit.sortOrder,\n }))\n\n return promotionEntities.map((promotion) => {\n const row: PromotionCacheRow = {\n id: promotion.id,\n name: promotion.name,\n order: promotion.order,\n cumulative: promotion.cumulative,\n tags: promotion.tags,\n excludedTags: promotion.excludedTags,\n excludeFlags: promotion.excludeFlags,\n eligibleCurrencies: promotion.eligibleCurrencies,\n startsAt: promotion.startsAt ?? null,\n endsAt: promotion.endsAt ?? null,\n maxBudget: promotion.maxBudget ?? null,\n budgetCurrency: promotion.budgetCurrency ?? null,\n }\n // Placeholder \u2014 `getActivePromotions` overlays the real snapshot for\n // budget-capped promotions from the separate counters cache key right after this\n // method returns (\u00A7Cache Build Contract item 4). Never read directly from the\n // tree cache for a budget decision.\n return buildNormalizedPromotion(row, ruleGroupRows, ruleRows, benefitRows, '0')\n })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Subscriber glue (`subscribers/*.ts` \u2014 ephemeral cache-invalidation handlers).\n// Kept here, not in `subscribers/` itself: every file under a module's `subscribers/`\n// folder is auto-discovered as a subscriber definition by `yarn generate` (it is scanned\n// unconditionally, with no `metadata` export treated as a critical bug further down the\n// pipeline, not as \"skip this file\") \u2014 mirrors `ecommerce/lib/cacheInvalidation.ts`\n// shared by that module's own `subscribers/*-cache-invalidation.ts` files.\n// ---------------------------------------------------------------------------\n\nexport type PromotionsSubscriberContext = {\n resolve: <T = unknown>(name: string) => T\n tenantId?: string | null\n organizationId?: string | null\n}\n\nconst subscriberLogger = createLogger('promotions').child({ component: 'cache' })\n\nfunction readSubscriberEventScope(\n payload: unknown,\n ctx: PromotionsSubscriberContext,\n): { tenantId: string | null; organizationId: string | null } {\n const data = (payload ?? {}) as Record<string, unknown>\n const tenantId =\n typeof data.tenantId === 'string' && data.tenantId.length > 0 ? data.tenantId : ctx.tenantId ?? null\n const organizationId =\n typeof data.organizationId === 'string' && data.organizationId.length > 0\n ? data.organizationId\n : ctx.organizationId ?? null\n return { tenantId, organizationId }\n}\n\nfunction readPromotionIdFromUsagePayload(payload: unknown): string | null {\n const data = (payload ?? {}) as Record<string, unknown>\n return typeof data.promotionId === 'string' && data.promotionId.length > 0 ? data.promotionId : null\n}\n\nfunction resolveSubscriberCache(ctx: PromotionsSubscriberContext): CacheStrategy | null {\n try {\n return ctx.resolve<CacheStrategy>('cache') ?? null\n } catch {\n return null\n }\n}\n\n// \u00A7Cache Build Contract item 5: the main tree cache is invalidated ONLY by promotion/\n// code mutation events (`promotions.promotion.created|updated|deleted`,\n// `promotions.code.created|updated|deleted`) \u2014 never by a usage event. Wired to those\n// six exact event ids (not a `promotions.promotion.*`/`promotions.code.*` wildcard) so\n// the lifecycle events declared alongside them in `events.ts`\n// (`promotions.promotion.budget_exhausted`/`.expiring_soon`, `promotions.code.used`/\n// `.exhausted`) never trigger a rebuild they were not asked to.\nexport async function handlePromotionTreeCacheInvalidation(\n payload: unknown,\n ctx: PromotionsSubscriberContext,\n): Promise<void> {\n const { tenantId, organizationId } = readSubscriberEventScope(payload, ctx)\n if (!tenantId || !organizationId) return\n const cache = resolveSubscriberCache(ctx)\n if (!cache) return\n try {\n await invalidatePromotionTreeCache(cache, tenantId, organizationId)\n } catch (err) {\n subscriberLogger.warn('[internal] promotion tree cache invalidation failed', { tenantId, organizationId, err })\n }\n}\n\n// \u00A7Cache Build Contract item 4/5: `promotions.usage.registered`/`.reverted` evict only\n// the one promotion's small counters-key entry, never the main tree cache.\nexport async function handlePromotionUsageCounterCacheInvalidation(\n payload: unknown,\n ctx: PromotionsSubscriberContext,\n): Promise<void> {\n const { tenantId, organizationId } = readSubscriberEventScope(payload, ctx)\n const promotionId = readPromotionIdFromUsagePayload(payload)\n if (!tenantId || !organizationId || !promotionId) return\n const cache = resolveSubscriberCache(ctx)\n if (!cache) return\n try {\n await invalidatePromotionUsageCounterCache(cache, tenantId, organizationId, promotionId)\n } catch (err) {\n subscriberLogger.warn('[internal] promotion usage counter cache invalidation failed', {\n tenantId,\n organizationId,\n promotionId,\n err,\n })\n }\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import { TransactionContext } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n Benefit,\n Promotion,\n Rule,\n RuleGroup,\n type PromotionBenefitAppliesTo,\n type PromotionRuleGroupOperator,\n} from '../data/entities'\nimport type {\n NormalizedBenefit,\n NormalizedPromotion,\n NormalizedRule,\n NormalizedRuleGroup,\n} from '../lib/types'\n\n// SPEC-055 \u00A7Cache Build Contract + \u00A7Normalized Promotion Shape. This file is the ONLY\n// place in the module that builds `NormalizedPromotion[]` from the database \u2014 the pure\n// evaluation pipeline (`lib/evaluation-engine.ts` and friends) never touches the\n// EntityManager, only ever receives the plain objects this file produces.\n\n// ---------------------------------------------------------------------------\n// Cache keys\n// ---------------------------------------------------------------------------\n\n// Literal format pinned by SPEC-055 \u00A7Evaluation Engine Data Flow pseudocode\n// (`key: promotions:active:{tenantId}:{organizationId}`). Key and tag share the same\n// value intentionally (one tree cache entry per tenant/org, mirrors\n// `inbox_ops/lib/cache.ts`'s COUNTS/SETTINGS keys) so invalidation never needs a\n// separate tag registry.\nexport function buildPromotionTreeCacheKey(tenantId: string, organizationId: string): string {\n return `promotions:active:${tenantId}:${organizationId}`\n}\n\n// Not specified verbatim by the spec (only \"a separate, small counters key\" is\n// required, \u00A7Cache Build Contract item 4) \u2014 scoped per-promotion, not per-tenant, so\n// a `usage.registered`/`usage.reverted` event for one promotion only ever evicts that\n// promotion's own counter, never the whole tenant's budget-capped set and never the\n// (expensive-to-rebuild) main tree cache.\nexport function buildPromotionUsageCounterCacheKey(\n tenantId: string,\n organizationId: string,\n promotionId: string,\n): string {\n return `promotions:usage-counter:${tenantId}:${organizationId}:${promotionId}`\n}\n\nconst PROMOTION_TREE_CACHE_TTL_MS = 5 * 60 * 1000\nconst PROMOTION_USAGE_COUNTER_CACHE_TTL_MS = 5 * 60 * 1000\n\nasync function readPromotionCacheEntry<T>(\n cache: CacheStrategy,\n tenantId: string,\n key: string,\n): Promise<T | null> {\n try {\n const value = await runWithCacheTenant(tenantId, () => cache.get(key))\n return value === null || value === undefined ? null : (value as T)\n } catch {\n return null\n }\n}\n\nasync function writePromotionCacheEntry(\n cache: CacheStrategy,\n tenantId: string,\n key: string,\n value: unknown,\n tags: string[],\n ttlMs: number,\n): Promise<void> {\n try {\n await runWithCacheTenant(tenantId, () => cache.set(key, value, { ttl: ttlMs, tags }))\n } catch {\n // Best-effort: a cache write failure must not fail the read it is backing.\n }\n}\n\n// The subscriber handlers below (and `subscribers/*.ts`, which just wire an event id to\n// one of those handlers) call these two directly \u2014 they never build the tag string\n// themselves, so the key format stays single-sourced here.\nexport async function invalidatePromotionTreeCache(\n cache: CacheStrategy | null | undefined,\n tenantId: string,\n organizationId: string,\n): Promise<void> {\n if (!cache) return\n const tag = buildPromotionTreeCacheKey(tenantId, organizationId)\n await runWithCacheTenant(tenantId, () => cache.deleteByTags([tag]))\n}\n\nexport async function invalidatePromotionUsageCounterCache(\n cache: CacheStrategy | null | undefined,\n tenantId: string,\n organizationId: string,\n promotionId: string,\n): Promise<void> {\n if (!cache) return\n const tag = buildPromotionUsageCounterCacheKey(tenantId, organizationId, promotionId)\n await runWithCacheTenant(tenantId, () => cache.deleteByTags([tag]))\n}\n\n// ---------------------------------------------------------------------------\n// Row shapes \u2014 the already-fetched, plain-object input to the pure normalization\n// below. Field names mirror `data/entities.ts` exactly (camelCase entity properties).\n// ---------------------------------------------------------------------------\n\nexport type PromotionCacheRow = {\n id: string\n name: string\n order: number\n cumulative: boolean\n tags: string[]\n excludedTags: string[]\n excludeFlags: Record<string, boolean>\n eligibleCurrencies: string[]\n startsAt: Date | null\n endsAt: Date | null\n maxBudget: string | null\n budgetCurrency: string | null\n}\n\nexport type RuleGroupCacheRow = {\n id: string\n promotionId: string\n parentRuleGroupId: string | null\n operator: PromotionRuleGroupOperator\n sortOrder: number\n}\n\nexport type RuleCacheRow = {\n ruleGroupId: string\n promotionId: string\n ruleType: string\n config: Record<string, unknown>\n sortOrder: number\n}\n\nexport type BenefitCacheRow = {\n ruleGroupId: string\n promotionId: string\n benefitType: string\n appliesTo: PromotionBenefitAppliesTo\n config: Record<string, unknown>\n sortOrder: number\n}\n\n// ---------------------------------------------------------------------------\n// Deep freeze (\u00A7Normalized Promotion Shape: \"In non-production builds, objects are\n// deep-frozen to detect accidental mutation\")\n// ---------------------------------------------------------------------------\n\nfunction deepFreeze<T>(value: T): T {\n if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {\n Object.freeze(value)\n for (const key of Object.getOwnPropertyNames(value)) {\n deepFreeze((value as Record<string, unknown>)[key])\n }\n }\n return value\n}\n\nfunction freezeOutsideProduction<T>(value: T): T {\n return process.env.NODE_ENV === 'production' ? value : deepFreeze(value)\n}\n\n// ---------------------------------------------------------------------------\n// Pure normalization / tree assembly \u2014 no EntityManager, no cache, fully testable\n// without a database connection. Extracted per SPEC-055 Step 2.5's test requirement.\n// ---------------------------------------------------------------------------\n\nfunction buildNormalizedRuleGroup(\n groupId: string,\n groupsForPromotion: RuleGroupCacheRow[],\n rulesForPromotion: RuleCacheRow[],\n benefitsForPromotion: BenefitCacheRow[],\n): NormalizedRuleGroup {\n const group = groupsForPromotion.find((candidate) => candidate.id === groupId)\n if (!group) {\n // \u00A7Cache Build Contract item 3: \"Any missing eager-loaded relation is treated as a\n // critical bug.\" A child id referenced by `parentRuleGroupId` that was not fetched\n // in the same batch is exactly that \u2014 never silently produce an incomplete tree.\n throw new Error(`[internal] promotion cache build is missing rule group ${groupId}`)\n }\n\n const rules: NormalizedRule[] = rulesForPromotion\n .filter((rule) => rule.ruleGroupId === groupId)\n .slice()\n .sort((a, b) => a.sortOrder - b.sortOrder)\n .map((rule) => ({ type: rule.ruleType, config: rule.config }))\n\n const benefits: NormalizedBenefit[] = benefitsForPromotion\n .filter((benefit) => benefit.ruleGroupId === groupId)\n .slice()\n .sort((a, b) => a.sortOrder - b.sortOrder)\n .map((benefit) => ({ type: benefit.benefitType, appliesTo: benefit.appliesTo, config: benefit.config }))\n\n const children: NormalizedRuleGroup[] = groupsForPromotion\n .filter((candidate) => candidate.parentRuleGroupId === groupId)\n .slice()\n .sort((a, b) => a.sortOrder - b.sortOrder)\n .map((child) => buildNormalizedRuleGroup(child.id, groupsForPromotion, rulesForPromotion, benefitsForPromotion))\n\n return { operator: group.operator, rules, benefits, children }\n}\n\n/**\n * Pure tree assembly for a single promotion. Accepts the full fetched batch for every\n * rule-group/rule/benefit row (not pre-filtered per promotion) and narrows internally by\n * `promotionId` \u2014 the real service fetches these arrays once for a whole batch of active\n * promotions (\u00A7Cache Build Contract item 1: \"a single cache build performs all required\n * database reads\", not N+1 per promotion) and calls this function once per promotion.\n *\n * `totalDiscountGranted` is passed in already resolved \u2014 this function never reads\n * `promotion_usages` itself (\u00A7Cache Build Contract item 4: the counter lives in a\n * separate, small cache key the caller resolves independently).\n */\nexport function buildNormalizedPromotion(\n promotionRow: PromotionCacheRow,\n ruleGroupRows: RuleGroupCacheRow[],\n ruleRows: RuleCacheRow[],\n benefitRows: BenefitCacheRow[],\n totalDiscountGranted: string,\n): NormalizedPromotion {\n const groupsForPromotion = ruleGroupRows.filter((group) => group.promotionId === promotionRow.id)\n const rulesForPromotion = ruleRows.filter((rule) => rule.promotionId === promotionRow.id)\n const benefitsForPromotion = benefitRows.filter((benefit) => benefit.promotionId === promotionRow.id)\n\n const root = groupsForPromotion.find((group) => group.parentRuleGroupId === null)\n if (!root) {\n throw new Error(\n `[internal] promotion cache build is missing the root rule group for promotion ${promotionRow.id}`,\n )\n }\n\n // \u00A7Cache Build Contract item 3: a rule/benefit whose owning `ruleGroupId` was not\n // among the batch-fetched groups is exactly the \"missing eager-loaded relation\"\n // scenario \u2014 never silently drop it from the tree.\n const groupIdsForPromotion = new Set(groupsForPromotion.map((group) => group.id))\n for (const rule of rulesForPromotion) {\n if (!groupIdsForPromotion.has(rule.ruleGroupId)) {\n throw new Error(\n `[internal] promotion cache build is missing rule group ${rule.ruleGroupId} referenced by a rule for promotion ${promotionRow.id}`,\n )\n }\n }\n for (const benefit of benefitsForPromotion) {\n if (!groupIdsForPromotion.has(benefit.ruleGroupId)) {\n throw new Error(\n `[internal] promotion cache build is missing rule group ${benefit.ruleGroupId} referenced by a benefit for promotion ${promotionRow.id}`,\n )\n }\n }\n\n const normalized: NormalizedPromotion = {\n id: promotionRow.id,\n name: promotionRow.name,\n order: promotionRow.order,\n cumulative: promotionRow.cumulative,\n tags: [...promotionRow.tags],\n excludedTags: [...promotionRow.excludedTags],\n excludeFlags: { ...promotionRow.excludeFlags },\n eligibleCurrencies: [...promotionRow.eligibleCurrencies],\n startsAt: promotionRow.startsAt ? promotionRow.startsAt.toISOString() : null,\n endsAt: promotionRow.endsAt ? promotionRow.endsAt.toISOString() : null,\n maxBudget: promotionRow.maxBudget,\n budgetCurrency: promotionRow.budgetCurrency,\n totalDiscountGranted,\n rootGroup: buildNormalizedRuleGroup(root.id, groupsForPromotion, rulesForPromotion, benefitsForPromotion),\n }\n\n return freezeOutsideProduction(normalized)\n}\n\n// ---------------------------------------------------------------------------\n// Cache-backed service \u2014 the only place in this module that ever queries the\n// database for the evaluation engine's input (\u00A7Cache Build Contract item 2).\n// ---------------------------------------------------------------------------\n\nexport type PromotionCacheContainer = {\n resolve: <T = unknown>(name: string) => T\n}\n\nfunction resolvePromotionCacheStrategy(container: PromotionCacheContainer): CacheStrategy | null {\n if (resolvePromotionEm(container).isInTransaction()) return null\n try {\n return container.resolve<CacheStrategy>('cache') ?? null\n } catch {\n return null\n }\n}\n\nfunction resolvePromotionEm(container: PromotionCacheContainer): EntityManager {\n const registeredEm = container.resolve<EntityManager>('em')\n const contextualEm = TransactionContext.getEntityManager(registeredEm.name) as EntityManager | undefined\n const em = contextualEm && contextualEm.getDriver() === registeredEm.getDriver() ? contextualEm : registeredEm\n return em.fork({ keepTransactionContext: true, disableContextResolution: Boolean(contextualEm && contextualEm !== em) })\n}\n\nexport class PromotionCacheService {\n constructor(private readonly container: PromotionCacheContainer) {}\n\n async getActivePromotions(tenantId: string, organizationId: string): Promise<NormalizedPromotion[]> {\n const em = resolvePromotionEm(this.container)\n if (em.isInTransaction()) {\n const promotions = await this.buildActivePromotionTrees(tenantId, organizationId)\n return this.buildTransactionalUsageSnapshot(em, promotions, tenantId, organizationId)\n }\n const cache = resolvePromotionCacheStrategy(this.container)\n const treeKey = buildPromotionTreeCacheKey(tenantId, organizationId)\n\n let promotions = cache ? await readPromotionCacheEntry<NormalizedPromotion[]>(cache, tenantId, treeKey) : null\n if (!promotions) {\n promotions = await this.buildActivePromotionTrees(tenantId, organizationId)\n if (cache) {\n await writePromotionCacheEntry(cache, tenantId, treeKey, promotions, [treeKey], PROMOTION_TREE_CACHE_TTL_MS)\n }\n }\n\n return Promise.all(\n promotions.map((promotion) => this.overlayUsageCounter(promotion, tenantId, organizationId)),\n )\n }\n\n private async buildTransactionalUsageSnapshot(\n em: EntityManager,\n promotions: NormalizedPromotion[],\n tenantId: string,\n organizationId: string,\n ): Promise<NormalizedPromotion[]> {\n const budgeted = promotions.filter((promotion) => promotion.maxBudget !== null)\n const currencies = [...new Set(budgeted.map((promotion) => promotion.budgetCurrency).filter((currency): currency is string => !!currency))]\n const totals = new Map<string, string>()\n if (budgeted.length > 0 && currencies.length > 0) {\n const promotionIds = budgeted.map((promotion) => promotion.id)\n const promotionSlots = promotionIds.map(() => '?').join(', ')\n const currencySlots = currencies.map(() => '?').join(', ')\n const rows = await em.execute<Array<{ promotion_id: string; currency: string; total: string }>>(\n `SELECT promotion_id, currency, COALESCE(SUM(total_discount_amount), 0)::text AS total\n FROM promotion_usages\n WHERE promotion_id IN (${promotionSlots}) AND currency IN (${currencySlots})\n AND organization_id = ? AND tenant_id = ? AND reverted_at IS NULL\n GROUP BY promotion_id, currency`,\n [...promotionIds, ...currencies, organizationId, tenantId],\n )\n for (const row of rows) totals.set(`${row.promotion_id}:${row.currency}`, row.total)\n }\n return promotions.map((promotion) => promotion.maxBudget === null ? promotion : freezeOutsideProduction({\n ...promotion,\n totalDiscountGranted: totals.get(`${promotion.id}:${promotion.budgetCurrency}`) ?? '0',\n }))\n }\n\n // \u00A7Cache Build Contract item 4: \"read only for budget-capped promotions\" \u2014 an\n // uncapped promotion's `totalDiscountGranted` is never read back from\n // `promotion_usages`, it stays the inert '0' baked into the tree cache.\n private async overlayUsageCounter(\n promotion: NormalizedPromotion,\n tenantId: string,\n organizationId: string,\n ): Promise<NormalizedPromotion> {\n if (promotion.maxBudget === null) return promotion\n const totalDiscountGranted = await this.getUsageCounter(\n promotion.id,\n tenantId,\n organizationId,\n promotion.budgetCurrency,\n )\n return freezeOutsideProduction({ ...promotion, totalDiscountGranted })\n }\n\n private async getUsageCounter(\n promotionId: string,\n tenantId: string,\n organizationId: string,\n budgetCurrency: string | null,\n ): Promise<string> {\n const cache = resolvePromotionCacheStrategy(this.container)\n const key = buildPromotionUsageCounterCacheKey(tenantId, organizationId, promotionId)\n\n if (cache) {\n const cached = await readPromotionCacheEntry<string>(cache, tenantId, key)\n if (cached !== null) return cached\n }\n\n const total = await this.sumPromotionUsage(promotionId, tenantId, organizationId, budgetCurrency)\n\n if (cache) {\n await writePromotionCacheEntry(cache, tenantId, key, total, [key], PROMOTION_USAGE_COUNTER_CACHE_TTL_MS)\n }\n\n return total\n }\n\n // \u00A7Cache Build Contract item 1 / \u00A7Promotion Usage Lifecycle: `SUM(total_discount_amount)\n // WHERE reverted_at IS NULL AND currency = budget_currency` \u2014 the same filter\n // `registerUsage`'s own re-read-sum uses.\n private async sumPromotionUsage(\n promotionId: string,\n tenantId: string,\n organizationId: string,\n budgetCurrency: string | null,\n ): Promise<string> {\n if (!budgetCurrency) return '0'\n const em = resolvePromotionEm(this.container)\n const rows = (await em.execute(\n `SELECT COALESCE(SUM(total_discount_amount), 0)::text AS total\n FROM promotion_usages\n WHERE promotion_id = ? AND organization_id = ? AND tenant_id = ? AND currency = ? AND reverted_at IS NULL`,\n [promotionId, organizationId, tenantId, budgetCurrency],\n )) as Array<{ total: string }>\n return rows[0]?.total ?? '0'\n }\n\n // \u00A7Cache Build Contract item 1: one query pass for the whole batch of active\n // promotions \u2014 every `RuleGroup`/`Rule`/`Benefit` row for them is fetched in three\n // fixed queries (run in parallel), never per-promotion.\n private async buildActivePromotionTrees(\n tenantId: string,\n organizationId: string,\n ): Promise<NormalizedPromotion[]> {\n const em = resolvePromotionEm(this.container)\n\n // \u00A7Cache Build Contract (rev R-H3b/P8): every `active = true` promotion, regardless\n // of `starts_at`/`ends_at` \u2014 date-window filtering happens only inside the evaluator\n // (`lib/evaluation-engine.ts`'s `isPromotionEligible`), never here.\n const promotionEntities = await em.find(\n Promotion,\n { tenantId, organizationId, active: true, deletedAt: null },\n // `lib/evaluation-engine.ts` documents that the cache is responsible for ordering\n // by `order ASC, id ASC` before evaluation ever sees the array.\n { orderBy: { order: 'asc', id: 'asc' } },\n )\n if (promotionEntities.length === 0) return []\n\n const promotionIds = promotionEntities.map((promotion) => promotion.id)\n\n const [ruleGroupEntities, ruleEntities, benefitEntities] = await Promise.all([\n em.find(RuleGroup, { tenantId, organizationId, promotionId: { $in: promotionIds } }),\n em.find(Rule, { tenantId, organizationId, promotionId: { $in: promotionIds } }),\n em.find(Benefit, { tenantId, organizationId, promotionId: { $in: promotionIds } }),\n ])\n\n const ruleGroupRows: RuleGroupCacheRow[] = ruleGroupEntities.map((group) => ({\n id: group.id,\n promotionId: group.promotionId,\n parentRuleGroupId: group.parentRuleGroupId ?? null,\n operator: group.operator,\n sortOrder: group.sortOrder,\n }))\n const ruleRows: RuleCacheRow[] = ruleEntities.map((rule) => ({\n ruleGroupId: rule.ruleGroupId,\n promotionId: rule.promotionId,\n ruleType: rule.ruleType,\n config: rule.config,\n sortOrder: rule.sortOrder,\n }))\n const benefitRows: BenefitCacheRow[] = benefitEntities.map((benefit) => ({\n ruleGroupId: benefit.ruleGroupId,\n promotionId: benefit.promotionId,\n benefitType: benefit.benefitType,\n appliesTo: benefit.appliesTo,\n config: benefit.config,\n sortOrder: benefit.sortOrder,\n }))\n\n return promotionEntities.map((promotion) => {\n const row: PromotionCacheRow = {\n id: promotion.id,\n name: promotion.name,\n order: promotion.order,\n cumulative: promotion.cumulative,\n tags: promotion.tags,\n excludedTags: promotion.excludedTags,\n excludeFlags: promotion.excludeFlags,\n eligibleCurrencies: promotion.eligibleCurrencies,\n startsAt: promotion.startsAt ?? null,\n endsAt: promotion.endsAt ?? null,\n maxBudget: promotion.maxBudget ?? null,\n budgetCurrency: promotion.budgetCurrency ?? null,\n }\n // Placeholder \u2014 `getActivePromotions` overlays the real snapshot for\n // budget-capped promotions from the separate counters cache key right after this\n // method returns (\u00A7Cache Build Contract item 4). Never read directly from the\n // tree cache for a budget decision.\n return buildNormalizedPromotion(row, ruleGroupRows, ruleRows, benefitRows, '0')\n })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Subscriber glue (`subscribers/*.ts` \u2014 ephemeral cache-invalidation handlers).\n// Kept here, not in `subscribers/` itself: every file under a module's `subscribers/`\n// folder is auto-discovered as a subscriber definition by `yarn generate` (it is scanned\n// unconditionally, with no `metadata` export treated as a critical bug further down the\n// pipeline, not as \"skip this file\") \u2014 mirrors `ecommerce/lib/cacheInvalidation.ts`\n// shared by that module's own `subscribers/*-cache-invalidation.ts` files.\n// ---------------------------------------------------------------------------\n\nexport type PromotionsSubscriberContext = {\n resolve: <T = unknown>(name: string) => T\n tenantId?: string | null\n organizationId?: string | null\n}\n\nconst subscriberLogger = createLogger('promotions').child({ component: 'cache' })\n\nfunction readSubscriberEventScope(\n payload: unknown,\n ctx: PromotionsSubscriberContext,\n): { tenantId: string | null; organizationId: string | null } {\n const data = (payload ?? {}) as Record<string, unknown>\n const tenantId =\n typeof data.tenantId === 'string' && data.tenantId.length > 0 ? data.tenantId : ctx.tenantId ?? null\n const organizationId =\n typeof data.organizationId === 'string' && data.organizationId.length > 0\n ? data.organizationId\n : ctx.organizationId ?? null\n return { tenantId, organizationId }\n}\n\nfunction readPromotionIdFromUsagePayload(payload: unknown): string | null {\n const data = (payload ?? {}) as Record<string, unknown>\n return typeof data.promotionId === 'string' && data.promotionId.length > 0 ? data.promotionId : null\n}\n\nfunction resolveSubscriberCache(ctx: PromotionsSubscriberContext): CacheStrategy | null {\n try {\n return ctx.resolve<CacheStrategy>('cache') ?? null\n } catch {\n return null\n }\n}\n\n// \u00A7Cache Build Contract item 5: the main tree cache is invalidated ONLY by promotion/\n// code mutation events (`promotions.promotion.created|updated|deleted`,\n// `promotions.code.created|updated|deleted`) \u2014 never by a usage event. Wired to those\n// six exact event ids (not a `promotions.promotion.*`/`promotions.code.*` wildcard) so\n// the lifecycle events declared alongside them in `events.ts`\n// (`promotions.promotion.budget_exhausted`/`.expiring_soon`, `promotions.code.used`/\n// `.exhausted`) never trigger a rebuild they were not asked to.\nexport async function handlePromotionTreeCacheInvalidation(\n payload: unknown,\n ctx: PromotionsSubscriberContext,\n): Promise<void> {\n const { tenantId, organizationId } = readSubscriberEventScope(payload, ctx)\n if (!tenantId || !organizationId) return\n const cache = resolveSubscriberCache(ctx)\n if (!cache) return\n try {\n await invalidatePromotionTreeCache(cache, tenantId, organizationId)\n } catch (err) {\n subscriberLogger.warn('[internal] promotion tree cache invalidation failed', { tenantId, organizationId, err })\n }\n}\n\n// \u00A7Cache Build Contract item 4/5: `promotions.usage.registered`/`.reverted` evict only\n// the one promotion's small counters-key entry, never the main tree cache.\nexport async function handlePromotionUsageCounterCacheInvalidation(\n payload: unknown,\n ctx: PromotionsSubscriberContext,\n): Promise<void> {\n const { tenantId, organizationId } = readSubscriberEventScope(payload, ctx)\n const promotionId = readPromotionIdFromUsagePayload(payload)\n if (!tenantId || !organizationId || !promotionId) return\n const cache = resolveSubscriberCache(ctx)\n if (!cache) return\n try {\n await invalidatePromotionUsageCounterCache(cache, tenantId, organizationId, promotionId)\n } catch (err) {\n subscriberLogger.warn('[internal] promotion usage counter cache invalidation failed', {\n tenantId,\n organizationId,\n promotionId,\n err,\n })\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["promotions"]
7
7
  }
@@ -360,7 +360,7 @@ function buildDocumentCrudOptions(binding) {
360
360
  indexer: {
361
361
  entityType: binding.entityId
362
362
  },
363
- enrichers: binding.kind === "order" ? { entityId: binding.entityId } : void 0,
363
+ enrichers: { entityId: binding.entityId },
364
364
  list: {
365
365
  schema: listSchema,
366
366
  entityId: binding.entityId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sales/api/documents/factory.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { splitCustomFieldPayload, extractAllCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { E } from '#generated/entities.ids.generated'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SalesOrder, SalesQuote } from '../../data/entities'\nimport { SalesChannel, SalesDocumentTagAssignment } from '../../data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n ORDER_PAYMENT_LEDGER_FIELDS,\n ORDER_PAYMENT_LEDGER_WARNING_CODE,\n orderCreateSchema,\n quoteCreateSchema,\n type OrderPaymentLedgerWarning,\n} from '../../data/validators'\nimport {\n createPagedListResponseSchema,\n createSalesCrudOpenApi,\n defaultDeleteRequestSchema,\n} from '../openapi'\nimport { parseScopedCommandInput, resolveCrudRecordId } from '../utils'\nimport { documentUpdateSchema } from '../../commands/documents'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { isUuidListParam, parseIdsParam, splitIdListParam } from '@open-mercato/shared/lib/crud/ids'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\n\ntype DocumentKind = 'order' | 'quote'\n\ntype DocumentBinding = {\n kind: DocumentKind\n entity: typeof SalesOrder | typeof SalesQuote\n entityId: (typeof E.sales)[keyof typeof E.sales]\n numberField: 'orderNumber' | 'quoteNumber'\n createCommandId: string\n updateCommandId: string\n deleteCommandId: string\n manageFeature: string\n viewFeature: string\n}\n\ntype DocumentCreateResult = {\n orderId?: string\n quoteId?: string\n id?: string\n warnings?: OrderPaymentLedgerWarning[]\n}\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst normalizeJsonRecord = (value: unknown): Record<string, unknown> | null => {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n return value as Record<string, unknown>\n }\n if (typeof value !== 'string') return null\n const parsed = parseDecryptedFieldValue(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : null\n}\n\nconst resolveCustomerName = (snapshot: Record<string, unknown> | null, fallback?: string | null) => {\n if (!snapshot) return fallback ?? null\n const customer = snapshot.customer as Record<string, unknown> | undefined\n const contact = snapshot.contact as Record<string, unknown> | undefined\n const displayName = typeof customer?.displayName === 'string' ? customer.displayName : null\n if (displayName) return displayName\n const first = typeof contact?.firstName === 'string' ? contact.firstName : null\n const last = typeof contact?.lastName === 'string' ? contact.lastName : null\n const preferred = typeof contact?.preferredName === 'string' ? contact.preferredName : null\n const parts = [preferred ?? first, last].filter((part) => part && part.trim().length)\n if (parts.length) return parts.join(' ')\n return fallback ?? null\n}\n\nconst resolveCustomerEmail = (snapshot: Record<string, unknown> | null) => {\n if (!snapshot) return null\n const customer = snapshot.customer as Record<string, unknown> | undefined\n const primary = typeof customer?.primaryEmail === 'string' ? customer.primaryEmail : null\n return primary ?? null\n}\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n id: z.string().uuid().optional(),\n customerId: z.string().uuid().optional(),\n channelId: z.string().uuid().optional(),\n channelIds: z\n .string()\n .optional()\n .describe(\n 'Comma-separated sales channel uuids; matches documents on any of them. Capped at 200 ids, malformed entries are dropped. Ignored when channelId is supplied; combines with channelIdsEmpty.',\n ),\n channelIdsEmpty: z\n .string()\n .optional()\n .describe(\n 'Boolean token; matches documents with no sales channel. Ignored when channelId is supplied; combines with channelIds.',\n ),\n lineItemCountMin: z.coerce.number().min(0).optional(),\n lineItemCountMax: z.coerce.number().min(0).optional(),\n totalNetMin: z.coerce.number().optional(),\n totalNetMax: z.coerce.number().optional(),\n totalGrossMin: z.coerce.number().optional(),\n totalGrossMax: z.coerce.number().optional(),\n dateFrom: z.string().optional(),\n dateTo: z.string().optional(),\n tagIds: z\n .string()\n .optional()\n .refine(isUuidListParam, { message: 'tagIds must be a comma-separated list of UUIDs' }),\n tagIdsEmpty: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n withDeleted: z.coerce.boolean().optional(),\n })\n .passthrough()\n\ntype ListQuery = z.infer<typeof listSchema>\n\nfunction buildFilters(query: ListQuery, numberColumn: string, kind: DocumentKind) {\n const filters: Record<string, unknown> = {}\n if (query.id) filters.id = { $eq: query.id }\n if (query.search && query.search.trim().length > 0) {\n const term = buildIlikeTerm(query.search.trim())\n filters[numberColumn] = { $ilike: term }\n }\n if (query.customerId) {\n filters.customer_entity_id = { $eq: query.customerId }\n }\n // Singular wins over plural, mirroring how `api/channels/route.ts` resolves `id` before `ids`.\n // An all-malformed `channelIds` narrows to no channel filter rather than an empty-set filter, so\n // a typo returns the unfiltered page instead of silently returning zero rows.\n if (query.channelId) {\n filters.channel_id = { $eq: query.channelId }\n } else {\n const channelIds = parseIdsParam(query.channelIds)\n const wantsUnassigned = parseBooleanToken(query.channelIdsEmpty) === true\n if (channelIds.length && wantsUnassigned) {\n // A channel multi-select with an \"(No channel)\" entry produces both at once, so they combine\n // rather than one silently dropping the other. `filters.$or` is a single key \u2014 a future filter\n // that also needs `$or` would clobber this one; nothing else in this factory writes it today.\n filters.$or = [{ channel_id: { $in: channelIds } }, { channel_id: { $exists: false } }]\n } else if (wantsUnassigned) {\n filters.channel_id = { $exists: false }\n } else if (channelIds.length) {\n filters.channel_id = { $in: channelIds }\n }\n }\n const lineRange: Record<string, number> = {}\n if (typeof query.lineItemCountMin === 'number') lineRange.$gte = query.lineItemCountMin\n if (typeof query.lineItemCountMax === 'number') lineRange.$lte = query.lineItemCountMax\n if (Object.keys(lineRange).length) {\n filters.line_item_count = lineRange\n }\n const netRange: Record<string, number> = {}\n if (typeof query.totalNetMin === 'number') netRange.$gte = query.totalNetMin\n if (typeof query.totalNetMax === 'number') netRange.$lte = query.totalNetMax\n if (Object.keys(netRange).length) {\n filters.grand_total_net_amount = netRange\n }\n const grossRange: Record<string, number> = {}\n if (typeof query.totalGrossMin === 'number') grossRange.$gte = query.totalGrossMin\n if (typeof query.totalGrossMax === 'number') grossRange.$lte = query.totalGrossMax\n if (Object.keys(grossRange).length) {\n filters.grand_total_gross_amount = grossRange\n }\n const dateRange: Record<string, Date> = {}\n if (query.dateFrom) {\n const from = new Date(query.dateFrom)\n if (!Number.isNaN(from.getTime())) dateRange.$gte = from\n }\n if (query.dateTo) {\n const to = new Date(query.dateTo)\n if (!Number.isNaN(to.getTime())) dateRange.$lte = to\n }\n if (Object.keys(dateRange).length) {\n filters.created_at = dateRange\n }\n const tagIds = splitIdListParam(query.tagIds)\n if (parseBooleanToken(query.tagIdsEmpty) === true) {\n filters.id = { $eq: '00000000-0000-0000-0000-000000000000' }\n } else if (tagIds.length) {\n filters['tag_assignments.tag_id'] = { $in: tagIds }\n filters['tag_assignments.document_kind'] = { $eq: kind }\n }\n return filters\n}\n\nfunction buildSortMap(numberColumn: string) {\n return {\n id: 'id',\n number: numberColumn,\n placedAt: 'placed_at',\n lineItemCount: 'line_item_count',\n grandTotalNetAmount: 'grand_total_net_amount',\n grandTotalGrossAmount: 'grand_total_gross_amount',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n }\n}\n\nconst mapUpdateResponse = (entity: any) => {\n const customerSnapshot = normalizeJsonRecord(entity?.customerSnapshot)\n const metadata = normalizeJsonRecord(entity?.metadata)\n\n return {\n id: entity?.id ?? null,\n orderNumber: entity?.orderNumber ?? null,\n quoteNumber: entity?.quoteNumber ?? null,\n customerEntityId: entity?.customerEntityId ?? null,\n customerContactId: entity?.customerContactId ?? null,\n customerSnapshot,\n metadata,\n externalReference: entity?.externalReference ?? null,\n customerReference: entity?.customerReference ?? null,\n comment: entity?.comments ?? null,\n statusEntryId: (entity as any)?.statusEntryId ?? null,\n status: (entity as any)?.status ?? null,\n channelId: (entity as any)?.channelId ?? null,\n customerName: resolveCustomerName(customerSnapshot, entity?.customerEntityId ?? null),\n contactEmail:\n resolveCustomerEmail(customerSnapshot) ??\n (typeof metadata?.customerEmail === 'string' ? metadata.customerEmail : null),\n currencyCode: entity?.currencyCode ?? null,\n placedAt: entity?.placedAt ? entity.placedAt.toISOString() : null,\n expectedDeliveryAt: entity?.expectedDeliveryAt ? entity.expectedDeliveryAt.toISOString() : null,\n shippingAddressId: entity?.shippingAddressId ?? null,\n billingAddressId: entity?.billingAddressId ?? null,\n shippingAddressSnapshot: normalizeJsonRecord(entity?.shippingAddressSnapshot),\n billingAddressSnapshot: normalizeJsonRecord(entity?.billingAddressSnapshot),\n shippingMethodId: entity?.shippingMethodId ?? null,\n shippingMethodCode: entity?.shippingMethodCode ?? null,\n shippingMethodSnapshot: normalizeJsonRecord(entity?.shippingMethodSnapshot),\n paymentMethodId: entity?.paymentMethodId ?? null,\n paymentMethodCode: entity?.paymentMethodCode ?? null,\n paymentMethodSnapshot: normalizeJsonRecord(entity?.paymentMethodSnapshot),\n // Return the fresh version so the client can refresh its optimistic-lock\n // token after a successful inline save \u2014 otherwise a second save on the same\n // page sends the now-stale updatedAt and falsely 409s (#2055 QA).\n updatedAt: entity?.updatedAt\n ? (entity.updatedAt instanceof Date ? entity.updatedAt.toISOString() : entity.updatedAt)\n : null,\n }\n}\n\nconst attachTags = async (payload: any, ctx: any) => {\n const items = Array.isArray(payload?.items) ? (payload.items as Array<Record<string, any>>) : []\n if (!items.length) return\n const ids = items\n .map((item) => (item && typeof item.id === 'string' ? item.id : null))\n .filter((id): id is string => !!id)\n if (!ids.length) return\n const em = ctx?.container?.resolve ? (ctx.container.resolve('em') as any) : null\n if (!em) return\n const where: Record<string, unknown> = {\n documentId: { $in: ids },\n documentKind: ctx?.bindingKind ?? null,\n }\n if (ctx?.auth?.tenantId) where.tenantId = ctx.auth.tenantId\n const orgIds =\n Array.isArray(ctx?.organizationIds) && ctx.organizationIds.length\n ? ctx.organizationIds.filter((val: string | null) => !!val)\n : ctx?.selectedOrganizationId\n ? [ctx.selectedOrganizationId]\n : []\n if (orgIds.length) where.organizationId = { $in: orgIds }\n const assignments = await em.find(\n SalesDocumentTagAssignment,\n where,\n { populate: ['tag'] },\n )\n const grouped = new Map<string, Array<{ id: string; label: string; color: string | null }>>()\n assignments.forEach((assignment: any) => {\n const tag = assignment?.tag\n const documentId = assignment?.documentId\n if (!tag || typeof tag.id !== 'string' || typeof documentId !== 'string') return\n const entry = {\n id: tag.id,\n label: typeof tag.label === 'string' && tag.label.trim().length ? tag.label : tag.slug ?? tag.id,\n color: typeof tag.color === 'string' && tag.color.trim().length ? tag.color : null,\n }\n const list = grouped.get(documentId) ?? []\n list.push(entry)\n grouped.set(documentId, list)\n })\n items.forEach((item: Record<string, any>) => {\n const id = item && typeof item.id === 'string' ? item.id : null\n if (!id) return\n const list = grouped.get(id)\n if (list) item.tags = list\n })\n}\n\n// Liveness note: this hook runs BEFORE the CRUD list cache stores the payload, so `channelName`\n// and `channelCode` are embedded in the cached entry. What keeps them fresh is that the hook also\n// runs on the cache-HIT path (shared/lib/crud/factory.ts:1683) and reassigns both fields\n// unconditionally for every item carrying a channel id. Anything that later skips this hook on a\n// hit \u2014 the way `skipEnrichersOnCacheHit` does for record-pure enrichers \u2014 would start serving the\n// stale names baked into the entry.\nexport const attachChannelNames = async (\n payload: { items?: Array<Record<string, unknown>> },\n ctx: CrudCtx,\n) => {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (!items.length) return\n const channelIds = Array.from(\n new Set(\n items\n .map((item) => (item && typeof item.channelId === 'string' ? item.channelId : null))\n .filter((value): value is string => !!value)\n )\n )\n if (!channelIds.length) return\n const em = ctx?.container?.resolve ? (ctx.container.resolve('em') as EntityManager) : null\n if (!em) return\n\n const where: Record<string, unknown> = { id: { $in: channelIds } }\n if (ctx?.auth?.tenantId) where.tenantId = ctx.auth.tenantId\n const orgIds =\n Array.isArray(ctx?.organizationIds) && ctx.organizationIds.length\n ? ctx.organizationIds.filter((val: string | null) => !!val)\n : ctx?.selectedOrganizationId\n ? [ctx.selectedOrganizationId]\n : []\n if (orgIds.length) where.organizationId = { $in: orgIds }\n\n // Only `name` and `code` are read. Without this projection the query loads the whole channel row,\n // and `sales/encryption.ts` declares eight of its columns encrypted (contact email/phone, the\n // address block) \u2014 so every documents-list request would decrypt PII it never renders, on the\n // cache-hit path too.\n const channels = await findWithDecryption(\n em,\n SalesChannel,\n where,\n { fields: ['id', 'name', 'code'] },\n {\n tenantId: ctx?.auth?.tenantId ?? null,\n organizationId: ctx?.selectedOrganizationId ?? ctx?.auth?.orgId ?? null,\n }\n )\n const byId = new Map(channels.map((channel) => [channel.id, channel]))\n items.forEach((item) => {\n if (!item || typeof item.channelId !== 'string') return\n const channel = byId.get(item.channelId)\n item.channelName = channel?.name ?? null\n item.channelCode = channel?.code ?? null\n })\n}\n\nasync function ensureNumberEditPermission(\n ctx: CrudCtx,\n translate: (key: string, fallback?: string) => string\n) {\n const rbac = ctx.container?.resolve?.('rbacService') as RbacService | null\n const auth = ctx.auth\n if (!rbac || !auth?.sub) return\n const ok = await rbac.userHasAllFeatures(auth.sub, ['sales.documents.number.edit'], {\n tenantId: auth.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? auth.orgId ?? null,\n })\n if (!ok) {\n throw new CrudHttpError(403, {\n error: translate('sales.documents.errors.number_edit_forbidden', 'You cannot edit document numbers.'),\n })\n }\n}\n\nexport function buildDocumentCrudOptions(binding: DocumentBinding) {\n const numberColumn = binding.numberField === 'orderNumber' ? 'order_number' : 'quote_number'\n const createSchema = binding.kind === 'order' ? orderCreateSchema : quoteCreateSchema\n\n const routeMetadata = {\n GET: { requireAuth: true, requireFeatures: [binding.viewFeature] },\n POST: { requireAuth: true, requireFeatures: [binding.manageFeature] },\n PUT: { requireAuth: true, requireFeatures: [binding.manageFeature] },\n DELETE: { requireAuth: true, requireFeatures: [binding.manageFeature] },\n }\n\n const commonFields = [\n 'id',\n numberColumn,\n 'status',\n 'status_entry_id',\n 'customer_entity_id',\n 'customer_contact_id',\n 'billing_address_id',\n 'shipping_address_id',\n 'customer_snapshot',\n 'billing_address_snapshot',\n 'shipping_address_snapshot',\n 'shipping_method_id',\n 'shipping_method_code',\n 'shipping_method_snapshot',\n 'payment_method_id',\n 'payment_method_code',\n 'payment_method_snapshot',\n 'customer_reference',\n 'metadata',\n 'external_reference',\n 'currency_code',\n 'comments',\n 'channel_id',\n 'placed_at',\n 'line_item_count',\n 'subtotal_net_amount',\n 'subtotal_gross_amount',\n 'tax_total_amount',\n 'discount_total_amount',\n 'grand_total_net_amount',\n 'grand_total_gross_amount',\n 'totals_snapshot',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ]\n\n const orderOnlyFields = [\n 'expected_delivery_at',\n 'totals_mode',\n 'shipping_net_amount',\n 'shipping_gross_amount',\n 'surcharge_total_amount',\n 'paid_total_amount',\n 'refunded_total_amount',\n 'outstanding_amount',\n ]\n\n const quoteOnlyFields = ['valid_from', 'valid_until']\n\n const listFields = [\n ...commonFields,\n ...(binding.kind === 'order' ? orderOnlyFields : quoteOnlyFields),\n ]\n\n // Large JSONB snapshot/payload columns that only the detail page renders. The\n // grid never reads them, so selecting them for list pages fetches and decrypts\n // blobs over the wire for nothing (#2233). `customer_snapshot` is intentionally\n // kept because the grid derives the customer name/email column from it.\n const detailOnlyProjectionFields = new Set([\n 'billing_address_snapshot',\n 'shipping_address_snapshot',\n 'shipping_method_snapshot',\n 'payment_method_snapshot',\n 'totals_snapshot',\n 'metadata',\n ])\n\n const gridFields = listFields.filter((field) => !detailOnlyProjectionFields.has(field))\n\n // The detail page fetches a single document through this same list route with an\n // `?id=` filter (there is no separate detail endpoint), so it needs the full\n // projection. Grid listings (no `id`) use the trimmed projection.\n const resolveListFields = (query: ListQuery) =>\n query && typeof query.id === 'string' && query.id.length ? listFields : gridFields\n\n return {\n metadata: routeMetadata,\n orm: {\n entity: binding.entity as any,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: {\n entityType: binding.entityId,\n },\n enrichers: binding.kind === 'order' ? { entityId: binding.entityId } : undefined,\n list: {\n schema: listSchema,\n entityId: binding.entityId,\n fields: (query: ListQuery) => resolveListFields(query),\n sortFieldMap: buildSortMap(numberColumn),\n buildFilters: async (query: any) => buildFilters(query, numberColumn, binding.kind),\n decorateCustomFields: { entityIds: [binding.entityId] },\n joins: [\n {\n alias: 'tag_assignments',\n table: 'sales_document_tag_assignments',\n from: { field: 'id' },\n to: { field: 'document_id' },\n type: 'left' as const,\n },\n ],\n transformItem: (item: any) => {\n const toNumber = (value: unknown): number | null => {\n if (typeof value === 'number') return Number.isNaN(value) ? null : value\n if (typeof value === 'string' && value.trim().length) {\n const parsed = Number(value)\n return Number.isNaN(parsed) ? null : parsed\n }\n return null\n }\n const base = {\n id: item.id,\n [binding.numberField]: item[numberColumn] ?? null,\n status: item.status ?? null,\n statusEntryId: item.status_entry_id ?? null,\n customerEntityId: item.customer_entity_id ?? null,\n customerContactId: item.customer_contact_id ?? null,\n billingAddressId: item.billing_address_id ?? null,\n shippingAddressId: item.shipping_address_id ?? null,\n shippingMethodId: item.shipping_method_id ?? null,\n shippingMethodCode: item.shipping_method_code ?? null,\n shippingMethodSnapshot: normalizeJsonRecord(item.shipping_method_snapshot),\n paymentMethodId: item.payment_method_id ?? null,\n paymentMethodCode: item.payment_method_code ?? null,\n paymentMethodSnapshot: normalizeJsonRecord(item.payment_method_snapshot),\n currencyCode: item.currency_code ?? null,\n channelId: item.channel_id ?? null,\n externalReference: item.external_reference ?? null,\n customerReference: item.customer_reference ?? null,\n placedAt: item.placed_at ?? null,\n expectedDeliveryAt: item.expected_delivery_at ?? null,\n comment: item.comments ?? null,\n validFrom: item.valid_from ?? null,\n validUntil: item.valid_until ?? null,\n lineItemCount: toNumber(item.line_item_count),\n subtotalNetAmount: toNumber(item.subtotal_net_amount),\n subtotalGrossAmount: toNumber(item.subtotal_gross_amount),\n discountTotalAmount: toNumber(item.discount_total_amount),\n taxTotalAmount: toNumber(item.tax_total_amount),\n shippingNetAmount: toNumber(item.shipping_net_amount),\n shippingGrossAmount: toNumber(item.shipping_gross_amount),\n surchargeTotalAmount: toNumber(item.surcharge_total_amount),\n grandTotalNetAmount: toNumber(item.grand_total_net_amount),\n grandTotalGrossAmount: toNumber(item.grand_total_gross_amount),\n paidTotalAmount: toNumber(item.paid_total_amount),\n refundedTotalAmount: toNumber(item.refunded_total_amount),\n outstandingAmount: toNumber(item.outstanding_amount),\n ...(binding.kind === 'order' ? { totalsMode: item.totals_mode ?? 'computed' } : {}),\n customerSnapshot: normalizeJsonRecord(item.customer_snapshot),\n billingAddressSnapshot: normalizeJsonRecord(item.billing_address_snapshot),\n shippingAddressSnapshot: normalizeJsonRecord(item.shipping_address_snapshot),\n metadata: normalizeJsonRecord(item.metadata),\n organizationId: item.organization_id ?? null,\n tenantId: item.tenant_id ?? null,\n createdAt: item.created_at,\n updatedAt: item.updated_at,\n }\n const cfEntries = extractAllCustomFieldEntries(item as Record<string, unknown>)\n const normalized = { ...base }\n Object.keys(normalized).forEach((key) => {\n if (key.startsWith('cf:')) delete (normalized as any)[key]\n })\n return Object.keys(cfEntries).length ? { ...normalized, ...cfEntries } : normalized\n },\n },\n actions: {\n create: {\n commandId: binding.createCommandId,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const { base, custom } = splitCustomFieldPayload(raw ?? {})\n const parsed = parseScopedCommandInput(\n createSchema,\n Object.keys(custom).length ? { ...base, customFields: custom } : base,\n ctx,\n translate,\n )\n return parsed\n },\n response: ({ result }: { result?: DocumentCreateResult | null }) => ({\n id: result?.orderId ?? result?.quoteId ?? result?.id ?? null,\n ...(binding.kind === 'order' && Array.isArray(result?.warnings)\n ? { warnings: result.warnings }\n : {}),\n }),\n status: 201,\n },\n update: {\n commandId: binding.updateCommandId,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const { base, custom } = splitCustomFieldPayload(raw ?? {})\n const numberValue =\n binding.kind === 'order'\n ? (base as Record<string, unknown>).orderNumber\n : (base as Record<string, unknown>).quoteNumber\n if (typeof numberValue === 'string') {\n await ensureNumberEditPermission(ctx, translate)\n }\n const parsed = parseScopedCommandInput(\n documentUpdateSchema,\n Object.keys(custom).length ? { ...base, customFields: custom } : base,\n ctx,\n translate,\n )\n return parsed\n },\n response: ({ result }: { result: any }) =>\n mapUpdateResponse((result as any)?.order ?? (result as any)?.quote ?? result),\n },\n delete: {\n commandId: binding.deleteCommandId,\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }: { parsed: any; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const id = resolveCrudRecordId(parsed, ctx, translate)\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n hooks: {\n afterList: async (payload: any, ctx: CrudCtx) => {\n await attachTags(payload, { ...ctx, bindingKind: binding.kind })\n await attachChannelNames(payload, ctx)\n },\n },\n }\n}\n\nexport function buildDocumentOpenApi(binding: DocumentBinding) {\n const createSchema = binding.kind === 'order' ? orderCreateSchema : quoteCreateSchema\n const itemSchema = z.object({\n id: z.string().uuid(),\n [binding.numberField]: z.string().nullable(),\n status: z.string().nullable(),\n statusEntryId: z.string().uuid().nullable().optional(),\n customerEntityId: z.string().uuid().nullable(),\n customerContactId: z.string().uuid().nullable(),\n billingAddressId: z.string().uuid().nullable(),\n shippingAddressId: z.string().uuid().nullable(),\n customerReference: z.string().nullable().optional(),\n externalReference: z.string().nullable().optional(),\n comment: z.string().nullable().optional(),\n placedAt: z.string().nullable().optional(),\n expectedDeliveryAt: z.string().nullable().optional(),\n customerSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n billingAddressSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n shippingAddressSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n shippingMethodId: z.string().uuid().nullable().optional(),\n shippingMethodCode: z.string().nullable().optional(),\n shippingMethodSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n paymentMethodId: z.string().uuid().nullable().optional(),\n paymentMethodCode: z.string().nullable().optional(),\n paymentMethodSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n currencyCode: z.string().nullable(),\n channelId: z.string().uuid().nullable(),\n channelName: z.string().nullable().optional(),\n channelCode: z.string().nullable().optional(),\n organizationId: z.string().uuid().nullable(),\n tenantId: z.string().uuid().nullable(),\n validFrom: z.string().nullable().optional(),\n validUntil: z.string().nullable().optional(),\n lineItemCount: z.number().nullable().optional(),\n subtotalNetAmount: z.number().nullable().optional(),\n subtotalGrossAmount: z.number().nullable().optional(),\n discountTotalAmount: z.number().nullable().optional(),\n taxTotalAmount: z.number().nullable().optional(),\n shippingNetAmount: z.number().nullable().optional(),\n shippingGrossAmount: z.number().nullable().optional(),\n surchargeTotalAmount: z.number().nullable().optional(),\n grandTotalNetAmount: z.number().nullable().optional(),\n grandTotalGrossAmount: z.number().nullable().optional(),\n paidTotalAmount: z.number().nullable().optional(),\n refundedTotalAmount: z.number().nullable().optional(),\n outstandingAmount: z.number().nullable().optional(),\n totalsMode: z.enum(['computed', 'external']).optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n customValues: z.record(z.string(), z.unknown()).optional(),\n })\n\n const listResponseSchema = createPagedListResponseSchema(itemSchema)\n\n return createSalesCrudOpenApi({\n resourceName: binding.kind === 'order' ? 'Order' : 'Quote',\n querySchema: listSchema,\n listResponseSchema,\n create: {\n schema: createSchema,\n responseSchema: binding.kind === 'order'\n ? z.object({\n id: z.string().uuid().nullable(),\n warnings: z.array(z.object({\n code: z.literal(ORDER_PAYMENT_LEDGER_WARNING_CODE),\n fields: z.array(z.enum(ORDER_PAYMENT_LEDGER_FIELDS)),\n })).optional(),\n })\n : z.object({ id: z.string().uuid().nullable() }),\n description: binding.kind === 'order'\n ? 'Creates a new sales order. paidTotalAmount, refundedTotalAmount, and outstandingAmount are deprecated compatibility inputs: supplied values are ignored and reported in warnings. Record payments through sales.payments.create or POST /api/sales/payments.'\n : 'Creates a new sales quote.',\n },\n del: {\n schema: defaultDeleteRequestSchema,\n responseSchema: z.object({ ok: z.boolean() }),\n description: `Deletes a sales ${binding.kind}.`,\n },\n })\n}\n\n// Compatibility wrapper\nexport function createDocumentCrudRoute(binding: DocumentBinding) {\n const crud = makeCrudRoute(buildDocumentCrudOptions(binding))\n const { GET, POST, PUT, DELETE } = crud\n return { GET, POST, PUT, DELETE, openApi: buildDocumentOpenApi(binding), metadata: crud.metadata }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { splitCustomFieldPayload, extractAllCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { E } from '#generated/entities.ids.generated'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SalesOrder, SalesQuote } from '../../data/entities'\nimport { SalesChannel, SalesDocumentTagAssignment } from '../../data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n ORDER_PAYMENT_LEDGER_FIELDS,\n ORDER_PAYMENT_LEDGER_WARNING_CODE,\n orderCreateSchema,\n quoteCreateSchema,\n type OrderPaymentLedgerWarning,\n} from '../../data/validators'\nimport {\n createPagedListResponseSchema,\n createSalesCrudOpenApi,\n defaultDeleteRequestSchema,\n} from '../openapi'\nimport { parseScopedCommandInput, resolveCrudRecordId } from '../utils'\nimport { documentUpdateSchema } from '../../commands/documents'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { isUuidListParam, parseIdsParam, splitIdListParam } from '@open-mercato/shared/lib/crud/ids'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\n\ntype DocumentKind = 'order' | 'quote'\n\ntype DocumentBinding = {\n kind: DocumentKind\n entity: typeof SalesOrder | typeof SalesQuote\n entityId: (typeof E.sales)[keyof typeof E.sales]\n numberField: 'orderNumber' | 'quoteNumber'\n createCommandId: string\n updateCommandId: string\n deleteCommandId: string\n manageFeature: string\n viewFeature: string\n}\n\ntype DocumentCreateResult = {\n orderId?: string\n quoteId?: string\n id?: string\n warnings?: OrderPaymentLedgerWarning[]\n}\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst normalizeJsonRecord = (value: unknown): Record<string, unknown> | null => {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n return value as Record<string, unknown>\n }\n if (typeof value !== 'string') return null\n const parsed = parseDecryptedFieldValue(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : null\n}\n\nconst resolveCustomerName = (snapshot: Record<string, unknown> | null, fallback?: string | null) => {\n if (!snapshot) return fallback ?? null\n const customer = snapshot.customer as Record<string, unknown> | undefined\n const contact = snapshot.contact as Record<string, unknown> | undefined\n const displayName = typeof customer?.displayName === 'string' ? customer.displayName : null\n if (displayName) return displayName\n const first = typeof contact?.firstName === 'string' ? contact.firstName : null\n const last = typeof contact?.lastName === 'string' ? contact.lastName : null\n const preferred = typeof contact?.preferredName === 'string' ? contact.preferredName : null\n const parts = [preferred ?? first, last].filter((part) => part && part.trim().length)\n if (parts.length) return parts.join(' ')\n return fallback ?? null\n}\n\nconst resolveCustomerEmail = (snapshot: Record<string, unknown> | null) => {\n if (!snapshot) return null\n const customer = snapshot.customer as Record<string, unknown> | undefined\n const primary = typeof customer?.primaryEmail === 'string' ? customer.primaryEmail : null\n return primary ?? null\n}\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n id: z.string().uuid().optional(),\n customerId: z.string().uuid().optional(),\n channelId: z.string().uuid().optional(),\n channelIds: z\n .string()\n .optional()\n .describe(\n 'Comma-separated sales channel uuids; matches documents on any of them. Capped at 200 ids, malformed entries are dropped. Ignored when channelId is supplied; combines with channelIdsEmpty.',\n ),\n channelIdsEmpty: z\n .string()\n .optional()\n .describe(\n 'Boolean token; matches documents with no sales channel. Ignored when channelId is supplied; combines with channelIds.',\n ),\n lineItemCountMin: z.coerce.number().min(0).optional(),\n lineItemCountMax: z.coerce.number().min(0).optional(),\n totalNetMin: z.coerce.number().optional(),\n totalNetMax: z.coerce.number().optional(),\n totalGrossMin: z.coerce.number().optional(),\n totalGrossMax: z.coerce.number().optional(),\n dateFrom: z.string().optional(),\n dateTo: z.string().optional(),\n tagIds: z\n .string()\n .optional()\n .refine(isUuidListParam, { message: 'tagIds must be a comma-separated list of UUIDs' }),\n tagIdsEmpty: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n withDeleted: z.coerce.boolean().optional(),\n })\n .passthrough()\n\ntype ListQuery = z.infer<typeof listSchema>\n\nfunction buildFilters(query: ListQuery, numberColumn: string, kind: DocumentKind) {\n const filters: Record<string, unknown> = {}\n if (query.id) filters.id = { $eq: query.id }\n if (query.search && query.search.trim().length > 0) {\n const term = buildIlikeTerm(query.search.trim())\n filters[numberColumn] = { $ilike: term }\n }\n if (query.customerId) {\n filters.customer_entity_id = { $eq: query.customerId }\n }\n // Singular wins over plural, mirroring how `api/channels/route.ts` resolves `id` before `ids`.\n // An all-malformed `channelIds` narrows to no channel filter rather than an empty-set filter, so\n // a typo returns the unfiltered page instead of silently returning zero rows.\n if (query.channelId) {\n filters.channel_id = { $eq: query.channelId }\n } else {\n const channelIds = parseIdsParam(query.channelIds)\n const wantsUnassigned = parseBooleanToken(query.channelIdsEmpty) === true\n if (channelIds.length && wantsUnassigned) {\n // A channel multi-select with an \"(No channel)\" entry produces both at once, so they combine\n // rather than one silently dropping the other. `filters.$or` is a single key \u2014 a future filter\n // that also needs `$or` would clobber this one; nothing else in this factory writes it today.\n filters.$or = [{ channel_id: { $in: channelIds } }, { channel_id: { $exists: false } }]\n } else if (wantsUnassigned) {\n filters.channel_id = { $exists: false }\n } else if (channelIds.length) {\n filters.channel_id = { $in: channelIds }\n }\n }\n const lineRange: Record<string, number> = {}\n if (typeof query.lineItemCountMin === 'number') lineRange.$gte = query.lineItemCountMin\n if (typeof query.lineItemCountMax === 'number') lineRange.$lte = query.lineItemCountMax\n if (Object.keys(lineRange).length) {\n filters.line_item_count = lineRange\n }\n const netRange: Record<string, number> = {}\n if (typeof query.totalNetMin === 'number') netRange.$gte = query.totalNetMin\n if (typeof query.totalNetMax === 'number') netRange.$lte = query.totalNetMax\n if (Object.keys(netRange).length) {\n filters.grand_total_net_amount = netRange\n }\n const grossRange: Record<string, number> = {}\n if (typeof query.totalGrossMin === 'number') grossRange.$gte = query.totalGrossMin\n if (typeof query.totalGrossMax === 'number') grossRange.$lte = query.totalGrossMax\n if (Object.keys(grossRange).length) {\n filters.grand_total_gross_amount = grossRange\n }\n const dateRange: Record<string, Date> = {}\n if (query.dateFrom) {\n const from = new Date(query.dateFrom)\n if (!Number.isNaN(from.getTime())) dateRange.$gte = from\n }\n if (query.dateTo) {\n const to = new Date(query.dateTo)\n if (!Number.isNaN(to.getTime())) dateRange.$lte = to\n }\n if (Object.keys(dateRange).length) {\n filters.created_at = dateRange\n }\n const tagIds = splitIdListParam(query.tagIds)\n if (parseBooleanToken(query.tagIdsEmpty) === true) {\n filters.id = { $eq: '00000000-0000-0000-0000-000000000000' }\n } else if (tagIds.length) {\n filters['tag_assignments.tag_id'] = { $in: tagIds }\n filters['tag_assignments.document_kind'] = { $eq: kind }\n }\n return filters\n}\n\nfunction buildSortMap(numberColumn: string) {\n return {\n id: 'id',\n number: numberColumn,\n placedAt: 'placed_at',\n lineItemCount: 'line_item_count',\n grandTotalNetAmount: 'grand_total_net_amount',\n grandTotalGrossAmount: 'grand_total_gross_amount',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n }\n}\n\nconst mapUpdateResponse = (entity: any) => {\n const customerSnapshot = normalizeJsonRecord(entity?.customerSnapshot)\n const metadata = normalizeJsonRecord(entity?.metadata)\n\n return {\n id: entity?.id ?? null,\n orderNumber: entity?.orderNumber ?? null,\n quoteNumber: entity?.quoteNumber ?? null,\n customerEntityId: entity?.customerEntityId ?? null,\n customerContactId: entity?.customerContactId ?? null,\n customerSnapshot,\n metadata,\n externalReference: entity?.externalReference ?? null,\n customerReference: entity?.customerReference ?? null,\n comment: entity?.comments ?? null,\n statusEntryId: (entity as any)?.statusEntryId ?? null,\n status: (entity as any)?.status ?? null,\n channelId: (entity as any)?.channelId ?? null,\n customerName: resolveCustomerName(customerSnapshot, entity?.customerEntityId ?? null),\n contactEmail:\n resolveCustomerEmail(customerSnapshot) ??\n (typeof metadata?.customerEmail === 'string' ? metadata.customerEmail : null),\n currencyCode: entity?.currencyCode ?? null,\n placedAt: entity?.placedAt ? entity.placedAt.toISOString() : null,\n expectedDeliveryAt: entity?.expectedDeliveryAt ? entity.expectedDeliveryAt.toISOString() : null,\n shippingAddressId: entity?.shippingAddressId ?? null,\n billingAddressId: entity?.billingAddressId ?? null,\n shippingAddressSnapshot: normalizeJsonRecord(entity?.shippingAddressSnapshot),\n billingAddressSnapshot: normalizeJsonRecord(entity?.billingAddressSnapshot),\n shippingMethodId: entity?.shippingMethodId ?? null,\n shippingMethodCode: entity?.shippingMethodCode ?? null,\n shippingMethodSnapshot: normalizeJsonRecord(entity?.shippingMethodSnapshot),\n paymentMethodId: entity?.paymentMethodId ?? null,\n paymentMethodCode: entity?.paymentMethodCode ?? null,\n paymentMethodSnapshot: normalizeJsonRecord(entity?.paymentMethodSnapshot),\n // Return the fresh version so the client can refresh its optimistic-lock\n // token after a successful inline save \u2014 otherwise a second save on the same\n // page sends the now-stale updatedAt and falsely 409s (#2055 QA).\n updatedAt: entity?.updatedAt\n ? (entity.updatedAt instanceof Date ? entity.updatedAt.toISOString() : entity.updatedAt)\n : null,\n }\n}\n\nconst attachTags = async (payload: any, ctx: any) => {\n const items = Array.isArray(payload?.items) ? (payload.items as Array<Record<string, any>>) : []\n if (!items.length) return\n const ids = items\n .map((item) => (item && typeof item.id === 'string' ? item.id : null))\n .filter((id): id is string => !!id)\n if (!ids.length) return\n const em = ctx?.container?.resolve ? (ctx.container.resolve('em') as any) : null\n if (!em) return\n const where: Record<string, unknown> = {\n documentId: { $in: ids },\n documentKind: ctx?.bindingKind ?? null,\n }\n if (ctx?.auth?.tenantId) where.tenantId = ctx.auth.tenantId\n const orgIds =\n Array.isArray(ctx?.organizationIds) && ctx.organizationIds.length\n ? ctx.organizationIds.filter((val: string | null) => !!val)\n : ctx?.selectedOrganizationId\n ? [ctx.selectedOrganizationId]\n : []\n if (orgIds.length) where.organizationId = { $in: orgIds }\n const assignments = await em.find(\n SalesDocumentTagAssignment,\n where,\n { populate: ['tag'] },\n )\n const grouped = new Map<string, Array<{ id: string; label: string; color: string | null }>>()\n assignments.forEach((assignment: any) => {\n const tag = assignment?.tag\n const documentId = assignment?.documentId\n if (!tag || typeof tag.id !== 'string' || typeof documentId !== 'string') return\n const entry = {\n id: tag.id,\n label: typeof tag.label === 'string' && tag.label.trim().length ? tag.label : tag.slug ?? tag.id,\n color: typeof tag.color === 'string' && tag.color.trim().length ? tag.color : null,\n }\n const list = grouped.get(documentId) ?? []\n list.push(entry)\n grouped.set(documentId, list)\n })\n items.forEach((item: Record<string, any>) => {\n const id = item && typeof item.id === 'string' ? item.id : null\n if (!id) return\n const list = grouped.get(id)\n if (list) item.tags = list\n })\n}\n\n// Liveness note: this hook runs BEFORE the CRUD list cache stores the payload, so `channelName`\n// and `channelCode` are embedded in the cached entry. What keeps them fresh is that the hook also\n// runs on the cache-HIT path (shared/lib/crud/factory.ts:1683) and reassigns both fields\n// unconditionally for every item carrying a channel id. Anything that later skips this hook on a\n// hit \u2014 the way `skipEnrichersOnCacheHit` does for record-pure enrichers \u2014 would start serving the\n// stale names baked into the entry.\nexport const attachChannelNames = async (\n payload: { items?: Array<Record<string, unknown>> },\n ctx: CrudCtx,\n) => {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (!items.length) return\n const channelIds = Array.from(\n new Set(\n items\n .map((item) => (item && typeof item.channelId === 'string' ? item.channelId : null))\n .filter((value): value is string => !!value)\n )\n )\n if (!channelIds.length) return\n const em = ctx?.container?.resolve ? (ctx.container.resolve('em') as EntityManager) : null\n if (!em) return\n\n const where: Record<string, unknown> = { id: { $in: channelIds } }\n if (ctx?.auth?.tenantId) where.tenantId = ctx.auth.tenantId\n const orgIds =\n Array.isArray(ctx?.organizationIds) && ctx.organizationIds.length\n ? ctx.organizationIds.filter((val: string | null) => !!val)\n : ctx?.selectedOrganizationId\n ? [ctx.selectedOrganizationId]\n : []\n if (orgIds.length) where.organizationId = { $in: orgIds }\n\n // Only `name` and `code` are read. Without this projection the query loads the whole channel row,\n // and `sales/encryption.ts` declares eight of its columns encrypted (contact email/phone, the\n // address block) \u2014 so every documents-list request would decrypt PII it never renders, on the\n // cache-hit path too.\n const channels = await findWithDecryption(\n em,\n SalesChannel,\n where,\n { fields: ['id', 'name', 'code'] },\n {\n tenantId: ctx?.auth?.tenantId ?? null,\n organizationId: ctx?.selectedOrganizationId ?? ctx?.auth?.orgId ?? null,\n }\n )\n const byId = new Map(channels.map((channel) => [channel.id, channel]))\n items.forEach((item) => {\n if (!item || typeof item.channelId !== 'string') return\n const channel = byId.get(item.channelId)\n item.channelName = channel?.name ?? null\n item.channelCode = channel?.code ?? null\n })\n}\n\nasync function ensureNumberEditPermission(\n ctx: CrudCtx,\n translate: (key: string, fallback?: string) => string\n) {\n const rbac = ctx.container?.resolve?.('rbacService') as RbacService | null\n const auth = ctx.auth\n if (!rbac || !auth?.sub) return\n const ok = await rbac.userHasAllFeatures(auth.sub, ['sales.documents.number.edit'], {\n tenantId: auth.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? auth.orgId ?? null,\n })\n if (!ok) {\n throw new CrudHttpError(403, {\n error: translate('sales.documents.errors.number_edit_forbidden', 'You cannot edit document numbers.'),\n })\n }\n}\n\nexport function buildDocumentCrudOptions(binding: DocumentBinding) {\n const numberColumn = binding.numberField === 'orderNumber' ? 'order_number' : 'quote_number'\n const createSchema = binding.kind === 'order' ? orderCreateSchema : quoteCreateSchema\n\n const routeMetadata = {\n GET: { requireAuth: true, requireFeatures: [binding.viewFeature] },\n POST: { requireAuth: true, requireFeatures: [binding.manageFeature] },\n PUT: { requireAuth: true, requireFeatures: [binding.manageFeature] },\n DELETE: { requireAuth: true, requireFeatures: [binding.manageFeature] },\n }\n\n const commonFields = [\n 'id',\n numberColumn,\n 'status',\n 'status_entry_id',\n 'customer_entity_id',\n 'customer_contact_id',\n 'billing_address_id',\n 'shipping_address_id',\n 'customer_snapshot',\n 'billing_address_snapshot',\n 'shipping_address_snapshot',\n 'shipping_method_id',\n 'shipping_method_code',\n 'shipping_method_snapshot',\n 'payment_method_id',\n 'payment_method_code',\n 'payment_method_snapshot',\n 'customer_reference',\n 'metadata',\n 'external_reference',\n 'currency_code',\n 'comments',\n 'channel_id',\n 'placed_at',\n 'line_item_count',\n 'subtotal_net_amount',\n 'subtotal_gross_amount',\n 'tax_total_amount',\n 'discount_total_amount',\n 'grand_total_net_amount',\n 'grand_total_gross_amount',\n 'totals_snapshot',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ]\n\n const orderOnlyFields = [\n 'expected_delivery_at',\n 'totals_mode',\n 'shipping_net_amount',\n 'shipping_gross_amount',\n 'surcharge_total_amount',\n 'paid_total_amount',\n 'refunded_total_amount',\n 'outstanding_amount',\n ]\n\n const quoteOnlyFields = ['valid_from', 'valid_until']\n\n const listFields = [\n ...commonFields,\n ...(binding.kind === 'order' ? orderOnlyFields : quoteOnlyFields),\n ]\n\n // Large JSONB snapshot/payload columns that only the detail page renders. The\n // grid never reads them, so selecting them for list pages fetches and decrypts\n // blobs over the wire for nothing (#2233). `customer_snapshot` is intentionally\n // kept because the grid derives the customer name/email column from it.\n const detailOnlyProjectionFields = new Set([\n 'billing_address_snapshot',\n 'shipping_address_snapshot',\n 'shipping_method_snapshot',\n 'payment_method_snapshot',\n 'totals_snapshot',\n 'metadata',\n ])\n\n const gridFields = listFields.filter((field) => !detailOnlyProjectionFields.has(field))\n\n // The detail page fetches a single document through this same list route with an\n // `?id=` filter (there is no separate detail endpoint), so it needs the full\n // projection. Grid listings (no `id`) use the trimmed projection.\n const resolveListFields = (query: ListQuery) =>\n query && typeof query.id === 'string' && query.id.length ? listFields : gridFields\n\n return {\n metadata: routeMetadata,\n orm: {\n entity: binding.entity as any,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: {\n entityType: binding.entityId,\n },\n enrichers: { entityId: binding.entityId },\n list: {\n schema: listSchema,\n entityId: binding.entityId,\n fields: (query: ListQuery) => resolveListFields(query),\n sortFieldMap: buildSortMap(numberColumn),\n buildFilters: async (query: any) => buildFilters(query, numberColumn, binding.kind),\n decorateCustomFields: { entityIds: [binding.entityId] },\n joins: [\n {\n alias: 'tag_assignments',\n table: 'sales_document_tag_assignments',\n from: { field: 'id' },\n to: { field: 'document_id' },\n type: 'left' as const,\n },\n ],\n transformItem: (item: any) => {\n const toNumber = (value: unknown): number | null => {\n if (typeof value === 'number') return Number.isNaN(value) ? null : value\n if (typeof value === 'string' && value.trim().length) {\n const parsed = Number(value)\n return Number.isNaN(parsed) ? null : parsed\n }\n return null\n }\n const base = {\n id: item.id,\n [binding.numberField]: item[numberColumn] ?? null,\n status: item.status ?? null,\n statusEntryId: item.status_entry_id ?? null,\n customerEntityId: item.customer_entity_id ?? null,\n customerContactId: item.customer_contact_id ?? null,\n billingAddressId: item.billing_address_id ?? null,\n shippingAddressId: item.shipping_address_id ?? null,\n shippingMethodId: item.shipping_method_id ?? null,\n shippingMethodCode: item.shipping_method_code ?? null,\n shippingMethodSnapshot: normalizeJsonRecord(item.shipping_method_snapshot),\n paymentMethodId: item.payment_method_id ?? null,\n paymentMethodCode: item.payment_method_code ?? null,\n paymentMethodSnapshot: normalizeJsonRecord(item.payment_method_snapshot),\n currencyCode: item.currency_code ?? null,\n channelId: item.channel_id ?? null,\n externalReference: item.external_reference ?? null,\n customerReference: item.customer_reference ?? null,\n placedAt: item.placed_at ?? null,\n expectedDeliveryAt: item.expected_delivery_at ?? null,\n comment: item.comments ?? null,\n validFrom: item.valid_from ?? null,\n validUntil: item.valid_until ?? null,\n lineItemCount: toNumber(item.line_item_count),\n subtotalNetAmount: toNumber(item.subtotal_net_amount),\n subtotalGrossAmount: toNumber(item.subtotal_gross_amount),\n discountTotalAmount: toNumber(item.discount_total_amount),\n taxTotalAmount: toNumber(item.tax_total_amount),\n shippingNetAmount: toNumber(item.shipping_net_amount),\n shippingGrossAmount: toNumber(item.shipping_gross_amount),\n surchargeTotalAmount: toNumber(item.surcharge_total_amount),\n grandTotalNetAmount: toNumber(item.grand_total_net_amount),\n grandTotalGrossAmount: toNumber(item.grand_total_gross_amount),\n paidTotalAmount: toNumber(item.paid_total_amount),\n refundedTotalAmount: toNumber(item.refunded_total_amount),\n outstandingAmount: toNumber(item.outstanding_amount),\n ...(binding.kind === 'order' ? { totalsMode: item.totals_mode ?? 'computed' } : {}),\n customerSnapshot: normalizeJsonRecord(item.customer_snapshot),\n billingAddressSnapshot: normalizeJsonRecord(item.billing_address_snapshot),\n shippingAddressSnapshot: normalizeJsonRecord(item.shipping_address_snapshot),\n metadata: normalizeJsonRecord(item.metadata),\n organizationId: item.organization_id ?? null,\n tenantId: item.tenant_id ?? null,\n createdAt: item.created_at,\n updatedAt: item.updated_at,\n }\n const cfEntries = extractAllCustomFieldEntries(item as Record<string, unknown>)\n const normalized = { ...base }\n Object.keys(normalized).forEach((key) => {\n if (key.startsWith('cf:')) delete (normalized as any)[key]\n })\n return Object.keys(cfEntries).length ? { ...normalized, ...cfEntries } : normalized\n },\n },\n actions: {\n create: {\n commandId: binding.createCommandId,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const { base, custom } = splitCustomFieldPayload(raw ?? {})\n const parsed = parseScopedCommandInput(\n createSchema,\n Object.keys(custom).length ? { ...base, customFields: custom } : base,\n ctx,\n translate,\n )\n return parsed\n },\n response: ({ result }: { result?: DocumentCreateResult | null }) => ({\n id: result?.orderId ?? result?.quoteId ?? result?.id ?? null,\n ...(binding.kind === 'order' && Array.isArray(result?.warnings)\n ? { warnings: result.warnings }\n : {}),\n }),\n status: 201,\n },\n update: {\n commandId: binding.updateCommandId,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const { base, custom } = splitCustomFieldPayload(raw ?? {})\n const numberValue =\n binding.kind === 'order'\n ? (base as Record<string, unknown>).orderNumber\n : (base as Record<string, unknown>).quoteNumber\n if (typeof numberValue === 'string') {\n await ensureNumberEditPermission(ctx, translate)\n }\n const parsed = parseScopedCommandInput(\n documentUpdateSchema,\n Object.keys(custom).length ? { ...base, customFields: custom } : base,\n ctx,\n translate,\n )\n return parsed\n },\n response: ({ result }: { result: any }) =>\n mapUpdateResponse((result as any)?.order ?? (result as any)?.quote ?? result),\n },\n delete: {\n commandId: binding.deleteCommandId,\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }: { parsed: any; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const id = resolveCrudRecordId(parsed, ctx, translate)\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n hooks: {\n afterList: async (payload: any, ctx: CrudCtx) => {\n await attachTags(payload, { ...ctx, bindingKind: binding.kind })\n await attachChannelNames(payload, ctx)\n },\n },\n }\n}\n\nexport function buildDocumentOpenApi(binding: DocumentBinding) {\n const createSchema = binding.kind === 'order' ? orderCreateSchema : quoteCreateSchema\n const itemSchema = z.object({\n id: z.string().uuid(),\n [binding.numberField]: z.string().nullable(),\n status: z.string().nullable(),\n statusEntryId: z.string().uuid().nullable().optional(),\n customerEntityId: z.string().uuid().nullable(),\n customerContactId: z.string().uuid().nullable(),\n billingAddressId: z.string().uuid().nullable(),\n shippingAddressId: z.string().uuid().nullable(),\n customerReference: z.string().nullable().optional(),\n externalReference: z.string().nullable().optional(),\n comment: z.string().nullable().optional(),\n placedAt: z.string().nullable().optional(),\n expectedDeliveryAt: z.string().nullable().optional(),\n customerSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n billingAddressSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n shippingAddressSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n shippingMethodId: z.string().uuid().nullable().optional(),\n shippingMethodCode: z.string().nullable().optional(),\n shippingMethodSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n paymentMethodId: z.string().uuid().nullable().optional(),\n paymentMethodCode: z.string().nullable().optional(),\n paymentMethodSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n currencyCode: z.string().nullable(),\n channelId: z.string().uuid().nullable(),\n channelName: z.string().nullable().optional(),\n channelCode: z.string().nullable().optional(),\n organizationId: z.string().uuid().nullable(),\n tenantId: z.string().uuid().nullable(),\n validFrom: z.string().nullable().optional(),\n validUntil: z.string().nullable().optional(),\n lineItemCount: z.number().nullable().optional(),\n subtotalNetAmount: z.number().nullable().optional(),\n subtotalGrossAmount: z.number().nullable().optional(),\n discountTotalAmount: z.number().nullable().optional(),\n taxTotalAmount: z.number().nullable().optional(),\n shippingNetAmount: z.number().nullable().optional(),\n shippingGrossAmount: z.number().nullable().optional(),\n surchargeTotalAmount: z.number().nullable().optional(),\n grandTotalNetAmount: z.number().nullable().optional(),\n grandTotalGrossAmount: z.number().nullable().optional(),\n paidTotalAmount: z.number().nullable().optional(),\n refundedTotalAmount: z.number().nullable().optional(),\n outstandingAmount: z.number().nullable().optional(),\n totalsMode: z.enum(['computed', 'external']).optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n customValues: z.record(z.string(), z.unknown()).optional(),\n })\n\n const listResponseSchema = createPagedListResponseSchema(itemSchema)\n\n return createSalesCrudOpenApi({\n resourceName: binding.kind === 'order' ? 'Order' : 'Quote',\n querySchema: listSchema,\n listResponseSchema,\n create: {\n schema: createSchema,\n responseSchema: binding.kind === 'order'\n ? z.object({\n id: z.string().uuid().nullable(),\n warnings: z.array(z.object({\n code: z.literal(ORDER_PAYMENT_LEDGER_WARNING_CODE),\n fields: z.array(z.enum(ORDER_PAYMENT_LEDGER_FIELDS)),\n })).optional(),\n })\n : z.object({ id: z.string().uuid().nullable() }),\n description: binding.kind === 'order'\n ? 'Creates a new sales order. paidTotalAmount, refundedTotalAmount, and outstandingAmount are deprecated compatibility inputs: supplied values are ignored and reported in warnings. Record payments through sales.payments.create or POST /api/sales/payments.'\n : 'Creates a new sales quote.',\n },\n del: {\n schema: defaultDeleteRequestSchema,\n responseSchema: z.object({ ok: z.boolean() }),\n description: `Deletes a sales ${binding.kind}.`,\n },\n })\n}\n\n// Compatibility wrapper\nexport function createDocumentCrudRoute(binding: DocumentBinding) {\n const crud = makeCrudRoute(buildDocumentCrudOptions(binding))\n const { GET, POST, PUT, DELETE } = crud\n return { GET, POST, PUT, DELETE, openApi: buildDocumentOpenApi(binding), metadata: crud.metadata }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,8 +2,8 @@ import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
2
2
  import { SalesOrder } from "../../data/entities.js";
3
3
  import { E } from "../../../../generated/entities.ids.generated.js";
4
4
  import { buildDocumentCrudOptions, buildDocumentOpenApi } from "../documents/factory.js";
5
- const crud = makeCrudRoute(
6
- buildDocumentCrudOptions({
5
+ const crud = makeCrudRoute({
6
+ ...buildDocumentCrudOptions({
7
7
  kind: "order",
8
8
  entity: SalesOrder,
9
9
  entityId: E.sales.sales_order,
@@ -13,8 +13,9 @@ const crud = makeCrudRoute(
13
13
  deleteCommandId: "sales.orders.delete",
14
14
  manageFeature: "sales.orders.manage",
15
15
  viewFeature: "sales.orders.view"
16
- })
17
- );
16
+ }),
17
+ enrichers: { entityId: E.sales.sales_order }
18
+ });
18
19
  const metadata = crud.metadata;
19
20
  const GET = crud.GET;
20
21
  const POST = crud.POST;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sales/api/orders/route.ts"],
4
- "sourcesContent": ["import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { SalesOrder } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'\n\nconst crud = makeCrudRoute(\n buildDocumentCrudOptions({\n kind: 'order',\n entity: SalesOrder,\n entityId: E.sales.sales_order,\n numberField: 'orderNumber',\n createCommandId: 'sales.orders.create',\n updateCommandId: 'sales.orders.update',\n deleteCommandId: 'sales.orders.delete',\n manageFeature: 'sales.orders.manage',\n viewFeature: 'sales.orders.view',\n }),\n)\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\nexport const openApi = buildDocumentOpenApi({\n kind: 'order',\n entity: SalesOrder,\n entityId: E.sales.sales_order,\n numberField: 'orderNumber',\n createCommandId: 'sales.orders.create',\n updateCommandId: 'sales.orders.update',\n deleteCommandId: 'sales.orders.delete',\n manageFeature: 'sales.orders.manage',\n viewFeature: 'sales.orders.view',\n})\n"],
5
- "mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,SAAS;AAClB,SAAS,0BAA0B,4BAA4B;AAE/D,MAAM,OAAO;AAAA,EACX,yBAAyB;AAAA,IACvB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,EAAE,MAAM;AAAA,IAClB,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,aAAa;AAAA,EACf,CAAC;AACH;AAEO,MAAM,WAAW,KAAK;AACtB,MAAM,MAAM,KAAK;AACjB,MAAM,OAAO,KAAK;AAClB,MAAM,MAAM,KAAK;AACjB,MAAM,SAAS,KAAK;AACpB,MAAM,UAAU,qBAAqB;AAAA,EAC1C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,UAAU,EAAE,MAAM;AAAA,EAClB,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,aAAa;AACf,CAAC;",
4
+ "sourcesContent": ["import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { SalesOrder } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'\n\nconst crud = makeCrudRoute({\n ...buildDocumentCrudOptions({\n kind: 'order',\n entity: SalesOrder,\n entityId: E.sales.sales_order,\n numberField: 'orderNumber',\n createCommandId: 'sales.orders.create',\n updateCommandId: 'sales.orders.update',\n deleteCommandId: 'sales.orders.delete',\n manageFeature: 'sales.orders.manage',\n viewFeature: 'sales.orders.view',\n }),\n enrichers: { entityId: E.sales.sales_order },\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\nexport const openApi = buildDocumentOpenApi({\n kind: 'order',\n entity: SalesOrder,\n entityId: E.sales.sales_order,\n numberField: 'orderNumber',\n createCommandId: 'sales.orders.create',\n updateCommandId: 'sales.orders.update',\n deleteCommandId: 'sales.orders.delete',\n manageFeature: 'sales.orders.manage',\n viewFeature: 'sales.orders.view',\n})\n"],
5
+ "mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,SAAS;AAClB,SAAS,0BAA0B,4BAA4B;AAE/D,MAAM,OAAO,cAAc;AAAA,EACzB,GAAG,yBAAyB;AAAA,IAC1B,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,EAAE,MAAM;AAAA,IAClB,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,aAAa;AAAA,EACf,CAAC;AAAA,EACD,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY;AAC7C,CAAC;AAEM,MAAM,WAAW,KAAK;AACtB,MAAM,MAAM,KAAK;AACjB,MAAM,OAAO,KAAK;AAClB,MAAM,MAAM,KAAK;AACjB,MAAM,SAAS,KAAK;AACpB,MAAM,UAAU,qBAAqB;AAAA,EAC1C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,UAAU,EAAE,MAAM;AAAA,EAClB,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,aAAa;AACf,CAAC;",
6
6
  "names": []
7
7
  }
@@ -2,8 +2,8 @@ import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
2
2
  import { SalesQuote } from "../../data/entities.js";
3
3
  import { E } from "../../../../generated/entities.ids.generated.js";
4
4
  import { buildDocumentCrudOptions, buildDocumentOpenApi } from "../documents/factory.js";
5
- const crud = makeCrudRoute(
6
- buildDocumentCrudOptions({
5
+ const crud = makeCrudRoute({
6
+ ...buildDocumentCrudOptions({
7
7
  kind: "quote",
8
8
  entity: SalesQuote,
9
9
  entityId: E.sales.sales_quote,
@@ -13,8 +13,9 @@ const crud = makeCrudRoute(
13
13
  deleteCommandId: "sales.quotes.delete",
14
14
  manageFeature: "sales.quotes.manage",
15
15
  viewFeature: "sales.quotes.view"
16
- })
17
- );
16
+ }),
17
+ enrichers: { entityId: E.sales.sales_quote }
18
+ });
18
19
  const metadata = crud.metadata;
19
20
  const GET = crud.GET;
20
21
  const POST = crud.POST;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sales/api/quotes/route.ts"],
4
- "sourcesContent": ["import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { SalesQuote } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'\n\nconst crud = makeCrudRoute(\n buildDocumentCrudOptions({\n kind: 'quote',\n entity: SalesQuote,\n entityId: E.sales.sales_quote,\n numberField: 'quoteNumber',\n createCommandId: 'sales.quotes.create',\n updateCommandId: 'sales.quotes.update',\n deleteCommandId: 'sales.quotes.delete',\n manageFeature: 'sales.quotes.manage',\n viewFeature: 'sales.quotes.view',\n }),\n)\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\nexport const openApi = buildDocumentOpenApi({\n kind: 'quote',\n entity: SalesQuote,\n entityId: E.sales.sales_quote,\n numberField: 'quoteNumber',\n createCommandId: 'sales.quotes.create',\n updateCommandId: 'sales.quotes.update',\n deleteCommandId: 'sales.quotes.delete',\n manageFeature: 'sales.quotes.manage',\n viewFeature: 'sales.quotes.view',\n})\n"],
5
- "mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,SAAS;AAClB,SAAS,0BAA0B,4BAA4B;AAE/D,MAAM,OAAO;AAAA,EACX,yBAAyB;AAAA,IACvB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,EAAE,MAAM;AAAA,IAClB,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,aAAa;AAAA,EACf,CAAC;AACH;AAEO,MAAM,WAAW,KAAK;AACtB,MAAM,MAAM,KAAK;AACjB,MAAM,OAAO,KAAK;AAClB,MAAM,MAAM,KAAK;AACjB,MAAM,SAAS,KAAK;AACpB,MAAM,UAAU,qBAAqB;AAAA,EAC1C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,UAAU,EAAE,MAAM;AAAA,EAClB,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,aAAa;AACf,CAAC;",
4
+ "sourcesContent": ["import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { SalesQuote } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'\n\nconst crud = makeCrudRoute({\n ...buildDocumentCrudOptions({\n kind: 'quote',\n entity: SalesQuote,\n entityId: E.sales.sales_quote,\n numberField: 'quoteNumber',\n createCommandId: 'sales.quotes.create',\n updateCommandId: 'sales.quotes.update',\n deleteCommandId: 'sales.quotes.delete',\n manageFeature: 'sales.quotes.manage',\n viewFeature: 'sales.quotes.view',\n }),\n enrichers: { entityId: E.sales.sales_quote },\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\nexport const openApi = buildDocumentOpenApi({\n kind: 'quote',\n entity: SalesQuote,\n entityId: E.sales.sales_quote,\n numberField: 'quoteNumber',\n createCommandId: 'sales.quotes.create',\n updateCommandId: 'sales.quotes.update',\n deleteCommandId: 'sales.quotes.delete',\n manageFeature: 'sales.quotes.manage',\n viewFeature: 'sales.quotes.view',\n})\n"],
5
+ "mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,SAAS;AAClB,SAAS,0BAA0B,4BAA4B;AAE/D,MAAM,OAAO,cAAc;AAAA,EACzB,GAAG,yBAAyB;AAAA,IAC1B,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,EAAE,MAAM;AAAA,IAClB,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,aAAa;AAAA,EACf,CAAC;AAAA,EACD,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY;AAC7C,CAAC;AAEM,MAAM,WAAW,KAAK;AACtB,MAAM,MAAM,KAAK;AACjB,MAAM,OAAO,KAAK;AAClB,MAAM,MAAM,KAAK;AACjB,MAAM,SAAS,KAAK;AACpB,MAAM,UAAU,qBAAqB;AAAA,EAC1C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,UAAU,EAAE,MAAM;AAAA,EAClB,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,aAAa;AACf,CAAC;",
6
6
  "names": []
7
7
  }
@@ -48,6 +48,7 @@ import { SalesDocumentAdjustmentsSection } from "@open-mercato/core/modules/sale
48
48
  import { SalesShipmentsSection } from "@open-mercato/core/modules/sales/components/documents/ShipmentsSection";
49
49
  import { SalesReturnsSection } from "@open-mercato/core/modules/sales/components/documents/ReturnsSection";
50
50
  import { DocumentTotals } from "@open-mercato/core/modules/sales/components/documents/DocumentTotals";
51
+ import { SalesDocumentExtensionSpot } from "@open-mercato/core/modules/sales/components/documents/SalesDocumentExtensionSpot";
51
52
  import { E } from "../../../../../../generated/entities.ids.generated.js";
52
53
  import { useCurrencyDictionary } from "@open-mercato/core/modules/customers/components/detail/hooks/useCurrencyDictionary";
53
54
  import { DictionaryValue, createDictionaryMap, renderDictionaryColor, renderDictionaryIcon } from "@open-mercato/core/modules/dictionaries/components/dictionaryAppearance";
@@ -1577,11 +1578,14 @@ function SalesDocumentDetailPage({
1577
1578
  const detailInjectionContext = React.useMemo(
1578
1579
  () => ({
1579
1580
  kind,
1581
+ documentId: record?.id ?? null,
1580
1582
  record,
1581
1583
  formId: mutationContextId,
1582
1584
  resourceKind: `sales.${kind}`,
1583
1585
  resourceId: record?.id ?? void 0,
1584
- retryLastMutation
1586
+ retryLastMutation,
1587
+ organizationId: record?.organizationId ?? null,
1588
+ tenantId: record?.tenantId ?? null
1585
1589
  }),
1586
1590
  [kind, mutationContextId, record, retryLastMutation]
1587
1591
  );
@@ -3958,6 +3962,15 @@ function SalesDocumentDetailPage({
3958
3962
  deleteLabel: t("sales.documents.detail.delete", "Delete")
3959
3963
  }
3960
3964
  ),
3965
+ /* @__PURE__ */ jsx(
3966
+ SalesDocumentExtensionSpot,
3967
+ {
3968
+ surface: "stage-bar",
3969
+ context: detailInjectionContext,
3970
+ data: record,
3971
+ onDataChange: setRecord
3972
+ }
3973
+ ),
3961
3974
  /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-4", children: [
3962
3975
  /* @__PURE__ */ jsx("div", { className: "md:col-span-3", children: /* @__PURE__ */ jsx(
3963
3976
  CustomerInlineEditor,
@@ -4183,6 +4196,15 @@ function SalesDocumentDetailPage({
4183
4196
  }
4184
4197
  )
4185
4198
  ] }),
4199
+ /* @__PURE__ */ jsx(
4200
+ SalesDocumentExtensionSpot,
4201
+ {
4202
+ surface: "closure",
4203
+ context: detailInjectionContext,
4204
+ data: record,
4205
+ onDataChange: setRecord
4206
+ }
4207
+ ),
4186
4208
  /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
4187
4209
  /* @__PURE__ */ jsx(
4188
4210
  CustomDataSection,