@open-mercato/core 0.6.6-develop.6399.1.93ba8ca030 → 0.6.6-develop.6402.1.080aab8ec9

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 (45) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/lib/backendChrome.js +3 -2
  3. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  4. package/dist/modules/currencies/backend/config/currency-fetching/page.meta.js +4 -13
  5. package/dist/modules/currencies/backend/config/currency-fetching/page.meta.js.map +2 -2
  6. package/dist/modules/currencies/backend/currencies/page.meta.js +1 -1
  7. package/dist/modules/currencies/backend/currencies/page.meta.js.map +2 -2
  8. package/dist/modules/currencies/backend/exchange-rates/page.meta.js +1 -0
  9. package/dist/modules/currencies/backend/exchange-rates/page.meta.js.map +2 -2
  10. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +9 -12
  11. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
  12. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +49 -10
  13. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
  14. package/dist/modules/entities/api/definitions.batch.js +29 -2
  15. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  16. package/dist/modules/entities/api/definitions.js +9 -2
  17. package/dist/modules/entities/api/definitions.js.map +2 -2
  18. package/dist/modules/entities/api/definitions.manage.js +14 -3
  19. package/dist/modules/entities/api/definitions.manage.js.map +2 -2
  20. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +43 -17
  21. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  22. package/dist/modules/entities/lib/definitions-version.js +55 -0
  23. package/dist/modules/entities/lib/definitions-version.js.map +7 -0
  24. package/dist/modules/sales/cli.js +38 -1
  25. package/dist/modules/sales/cli.js.map +2 -2
  26. package/dist/modules/sales/lib/dictionaries.js +70 -0
  27. package/dist/modules/sales/lib/dictionaries.js.map +2 -2
  28. package/package.json +7 -7
  29. package/src/modules/auth/lib/backendChrome.tsx +3 -2
  30. package/src/modules/currencies/backend/config/currency-fetching/page.meta.ts +4 -15
  31. package/src/modules/currencies/backend/currencies/page.meta.ts +1 -1
  32. package/src/modules/currencies/backend/exchange-rates/page.meta.ts +1 -0
  33. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +9 -13
  34. package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +58 -8
  35. package/src/modules/customers/i18n/de.json +4 -0
  36. package/src/modules/customers/i18n/en.json +4 -0
  37. package/src/modules/customers/i18n/es.json +4 -0
  38. package/src/modules/customers/i18n/pl.json +4 -0
  39. package/src/modules/entities/api/definitions.batch.ts +38 -1
  40. package/src/modules/entities/api/definitions.manage.ts +14 -1
  41. package/src/modules/entities/api/definitions.ts +10 -1
  42. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +53 -17
  43. package/src/modules/entities/lib/definitions-version.ts +102 -0
  44. package/src/modules/sales/cli.ts +39 -1
  45. package/src/modules/sales/lib/dictionaries.ts +90 -0
@@ -201,6 +201,75 @@ const DEAL_LOSS_REASON_DEFAULTS = [
201
201
  { value: "timing", label: "Bad timing", color: "#6366f1", icon: "lucide:clock" },
202
202
  { value: "other", label: "Other", color: "#71717a", icon: "lucide:minus-circle" }
203
203
  ];
204
+ async function backfillDealLossReasonDictionary(em, scope) {
205
+ const definition = getSalesDictionaryDefinition("deal-loss-reasons");
206
+ const existingDictionary = await em.findOne(Dictionary, {
207
+ tenantId: scope.tenantId,
208
+ organizationId: scope.organizationId,
209
+ key: definition.key,
210
+ deletedAt: null
211
+ });
212
+ const dictionary = existingDictionary ?? await ensureSalesDictionary({
213
+ em,
214
+ tenantId: scope.tenantId,
215
+ organizationId: scope.organizationId,
216
+ kind: "deal-loss-reasons"
217
+ });
218
+ const targetEntries = await em.find(DictionaryEntry, {
219
+ dictionary,
220
+ tenantId: scope.tenantId,
221
+ organizationId: scope.organizationId
222
+ });
223
+ if (targetEntries.length > 0) {
224
+ return {
225
+ dictionaryId: dictionary.id,
226
+ createdDictionary: !existingDictionary,
227
+ copiedLegacyEntries: 0,
228
+ seededDefaultEntries: 0,
229
+ existingEntries: targetEntries.length
230
+ };
231
+ }
232
+ const legacyDictionary = await em.findOne(Dictionary, {
233
+ tenantId: scope.tenantId,
234
+ organizationId: scope.organizationId,
235
+ key: "customer_lost_reason",
236
+ deletedAt: null
237
+ });
238
+ const legacyEntries = legacyDictionary ? await em.find(
239
+ DictionaryEntry,
240
+ {
241
+ dictionary: legacyDictionary,
242
+ tenantId: scope.tenantId,
243
+ organizationId: scope.organizationId
244
+ },
245
+ { orderBy: { position: "asc", label: "asc" } }
246
+ ) : [];
247
+ let copiedLegacyEntries = 0;
248
+ let seededDefaultEntries = 0;
249
+ const processedValues = /* @__PURE__ */ new Set();
250
+ const seeds = legacyEntries.length ? legacyEntries.map((entry) => ({
251
+ value: entry.value,
252
+ label: entry.label,
253
+ color: entry.color,
254
+ icon: entry.icon
255
+ })) : DEAL_LOSS_REASON_DEFAULTS;
256
+ for (const seed of seeds) {
257
+ const normalizedValue = normalizeDictionaryValue(seed.value ?? "");
258
+ if (!normalizedValue || processedValues.has(normalizedValue)) continue;
259
+ const entry = await ensureSalesDictionaryEntry(em, scope, "deal-loss-reasons", seed);
260
+ if (!entry) continue;
261
+ processedValues.add(normalizedValue);
262
+ if (legacyEntries.length) copiedLegacyEntries += 1;
263
+ else seededDefaultEntries += 1;
264
+ }
265
+ return {
266
+ dictionaryId: dictionary.id,
267
+ createdDictionary: !existingDictionary,
268
+ copiedLegacyEntries,
269
+ seededDefaultEntries,
270
+ existingEntries: 0
271
+ };
272
+ }
204
273
  async function seedSalesStatusDictionaries(em, scope) {
205
274
  await seedSalesDictionary(em, scope, "order-status", ORDER_STATUS_DEFAULTS);
206
275
  await seedSalesDictionary(em, scope, "order-line-status", ORDER_LINE_STATUS_DEFAULTS);
@@ -218,6 +287,7 @@ async function seedSalesDictionaries(em, scope) {
218
287
  const DEFAULT_ADJUSTMENT_KIND_VALUES = ADJUSTMENT_KIND_DEFAULTS.map((entry) => entry.value);
219
288
  export {
220
289
  DEFAULT_ADJUSTMENT_KIND_VALUES,
290
+ backfillDealLossReasonDictionary,
221
291
  ensureSalesDictionary,
222
292
  getSalesDictionaryDefinition,
223
293
  normalizeDictionaryValue,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sales/lib/dictionaries.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n normalizeDictionaryValue,\n sanitizeDictionaryColor,\n sanitizeDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/lib/utils'\n\nexport type SalesDictionaryKind =\n | 'order-status'\n | 'order-line-status'\n | 'shipment-status'\n | 'payment-status'\n | 'deal-loss-reasons'\n | 'adjustment-kind'\n\ntype SalesDictionaryDefinition = {\n key: string\n name: string\n singular: string\n description: string\n resourceKind: string\n commandPrefix: string\n}\n\nconst DEFINITIONS: Record<SalesDictionaryKind, SalesDictionaryDefinition> = {\n 'order-status': {\n key: 'sales.order_status',\n name: 'Sales order statuses',\n singular: 'Sales order status',\n description: 'Configurable set of statuses used by sales orders.',\n resourceKind: 'sales.order-status',\n commandPrefix: 'sales.order-statuses',\n },\n 'order-line-status': {\n key: 'sales.order_line_status',\n name: 'Sales order line statuses',\n singular: 'Sales order line status',\n description: 'Configurable set of statuses used by sales order lines.',\n resourceKind: 'sales.order-line-status',\n commandPrefix: 'sales.order-line-statuses',\n },\n 'shipment-status': {\n key: 'sales.shipment_status',\n name: 'Shipment statuses',\n singular: 'Shipment status',\n description: 'Configurable set of statuses used by shipments.',\n resourceKind: 'sales.shipment-status',\n commandPrefix: 'sales.shipment-statuses',\n },\n 'payment-status': {\n key: 'sales.payment_status',\n name: 'Payment statuses',\n singular: 'Payment status',\n description: 'Configurable set of statuses used by payments.',\n resourceKind: 'sales.payment-status',\n commandPrefix: 'sales.payment-statuses',\n },\n 'deal-loss-reasons': {\n key: 'sales.deal_loss_reason',\n name: 'Deal loss reasons',\n singular: 'Deal loss reason',\n description: 'Reusable reasons used when closing deals as lost.',\n resourceKind: 'sales.deal-loss-reason',\n commandPrefix: 'sales.deal-loss-reasons',\n },\n 'adjustment-kind': {\n key: 'sales.adjustment_kind',\n name: 'Sales adjustment kinds',\n singular: 'Sales adjustment kind',\n description: 'Reusable adjustment kinds applied to sales documents.',\n resourceKind: 'sales.adjustment-kind',\n commandPrefix: 'sales.adjustment-kinds',\n },\n}\n\nexport function getSalesDictionaryDefinition(kind: SalesDictionaryKind): SalesDictionaryDefinition {\n return DEFINITIONS[kind]\n}\n\nexport async function ensureSalesDictionary(params: {\n em: EntityManager\n tenantId: string\n organizationId: string\n kind: SalesDictionaryKind\n}): Promise<Dictionary> {\n const { em, tenantId, organizationId, kind } = params\n const def = getSalesDictionaryDefinition(kind)\n let dictionary = await em.findOne(Dictionary, {\n tenantId,\n organizationId,\n key: def.key,\n deletedAt: null,\n })\n if (!dictionary) {\n dictionary = em.create(Dictionary, {\n tenantId,\n organizationId,\n key: def.key,\n name: def.name,\n description: def.description,\n isSystem: true,\n isActive: true,\n managerVisibility: 'hidden',\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(dictionary)\n await em.flush()\n }\n return dictionary\n}\n\nexport async function resolveDictionaryEntryValue(\n em: EntityManager,\n entryId: string | null | undefined,\n scope: { tenantId: string }\n): Promise<string | null> {\n if (!entryId) return null\n const entry = await em.findOne(DictionaryEntry, { id: entryId, tenantId: scope.tenantId })\n if (!entry) return null\n return entry.value?.trim() || null\n}\n\nexport { normalizeDictionaryValue, sanitizeDictionaryColor, sanitizeDictionaryIcon }\n\ntype SalesDictionarySeed = {\n value: string\n label: string\n color?: string | null\n icon?: string | null\n}\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nconst ORDER_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'draft', label: 'Draft', color: '#94a3b8', icon: 'lucide:file-pen-line' },\n { value: 'pending_approval', label: 'Pending Approval', color: '#f59e0b', icon: 'lucide:hourglass' },\n { value: 'approved', label: 'Approved', color: '#16a34a', icon: 'lucide:check-circle' },\n { value: 'rejected', label: 'Rejected', color: '#ef4444', icon: 'lucide:x-circle' },\n { value: 'sent', label: 'Sent', color: '#0ea5e9', icon: 'lucide:send' },\n { value: 'confirmed', label: 'Confirmed', color: '#2563eb', icon: 'lucide:badge-check' },\n { value: 'in_fulfillment', label: 'In fulfillment', color: '#f59e0b', icon: 'lucide:loader-2' },\n { value: 'fulfilled', label: 'Fulfilled', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'on_hold', label: 'On hold', color: '#a855f7', icon: 'lucide:pause-circle' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst ORDER_LINE_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock' },\n { value: 'allocated', label: 'Allocated', color: '#6366f1', icon: 'lucide:inbox' },\n { value: 'picking', label: 'Picking', color: '#f59e0b', icon: 'lucide:hand' },\n { value: 'packed', label: 'Packed', color: '#0ea5e9', icon: 'lucide:package' },\n { value: 'shipped', label: 'Shipped', color: '#2563eb', icon: 'lucide:truck' },\n { value: 'delivered', label: 'Delivered', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'backordered', label: 'Backordered', color: '#d946ef', icon: 'lucide:alert-octagon' },\n { value: 'returned', label: 'Returned', color: '#0d9488', icon: 'lucide:undo-2' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst SHIPMENT_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock-3' },\n { value: 'packed', label: 'Packed', color: '#22c55e', icon: 'lucide:package-check' },\n { value: 'shipped', label: 'Shipped', color: '#2563eb', icon: 'lucide:truck' },\n { value: 'in_transit', label: 'In transit', color: '#0ea5e9', icon: 'lucide:loader' },\n { value: 'delivered', label: 'Delivered', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n { value: 'returned', label: 'Returned', color: '#0d9488', icon: 'lucide:undo-2' },\n]\n\nconst PAYMENT_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock-3' },\n { value: 'authorized', label: 'Authorized', color: '#6366f1', icon: 'lucide:badge-check' },\n { value: 'captured', label: 'Captured', color: '#0ea5e9', icon: 'lucide:banknote' },\n { value: 'received', label: 'Received', color: '#16a34a', icon: 'lucide:check' },\n { value: 'refunded', label: 'Refunded', color: '#f59e0b', icon: 'lucide:rotate-ccw' },\n { value: 'failed', label: 'Failed', color: '#ef4444', icon: 'lucide:triangle-alert' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst ADJUSTMENT_KIND_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'discount', label: 'Discount' },\n { value: 'tax', label: 'Tax' },\n { value: 'shipping', label: 'Shipping' },\n { value: 'surcharge', label: 'Surcharge' },\n { value: 'return', label: 'Return' },\n { value: 'custom', label: 'Custom' },\n]\n\nasync function ensureSalesDictionaryEntry(\n em: EntityManager,\n scope: SeedScope,\n kind: SalesDictionaryKind,\n seed: SalesDictionarySeed\n): Promise<DictionaryEntry | null> {\n const value = seed.value?.trim()\n if (!value) return null\n const dictionary = await ensureSalesDictionary({\n em,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n kind,\n })\n const normalizedValue = normalizeDictionaryValue(value)\n const color = seed.color === undefined ? undefined : sanitizeDictionaryColor(seed.color)\n const icon = seed.icon === undefined ? undefined : sanitizeDictionaryIcon(seed.icon)\n const existing = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue,\n })\n if (existing) {\n let changed = false\n if (color !== undefined && existing.color !== color) {\n existing.color = color ?? null\n changed = true\n }\n if (icon !== undefined && existing.icon !== icon) {\n existing.icon = icon ?? null\n changed = true\n }\n if (changed) {\n existing.updatedAt = new Date()\n em.persist(existing)\n }\n return existing\n }\n const now = new Date()\n const entry = em.create(DictionaryEntry, {\n dictionary,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n value,\n label: seed.label?.trim() || value,\n normalizedValue,\n color: color ?? null,\n icon: icon ?? null,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entry)\n return entry\n}\n\nasync function seedSalesDictionary(\n em: EntityManager,\n scope: SeedScope,\n kind: SalesDictionaryKind,\n defaults: SalesDictionarySeed[]\n): Promise<void> {\n for (const seed of defaults) {\n await ensureSalesDictionaryEntry(em, scope, kind, seed)\n }\n}\n\nconst DEAL_LOSS_REASON_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'lost_to_competitor', label: 'Lost to competitor', color: '#ef4444', icon: 'lucide:users' },\n { value: 'no_budget', label: 'No budget', color: '#f59e0b', icon: 'lucide:wallet' },\n { value: 'no_decision', label: 'No decision made', color: '#94a3b8', icon: 'lucide:help-circle' },\n { value: 'timing', label: 'Bad timing', color: '#6366f1', icon: 'lucide:clock' },\n { value: 'other', label: 'Other', color: '#71717a', icon: 'lucide:minus-circle' },\n]\n\nexport async function seedSalesStatusDictionaries(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesDictionary(em, scope, 'order-status', ORDER_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'order-line-status', ORDER_LINE_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'shipment-status', SHIPMENT_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'payment-status', PAYMENT_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'deal-loss-reasons', DEAL_LOSS_REASON_DEFAULTS)\n}\n\nexport async function seedSalesAdjustmentKinds(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesDictionary(em, scope, 'adjustment-kind', ADJUSTMENT_KIND_DEFAULTS)\n}\n\nexport async function seedSalesDictionaries(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesStatusDictionaries(em, scope)\n await seedSalesAdjustmentKinds(em, scope)\n}\n\nexport const DEFAULT_ADJUSTMENT_KIND_VALUES = ADJUSTMENT_KIND_DEFAULTS.map((entry) => entry.value)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n normalizeDictionaryValue,\n sanitizeDictionaryColor,\n sanitizeDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/lib/utils'\n\nexport type SalesDictionaryKind =\n | 'order-status'\n | 'order-line-status'\n | 'shipment-status'\n | 'payment-status'\n | 'deal-loss-reasons'\n | 'adjustment-kind'\n\ntype SalesDictionaryDefinition = {\n key: string\n name: string\n singular: string\n description: string\n resourceKind: string\n commandPrefix: string\n}\n\nconst DEFINITIONS: Record<SalesDictionaryKind, SalesDictionaryDefinition> = {\n 'order-status': {\n key: 'sales.order_status',\n name: 'Sales order statuses',\n singular: 'Sales order status',\n description: 'Configurable set of statuses used by sales orders.',\n resourceKind: 'sales.order-status',\n commandPrefix: 'sales.order-statuses',\n },\n 'order-line-status': {\n key: 'sales.order_line_status',\n name: 'Sales order line statuses',\n singular: 'Sales order line status',\n description: 'Configurable set of statuses used by sales order lines.',\n resourceKind: 'sales.order-line-status',\n commandPrefix: 'sales.order-line-statuses',\n },\n 'shipment-status': {\n key: 'sales.shipment_status',\n name: 'Shipment statuses',\n singular: 'Shipment status',\n description: 'Configurable set of statuses used by shipments.',\n resourceKind: 'sales.shipment-status',\n commandPrefix: 'sales.shipment-statuses',\n },\n 'payment-status': {\n key: 'sales.payment_status',\n name: 'Payment statuses',\n singular: 'Payment status',\n description: 'Configurable set of statuses used by payments.',\n resourceKind: 'sales.payment-status',\n commandPrefix: 'sales.payment-statuses',\n },\n 'deal-loss-reasons': {\n key: 'sales.deal_loss_reason',\n name: 'Deal loss reasons',\n singular: 'Deal loss reason',\n description: 'Reusable reasons used when closing deals as lost.',\n resourceKind: 'sales.deal-loss-reason',\n commandPrefix: 'sales.deal-loss-reasons',\n },\n 'adjustment-kind': {\n key: 'sales.adjustment_kind',\n name: 'Sales adjustment kinds',\n singular: 'Sales adjustment kind',\n description: 'Reusable adjustment kinds applied to sales documents.',\n resourceKind: 'sales.adjustment-kind',\n commandPrefix: 'sales.adjustment-kinds',\n },\n}\n\nexport function getSalesDictionaryDefinition(kind: SalesDictionaryKind): SalesDictionaryDefinition {\n return DEFINITIONS[kind]\n}\n\nexport async function ensureSalesDictionary(params: {\n em: EntityManager\n tenantId: string\n organizationId: string\n kind: SalesDictionaryKind\n}): Promise<Dictionary> {\n const { em, tenantId, organizationId, kind } = params\n const def = getSalesDictionaryDefinition(kind)\n let dictionary = await em.findOne(Dictionary, {\n tenantId,\n organizationId,\n key: def.key,\n deletedAt: null,\n })\n if (!dictionary) {\n dictionary = em.create(Dictionary, {\n tenantId,\n organizationId,\n key: def.key,\n name: def.name,\n description: def.description,\n isSystem: true,\n isActive: true,\n managerVisibility: 'hidden',\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(dictionary)\n await em.flush()\n }\n return dictionary\n}\n\nexport async function resolveDictionaryEntryValue(\n em: EntityManager,\n entryId: string | null | undefined,\n scope: { tenantId: string }\n): Promise<string | null> {\n if (!entryId) return null\n const entry = await em.findOne(DictionaryEntry, { id: entryId, tenantId: scope.tenantId })\n if (!entry) return null\n return entry.value?.trim() || null\n}\n\nexport { normalizeDictionaryValue, sanitizeDictionaryColor, sanitizeDictionaryIcon }\n\ntype SalesDictionarySeed = {\n value: string\n label: string\n color?: string | null\n icon?: string | null\n}\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nexport type BackfillDealLossReasonDictionaryResult = {\n dictionaryId: string\n createdDictionary: boolean\n copiedLegacyEntries: number\n seededDefaultEntries: number\n existingEntries: number\n}\n\nconst ORDER_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'draft', label: 'Draft', color: '#94a3b8', icon: 'lucide:file-pen-line' },\n { value: 'pending_approval', label: 'Pending Approval', color: '#f59e0b', icon: 'lucide:hourglass' },\n { value: 'approved', label: 'Approved', color: '#16a34a', icon: 'lucide:check-circle' },\n { value: 'rejected', label: 'Rejected', color: '#ef4444', icon: 'lucide:x-circle' },\n { value: 'sent', label: 'Sent', color: '#0ea5e9', icon: 'lucide:send' },\n { value: 'confirmed', label: 'Confirmed', color: '#2563eb', icon: 'lucide:badge-check' },\n { value: 'in_fulfillment', label: 'In fulfillment', color: '#f59e0b', icon: 'lucide:loader-2' },\n { value: 'fulfilled', label: 'Fulfilled', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'on_hold', label: 'On hold', color: '#a855f7', icon: 'lucide:pause-circle' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst ORDER_LINE_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock' },\n { value: 'allocated', label: 'Allocated', color: '#6366f1', icon: 'lucide:inbox' },\n { value: 'picking', label: 'Picking', color: '#f59e0b', icon: 'lucide:hand' },\n { value: 'packed', label: 'Packed', color: '#0ea5e9', icon: 'lucide:package' },\n { value: 'shipped', label: 'Shipped', color: '#2563eb', icon: 'lucide:truck' },\n { value: 'delivered', label: 'Delivered', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'backordered', label: 'Backordered', color: '#d946ef', icon: 'lucide:alert-octagon' },\n { value: 'returned', label: 'Returned', color: '#0d9488', icon: 'lucide:undo-2' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst SHIPMENT_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock-3' },\n { value: 'packed', label: 'Packed', color: '#22c55e', icon: 'lucide:package-check' },\n { value: 'shipped', label: 'Shipped', color: '#2563eb', icon: 'lucide:truck' },\n { value: 'in_transit', label: 'In transit', color: '#0ea5e9', icon: 'lucide:loader' },\n { value: 'delivered', label: 'Delivered', color: '#16a34a', icon: 'lucide:check-circle-2' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n { value: 'returned', label: 'Returned', color: '#0d9488', icon: 'lucide:undo-2' },\n]\n\nconst PAYMENT_STATUS_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'pending', label: 'Pending', color: '#94a3b8', icon: 'lucide:clock-3' },\n { value: 'authorized', label: 'Authorized', color: '#6366f1', icon: 'lucide:badge-check' },\n { value: 'captured', label: 'Captured', color: '#0ea5e9', icon: 'lucide:banknote' },\n { value: 'received', label: 'Received', color: '#16a34a', icon: 'lucide:check' },\n { value: 'refunded', label: 'Refunded', color: '#f59e0b', icon: 'lucide:rotate-ccw' },\n { value: 'failed', label: 'Failed', color: '#ef4444', icon: 'lucide:triangle-alert' },\n { value: 'canceled', label: 'Canceled', color: '#ef4444', icon: 'lucide:x-circle' },\n]\n\nconst ADJUSTMENT_KIND_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'discount', label: 'Discount' },\n { value: 'tax', label: 'Tax' },\n { value: 'shipping', label: 'Shipping' },\n { value: 'surcharge', label: 'Surcharge' },\n { value: 'return', label: 'Return' },\n { value: 'custom', label: 'Custom' },\n]\n\nasync function ensureSalesDictionaryEntry(\n em: EntityManager,\n scope: SeedScope,\n kind: SalesDictionaryKind,\n seed: SalesDictionarySeed\n): Promise<DictionaryEntry | null> {\n const value = seed.value?.trim()\n if (!value) return null\n const dictionary = await ensureSalesDictionary({\n em,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n kind,\n })\n const normalizedValue = normalizeDictionaryValue(value)\n const color = seed.color === undefined ? undefined : sanitizeDictionaryColor(seed.color)\n const icon = seed.icon === undefined ? undefined : sanitizeDictionaryIcon(seed.icon)\n const existing = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue,\n })\n if (existing) {\n let changed = false\n if (color !== undefined && existing.color !== color) {\n existing.color = color ?? null\n changed = true\n }\n if (icon !== undefined && existing.icon !== icon) {\n existing.icon = icon ?? null\n changed = true\n }\n if (changed) {\n existing.updatedAt = new Date()\n em.persist(existing)\n }\n return existing\n }\n const now = new Date()\n const entry = em.create(DictionaryEntry, {\n dictionary,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n value,\n label: seed.label?.trim() || value,\n normalizedValue,\n color: color ?? null,\n icon: icon ?? null,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entry)\n return entry\n}\n\nasync function seedSalesDictionary(\n em: EntityManager,\n scope: SeedScope,\n kind: SalesDictionaryKind,\n defaults: SalesDictionarySeed[]\n): Promise<void> {\n for (const seed of defaults) {\n await ensureSalesDictionaryEntry(em, scope, kind, seed)\n }\n}\n\nconst DEAL_LOSS_REASON_DEFAULTS: SalesDictionarySeed[] = [\n { value: 'lost_to_competitor', label: 'Lost to competitor', color: '#ef4444', icon: 'lucide:users' },\n { value: 'no_budget', label: 'No budget', color: '#f59e0b', icon: 'lucide:wallet' },\n { value: 'no_decision', label: 'No decision made', color: '#94a3b8', icon: 'lucide:help-circle' },\n { value: 'timing', label: 'Bad timing', color: '#6366f1', icon: 'lucide:clock' },\n { value: 'other', label: 'Other', color: '#71717a', icon: 'lucide:minus-circle' },\n]\n\nexport async function backfillDealLossReasonDictionary(\n em: EntityManager,\n scope: SeedScope\n): Promise<BackfillDealLossReasonDictionaryResult> {\n const definition = getSalesDictionaryDefinition('deal-loss-reasons')\n const existingDictionary = await em.findOne(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: definition.key,\n deletedAt: null,\n })\n const dictionary = existingDictionary ?? await ensureSalesDictionary({\n em,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n kind: 'deal-loss-reasons',\n })\n const targetEntries = await em.find(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n\n if (targetEntries.length > 0) {\n return {\n dictionaryId: dictionary.id,\n createdDictionary: !existingDictionary,\n copiedLegacyEntries: 0,\n seededDefaultEntries: 0,\n existingEntries: targetEntries.length,\n }\n }\n\n const legacyDictionary = await em.findOne(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: 'customer_lost_reason',\n deletedAt: null,\n })\n const legacyEntries = legacyDictionary\n ? await em.find(\n DictionaryEntry,\n {\n dictionary: legacyDictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n { orderBy: { position: 'asc', label: 'asc' } },\n )\n : []\n\n let copiedLegacyEntries = 0\n let seededDefaultEntries = 0\n const processedValues = new Set<string>()\n const seeds = legacyEntries.length\n ? legacyEntries.map((entry) => ({\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n }))\n : DEAL_LOSS_REASON_DEFAULTS\n\n for (const seed of seeds) {\n const normalizedValue = normalizeDictionaryValue(seed.value ?? '')\n if (!normalizedValue || processedValues.has(normalizedValue)) continue\n const entry = await ensureSalesDictionaryEntry(em, scope, 'deal-loss-reasons', seed)\n if (!entry) continue\n processedValues.add(normalizedValue)\n if (legacyEntries.length) copiedLegacyEntries += 1\n else seededDefaultEntries += 1\n }\n\n return {\n dictionaryId: dictionary.id,\n createdDictionary: !existingDictionary,\n copiedLegacyEntries,\n seededDefaultEntries,\n existingEntries: 0,\n }\n}\n\nexport async function seedSalesStatusDictionaries(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesDictionary(em, scope, 'order-status', ORDER_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'order-line-status', ORDER_LINE_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'shipment-status', SHIPMENT_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'payment-status', PAYMENT_STATUS_DEFAULTS)\n await seedSalesDictionary(em, scope, 'deal-loss-reasons', DEAL_LOSS_REASON_DEFAULTS)\n}\n\nexport async function seedSalesAdjustmentKinds(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesDictionary(em, scope, 'adjustment-kind', ADJUSTMENT_KIND_DEFAULTS)\n}\n\nexport async function seedSalesDictionaries(\n em: EntityManager,\n scope: SeedScope\n): Promise<void> {\n await seedSalesStatusDictionaries(em, scope)\n await seedSalesAdjustmentKinds(em, scope)\n}\n\nexport const DEFAULT_ADJUSTMENT_KIND_VALUES = ADJUSTMENT_KIND_DEFAULTS.map((entry) => entry.value)\n"],
5
+ "mappings": "AACA,SAAS,YAAY,uBAAuB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAmBP,MAAM,cAAsE;AAAA,EAC1E,gBAAgB;AAAA,IACd,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,qBAAqB;AAAA,IACnB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,mBAAmB;AAAA,IACjB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,kBAAkB;AAAA,IAChB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,qBAAqB;AAAA,IACnB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA,EACA,mBAAmB;AAAA,IACjB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AACF;AAEO,SAAS,6BAA6B,MAAsD;AACjG,SAAO,YAAY,IAAI;AACzB;AAEA,eAAsB,sBAAsB,QAKpB;AACtB,QAAM,EAAE,IAAI,UAAU,gBAAgB,KAAK,IAAI;AAC/C,QAAM,MAAM,6BAA6B,IAAI;AAC7C,MAAI,aAAa,MAAM,GAAG,QAAQ,YAAY;AAAA,IAC5C;AAAA,IACA;AAAA,IACA,KAAK,IAAI;AAAA,IACT,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,YAAY;AACf,iBAAa,GAAG,OAAO,YAAY;AAAA,MACjC;AAAA,MACA;AAAA,MACA,KAAK,IAAI;AAAA,MACT,MAAM,IAAI;AAAA,MACV,aAAa,IAAI;AAAA,MACjB,UAAU;AAAA,MACV,UAAU;AAAA,MACV,mBAAmB;AAAA,MACnB,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC;AACD,OAAG,QAAQ,UAAU;AACrB,UAAM,GAAG,MAAM;AAAA,EACjB;AACA,SAAO;AACT;AAEA,eAAsB,4BACpB,IACA,SACA,OACwB;AACxB,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,MAAM,GAAG,QAAQ,iBAAiB,EAAE,IAAI,SAAS,UAAU,MAAM,SAAS,CAAC;AACzF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,OAAO,KAAK,KAAK;AAChC;AAqBA,MAAM,wBAA+C;AAAA,EACnD,EAAE,OAAO,SAAS,OAAO,SAAS,OAAO,WAAW,MAAM,uBAAuB;AAAA,EACjF,EAAE,OAAO,oBAAoB,OAAO,oBAAoB,OAAO,WAAW,MAAM,mBAAmB;AAAA,EACnG,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,sBAAsB;AAAA,EACtF,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,kBAAkB;AAAA,EAClF,EAAE,OAAO,QAAQ,OAAO,QAAQ,OAAO,WAAW,MAAM,cAAc;AAAA,EACtE,EAAE,OAAO,aAAa,OAAO,aAAa,OAAO,WAAW,MAAM,qBAAqB;AAAA,EACvF,EAAE,OAAO,kBAAkB,OAAO,kBAAkB,OAAO,WAAW,MAAM,kBAAkB;AAAA,EAC9F,EAAE,OAAO,aAAa,OAAO,aAAa,OAAO,WAAW,MAAM,wBAAwB;AAAA,EAC1F,EAAE,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,MAAM,sBAAsB;AAAA,EACpF,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,kBAAkB;AACpF;AAEA,MAAM,6BAAoD;AAAA,EACxD,EAAE,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,MAAM,eAAe;AAAA,EAC7E,EAAE,OAAO,aAAa,OAAO,aAAa,OAAO,WAAW,MAAM,eAAe;AAAA,EACjF,EAAE,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,MAAM,cAAc;AAAA,EAC5E,EAAE,OAAO,UAAU,OAAO,UAAU,OAAO,WAAW,MAAM,iBAAiB;AAAA,EAC7E,EAAE,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,MAAM,eAAe;AAAA,EAC7E,EAAE,OAAO,aAAa,OAAO,aAAa,OAAO,WAAW,MAAM,wBAAwB;AAAA,EAC1F,EAAE,OAAO,eAAe,OAAO,eAAe,OAAO,WAAW,MAAM,uBAAuB;AAAA,EAC7F,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,gBAAgB;AAAA,EAChF,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,kBAAkB;AACpF;AAEA,MAAM,2BAAkD;AAAA,EACtD,EAAE,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,MAAM,iBAAiB;AAAA,EAC/E,EAAE,OAAO,UAAU,OAAO,UAAU,OAAO,WAAW,MAAM,uBAAuB;AAAA,EACnF,EAAE,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,MAAM,eAAe;AAAA,EAC7E,EAAE,OAAO,cAAc,OAAO,cAAc,OAAO,WAAW,MAAM,gBAAgB;AAAA,EACpF,EAAE,OAAO,aAAa,OAAO,aAAa,OAAO,WAAW,MAAM,wBAAwB;AAAA,EAC1F,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,kBAAkB;AAAA,EAClF,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,gBAAgB;AAClF;AAEA,MAAM,0BAAiD;AAAA,EACrD,EAAE,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,MAAM,iBAAiB;AAAA,EAC/E,EAAE,OAAO,cAAc,OAAO,cAAc,OAAO,WAAW,MAAM,qBAAqB;AAAA,EACzF,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,kBAAkB;AAAA,EAClF,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,eAAe;AAAA,EAC/E,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,oBAAoB;AAAA,EACpF,EAAE,OAAO,UAAU,OAAO,UAAU,OAAO,WAAW,MAAM,wBAAwB;AAAA,EACpF,EAAE,OAAO,YAAY,OAAO,YAAY,OAAO,WAAW,MAAM,kBAAkB;AACpF;AAEA,MAAM,2BAAkD;AAAA,EACtD,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,EACvC,EAAE,OAAO,OAAO,OAAO,MAAM;AAAA,EAC7B,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,EACvC,EAAE,OAAO,aAAa,OAAO,YAAY;AAAA,EACzC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,UAAU,OAAO,SAAS;AACrC;AAEA,eAAe,2BACb,IACA,OACA,MACA,MACiC;AACjC,QAAM,QAAQ,KAAK,OAAO,KAAK;AAC/B,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,aAAa,MAAM,sBAAsB;AAAA,IAC7C;AAAA,IACA,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB;AAAA,EACF,CAAC;AACD,QAAM,kBAAkB,yBAAyB,KAAK;AACtD,QAAM,QAAQ,KAAK,UAAU,SAAY,SAAY,wBAAwB,KAAK,KAAK;AACvF,QAAM,OAAO,KAAK,SAAS,SAAY,SAAY,uBAAuB,KAAK,IAAI;AACnF,QAAM,WAAW,MAAM,GAAG,QAAQ,iBAAiB;AAAA,IACjD;AAAA,IACA,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB;AAAA,EACF,CAAC;AACD,MAAI,UAAU;AACZ,QAAI,UAAU;AACd,QAAI,UAAU,UAAa,SAAS,UAAU,OAAO;AACnD,eAAS,QAAQ,SAAS;AAC1B,gBAAU;AAAA,IACZ;AACA,QAAI,SAAS,UAAa,SAAS,SAAS,MAAM;AAChD,eAAS,OAAO,QAAQ;AACxB,gBAAU;AAAA,IACZ;AACA,QAAI,SAAS;AACX,eAAS,YAAY,oBAAI,KAAK;AAC9B,SAAG,QAAQ,QAAQ;AAAA,IACrB;AACA,WAAO;AAAA,EACT;AACA,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,QAAQ,GAAG,OAAO,iBAAiB;AAAA,IACvC;AAAA,IACA,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,IAChB;AAAA,IACA,OAAO,KAAK,OAAO,KAAK,KAAK;AAAA,IAC7B;AAAA,IACA,OAAO,SAAS;AAAA,IAChB,MAAM,QAAQ;AAAA,IACd,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC;AACD,KAAG,QAAQ,KAAK;AAChB,SAAO;AACT;AAEA,eAAe,oBACb,IACA,OACA,MACA,UACe;AACf,aAAW,QAAQ,UAAU;AAC3B,UAAM,2BAA2B,IAAI,OAAO,MAAM,IAAI;AAAA,EACxD;AACF;AAEA,MAAM,4BAAmD;AAAA,EACvD,EAAE,OAAO,sBAAsB,OAAO,sBAAsB,OAAO,WAAW,MAAM,eAAe;AAAA,EACnG,EAAE,OAAO,aAAa,OAAO,aAAa,OAAO,WAAW,MAAM,gBAAgB;AAAA,EAClF,EAAE,OAAO,eAAe,OAAO,oBAAoB,OAAO,WAAW,MAAM,qBAAqB;AAAA,EAChG,EAAE,OAAO,UAAU,OAAO,cAAc,OAAO,WAAW,MAAM,eAAe;AAAA,EAC/E,EAAE,OAAO,SAAS,OAAO,SAAS,OAAO,WAAW,MAAM,sBAAsB;AAClF;AAEA,eAAsB,iCACpB,IACA,OACiD;AACjD,QAAM,aAAa,6BAA6B,mBAAmB;AACnE,QAAM,qBAAqB,MAAM,GAAG,QAAQ,YAAY;AAAA,IACtD,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,KAAK,WAAW;AAAA,IAChB,WAAW;AAAA,EACb,CAAC;AACD,QAAM,aAAa,sBAAsB,MAAM,sBAAsB;AAAA,IACnE;AAAA,IACA,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,MAAM;AAAA,EACR,CAAC;AACD,QAAM,gBAAgB,MAAM,GAAG,KAAK,iBAAiB;AAAA,IACnD;AAAA,IACA,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AAED,MAAI,cAAc,SAAS,GAAG;AAC5B,WAAO;AAAA,MACL,cAAc,WAAW;AAAA,MACzB,mBAAmB,CAAC;AAAA,MACpB,qBAAqB;AAAA,MACrB,sBAAsB;AAAA,MACtB,iBAAiB,cAAc;AAAA,IACjC;AAAA,EACF;AAEA,QAAM,mBAAmB,MAAM,GAAG,QAAQ,YAAY;AAAA,IACpD,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,KAAK;AAAA,IACL,WAAW;AAAA,EACb,CAAC;AACD,QAAM,gBAAgB,mBAClB,MAAM,GAAG;AAAA,IACT;AAAA,IACA;AAAA,MACE,YAAY;AAAA,MACZ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,IACxB;AAAA,IACA,EAAE,SAAS,EAAE,UAAU,OAAO,OAAO,MAAM,EAAE;AAAA,EAC/C,IACE,CAAC;AAEL,MAAI,sBAAsB;AAC1B,MAAI,uBAAuB;AAC3B,QAAM,kBAAkB,oBAAI,IAAY;AACxC,QAAM,QAAQ,cAAc,SACxB,cAAc,IAAI,CAAC,WAAW;AAAA,IAC9B,OAAO,MAAM;AAAA,IACb,OAAO,MAAM;AAAA,IACb,OAAO,MAAM;AAAA,IACb,MAAM,MAAM;AAAA,EACd,EAAE,IACA;AAEJ,aAAW,QAAQ,OAAO;AACxB,UAAM,kBAAkB,yBAAyB,KAAK,SAAS,EAAE;AACjE,QAAI,CAAC,mBAAmB,gBAAgB,IAAI,eAAe,EAAG;AAC9D,UAAM,QAAQ,MAAM,2BAA2B,IAAI,OAAO,qBAAqB,IAAI;AACnF,QAAI,CAAC,MAAO;AACZ,oBAAgB,IAAI,eAAe;AACnC,QAAI,cAAc,OAAQ,wBAAuB;AAAA,QAC5C,yBAAwB;AAAA,EAC/B;AAEA,SAAO;AAAA,IACL,cAAc,WAAW;AAAA,IACzB,mBAAmB,CAAC;AAAA,IACpB;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,EACnB;AACF;AAEA,eAAsB,4BACpB,IACA,OACe;AACf,QAAM,oBAAoB,IAAI,OAAO,gBAAgB,qBAAqB;AAC1E,QAAM,oBAAoB,IAAI,OAAO,qBAAqB,0BAA0B;AACpF,QAAM,oBAAoB,IAAI,OAAO,mBAAmB,wBAAwB;AAChF,QAAM,oBAAoB,IAAI,OAAO,kBAAkB,uBAAuB;AAC9E,QAAM,oBAAoB,IAAI,OAAO,qBAAqB,yBAAyB;AACrF;AAEA,eAAsB,yBACpB,IACA,OACe;AACf,QAAM,oBAAoB,IAAI,OAAO,mBAAmB,wBAAwB;AAClF;AAEA,eAAsB,sBACpB,IACA,OACe;AACf,QAAM,4BAA4B,IAAI,KAAK;AAC3C,QAAM,yBAAyB,IAAI,KAAK;AAC1C;AAEO,MAAM,iCAAiC,yBAAyB,IAAI,CAAC,UAAU,MAAM,KAAK;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6399.1.93ba8ca030",
3
+ "version": "0.6.6-develop.6402.1.080aab8ec9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -248,16 +248,16 @@
248
248
  "zod": "^4.4.3"
249
249
  },
250
250
  "peerDependencies": {
251
- "@open-mercato/ai-assistant": "0.6.6-develop.6399.1.93ba8ca030",
252
- "@open-mercato/shared": "0.6.6-develop.6399.1.93ba8ca030",
253
- "@open-mercato/ui": "0.6.6-develop.6399.1.93ba8ca030",
251
+ "@open-mercato/ai-assistant": "0.6.6-develop.6402.1.080aab8ec9",
252
+ "@open-mercato/shared": "0.6.6-develop.6402.1.080aab8ec9",
253
+ "@open-mercato/ui": "0.6.6-develop.6402.1.080aab8ec9",
254
254
  "react": "^19.0.0",
255
255
  "react-dom": "^19.0.0"
256
256
  },
257
257
  "devDependencies": {
258
- "@open-mercato/ai-assistant": "0.6.6-develop.6399.1.93ba8ca030",
259
- "@open-mercato/shared": "0.6.6-develop.6399.1.93ba8ca030",
260
- "@open-mercato/ui": "0.6.6-develop.6399.1.93ba8ca030",
258
+ "@open-mercato/ai-assistant": "0.6.6-develop.6402.1.080aab8ec9",
259
+ "@open-mercato/shared": "0.6.6-develop.6402.1.080aab8ec9",
260
+ "@open-mercato/ui": "0.6.6-develop.6402.1.080aab8ec9",
261
261
  "@testing-library/dom": "^10.4.1",
262
262
  "@testing-library/jest-dom": "^6.9.1",
263
263
  "@testing-library/react": "^16.3.1",
@@ -95,8 +95,9 @@ const settingsSectionOrder: Record<string, number> = {
95
95
  'customer-portal': 3,
96
96
  'data-designer': 4,
97
97
  'module-configs': 5,
98
- directory: 6,
99
- 'feature-toggles': 7,
98
+ currencies: 6,
99
+ directory: 7,
100
+ 'feature-toggles': 8,
100
101
  }
101
102
 
102
103
  type NavGroupWithWeight = Omit<BackendChromeNavGroup, 'id' | 'defaultName' | 'items'> & {
@@ -1,22 +1,11 @@
1
- import React from 'react'
2
-
3
- const exchangeRateIcon = React.createElement(
4
- 'svg',
5
- { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
6
- React.createElement('path', { d: 'M7 16l-4-4 4-4' }),
7
- React.createElement('path', { d: 'M3 12h11' }),
8
- React.createElement('path', { d: 'M17 8l4 4-4 4' }),
9
- React.createElement('path', { d: 'M21 12H10' }),
10
- )
11
-
12
1
  export const metadata = {
13
- icon: exchangeRateIcon,
2
+ icon: 'download',
14
3
  requireAuth: true,
15
4
  requireFeatures: ['currencies.fetch.view'],
16
- pageGroup: 'Module Configs',
17
- pageGroupKey: 'settings.sections.moduleConfigs',
5
+ pageGroup: 'Currencies',
6
+ pageGroupKey: 'currencies.nav.group',
18
7
  pageTitle: 'Currency Rate Fetching',
19
8
  pageTitleKey: 'currencies.fetch.title',
20
- pageOrder: 4,
9
+ pageOrder: 30,
21
10
  pageContext: 'settings' as const,
22
11
  }
@@ -5,8 +5,8 @@ export const metadata = {
5
5
  pageTitleKey: 'currencies.page.title',
6
6
  pageGroup: 'Currencies',
7
7
  pageGroupKey: 'currencies.nav.group',
8
- pagePriority: 60,
9
8
  pageOrder: 10,
9
+ pageContext: 'settings' as const,
10
10
  icon: 'coins',
11
11
  breadcrumb: [{ label: 'Currencies', labelKey: 'currencies.page.title' }],
12
12
  }
@@ -6,6 +6,7 @@ export const metadata = {
6
6
  pageGroup: 'Currencies',
7
7
  pageGroupKey: 'currencies.nav.group',
8
8
  pageOrder: 20,
9
+ pageContext: 'settings' as const,
9
10
  icon: 'refresh-cw',
10
11
  breadcrumb: [
11
12
  { label: 'Currencies', labelKey: 'currencies.page.title', href: '/backend/currencies' },
@@ -82,25 +82,21 @@ export function useDealClosure({
82
82
  async (input: { lossReasonId: string; lossNotes?: string }) => {
83
83
  if (!currentDealId) return
84
84
  if (!(await confirmDiscardIfDirty())) return
85
+ const lossPayload = {
86
+ id: currentDealId,
87
+ closureOutcome: 'lost' as const,
88
+ status: 'loose',
89
+ lossReasonId: input.lossReasonId,
90
+ ...(input.lossNotes ? { lossNotes: input.lossNotes } : {}),
91
+ }
85
92
  try {
86
93
  await runMutationWithContext(
87
94
  () => withScopedApiRequestHeaders(
88
95
  buildOptimisticLockHeader(dealUpdatedAt),
89
- () =>
90
- updateCrud('customers/deals', {
91
- id: currentDealId,
92
- closureOutcome: 'lost',
93
- status: 'loose',
94
- lossReasonId: input.lossReasonId,
95
- lossNotes: input.lossNotes ?? null,
96
- }),
96
+ () => updateCrud('customers/deals', lossPayload),
97
97
  ),
98
98
  {
99
- id: currentDealId,
100
- closureOutcome: 'lost',
101
- status: 'loose',
102
- lossReasonId: input.lossReasonId,
103
- lossNotes: input.lossNotes ?? null,
99
+ ...lossPayload,
104
100
  operation: 'closeLost',
105
101
  },
106
102
  )
@@ -40,18 +40,28 @@ export function ConfirmDealLostDialog({
40
40
  const [lossReasons, setLossReasons] = React.useState<LossReasonOption[]>([])
41
41
  const [reasonListOpen, setReasonListOpen] = React.useState(false)
42
42
  const [error, setError] = React.useState('')
43
+ const [dictionaryLoadFailed, setDictionaryLoadFailed] = React.useState(false)
44
+ const [isLoadingReasons, setIsLoadingReasons] = React.useState(false)
43
45
  const [isConfirming, setIsConfirming] = React.useState(false)
44
46
 
45
47
  React.useEffect(() => {
46
48
  if (!open) return
47
49
  let cancelled = false
50
+ setIsLoadingReasons(true)
51
+ setDictionaryLoadFailed(false)
48
52
  loadDictionaryEntriesByKey('sales.deal_loss_reason')
49
53
  .then((items) => {
50
54
  if (!cancelled) setLossReasons(items)
51
55
  })
52
56
  .catch((loadError) => {
53
57
  console.error('customers.deals.detail.lossReasons failed', loadError)
54
- if (!cancelled) setLossReasons([])
58
+ if (!cancelled) {
59
+ setLossReasons([])
60
+ setDictionaryLoadFailed(true)
61
+ }
62
+ })
63
+ .finally(() => {
64
+ if (!cancelled) setIsLoadingReasons(false)
55
65
  })
56
66
  return () => {
57
67
  cancelled = true
@@ -64,14 +74,35 @@ export function ConfirmDealLostDialog({
64
74
  setLossNotes('')
65
75
  setReasonListOpen(false)
66
76
  setError('')
77
+ setDictionaryLoadFailed(false)
78
+ setIsConfirming(false)
67
79
  }, [open])
68
80
 
69
81
  const selectedLossReason = React.useMemo(
70
82
  () => lossReasons.find((reason) => reason.id === lossReasonId) ?? null,
71
83
  [lossReasonId, lossReasons],
72
84
  )
85
+ const hasLossReasons = lossReasons.length > 0
86
+ const reasonUnavailable = !isLoadingReasons && !hasLossReasons
87
+ const unavailableReasonText = dictionaryLoadFailed
88
+ ? t('customers.deals.detail.lost.reasonLoadError', 'Loss reasons could not be loaded.')
89
+ : t('customers.deals.detail.lost.reasonUnavailable', 'No loss reasons are configured.')
90
+ const reasonHelpText = React.useMemo(() => {
91
+ if (selectedLossReason?.description) return selectedLossReason.description
92
+ if (isLoadingReasons) {
93
+ return t('customers.deals.detail.lost.reasonLoading', 'Loading loss reasons...')
94
+ }
95
+ if (reasonUnavailable) {
96
+ return unavailableReasonText
97
+ }
98
+ return t('customers.deals.detail.lost.reasonHelp', 'Choose the closest reason from the dictionary.')
99
+ }, [isLoadingReasons, reasonUnavailable, selectedLossReason?.description, t, unavailableReasonText])
73
100
 
74
101
  const handleConfirm = React.useCallback(async () => {
102
+ if (isLoadingReasons || reasonUnavailable) {
103
+ setError(unavailableReasonText)
104
+ return
105
+ }
75
106
  if (!lossReasonId) {
76
107
  setError(t('customers.deals.detail.lost.reasonRequired', 'Please select a loss reason'))
77
108
  return
@@ -85,11 +116,13 @@ export function ConfirmDealLostDialog({
85
116
  } finally {
86
117
  setIsConfirming(false)
87
118
  }
88
- }, [lossNotes, lossReasonId, onConfirm, t])
119
+ }, [isLoadingReasons, lossNotes, lossReasonId, onConfirm, reasonUnavailable, t, unavailableReasonText])
120
+
121
+ const confirmDisabled = isConfirming || isLoadingReasons || reasonUnavailable || !lossReasonId
89
122
 
90
123
  const handleKeyDown = useDialogKeyHandler({
91
124
  onConfirm: () => void handleConfirm(),
92
- disabled: isConfirming,
125
+ disabled: confirmDisabled,
93
126
  })
94
127
 
95
128
  return (
@@ -138,10 +171,13 @@ export function ConfirmDealLostDialog({
138
171
  >
139
172
  <div className="min-w-0">
140
173
  <div className="truncate text-base font-semibold text-foreground">
141
- {selectedLossReason?.label ?? t('customers.deals.detail.lost.reasonPlaceholder', 'Select loss reason')}
174
+ {selectedLossReason?.label
175
+ ?? (isLoadingReasons
176
+ ? t('customers.deals.detail.lost.reasonLoadingShort', 'Loading...')
177
+ : t('customers.deals.detail.lost.reasonPlaceholder', 'Select loss reason'))}
142
178
  </div>
143
179
  <div className="truncate text-sm text-muted-foreground">
144
- {selectedLossReason?.description ?? t('customers.deals.detail.lost.reasonHelp', 'Choose the closest reason from the dictionary.')}
180
+ {reasonHelpText}
145
181
  </div>
146
182
  </div>
147
183
  <ChevronDown className="ml-3 size-4 shrink-0 text-muted-foreground" />
@@ -149,7 +185,7 @@ export function ConfirmDealLostDialog({
149
185
 
150
186
  {reasonListOpen ? (
151
187
  <div className="overflow-hidden rounded-md border border-border/80 bg-background">
152
- {lossReasons.map((reason, index) => {
188
+ {hasLossReasons ? lossReasons.map((reason, index) => {
153
189
  const isSelected = reason.id === lossReasonId
154
190
  return (
155
191
  <Button
@@ -178,10 +214,19 @@ export function ConfirmDealLostDialog({
178
214
  ) : null}
179
215
  </Button>
180
216
  )
181
- })}
217
+ }) : (
218
+ <div className="px-4 py-3 text-sm text-muted-foreground">
219
+ {unavailableReasonText}
220
+ </div>
221
+ )}
182
222
  </div>
183
223
  ) : null}
184
224
  </div>
225
+ {reasonUnavailable ? (
226
+ <p className="text-xs text-destructive">
227
+ {unavailableReasonText}
228
+ </p>
229
+ ) : null}
185
230
  {error ? <p className="text-xs text-destructive">{error}</p> : null}
186
231
  </div>
187
232
 
@@ -203,7 +248,12 @@ export function ConfirmDealLostDialog({
203
248
  <Button type="button" variant="outline" onClick={onClose}>
204
249
  {t('customers.deals.detail.lost.cancel', 'Cancel')}
205
250
  </Button>
206
- <Button type="button" variant="destructive" onClick={() => { void handleConfirm() }}>
251
+ <Button
252
+ type="button"
253
+ variant="destructive"
254
+ disabled={confirmDisabled}
255
+ onClick={() => { void handleConfirm() }}
256
+ >
207
257
  {t('customers.deals.detail.lost.confirm', 'Mark as Lost')}
208
258
  </Button>
209
259
  </DialogFooter>
@@ -992,8 +992,12 @@
992
992
  "customers.deals.detail.lost.reasonFallbackDescription": "No description available.",
993
993
  "customers.deals.detail.lost.reasonHelp": "Choose the closest reason from the dictionary.",
994
994
  "customers.deals.detail.lost.reasonLabel": "Loss reason",
995
+ "customers.deals.detail.lost.reasonLoadError": "Loss reasons could not be loaded.",
996
+ "customers.deals.detail.lost.reasonLoading": "Loading loss reasons...",
997
+ "customers.deals.detail.lost.reasonLoadingShort": "Loading...",
995
998
  "customers.deals.detail.lost.reasonPlaceholder": "Select loss reason",
996
999
  "customers.deals.detail.lost.reasonRequired": "Please select a loss reason",
1000
+ "customers.deals.detail.lost.reasonUnavailable": "No loss reasons are configured.",
997
1001
  "customers.deals.detail.lost.salesCycle": "Sales cycle",
998
1002
  "customers.deals.detail.lost.secondaryAction": "Set reminder for Q3",
999
1003
  "customers.deals.detail.lost.title": "Mark deal as Lost?",
@@ -992,8 +992,12 @@
992
992
  "customers.deals.detail.lost.reasonFallbackDescription": "No description available.",
993
993
  "customers.deals.detail.lost.reasonHelp": "Choose the closest reason from the dictionary.",
994
994
  "customers.deals.detail.lost.reasonLabel": "Loss reason",
995
+ "customers.deals.detail.lost.reasonLoadError": "Loss reasons could not be loaded.",
996
+ "customers.deals.detail.lost.reasonLoading": "Loading loss reasons...",
997
+ "customers.deals.detail.lost.reasonLoadingShort": "Loading...",
995
998
  "customers.deals.detail.lost.reasonPlaceholder": "Select loss reason",
996
999
  "customers.deals.detail.lost.reasonRequired": "Please select a loss reason",
1000
+ "customers.deals.detail.lost.reasonUnavailable": "No loss reasons are configured.",
997
1001
  "customers.deals.detail.lost.salesCycle": "Sales cycle",
998
1002
  "customers.deals.detail.lost.secondaryAction": "Set reminder for Q3",
999
1003
  "customers.deals.detail.lost.title": "Mark deal as Lost?",
@@ -992,8 +992,12 @@
992
992
  "customers.deals.detail.lost.reasonFallbackDescription": "No description available.",
993
993
  "customers.deals.detail.lost.reasonHelp": "Choose the closest reason from the dictionary.",
994
994
  "customers.deals.detail.lost.reasonLabel": "Loss reason",
995
+ "customers.deals.detail.lost.reasonLoadError": "Loss reasons could not be loaded.",
996
+ "customers.deals.detail.lost.reasonLoading": "Loading loss reasons...",
997
+ "customers.deals.detail.lost.reasonLoadingShort": "Loading...",
995
998
  "customers.deals.detail.lost.reasonPlaceholder": "Select loss reason",
996
999
  "customers.deals.detail.lost.reasonRequired": "Please select a loss reason",
1000
+ "customers.deals.detail.lost.reasonUnavailable": "No loss reasons are configured.",
997
1001
  "customers.deals.detail.lost.salesCycle": "Sales cycle",
998
1002
  "customers.deals.detail.lost.secondaryAction": "Set reminder for Q3",
999
1003
  "customers.deals.detail.lost.title": "Mark deal as Lost?",
@@ -992,8 +992,12 @@
992
992
  "customers.deals.detail.lost.reasonFallbackDescription": "No description available.",
993
993
  "customers.deals.detail.lost.reasonHelp": "Choose the closest reason from the dictionary.",
994
994
  "customers.deals.detail.lost.reasonLabel": "Loss reason",
995
+ "customers.deals.detail.lost.reasonLoadError": "Loss reasons could not be loaded.",
996
+ "customers.deals.detail.lost.reasonLoading": "Loading loss reasons...",
997
+ "customers.deals.detail.lost.reasonLoadingShort": "Loading...",
995
998
  "customers.deals.detail.lost.reasonPlaceholder": "Select loss reason",
996
999
  "customers.deals.detail.lost.reasonRequired": "Please select a loss reason",
1000
+ "customers.deals.detail.lost.reasonUnavailable": "No loss reasons are configured.",
997
1001
  "customers.deals.detail.lost.salesCycle": "Sales cycle",
998
1002
  "customers.deals.detail.lost.secondaryAction": "Set reminder for Q3",
999
1003
  "customers.deals.detail.lost.title": "Mark deal as Lost?",
@@ -5,9 +5,12 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { CustomFieldDef, CustomFieldEntityConfig } from '@open-mercato/core/modules/entities/data/entities'
6
6
  import { customFieldEntityConfigSchema, upsertCustomFieldDefSchema } from '@open-mercato/core/modules/entities/data/validators'
7
7
  import { z } from 'zod'
8
+ import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
9
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
10
  import { invalidateDefinitionsCache } from './definitions.cache'
9
11
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
10
12
  import { mergeEntityFieldsetConfig, normalizeEntityFieldsetConfig } from '../lib/fieldsets'
13
+ import { resolveEntityDefinitionsVersion } from '../lib/definitions-version'
11
14
  import {
12
15
  beginEntitiesMutationGuard,
13
16
  FIELD_DEFINITION_RESOURCE_KIND,
@@ -91,6 +94,31 @@ export async function POST(req: Request) {
91
94
  })
92
95
  if (guard.blockedResponse) return guard.blockedResponse
93
96
 
97
+ // Optimistic locking (issue #3152): the batch upserts a whole definition set,
98
+ // so guard the entity's aggregate schema version (the newest updated_at across
99
+ // its field definitions and fieldset config). A stale save — another tab changed
100
+ // the schema after this form loaded — fails with the same structured 409 the CRUD
101
+ // path returns. Strictly additive: callers that do not send the expected version
102
+ // header pass through unchanged.
103
+ const currentVersion = await resolveEntityDefinitionsVersion(em, {
104
+ entityId,
105
+ tenantId: scope.tenantId,
106
+ organizationId: scope.organizationId,
107
+ })
108
+ try {
109
+ enforceCommandOptimisticLock({
110
+ resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
111
+ resourceId: entityId,
112
+ current: currentVersion,
113
+ request: req,
114
+ })
115
+ } catch (err) {
116
+ if (isCrudHttpError(err) && err.status === 409) {
117
+ return NextResponse.json(err.body, { status: err.status })
118
+ }
119
+ throw err
120
+ }
121
+
94
122
  await em.begin()
95
123
  try {
96
124
  // Prefetch every existing definition for this entity in a single query, then index
@@ -206,11 +234,20 @@ export async function POST(req: Request) {
206
234
  entityIds: [entityId],
207
235
  })
208
236
 
209
- return NextResponse.json({ ok: true })
237
+ // Return the post-save version so the form can round-trip the token for a
238
+ // subsequent save (reorder, delete, second save) without a false conflict.
239
+ const nextVersion = await resolveEntityDefinitionsVersion(em, {
240
+ entityId,
241
+ tenantId: scope.tenantId,
242
+ organizationId: scope.organizationId,
243
+ })
244
+
245
+ return NextResponse.json({ ok: true, version: nextVersion })
210
246
  }
211
247
 
212
248
  const batchResponseSchema = z.object({
213
249
  ok: z.literal(true),
250
+ version: z.string().nullable().optional(),
214
251
  })
215
252
 
216
253
  export const openApi: OpenApiRouteDoc = {
@@ -5,6 +5,8 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'
6
6
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
7
7
  import { loadEntityFieldsetConfigs } from '../lib/fieldsets'
8
+ import { resolveDefinitionMutationScope } from '../lib/definition-scope'
9
+ import { resolveEntityDefinitionsVersion } from '../lib/definitions-version'
8
10
 
9
11
  export const metadata = {
10
12
  GET: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
@@ -17,8 +19,12 @@ export async function GET(req: Request) {
17
19
  const auth = await getAuthFromRequest(req)
18
20
  if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
19
21
 
20
- const { resolve } = await createRequestContainer()
22
+ const container = await createRequestContainer()
23
+ const { resolve } = container
21
24
  const em = resolve('em') as any
25
+ // Resolve the same definition scope the mutating endpoints use so the version
26
+ // token round-trips consistently (issue #3152).
27
+ const versionScope = await resolveDefinitionMutationScope({ auth, container, request: req })
22
28
  // Load all scoped records (active/inactive/deleted) so that per-scope tombstones
23
29
  // can shadow global definitions. We'll filter out deleted winners later.
24
30
  const [defs, tombstones, configMap] = await Promise.all([
@@ -84,11 +90,17 @@ export async function GET(req: Request) {
84
90
  }))
85
91
  const deletedKeys = Array.from(tombstonedKeys)
86
92
  const cfg = configMap.get(entityId) ?? { fieldsets: [], singleFieldsetPerRecord: true }
93
+ const version = await resolveEntityDefinitionsVersion(em, {
94
+ entityId,
95
+ tenantId: versionScope.tenantId,
96
+ organizationId: versionScope.organizationId,
97
+ })
87
98
  return NextResponse.json({
88
99
  items,
89
100
  deletedKeys,
90
101
  fieldsets: cfg.fieldsets,
91
102
  settings: { singleFieldsetPerRecord: cfg.singleFieldsetPerRecord },
103
+ version,
92
104
  })
93
105
  }
94
106
 
@@ -125,6 +137,7 @@ const definitionsManageResponseSchema = z.object({
125
137
  deletedKeys: z.array(z.string()),
126
138
  fieldsets: z.array(manageFieldsetSchema).optional(),
127
139
  settings: z.object({ singleFieldsetPerRecord: z.boolean().optional() }).optional(),
140
+ version: z.string().nullable().optional(),
128
141
  })
129
142
 
130
143
  export const openApi: OpenApiRouteDoc = {