@open-mercato/core 0.6.7-develop.6648.1.61f5523138 → 0.6.7-develop.6650.1.df84bf47a1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/sales/commands/documents.js +5 -4
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/lib/dictionaries.js +27 -2
- package/dist/modules/sales/lib/dictionaries.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/sales/commands/documents.ts +10 -4
- package/src/modules/sales/lib/dictionaries.ts +41 -2
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
sanitizeDictionaryColor,
|
|
5
5
|
sanitizeDictionaryIcon
|
|
6
6
|
} from "@open-mercato/core/modules/dictionaries/lib/utils";
|
|
7
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
8
|
+
import { buildRecordTag } from "@open-mercato/shared/lib/crud/cache";
|
|
7
9
|
const DEFINITIONS = {
|
|
8
10
|
"order-status": {
|
|
9
11
|
key: "sales.order_status",
|
|
@@ -87,8 +89,30 @@ async function ensureSalesDictionary(params) {
|
|
|
87
89
|
async function resolveDictionaryEntryValue(em, entryId, scope) {
|
|
88
90
|
if (!entryId) return null;
|
|
89
91
|
const entry = await em.findOne(DictionaryEntry, { id: entryId, tenantId: scope.tenantId });
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
return entry?.value?.trim() || null;
|
|
93
|
+
}
|
|
94
|
+
const DICTIONARY_ENTRY_VALUE_CACHE_TTL_MS = 6e4;
|
|
95
|
+
const DICTIONARY_ENTRY_RESOURCE_KIND = "dictionaries.entry";
|
|
96
|
+
const dictionaryEntryValueCacheKey = (entryId) => `sales:dictionary-entry-value:${entryId}`;
|
|
97
|
+
const dictionaryEntryValueCacheTags = (entryId, tenantId) => [
|
|
98
|
+
buildRecordTag(DICTIONARY_ENTRY_RESOURCE_KIND, tenantId, entryId)
|
|
99
|
+
];
|
|
100
|
+
async function resolveCachedDictionaryEntryValue(em, entryId, scope, cache) {
|
|
101
|
+
if (!entryId) return null;
|
|
102
|
+
if (!cache) return resolveDictionaryEntryValue(em, entryId, scope);
|
|
103
|
+
return runWithCacheTenant(scope.tenantId, async () => {
|
|
104
|
+
const cacheKey = dictionaryEntryValueCacheKey(entryId);
|
|
105
|
+
const cached = await cache.get(cacheKey);
|
|
106
|
+
if (typeof cached === "string") return cached;
|
|
107
|
+
const value = await resolveDictionaryEntryValue(em, entryId, scope);
|
|
108
|
+
if (value != null) {
|
|
109
|
+
await cache.set(cacheKey, value, {
|
|
110
|
+
ttl: DICTIONARY_ENTRY_VALUE_CACHE_TTL_MS,
|
|
111
|
+
tags: dictionaryEntryValueCacheTags(entryId, scope.tenantId)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
});
|
|
92
116
|
}
|
|
93
117
|
const ORDER_STATUS_DEFAULTS = [
|
|
94
118
|
{ value: "draft", label: "Draft", color: "#94a3b8", icon: "lucide:file-pen-line" },
|
|
@@ -291,6 +315,7 @@ export {
|
|
|
291
315
|
ensureSalesDictionary,
|
|
292
316
|
getSalesDictionaryDefinition,
|
|
293
317
|
normalizeDictionaryValue,
|
|
318
|
+
resolveCachedDictionaryEntryValue,
|
|
294
319
|
resolveDictionaryEntryValue,
|
|
295
320
|
sanitizeDictionaryColor,
|
|
296
321
|
sanitizeDictionaryIcon,
|
|
@@ -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\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;
|
|
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'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { buildRecordTag } from '@open-mercato/shared/lib/crud/cache'\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 return entry?.value?.trim() || null\n}\n\n// Read-through cache in front of `resolveDictionaryEntryValue`, for the hot order-write path: the same\n// few status / fulfillment / payment entry ids are re-resolved for EVERY order, so a bulk import\n// re-reads them per order \u2014 a measured N+1 that dominates once the aggregate write itself is cheap.\n// The value is non-PII config, tenant-scoped via `runWithCacheTenant` and short-TTL'd. It is tagged with\n// the platform CRUD record tag for `dictionaries.entry`, so the command bus's own `invalidateCrudCache`\n// (fired on every entry create/update/delete) drops this entry on edit \u2014 the 60s TTL is only a backstop.\n// Callers pass the (soft-resolved) cache; `undefined` reads straight through.\nconst DICTIONARY_ENTRY_VALUE_CACHE_TTL_MS = 60_000\nconst DICTIONARY_ENTRY_RESOURCE_KIND = 'dictionaries.entry'\nconst dictionaryEntryValueCacheKey = (entryId: string): string => `sales:dictionary-entry-value:${entryId}`\nconst dictionaryEntryValueCacheTags = (entryId: string, tenantId: string): string[] => [\n buildRecordTag(DICTIONARY_ENTRY_RESOURCE_KIND, tenantId, entryId),\n]\n\nexport async function resolveCachedDictionaryEntryValue(\n em: EntityManager,\n entryId: string | null | undefined,\n scope: { tenantId: string },\n cache: CacheStrategy | undefined\n): Promise<string | null> {\n if (!entryId) return null\n if (!cache) return resolveDictionaryEntryValue(em, entryId, scope)\n return runWithCacheTenant(scope.tenantId, async () => {\n const cacheKey = dictionaryEntryValueCacheKey(entryId)\n const cached = await cache.get(cacheKey)\n if (typeof cached === 'string') return cached\n const value = await resolveDictionaryEntryValue(em, entryId, scope)\n if (value != null) {\n await cache.set(cacheKey, value, {\n ttl: DICTIONARY_ENTRY_VALUE_CACHE_TTL_MS,\n tags: dictionaryEntryValueCacheTags(entryId, scope.tenantId),\n })\n }\n return value\n })\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": "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",
|
|
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.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6650.1.df84bf47a1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -254,16 +254,16 @@
|
|
|
254
254
|
"zod": "^4.4.3"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
258
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6650.1.df84bf47a1",
|
|
258
|
+
"@open-mercato/shared": "0.6.7-develop.6650.1.df84bf47a1",
|
|
259
|
+
"@open-mercato/ui": "0.6.7-develop.6650.1.df84bf47a1",
|
|
260
260
|
"react": "^19.0.0",
|
|
261
261
|
"react-dom": "^19.0.0"
|
|
262
262
|
},
|
|
263
263
|
"devDependencies": {
|
|
264
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
265
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6650.1.df84bf47a1",
|
|
265
|
+
"@open-mercato/shared": "0.6.7-develop.6650.1.df84bf47a1",
|
|
266
|
+
"@open-mercato/ui": "0.6.7-develop.6650.1.df84bf47a1",
|
|
267
267
|
"@testing-library/dom": "^10.4.1",
|
|
268
268
|
"@testing-library/jest-dom": "^6.9.1",
|
|
269
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -126,7 +126,8 @@ import {
|
|
|
126
126
|
type SalesDocumentCalculationResult,
|
|
127
127
|
} from "../lib/types";
|
|
128
128
|
import { loadShippedQuantityByLine } from "../lib/shipments/snapshots";
|
|
129
|
-
import { resolveDictionaryEntryValue } from "../lib/dictionaries";
|
|
129
|
+
import { resolveDictionaryEntryValue, resolveCachedDictionaryEntryValue } from "../lib/dictionaries";
|
|
130
|
+
import type { CacheStrategy } from "@open-mercato/cache";
|
|
130
131
|
import { resolveStatusEntryIdByValue } from "../lib/statusHelpers";
|
|
131
132
|
import { SalesDocumentNumberGenerator } from "../services/salesDocumentNumberGenerator";
|
|
132
133
|
import { loadSalesSettings } from "./settings";
|
|
@@ -5464,10 +5465,15 @@ const createOrderCommand: CommandHandler<
|
|
|
5464
5465
|
}
|
|
5465
5466
|
ensureOrderScope(ctx, parsed.organizationId, parsed.tenantId);
|
|
5466
5467
|
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
5468
|
+
// Soft-resolve the cache so the per-order status/fulfillment/payment dictionary lookups are
|
|
5469
|
+
// memoized across a bulk import; degrades to a straight EM read when no cache is registered.
|
|
5470
|
+
const dictionaryCache = ctx.container.resolve("cache", { allowUnregistered: true }) as
|
|
5471
|
+
| CacheStrategy
|
|
5472
|
+
| undefined;
|
|
5467
5473
|
const [status, fulfillmentStatus, paymentStatus] = await Promise.all([
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5474
|
+
resolveCachedDictionaryEntryValue(em, parsed.statusEntryId ?? null, { tenantId: parsed.tenantId }, dictionaryCache),
|
|
5475
|
+
resolveCachedDictionaryEntryValue(em, parsed.fulfillmentStatusEntryId ?? null, { tenantId: parsed.tenantId }, dictionaryCache),
|
|
5476
|
+
resolveCachedDictionaryEntryValue(em, parsed.paymentStatusEntryId ?? null, { tenantId: parsed.tenantId }, dictionaryCache),
|
|
5471
5477
|
]);
|
|
5472
5478
|
const {
|
|
5473
5479
|
customerSnapshot: resolvedCustomerSnapshot,
|
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
sanitizeDictionaryColor,
|
|
6
6
|
sanitizeDictionaryIcon,
|
|
7
7
|
} from '@open-mercato/core/modules/dictionaries/lib/utils'
|
|
8
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
9
|
+
import type { CacheStrategy } from '@open-mercato/cache'
|
|
10
|
+
import { buildRecordTag } from '@open-mercato/shared/lib/crud/cache'
|
|
8
11
|
|
|
9
12
|
export type SalesDictionaryKind =
|
|
10
13
|
| 'order-status'
|
|
@@ -118,8 +121,44 @@ export async function resolveDictionaryEntryValue(
|
|
|
118
121
|
): Promise<string | null> {
|
|
119
122
|
if (!entryId) return null
|
|
120
123
|
const entry = await em.findOne(DictionaryEntry, { id: entryId, tenantId: scope.tenantId })
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
return entry?.value?.trim() || null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Read-through cache in front of `resolveDictionaryEntryValue`, for the hot order-write path: the same
|
|
128
|
+
// few status / fulfillment / payment entry ids are re-resolved for EVERY order, so a bulk import
|
|
129
|
+
// re-reads them per order — a measured N+1 that dominates once the aggregate write itself is cheap.
|
|
130
|
+
// The value is non-PII config, tenant-scoped via `runWithCacheTenant` and short-TTL'd. It is tagged with
|
|
131
|
+
// the platform CRUD record tag for `dictionaries.entry`, so the command bus's own `invalidateCrudCache`
|
|
132
|
+
// (fired on every entry create/update/delete) drops this entry on edit — the 60s TTL is only a backstop.
|
|
133
|
+
// Callers pass the (soft-resolved) cache; `undefined` reads straight through.
|
|
134
|
+
const DICTIONARY_ENTRY_VALUE_CACHE_TTL_MS = 60_000
|
|
135
|
+
const DICTIONARY_ENTRY_RESOURCE_KIND = 'dictionaries.entry'
|
|
136
|
+
const dictionaryEntryValueCacheKey = (entryId: string): string => `sales:dictionary-entry-value:${entryId}`
|
|
137
|
+
const dictionaryEntryValueCacheTags = (entryId: string, tenantId: string): string[] => [
|
|
138
|
+
buildRecordTag(DICTIONARY_ENTRY_RESOURCE_KIND, tenantId, entryId),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
export async function resolveCachedDictionaryEntryValue(
|
|
142
|
+
em: EntityManager,
|
|
143
|
+
entryId: string | null | undefined,
|
|
144
|
+
scope: { tenantId: string },
|
|
145
|
+
cache: CacheStrategy | undefined
|
|
146
|
+
): Promise<string | null> {
|
|
147
|
+
if (!entryId) return null
|
|
148
|
+
if (!cache) return resolveDictionaryEntryValue(em, entryId, scope)
|
|
149
|
+
return runWithCacheTenant(scope.tenantId, async () => {
|
|
150
|
+
const cacheKey = dictionaryEntryValueCacheKey(entryId)
|
|
151
|
+
const cached = await cache.get(cacheKey)
|
|
152
|
+
if (typeof cached === 'string') return cached
|
|
153
|
+
const value = await resolveDictionaryEntryValue(em, entryId, scope)
|
|
154
|
+
if (value != null) {
|
|
155
|
+
await cache.set(cacheKey, value, {
|
|
156
|
+
ttl: DICTIONARY_ENTRY_VALUE_CACHE_TTL_MS,
|
|
157
|
+
tags: dictionaryEntryValueCacheTags(entryId, scope.tenantId),
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
return value
|
|
161
|
+
})
|
|
123
162
|
}
|
|
124
163
|
|
|
125
164
|
export { normalizeDictionaryValue, sanitizeDictionaryColor, sanitizeDictionaryIcon }
|