@open-mercato/core 0.8.1-develop.7228.1.b7ea09d2d3 → 0.8.1-develop.7238.1.3abe669ea5
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/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/availability_policy/index.js +41 -0
- package/dist/generated/entities/availability_policy/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +4 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +21 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -3
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/availability/acl.js +16 -0
- package/dist/modules/availability/acl.js.map +7 -0
- package/dist/modules/availability/api/check/route.js +95 -0
- package/dist/modules/availability/api/check/route.js.map +7 -0
- package/dist/modules/availability/api/openapi.js +27 -0
- package/dist/modules/availability/api/openapi.js.map +7 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js +58 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js.map +7 -0
- package/dist/modules/availability/api/policies/route.js +188 -0
- package/dist/modules/availability/api/policies/route.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.js +185 -0
- package/dist/modules/availability/backend/availability/check/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js +79 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js +53 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js +152 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js +94 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js +75 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.js +176 -0
- package/dist/modules/availability/backend/availability/policies/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js.map +7 -0
- package/dist/modules/availability/commands/policies.js +312 -0
- package/dist/modules/availability/commands/policies.js.map +7 -0
- package/dist/modules/availability/commands/scope.js +19 -0
- package/dist/modules/availability/commands/scope.js.map +7 -0
- package/dist/modules/availability/data/entities.js +95 -0
- package/dist/modules/availability/data/entities.js.map +7 -0
- package/dist/modules/availability/data/validators.js +53 -0
- package/dist/modules/availability/data/validators.js.map +7 -0
- package/dist/modules/availability/di.js +41 -0
- package/dist/modules/availability/di.js.map +7 -0
- package/dist/modules/availability/events.js +18 -0
- package/dist/modules/availability/events.js.map +7 -0
- package/dist/modules/availability/index.js +15 -0
- package/dist/modules/availability/index.js.map +7 -0
- package/dist/modules/availability/lib/policyResolution.js +155 -0
- package/dist/modules/availability/lib/policyResolution.js.map +7 -0
- package/dist/modules/availability/lib/tryResolve.js +11 -0
- package/dist/modules/availability/lib/tryResolve.js.map +7 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js +16 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js.map +7 -0
- package/dist/modules/availability/setup.js +12 -0
- package/dist/modules/availability/setup.js.map +7 -0
- package/dist/modules/entities/cli.js +33 -12
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +7 -32
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +1 -1
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/inbox_ops/lib/ensure-settings.js +47 -0
- package/dist/modules/inbox_ops/lib/ensure-settings.js.map +7 -0
- package/dist/modules/inbox_ops/lib/executionEngine.js.map +1 -1
- package/dist/modules/inbox_ops/lib/executionHelpers.js +0 -23
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js +0 -3
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js.map +2 -2
- package/dist/modules/inbox_ops/setup.js +2 -21
- package/dist/modules/inbox_ops/setup.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +1 -1
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +7 -0
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/data/entities.js +3 -1
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/inbox-actions.js +0 -12
- package/dist/modules/sales/inbox-actions.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js +20 -0
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js.map +7 -0
- package/dist/modules/wms/di.js +3 -0
- package/dist/modules/wms/di.js.map +2 -2
- package/dist/modules/wms/lib/availabilityCache.js +85 -0
- package/dist/modules/wms/lib/availabilityCache.js.map +7 -0
- package/dist/modules/wms/lib/availabilityCalculation.js +178 -0
- package/dist/modules/wms/lib/availabilityCalculation.js.map +7 -0
- package/dist/modules/wms/lib/availabilityProvider.js +14 -0
- package/dist/modules/wms/lib/availabilityProvider.js.map +7 -0
- package/dist/modules/wms/lib/sqlInClause.js +9 -0
- package/dist/modules/wms/lib/sqlInClause.js.map +7 -0
- package/dist/modules/wms/lib/tryResolve.js +11 -0
- package/dist/modules/wms/lib/tryResolve.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js +16 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js.map +7 -0
- package/generated/entities/availability_policy/index.ts +19 -0
- package/generated/entities.ids.generated.ts +4 -0
- package/generated/entity-fields-registry.ts +21 -0
- package/package.json +7 -7
- package/src/modules/attachments/data/entities.ts +5 -3
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/availability/acl.ts +12 -0
- package/src/modules/availability/api/check/route.ts +114 -0
- package/src/modules/availability/api/openapi.ts +28 -0
- package/src/modules/availability/api/policies/resolve-preview/route.ts +67 -0
- package/src/modules/availability/api/policies/route.ts +224 -0
- package/src/modules/availability/backend/availability/check/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/check/page.tsx +223 -0
- package/src/modules/availability/backend/availability/policies/ResolutionPreviewPanel.tsx +106 -0
- package/src/modules/availability/backend/availability/policies/ScopeFields.tsx +55 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.tsx +182 -0
- package/src/modules/availability/backend/availability/policies/create/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/create/page.tsx +105 -0
- package/src/modules/availability/backend/availability/policies/formGroups.ts +77 -0
- package/src/modules/availability/backend/availability/policies/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/policies/page.tsx +196 -0
- package/src/modules/availability/commands/policies.ts +358 -0
- package/src/modules/availability/commands/scope.ts +26 -0
- package/src/modules/availability/data/entities.ts +92 -0
- package/src/modules/availability/data/validators.ts +70 -0
- package/src/modules/availability/di.ts +54 -0
- package/src/modules/availability/events.ts +17 -0
- package/src/modules/availability/i18n/de.json +81 -0
- package/src/modules/availability/i18n/en.json +81 -0
- package/src/modules/availability/i18n/es.json +81 -0
- package/src/modules/availability/i18n/ko.json +81 -0
- package/src/modules/availability/i18n/pl.json +81 -0
- package/src/modules/availability/index.ts +13 -0
- package/src/modules/availability/lib/policyResolution.ts +264 -0
- package/src/modules/availability/lib/tryResolve.ts +8 -0
- package/src/modules/availability/migrations/.snapshot-open-mercato.json +375 -0
- package/src/modules/availability/migrations/Migration20260922095844_availability.ts +13 -0
- package/src/modules/availability/setup.ts +10 -0
- package/src/modules/entities/cli.ts +53 -13
- package/src/modules/inbox_ops/api/settings/route.ts +9 -32
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +2 -2
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +1 -1
- package/src/modules/inbox_ops/i18n/de.json +1 -1
- package/src/modules/inbox_ops/i18n/en.json +1 -1
- package/src/modules/inbox_ops/i18n/es.json +1 -1
- package/src/modules/inbox_ops/i18n/ko.json +1 -1
- package/src/modules/inbox_ops/i18n/pl.json +1 -1
- package/src/modules/inbox_ops/lib/ensure-settings.ts +77 -0
- package/src/modules/inbox_ops/lib/executionEngine.ts +1 -1
- package/src/modules/inbox_ops/lib/executionHelpers.ts +0 -27
- package/src/modules/inbox_ops/lib/payloadEnrichment.ts +1 -5
- package/src/modules/inbox_ops/setup.ts +2 -21
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +1 -1
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +13 -0
- package/src/modules/sales/data/entities.ts +2 -0
- package/src/modules/sales/inbox-actions.ts +0 -20
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +20 -0
- package/src/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.ts +74 -0
- package/src/modules/wms/di.ts +6 -0
- package/src/modules/wms/lib/availabilityCache.ts +136 -0
- package/src/modules/wms/lib/availabilityCalculation.ts +289 -0
- package/src/modules/wms/lib/availabilityProvider.ts +29 -0
- package/src/modules/wms/lib/sqlInClause.ts +21 -0
- package/src/modules/wms/lib/tryResolve.ts +8 -0
- package/src/modules/wms/subscribers/invalidate-availability-cache.ts +30 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ArrowLeft, Copy, CheckCircle } from 'lucide-react'\n\nconst LANGUAGE_KEYS = ['en', 'de', 'es', 'pl'] as const\n\nexport default function InboxSettingsPage() {\n const t = useT()\n const { runMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: 'inbox-ops-settings',\n })\n\n const languageOptions = LANGUAGE_KEYS.map((key) => ({\n value: key,\n label: t(`inbox_ops.settings.language_${key}` as never, key),\n }))\n const [settings, setSettings] = React.useState<{ inboxAddress?: string; isActive?: boolean; workingLanguage?: string; updatedAt?: string | null } | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [copied, setCopied] = React.useState(false)\n const [isSavingLanguage, setIsSavingLanguage] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setError(null)\n try {\n const result = await apiCall<{ settings: { inboxAddress?: string; isActive?: boolean; workingLanguage?: string; updatedAt?: string | null } | null }>('/api/inbox_ops/settings')\n if (!cancelled) {\n if (result?.ok
|
|
5
|
-
"mappings": ";AA+FM,SAEoD,KAFpD;AA7FN,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS,mCAAmC;AACrD,SAAS,iCAAiC;AAC1C,SAAS,6BAA6B;AACtC,SAAS,aAAa;AACtB,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,YAAY;AACrB,SAAS,0BAA0B;AACnC,SAAS,WAAW,MAAM,mBAAmB;AAE7C,MAAM,gBAAgB,CAAC,MAAM,MAAM,MAAM,IAAI;AAE9B,SAAR,oBAAqC;AAC1C,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,YAAY,IAAI,mBAA4C;AAAA,IAClE,WAAW;AAAA,EACb,CAAC;AAED,QAAM,kBAAkB,cAAc,IAAI,CAAC,SAAS;AAAA,IAClD,OAAO;AAAA,IACP,OAAO,EAAE,+BAA+B,GAAG,IAAa,GAAG;AAAA,EAC7D,EAAE;AACF,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAoH,IAAI;AAC9J,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAChD,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAS,KAAK;AAEpE,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,mBAAe,OAAO;AACpB,mBAAa,IAAI;AACjB,eAAS,IAAI;AACb,UAAI;AACF,cAAM,SAAS,MAAM,QAAiI,yBAAyB;AAC/K,YAAI,CAAC,WAAW;AACd,cAAI,QAAQ,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ArrowLeft, Copy, CheckCircle } from 'lucide-react'\n\nconst LANGUAGE_KEYS = ['en', 'de', 'es', 'pl'] as const\n\nexport default function InboxSettingsPage() {\n const t = useT()\n const { runMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: 'inbox-ops-settings',\n })\n\n const languageOptions = LANGUAGE_KEYS.map((key) => ({\n value: key,\n label: t(`inbox_ops.settings.language_${key}` as never, key),\n }))\n const [settings, setSettings] = React.useState<{ inboxAddress?: string; isActive?: boolean; workingLanguage?: string; updatedAt?: string | null } | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [copied, setCopied] = React.useState(false)\n const [isSavingLanguage, setIsSavingLanguage] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setError(null)\n try {\n const result = await apiCall<{ settings: { inboxAddress?: string; isActive?: boolean; workingLanguage?: string; updatedAt?: string | null } | null }>('/api/inbox_ops/settings')\n if (!cancelled) {\n if (result?.ok) {\n setSettings(result.result?.settings ?? null)\n } else {\n setError(t('inbox_ops.settings.load_failed', 'Failed to load settings'))\n }\n }\n } catch {\n if (!cancelled) setError(t('inbox_ops.settings.load_failed', 'Failed to load settings'))\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const handleCopy = React.useCallback(() => {\n if (settings?.inboxAddress) {\n navigator.clipboard.writeText(settings.inboxAddress)\n setCopied(true)\n setTimeout(() => setCopied(false), 2000)\n }\n }, [settings])\n\n const handleLanguageChange = React.useCallback(async (workingLanguage: string) => {\n setIsSavingLanguage(true)\n const result = await runMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(settings?.updatedAt ?? null),\n () => apiCall<{ ok: boolean; settings: { workingLanguage: string; updatedAt?: string | null } }>('/api/inbox_ops/settings', {\n method: 'PATCH',\n body: JSON.stringify({ workingLanguage }),\n }),\n ),\n context: {},\n })\n if (result?.ok && result.result?.ok) {\n setSettings((prev) => prev ? { ...prev, workingLanguage: result.result!.settings.workingLanguage, updatedAt: result.result!.settings.updatedAt ?? prev.updatedAt } : prev)\n flash(t('inbox_ops.settings.language_saved', 'Working language updated'), 'success')\n } else if (!surfaceRecordConflict({ status: result?.status, body: result?.result }, t)) {\n flash(t('inbox_ops.settings.language_save_failed', 'Failed to update working language'), 'error')\n }\n setIsSavingLanguage(false)\n }, [t, runMutation, settings?.updatedAt])\n\n return (\n <Page>\n <div className=\"flex items-center gap-3 px-3 py-3 md:px-6 md:py-4\">\n <Link href=\"/backend/inbox-ops\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\"><ArrowLeft className=\"h-4 w-4\" /></Button>\n </Link>\n <h1 className=\"text-lg font-semibold\">{t('inbox_ops.settings.title', 'Inbox Settings')}</h1>\n </div>\n\n <PageBody>\n <div className=\"max-w-lg\">\n {isLoading ? (\n <LoadingMessage label={t('inbox_ops.settings.loading', 'Loading settings...')} />\n ) : error ? (\n <ErrorMessage label={error} />\n ) : settings ? (\n <div className=\"space-y-6\">\n <div>\n <label className=\"text-sm font-medium text-foreground\">\n {t('inbox_ops.settings.forwarding_address', 'Forwarding Address')}\n </label>\n <p className=\"text-xs text-muted-foreground mt-1\">\n {t('inbox_ops.settings.forwarding_hint', 'Forward email threads to this address to create proposals')}\n </p>\n <div className=\"mt-2 flex items-center gap-2\">\n <div className=\"flex-1 bg-muted rounded-lg px-4 py-3\">\n <code className=\"text-sm font-mono\">{settings.inboxAddress}</code>\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"h-11 md:h-9\" onClick={handleCopy}>\n {copied ? <CheckCircle className=\"h-4 w-4 text-green-600\" /> : <Copy className=\"h-4 w-4\" />}\n <span className=\"ml-1\">{copied ? t('inbox_ops.settings.copied', 'Copied') : t('inbox_ops.settings.copy', 'Copy')}</span>\n </Button>\n </div>\n </div>\n\n <div>\n <label className=\"text-sm font-medium text-foreground\">{t('inbox_ops.settings.status', 'Status')}</label>\n <div className=\"mt-1\">\n <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${\n settings.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'\n }`}>\n {settings.isActive ? t('inbox_ops.settings.active', 'Active') : t('inbox_ops.settings.inactive', 'Inactive')}\n </span>\n </div>\n </div>\n\n <div>\n <label htmlFor=\"working-language\" className=\"text-sm font-medium text-foreground\">\n {t('inbox_ops.settings.working_language', 'Working Language')}\n </label>\n <p className=\"text-xs text-muted-foreground mt-1\">\n {t('inbox_ops.settings.working_language_hint', 'AI summaries and action descriptions will be generated in this language')}\n </p>\n <Select\n value={settings.workingLanguage || 'en'}\n onValueChange={(value) => handleLanguageChange(value)}\n disabled={isSavingLanguage}\n >\n <SelectTrigger id=\"working-language\" className=\"mt-2 sm:w-[200px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {languageOptions.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">{t('inbox_ops.settings.not_found', 'No inbox settings found. Settings are created when a new tenant is provisioned.')}</p>\n )}\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -22,7 +22,7 @@ function useDiscrepancyDescriptions() {
|
|
|
22
22
|
const t = useT();
|
|
23
23
|
const translations = {
|
|
24
24
|
"inbox_ops.discrepancy.desc.no_channel": t("inbox_ops.discrepancy.desc.no_channel", "No sales channel available. Create a channel in Sales settings before accepting this order."),
|
|
25
|
-
"inbox_ops.discrepancy.desc.no_currency": t("inbox_ops.discrepancy.desc.no_currency", "No currency could be resolved for this order. Set a currency code
|
|
25
|
+
"inbox_ops.discrepancy.desc.no_currency": t("inbox_ops.discrepancy.desc.no_currency", "No currency could be resolved for this order. Set a currency code on the order."),
|
|
26
26
|
"inbox_ops.discrepancy.desc.product_not_matched": t("inbox_ops.discrepancy.desc.product_not_matched", "Product could not be matched to any catalog product"),
|
|
27
27
|
"inbox_ops.discrepancy.desc.no_matching_contact": t("inbox_ops.discrepancy.desc.no_matching_contact", "No matching contact found"),
|
|
28
28
|
"inbox_ops.discrepancy.desc.draft_reply_no_contact": t("inbox_ops.discrepancy.desc.draft_reply_no_contact", "Draft reply target has no matching contact. Create the contact first."),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/inbox_ops/components/proposals/ActionCard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n CheckCircle,\n XCircle,\n Pencil,\n AlertTriangle,\n RefreshCw,\n Package,\n FileText,\n MessageSquare,\n Truck,\n UserPlus,\n Link2,\n Activity,\n ShoppingBag,\n} from 'lucide-react'\nimport type { ActionDetail, DiscrepancyDetail } from './types'\nimport { hasContactNameIssue } from '../../lib/contactValidation'\n\nexport { hasContactNameIssue }\n\n/**\n * Resolves discrepancy description i18n keys stored in the database.\n * Falls back to the raw description string for legacy data or LLM-generated descriptions.\n */\nexport function useDiscrepancyDescriptions(): (description: string, foundValue?: string | null) => string {\n const t = useT()\n const translations: Record<string, string> = {\n 'inbox_ops.discrepancy.desc.no_channel': t('inbox_ops.discrepancy.desc.no_channel', 'No sales channel available. Create a channel in Sales settings before accepting this order.'),\n 'inbox_ops.discrepancy.desc.no_currency': t('inbox_ops.discrepancy.desc.no_currency', 'No currency could be resolved for this order. Set a currency code or configure a sales channel with a default currency.'),\n 'inbox_ops.discrepancy.desc.product_not_matched': t('inbox_ops.discrepancy.desc.product_not_matched', 'Product could not be matched to any catalog product'),\n 'inbox_ops.discrepancy.desc.no_matching_contact': t('inbox_ops.discrepancy.desc.no_matching_contact', 'No matching contact found'),\n 'inbox_ops.discrepancy.desc.draft_reply_no_contact': t('inbox_ops.discrepancy.desc.draft_reply_no_contact', 'Draft reply target has no matching contact. Create the contact first.'),\n 'inbox_ops.discrepancy.desc.duplicate_order_reference': t('inbox_ops.discrepancy.desc.duplicate_order_reference', 'An order with this customer reference already exists'),\n }\n return (description: string, foundValue?: string | null) => {\n const translated = translations[description]\n if (!translated) return description\n if (foundValue && (description === 'inbox_ops.discrepancy.desc.product_not_matched' || description === 'inbox_ops.discrepancy.desc.no_matching_contact')) {\n return `${translated}: ${foundValue}`\n }\n return translated\n }\n}\n\n/**\n * Resolves action description i18n keys stored in the database.\n * Auto-generated actions store keys like `inbox_ops.action.desc.create_contact`;\n * LLM-generated actions store plain text which is returned as-is.\n */\nexport function useActionDescriptionResolver(): (description: string, payload: Record<string, unknown>) => string {\n const t = useT()\n return (description: string, payload: Record<string, unknown>) => {\n if (!description.startsWith('inbox_ops.action.desc.')) return description\n const name = (payload.name as string) || (payload.contactName as string) || ''\n const email = (payload.email as string) || (payload.emailAddress as string) || ''\n const title = (payload.title as string) || ''\n const toName = (payload.toName as string) || (payload.to as string) || ''\n const subject = (payload.subject as string) || ''\n const translations: Record<string, string> = {\n 'inbox_ops.action.desc.create_contact': t('inbox_ops.action.desc.create_contact', 'Create contact for {name} ({email})')\n .replace('{name}', name).replace('{email}', email),\n 'inbox_ops.action.desc.link_contact': t('inbox_ops.action.desc.link_contact', 'Link {name} ({email}) to existing contact')\n .replace('{name}', name).replace('{email}', email),\n 'inbox_ops.action.desc.create_product': t('inbox_ops.action.desc.create_product', 'Create catalog product \"{title}\"')\n .replace('{title}', title),\n 'inbox_ops.action.desc.draft_reply': t('inbox_ops.action.desc.draft_reply', 'Draft reply to {toName}: {subject}')\n .replace('{toName}', toName).replace('{subject}', subject),\n }\n return translations[description] || description\n }\n}\n\nconst ACTION_TYPE_ICONS: Record<string, React.ElementType> = {\n create_order: Package,\n create_quote: FileText,\n update_order: Package,\n update_shipment: Truck,\n create_contact: UserPlus,\n create_product: ShoppingBag,\n link_contact: Link2,\n log_activity: Activity,\n draft_reply: MessageSquare,\n}\n\nexport function useActionTypeLabels(): Record<string, string> {\n const t = useT()\n return {\n create_order: t('inbox_ops.action_type.create_order', 'Create Sales Order'),\n create_quote: t('inbox_ops.action_type.create_quote', 'Create Quote'),\n update_order: t('inbox_ops.action_type.update_order', 'Update Order'),\n update_shipment: t('inbox_ops.action_type.update_shipment', 'Update Shipment'),\n create_contact: t('inbox_ops.action_type.create_contact', 'Create Contact'),\n create_product: t('inbox_ops.action_type.create_product', 'Create Product'),\n link_contact: t('inbox_ops.action_type.link_contact', 'Link Contact'),\n log_activity: t('inbox_ops.action_type.log_activity', 'Log Activity'),\n draft_reply: t('inbox_ops.action_type.draft_reply', 'Draft Reply'),\n }\n}\n\nexport function ConfidenceBadge({ value }: { value: string }) {\n const num = parseFloat(value)\n const pct = Math.round(num * 100)\n const color = num >= 0.8 ? 'text-status-success-text' : num >= 0.6 ? 'text-status-warning-text' : 'text-status-error-text'\n const bgColor = num >= 0.8 ? 'bg-status-success-icon' : num >= 0.6 ? 'bg-status-warning-icon' : 'bg-status-error-icon'\n const width = Math.round(num * 100)\n return (\n <div className=\"flex items-center gap-2\">\n <span className={`text-sm font-medium ${color}`}>{pct}%</span>\n <div className=\"w-24 h-2 bg-muted rounded-full overflow-hidden\">\n <div className={`h-full ${bgColor} rounded-full`} style={{ width: `${width}%` }} />\n </div>\n </div>\n )\n}\n\nfunction OrderPreview({ payload }: { payload: Record<string, unknown> }) {\n const t = useT()\n const lineItems = (payload.lineItems as Record<string, unknown>[]) || []\n const customerName = (payload.customerName as string) || ''\n const currencyCode = (payload.currencyCode as string) || ''\n const notes = (payload.notes as string) || ''\n const deliveryDate = (payload.requestedDeliveryDate as string) || ''\n\n return (\n <div className=\"mt-2 space-y-2 text-xs\">\n {customerName && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.customer', 'Customer')}:</span>\n <span>{customerName}</span>\n {typeof payload.customerEmail === 'string' && <span className=\"text-muted-foreground\">({payload.customerEmail})</span>}\n </div>\n )}\n {lineItems.length > 0 && (\n <div className=\"border rounded overflow-hidden\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"bg-muted/50\">\n <th className=\"text-left px-2 py-1 font-medium\">{t('inbox_ops.preview.product', 'Product')}</th>\n <th className=\"text-right px-2 py-1 font-medium\">{t('inbox_ops.preview.qty', 'Qty')}</th>\n <th className=\"text-right px-2 py-1 font-medium\">{t('inbox_ops.preview.price', 'Price')}</th>\n </tr>\n </thead>\n <tbody>\n {lineItems.map((item, index) => (\n <tr key={index} className=\"border-t\">\n <td className=\"px-2 py-1\">{(item.productName as string) || '\u2014'}</td>\n <td className=\"px-2 py-1 text-right\">{String(item.quantity ?? '')}</td>\n <td className=\"px-2 py-1 text-right\">{item.unitPrice ? `${item.unitPrice} ${currencyCode}` : '\u2014'}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n {(deliveryDate || notes) && (\n <div className=\"flex flex-wrap gap-3\">\n {deliveryDate && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.delivery', 'Delivery')}:</span>\n <span>{deliveryDate}</span>\n </div>\n )}\n {notes && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.notes', 'Notes')}:</span>\n <span className=\"truncate max-w-[200px]\">{notes}</span>\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProductPreview({ payload }: { payload: Record<string, unknown> }) {\n const t = useT()\n const title = (payload.title as string) || ''\n const sku = (payload.sku as string) || ''\n const unitPrice = (payload.unitPrice as string) || ''\n const currencyCode = (payload.currencyCode as string) || ''\n const kind = (payload.kind as string) || 'product'\n\n return (\n <div className=\"mt-2 space-y-1 text-xs\">\n {title && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.product_title', 'Title')}:</span>\n <span className=\"font-medium\">{title}</span>\n </div>\n )}\n <div className=\"flex flex-wrap gap-3\">\n {sku && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.sku', 'SKU')}:</span>\n <span>{sku}</span>\n </div>\n )}\n {unitPrice && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.price', 'Price')}:</span>\n <span>{unitPrice}{currencyCode ? ` ${currencyCode}` : ''}</span>\n </div>\n )}\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.edit_dialog.kind', 'Kind')}:</span>\n <span>{kind}</span>\n </div>\n </div>\n </div>\n )\n}\n\nexport function ActionCard({\n action,\n discrepancies,\n actionTypeLabels,\n onAccept,\n onReject,\n onRetry,\n onEdit,\n translatedDescription,\n resolveDiscrepancyDescription,\n}: {\n action: ActionDetail\n discrepancies: DiscrepancyDetail[]\n actionTypeLabels: Record<string, string>\n onAccept: (id: string) => void\n onReject: (id: string) => void\n onRetry: (id: string) => void\n onEdit: (action: ActionDetail) => void\n translatedDescription?: string\n resolveDiscrepancyDescription?: (description: string, foundValue?: string | null) => string\n}) {\n const t = useT()\n const Icon = ACTION_TYPE_ICONS[action.actionType] || Package\n const label = actionTypeLabels[action.actionType] || action.actionType\n const resolveActionDescription = useActionDescriptionResolver()\n\n const actionDiscrepancies = discrepancies.filter((d) => d.actionId === action.id && !d.resolved)\n const hasBlockingDiscrepancies = actionDiscrepancies.some((d) => d.severity === 'error')\n const displayDescription = translatedDescription || resolveActionDescription(action.description, action.payload)\n\n if (action.status === 'executed') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-status-success-bg border-status-success-border\">\n <div className=\"flex items-center gap-2 mb-2\">\n <CheckCircle className=\"h-5 w-5 text-status-success-icon flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n {action.createdEntityId && (\n <div className=\"mt-2\">\n <span className=\"text-xs text-status-success-text\">\n {t('inbox_ops.action.created_entity', 'Created {type}').replace('{type}', action.createdEntityType || '')} \u00B7 {action.executedAt && new Date(action.executedAt).toLocaleString()}\n </span>\n </div>\n )}\n </div>\n )\n }\n\n if (action.status === 'rejected') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-muted/50 opacity-60\">\n <div className=\"flex items-center gap-2 mb-2\">\n <XCircle className=\"h-5 w-5 text-muted-foreground flex-shrink-0\" />\n <span className=\"text-sm font-medium line-through\">{label}</span>\n <span className=\"text-xs text-muted-foreground\">{t('inbox_ops.status.rejected', 'Rejected')}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n </div>\n )\n }\n\n if (action.status === 'failed') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-status-error-bg border-status-error-border\">\n <div className=\"flex items-center gap-2 mb-2\">\n <AlertTriangle className=\"h-5 w-5 text-status-error-icon flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n <span className=\"text-xs text-status-error-text\">{t('inbox_ops.extraction_failed', 'Failed')}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n {action.executionError && (\n <p className=\"text-xs text-status-error-text mt-1\">{action.executionError}</p>\n )}\n <div className=\"mt-3 flex items-center gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onRetry(action.id)}\n >\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.retry', 'Retry')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onEdit(action)}\n >\n <Pencil className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.edit', 'Edit')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onReject(action.id)}\n >\n <XCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.reject', 'Reject')}\n </Button>\n </div>\n </div>\n )\n }\n\n if (action.status !== 'pending') {\n const isProcessing = action.status === 'processing'\n const statusLabel = action.status === 'accepted'\n ? t('inbox_ops.status.accepted', 'Accepted')\n : isProcessing\n ? t('inbox_ops.status.processing', 'Processing')\n : action.status\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-muted/50\">\n <div className=\"flex items-center gap-2 mb-2\">\n {isProcessing ? (\n <RefreshCw className=\"h-5 w-5 text-muted-foreground flex-shrink-0 animate-spin\" />\n ) : (\n <CheckCircle className=\"h-5 w-5 text-muted-foreground flex-shrink-0\" />\n )}\n <span className=\"text-sm font-medium\">{label}</span>\n <span className=\"text-xs text-muted-foreground\">{statusLabel}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n </div>\n )\n }\n\n const hasNameIssue = hasContactNameIssue(action)\n\n return (\n <div className=\"border rounded-lg p-3 md:p-4\">\n <div className=\"flex items-center gap-2 mb-2\">\n <Icon className=\"h-5 w-5 text-primary flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n <ConfidenceBadge value={action.confidence} />\n </div>\n <p className=\"text-sm text-foreground/80 mb-2\">{displayDescription}</p>\n\n {(action.actionType === 'create_order' || action.actionType === 'create_quote') && (\n <OrderPreview payload={action.payload} />\n )}\n\n {action.actionType === 'create_product' && (\n <ProductPreview payload={action.payload} />\n )}\n\n {actionDiscrepancies.length > 0 && (\n <div className=\"mb-3 space-y-1\">\n {actionDiscrepancies.map((d) => (\n <div key={d.id} className={`flex items-start gap-2 text-xs rounded px-2 py-1.5 ${\n d.severity === 'error' ? 'bg-status-error-bg text-status-error-text' : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n <AlertTriangle className=\"h-3 w-3 mt-0.5 flex-shrink-0\" />\n <div>\n <span>{resolveDiscrepancyDescription ? resolveDiscrepancyDescription(d.description, d.foundValue) : d.description}</span>\n {(d.expectedValue || d.foundValue) && (\n <div className=\"mt-0.5 text-overline opacity-80\">\n {d.expectedValue && <span>{t('inbox_ops.discrepancy.expected', 'Expected')}: {d.expectedValue}</span>}\n {d.expectedValue && d.foundValue && <span> \u00B7 </span>}\n {d.foundValue && <span>{t('inbox_ops.discrepancy.found', 'Found')}: {d.foundValue}</span>}\n </div>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {hasNameIssue && (\n <div className=\"mb-3 flex items-start gap-2 text-xs rounded px-2 py-1.5 bg-status-warning-bg text-status-warning-text\">\n <AlertTriangle className=\"h-3 w-3 mt-0.5 flex-shrink-0\" />\n <span>{action.actionType === 'link_contact'\n ? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')\n : t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')\n }</span>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <div title={\n hasNameIssue\n ? action.actionType === 'link_contact'\n ? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')\n : t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')\n : hasBlockingDiscrepancies\n ? t('inbox_ops.action.accept_blocked', 'Resolve errors before accepting')\n : undefined\n }>\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onAccept(action.id)}\n disabled={hasBlockingDiscrepancies || hasNameIssue}\n >\n <CheckCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.accept', 'Accept')}\n </Button>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onEdit(action)}\n >\n <Pencil className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.edit', 'Edit')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onReject(action.id)}\n >\n <XCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.reject', 'Reject')}\n </Button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAgHM,SAEE,KAFF;AA7GN,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,2BAA2B;AAQ7B,SAAS,6BAA0F;AACxG,QAAM,IAAI,KAAK;AACf,QAAM,eAAuC;AAAA,IAC3C,yCAAyC,EAAE,yCAAyC,6FAA6F;AAAA,IACjL,0CAA0C,EAAE,0CAA0C,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n CheckCircle,\n XCircle,\n Pencil,\n AlertTriangle,\n RefreshCw,\n Package,\n FileText,\n MessageSquare,\n Truck,\n UserPlus,\n Link2,\n Activity,\n ShoppingBag,\n} from 'lucide-react'\nimport type { ActionDetail, DiscrepancyDetail } from './types'\nimport { hasContactNameIssue } from '../../lib/contactValidation'\n\nexport { hasContactNameIssue }\n\n/**\n * Resolves discrepancy description i18n keys stored in the database.\n * Falls back to the raw description string for legacy data or LLM-generated descriptions.\n */\nexport function useDiscrepancyDescriptions(): (description: string, foundValue?: string | null) => string {\n const t = useT()\n const translations: Record<string, string> = {\n 'inbox_ops.discrepancy.desc.no_channel': t('inbox_ops.discrepancy.desc.no_channel', 'No sales channel available. Create a channel in Sales settings before accepting this order.'),\n 'inbox_ops.discrepancy.desc.no_currency': t('inbox_ops.discrepancy.desc.no_currency', 'No currency could be resolved for this order. Set a currency code on the order.'),\n 'inbox_ops.discrepancy.desc.product_not_matched': t('inbox_ops.discrepancy.desc.product_not_matched', 'Product could not be matched to any catalog product'),\n 'inbox_ops.discrepancy.desc.no_matching_contact': t('inbox_ops.discrepancy.desc.no_matching_contact', 'No matching contact found'),\n 'inbox_ops.discrepancy.desc.draft_reply_no_contact': t('inbox_ops.discrepancy.desc.draft_reply_no_contact', 'Draft reply target has no matching contact. Create the contact first.'),\n 'inbox_ops.discrepancy.desc.duplicate_order_reference': t('inbox_ops.discrepancy.desc.duplicate_order_reference', 'An order with this customer reference already exists'),\n }\n return (description: string, foundValue?: string | null) => {\n const translated = translations[description]\n if (!translated) return description\n if (foundValue && (description === 'inbox_ops.discrepancy.desc.product_not_matched' || description === 'inbox_ops.discrepancy.desc.no_matching_contact')) {\n return `${translated}: ${foundValue}`\n }\n return translated\n }\n}\n\n/**\n * Resolves action description i18n keys stored in the database.\n * Auto-generated actions store keys like `inbox_ops.action.desc.create_contact`;\n * LLM-generated actions store plain text which is returned as-is.\n */\nexport function useActionDescriptionResolver(): (description: string, payload: Record<string, unknown>) => string {\n const t = useT()\n return (description: string, payload: Record<string, unknown>) => {\n if (!description.startsWith('inbox_ops.action.desc.')) return description\n const name = (payload.name as string) || (payload.contactName as string) || ''\n const email = (payload.email as string) || (payload.emailAddress as string) || ''\n const title = (payload.title as string) || ''\n const toName = (payload.toName as string) || (payload.to as string) || ''\n const subject = (payload.subject as string) || ''\n const translations: Record<string, string> = {\n 'inbox_ops.action.desc.create_contact': t('inbox_ops.action.desc.create_contact', 'Create contact for {name} ({email})')\n .replace('{name}', name).replace('{email}', email),\n 'inbox_ops.action.desc.link_contact': t('inbox_ops.action.desc.link_contact', 'Link {name} ({email}) to existing contact')\n .replace('{name}', name).replace('{email}', email),\n 'inbox_ops.action.desc.create_product': t('inbox_ops.action.desc.create_product', 'Create catalog product \"{title}\"')\n .replace('{title}', title),\n 'inbox_ops.action.desc.draft_reply': t('inbox_ops.action.desc.draft_reply', 'Draft reply to {toName}: {subject}')\n .replace('{toName}', toName).replace('{subject}', subject),\n }\n return translations[description] || description\n }\n}\n\nconst ACTION_TYPE_ICONS: Record<string, React.ElementType> = {\n create_order: Package,\n create_quote: FileText,\n update_order: Package,\n update_shipment: Truck,\n create_contact: UserPlus,\n create_product: ShoppingBag,\n link_contact: Link2,\n log_activity: Activity,\n draft_reply: MessageSquare,\n}\n\nexport function useActionTypeLabels(): Record<string, string> {\n const t = useT()\n return {\n create_order: t('inbox_ops.action_type.create_order', 'Create Sales Order'),\n create_quote: t('inbox_ops.action_type.create_quote', 'Create Quote'),\n update_order: t('inbox_ops.action_type.update_order', 'Update Order'),\n update_shipment: t('inbox_ops.action_type.update_shipment', 'Update Shipment'),\n create_contact: t('inbox_ops.action_type.create_contact', 'Create Contact'),\n create_product: t('inbox_ops.action_type.create_product', 'Create Product'),\n link_contact: t('inbox_ops.action_type.link_contact', 'Link Contact'),\n log_activity: t('inbox_ops.action_type.log_activity', 'Log Activity'),\n draft_reply: t('inbox_ops.action_type.draft_reply', 'Draft Reply'),\n }\n}\n\nexport function ConfidenceBadge({ value }: { value: string }) {\n const num = parseFloat(value)\n const pct = Math.round(num * 100)\n const color = num >= 0.8 ? 'text-status-success-text' : num >= 0.6 ? 'text-status-warning-text' : 'text-status-error-text'\n const bgColor = num >= 0.8 ? 'bg-status-success-icon' : num >= 0.6 ? 'bg-status-warning-icon' : 'bg-status-error-icon'\n const width = Math.round(num * 100)\n return (\n <div className=\"flex items-center gap-2\">\n <span className={`text-sm font-medium ${color}`}>{pct}%</span>\n <div className=\"w-24 h-2 bg-muted rounded-full overflow-hidden\">\n <div className={`h-full ${bgColor} rounded-full`} style={{ width: `${width}%` }} />\n </div>\n </div>\n )\n}\n\nfunction OrderPreview({ payload }: { payload: Record<string, unknown> }) {\n const t = useT()\n const lineItems = (payload.lineItems as Record<string, unknown>[]) || []\n const customerName = (payload.customerName as string) || ''\n const currencyCode = (payload.currencyCode as string) || ''\n const notes = (payload.notes as string) || ''\n const deliveryDate = (payload.requestedDeliveryDate as string) || ''\n\n return (\n <div className=\"mt-2 space-y-2 text-xs\">\n {customerName && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.customer', 'Customer')}:</span>\n <span>{customerName}</span>\n {typeof payload.customerEmail === 'string' && <span className=\"text-muted-foreground\">({payload.customerEmail})</span>}\n </div>\n )}\n {lineItems.length > 0 && (\n <div className=\"border rounded overflow-hidden\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"bg-muted/50\">\n <th className=\"text-left px-2 py-1 font-medium\">{t('inbox_ops.preview.product', 'Product')}</th>\n <th className=\"text-right px-2 py-1 font-medium\">{t('inbox_ops.preview.qty', 'Qty')}</th>\n <th className=\"text-right px-2 py-1 font-medium\">{t('inbox_ops.preview.price', 'Price')}</th>\n </tr>\n </thead>\n <tbody>\n {lineItems.map((item, index) => (\n <tr key={index} className=\"border-t\">\n <td className=\"px-2 py-1\">{(item.productName as string) || '\u2014'}</td>\n <td className=\"px-2 py-1 text-right\">{String(item.quantity ?? '')}</td>\n <td className=\"px-2 py-1 text-right\">{item.unitPrice ? `${item.unitPrice} ${currencyCode}` : '\u2014'}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n {(deliveryDate || notes) && (\n <div className=\"flex flex-wrap gap-3\">\n {deliveryDate && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.delivery', 'Delivery')}:</span>\n <span>{deliveryDate}</span>\n </div>\n )}\n {notes && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.notes', 'Notes')}:</span>\n <span className=\"truncate max-w-[200px]\">{notes}</span>\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProductPreview({ payload }: { payload: Record<string, unknown> }) {\n const t = useT()\n const title = (payload.title as string) || ''\n const sku = (payload.sku as string) || ''\n const unitPrice = (payload.unitPrice as string) || ''\n const currencyCode = (payload.currencyCode as string) || ''\n const kind = (payload.kind as string) || 'product'\n\n return (\n <div className=\"mt-2 space-y-1 text-xs\">\n {title && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.product_title', 'Title')}:</span>\n <span className=\"font-medium\">{title}</span>\n </div>\n )}\n <div className=\"flex flex-wrap gap-3\">\n {sku && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.sku', 'SKU')}:</span>\n <span>{sku}</span>\n </div>\n )}\n {unitPrice && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.price', 'Price')}:</span>\n <span>{unitPrice}{currencyCode ? ` ${currencyCode}` : ''}</span>\n </div>\n )}\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.edit_dialog.kind', 'Kind')}:</span>\n <span>{kind}</span>\n </div>\n </div>\n </div>\n )\n}\n\nexport function ActionCard({\n action,\n discrepancies,\n actionTypeLabels,\n onAccept,\n onReject,\n onRetry,\n onEdit,\n translatedDescription,\n resolveDiscrepancyDescription,\n}: {\n action: ActionDetail\n discrepancies: DiscrepancyDetail[]\n actionTypeLabels: Record<string, string>\n onAccept: (id: string) => void\n onReject: (id: string) => void\n onRetry: (id: string) => void\n onEdit: (action: ActionDetail) => void\n translatedDescription?: string\n resolveDiscrepancyDescription?: (description: string, foundValue?: string | null) => string\n}) {\n const t = useT()\n const Icon = ACTION_TYPE_ICONS[action.actionType] || Package\n const label = actionTypeLabels[action.actionType] || action.actionType\n const resolveActionDescription = useActionDescriptionResolver()\n\n const actionDiscrepancies = discrepancies.filter((d) => d.actionId === action.id && !d.resolved)\n const hasBlockingDiscrepancies = actionDiscrepancies.some((d) => d.severity === 'error')\n const displayDescription = translatedDescription || resolveActionDescription(action.description, action.payload)\n\n if (action.status === 'executed') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-status-success-bg border-status-success-border\">\n <div className=\"flex items-center gap-2 mb-2\">\n <CheckCircle className=\"h-5 w-5 text-status-success-icon flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n {action.createdEntityId && (\n <div className=\"mt-2\">\n <span className=\"text-xs text-status-success-text\">\n {t('inbox_ops.action.created_entity', 'Created {type}').replace('{type}', action.createdEntityType || '')} \u00B7 {action.executedAt && new Date(action.executedAt).toLocaleString()}\n </span>\n </div>\n )}\n </div>\n )\n }\n\n if (action.status === 'rejected') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-muted/50 opacity-60\">\n <div className=\"flex items-center gap-2 mb-2\">\n <XCircle className=\"h-5 w-5 text-muted-foreground flex-shrink-0\" />\n <span className=\"text-sm font-medium line-through\">{label}</span>\n <span className=\"text-xs text-muted-foreground\">{t('inbox_ops.status.rejected', 'Rejected')}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n </div>\n )\n }\n\n if (action.status === 'failed') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-status-error-bg border-status-error-border\">\n <div className=\"flex items-center gap-2 mb-2\">\n <AlertTriangle className=\"h-5 w-5 text-status-error-icon flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n <span className=\"text-xs text-status-error-text\">{t('inbox_ops.extraction_failed', 'Failed')}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n {action.executionError && (\n <p className=\"text-xs text-status-error-text mt-1\">{action.executionError}</p>\n )}\n <div className=\"mt-3 flex items-center gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onRetry(action.id)}\n >\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.retry', 'Retry')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onEdit(action)}\n >\n <Pencil className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.edit', 'Edit')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onReject(action.id)}\n >\n <XCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.reject', 'Reject')}\n </Button>\n </div>\n </div>\n )\n }\n\n if (action.status !== 'pending') {\n const isProcessing = action.status === 'processing'\n const statusLabel = action.status === 'accepted'\n ? t('inbox_ops.status.accepted', 'Accepted')\n : isProcessing\n ? t('inbox_ops.status.processing', 'Processing')\n : action.status\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-muted/50\">\n <div className=\"flex items-center gap-2 mb-2\">\n {isProcessing ? (\n <RefreshCw className=\"h-5 w-5 text-muted-foreground flex-shrink-0 animate-spin\" />\n ) : (\n <CheckCircle className=\"h-5 w-5 text-muted-foreground flex-shrink-0\" />\n )}\n <span className=\"text-sm font-medium\">{label}</span>\n <span className=\"text-xs text-muted-foreground\">{statusLabel}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n </div>\n )\n }\n\n const hasNameIssue = hasContactNameIssue(action)\n\n return (\n <div className=\"border rounded-lg p-3 md:p-4\">\n <div className=\"flex items-center gap-2 mb-2\">\n <Icon className=\"h-5 w-5 text-primary flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n <ConfidenceBadge value={action.confidence} />\n </div>\n <p className=\"text-sm text-foreground/80 mb-2\">{displayDescription}</p>\n\n {(action.actionType === 'create_order' || action.actionType === 'create_quote') && (\n <OrderPreview payload={action.payload} />\n )}\n\n {action.actionType === 'create_product' && (\n <ProductPreview payload={action.payload} />\n )}\n\n {actionDiscrepancies.length > 0 && (\n <div className=\"mb-3 space-y-1\">\n {actionDiscrepancies.map((d) => (\n <div key={d.id} className={`flex items-start gap-2 text-xs rounded px-2 py-1.5 ${\n d.severity === 'error' ? 'bg-status-error-bg text-status-error-text' : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n <AlertTriangle className=\"h-3 w-3 mt-0.5 flex-shrink-0\" />\n <div>\n <span>{resolveDiscrepancyDescription ? resolveDiscrepancyDescription(d.description, d.foundValue) : d.description}</span>\n {(d.expectedValue || d.foundValue) && (\n <div className=\"mt-0.5 text-overline opacity-80\">\n {d.expectedValue && <span>{t('inbox_ops.discrepancy.expected', 'Expected')}: {d.expectedValue}</span>}\n {d.expectedValue && d.foundValue && <span> \u00B7 </span>}\n {d.foundValue && <span>{t('inbox_ops.discrepancy.found', 'Found')}: {d.foundValue}</span>}\n </div>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {hasNameIssue && (\n <div className=\"mb-3 flex items-start gap-2 text-xs rounded px-2 py-1.5 bg-status-warning-bg text-status-warning-text\">\n <AlertTriangle className=\"h-3 w-3 mt-0.5 flex-shrink-0\" />\n <span>{action.actionType === 'link_contact'\n ? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')\n : t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')\n }</span>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <div title={\n hasNameIssue\n ? action.actionType === 'link_contact'\n ? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')\n : t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')\n : hasBlockingDiscrepancies\n ? t('inbox_ops.action.accept_blocked', 'Resolve errors before accepting')\n : undefined\n }>\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onAccept(action.id)}\n disabled={hasBlockingDiscrepancies || hasNameIssue}\n >\n <CheckCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.accept', 'Accept')}\n </Button>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onEdit(action)}\n >\n <Pencil className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.edit', 'Edit')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onReject(action.id)}\n >\n <XCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.reject', 'Reject')}\n </Button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { UniqueConstraintViolationException } from "@mikro-orm/core";
|
|
2
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { InboxSettings } from "../data/entities.js";
|
|
4
|
+
function isUniqueViolation(error) {
|
|
5
|
+
if (error instanceof UniqueConstraintViolationException) return true;
|
|
6
|
+
if (!error || typeof error !== "object") return false;
|
|
7
|
+
if (error.code === "23505") return true;
|
|
8
|
+
const message = error.message;
|
|
9
|
+
return typeof message === "string" && message.includes("duplicate key");
|
|
10
|
+
}
|
|
11
|
+
async function findExisting(em, scope) {
|
|
12
|
+
return findOneWithDecryption(
|
|
13
|
+
em,
|
|
14
|
+
InboxSettings,
|
|
15
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
16
|
+
void 0,
|
|
17
|
+
scope
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
async function ensureInboxSettings(em, scope) {
|
|
21
|
+
const existing = await findExisting(em, scope);
|
|
22
|
+
if (existing) return { em, settings: existing };
|
|
23
|
+
const domain = process.env.INBOX_OPS_DOMAIN || "inbox.mercato.local";
|
|
24
|
+
const slug = scope.organizationId.slice(0, 8);
|
|
25
|
+
const inboxAddress = `ops-${slug}@${domain}`;
|
|
26
|
+
const settings = em.create(InboxSettings, {
|
|
27
|
+
tenantId: scope.tenantId,
|
|
28
|
+
organizationId: scope.organizationId,
|
|
29
|
+
inboxAddress,
|
|
30
|
+
isActive: true
|
|
31
|
+
});
|
|
32
|
+
em.persist(settings);
|
|
33
|
+
try {
|
|
34
|
+
await em.flush();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (!isUniqueViolation(error)) throw error;
|
|
37
|
+
const recoveryEm = em.fork();
|
|
38
|
+
const winner = await findExisting(recoveryEm, scope);
|
|
39
|
+
if (!winner) throw error;
|
|
40
|
+
return { em: recoveryEm, settings: winner };
|
|
41
|
+
}
|
|
42
|
+
return { em, settings };
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
ensureInboxSettings
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=ensure-settings.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/inbox_ops/lib/ensure-settings.ts"],
|
|
4
|
+
"sourcesContent": ["import { UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxSettings } from '../data/entities'\n\nexport interface EnsureInboxSettingsScope {\n tenantId: string\n organizationId: string\n}\n\nexport interface EnsureInboxSettingsResult {\n // The manager the returned `settings` row is actually managed by \u2014 a\n // concurrent-bootstrap recovery returns a clean fork, never the manager\n // whose insert just failed, so callers must flush further writes through\n // this manager rather than the one they passed in.\n em: EntityManager\n settings: InboxSettings\n}\n\nfunction isUniqueViolation(error: unknown): boolean {\n if (error instanceof UniqueConstraintViolationException) return true\n if (!error || typeof error !== 'object') return false\n if ((error as { code?: string }).code === '23505') return true\n const message = (error as { message?: string }).message\n return typeof message === 'string' && message.includes('duplicate key')\n}\n\nasync function findExisting(em: EntityManager, scope: EnsureInboxSettingsScope) {\n return findOneWithDecryption(\n em,\n InboxSettings,\n { tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n undefined,\n scope,\n )\n}\n\n// Lazily bootstraps the tenant's InboxSettings row when missing \u2014 e.g. a\n// tenant provisioned before this module existed and never ran setup.ts's\n// onTenantCreated hook. Idempotent: a second call finds the row just created.\nexport async function ensureInboxSettings(\n em: EntityManager,\n scope: EnsureInboxSettingsScope,\n): Promise<EnsureInboxSettingsResult> {\n const existing = await findExisting(em, scope)\n if (existing) return { em, settings: existing }\n\n const domain = process.env.INBOX_OPS_DOMAIN || 'inbox.mercato.local'\n const slug = scope.organizationId.slice(0, 8)\n const inboxAddress = `ops-${slug}@${domain}`\n const settings = em.create(InboxSettings, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n inboxAddress,\n isActive: true,\n })\n em.persist(settings)\n\n try {\n await em.flush()\n } catch (error) {\n // Two concurrent bootstraps (e.g. two tabs) race to insert the same\n // deterministic inboxAddress; the loser refetches the winner's row\n // instead of surfacing a raw unique-constraint error to the caller.\n // Recover on a clean fork: `em`'s unit of work still owns the\n // half-persisted `settings` entity scheduled for the failed insert, so\n // flushing `em` again later (e.g. after a caller-side PATCH mutation)\n // would retry that same rejected insert.\n if (!isUniqueViolation(error)) throw error\n const recoveryEm = em.fork()\n const winner = await findExisting(recoveryEm, scope)\n if (!winner) throw error\n return { em: recoveryEm, settings: winner }\n }\n\n return { em, settings }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,0CAA0C;AAEnD,SAAS,6BAA6B;AACtC,SAAS,qBAAqB;AAgB9B,SAAS,kBAAkB,OAAyB;AAClD,MAAI,iBAAiB,mCAAoC,QAAO;AAChE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,MAAK,MAA4B,SAAS,QAAS,QAAO;AAC1D,QAAM,UAAW,MAA+B;AAChD,SAAO,OAAO,YAAY,YAAY,QAAQ,SAAS,eAAe;AACxE;AAEA,eAAe,aAAa,IAAmB,OAAiC;AAC9E,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,gBAAgB,WAAW,KAAK;AAAA,IAClF;AAAA,IACA;AAAA,EACF;AACF;AAKA,eAAsB,oBACpB,IACA,OACoC;AACpC,QAAM,WAAW,MAAM,aAAa,IAAI,KAAK;AAC7C,MAAI,SAAU,QAAO,EAAE,IAAI,UAAU,SAAS;AAE9C,QAAM,SAAS,QAAQ,IAAI,oBAAoB;AAC/C,QAAM,OAAO,MAAM,eAAe,MAAM,GAAG,CAAC;AAC5C,QAAM,eAAe,OAAO,IAAI,IAAI,MAAM;AAC1C,QAAM,WAAW,GAAG,OAAO,eAAe;AAAA,IACxC,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB;AAAA,IACA,UAAU;AAAA,EACZ,CAAC;AACD,KAAG,QAAQ,QAAQ;AAEnB,MAAI;AACF,UAAM,GAAG,MAAM;AAAA,EACjB,SAAS,OAAO;AAQd,QAAI,CAAC,kBAAkB,KAAK,EAAG,OAAM;AACrC,UAAM,aAAa,GAAG,KAAK;AAC3B,UAAM,SAAS,MAAM,aAAa,YAAY,KAAK;AACnD,QAAI,CAAC,OAAQ,OAAM;AACnB,WAAO,EAAE,IAAI,YAAY,UAAU,OAAO;AAAA,EAC5C;AAEA,SAAO,EAAE,IAAI,SAAS;AACxB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/inbox_ops/lib/executionEngine.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EntityClass } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport type { EventBus } from '@open-mercato/events/types'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'\nimport { InboxProposal, InboxProposalAction, InboxDiscrepancy } from '../data/entities'\nimport type { InboxActionStatus, InboxActionType, InboxProposalStatus } from '../data/entities'\nimport { REQUIRED_FEATURES_MAP } from './constants'\nimport { formatZodErrors } from './validation'\nimport {\n clearLatestCommandLogEntry,\n ExecutionError,\n executeCommand,\n readLatestCommandLogEntry,\n} from './executionHelpers'\n\ninterface CommonEntityFields {\n tenantId?: string\n organizationId?: string\n deletedAt?: Date | null\n createdAt?: Date\n}\n\nexport interface CrossModuleEntities {\n CustomerEntity: EntityClass<CommonEntityFields & { id: string; kind: string; displayName: string; primaryEmail?: string | null }>\n SalesOrder: EntityClass<CommonEntityFields & { id: string; orderNumber: string; currencyCode: string; comments?: string | null; customerReference?: string | null }>\n SalesShipment: EntityClass<CommonEntityFields & { id: string; order: unknown }>\n SalesChannel: EntityClass<CommonEntityFields & { id: string; name: string; currencyCode?: string; metadata?: Record<string, unknown> | null }>\n Dictionary: EntityClass<CommonEntityFields & { id: string; key: string }>\n DictionaryEntry: EntityClass<CommonEntityFields & { id: string; label: string; value: string; normalizedValue?: string | null; dictionary: unknown }>\n}\n\ninterface ExecutionContext {\n em: EntityManager\n userId: string\n tenantId: string\n organizationId: string\n eventBus?: EventBus | null\n container: AwilixContainer\n auth?: AuthContext\n entities?: CrossModuleEntities\n}\n\ninterface ExecutionResult {\n success: boolean\n createdEntityId?: string | null\n createdEntityType?: string | null\n operationLogEntry?: {\n id?: string | null\n undoToken?: string | null\n commandId?: string | null\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n createdAt?: Date | string | null\n } | null\n error?: string\n statusCode?: number\n}\n\ninterface TypeExecutionResult {\n createdEntityId?: string | null\n createdEntityType?: string | null\n matchedEntityId?: string | null\n matchedEntityType?: string | null\n operationLogEntry?: ExecutionResult['operationLogEntry']\n}\n\nconst ACTION_EXECUTABLE_STATUSES: InboxActionStatus[] = ['pending', 'failed']\n\nexport async function executeAction(\n action: InboxProposalAction,\n ctx: ExecutionContext,\n): Promise<ExecutionResult> {\n const em = ctx.em.fork()\n\n try {\n await ensureUserCanExecuteAction(action, ctx)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unable to verify permissions'\n const statusCode = err instanceof ExecutionError ? err.statusCode : 503\n return { success: false, error: message, statusCode }\n }\n\n const claimed = await em.nativeUpdate(\n InboxProposalAction,\n {\n id: action.id,\n proposalId: action.proposalId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: { $in: ACTION_EXECUTABLE_STATUSES },\n deletedAt: null,\n },\n {\n status: 'processing',\n executionError: null,\n },\n )\n\n if (claimed === 0) {\n return { success: false, error: 'Action already processed', statusCode: 409 }\n }\n\n const freshAction = await findOneWithDecryption(\n em,\n InboxProposalAction,\n { id: action.id, deletedAt: null },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!freshAction) {\n return { success: false, error: 'Action not found', statusCode: 404 }\n }\n\n try {\n const result = await executeByType(freshAction, ctx)\n\n freshAction.status = 'executed'\n freshAction.executedAt = new Date()\n freshAction.executedByUserId = ctx.userId\n freshAction.createdEntityId = result.createdEntityId || null\n freshAction.createdEntityType = result.createdEntityType || null\n if (result.matchedEntityId !== undefined) {\n freshAction.matchedEntityId = result.matchedEntityId\n }\n if (result.matchedEntityType !== undefined) {\n freshAction.matchedEntityType = result.matchedEntityType\n }\n freshAction.executionError = null\n\n await em.flush()\n const encScope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n await resolveActionDiscrepancies(em, freshAction.id, encScope)\n\n // After create_contact or link_contact, resolve unknown_contact discrepancies\n // on ALL other actions in the same proposal that reference the same email\n if (freshAction.actionType === 'create_contact' || freshAction.actionType === 'link_contact') {\n const payload = freshAction.payload as Record<string, unknown> | null\n const contactEmail =\n typeof payload?.email === 'string' ? payload.email\n : typeof payload?.emailAddress === 'string' ? payload.emailAddress\n : null\n if (contactEmail) {\n await resolveUnknownContactDiscrepanciesInProposal(\n em, freshAction.proposalId, contactEmail, encScope,\n )\n }\n }\n\n await recalculateProposalStatus(em, freshAction.proposalId, encScope)\n\n if (ctx.eventBus) {\n await ctx.eventBus.emit('inbox_ops.action.executed', {\n actionId: freshAction.id,\n proposalId: freshAction.proposalId,\n actionType: freshAction.actionType,\n createdEntityId: result.createdEntityId || null,\n createdEntityType: result.createdEntityType || null,\n executedByUserId: ctx.userId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n }\n\n return { success: true, ...result, statusCode: 200 }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n const statusCode = err instanceof ExecutionError ? err.statusCode : 500\n\n freshAction.status = 'failed'\n freshAction.executionError = message\n freshAction.executedAt = new Date()\n freshAction.executedByUserId = ctx.userId\n await em.flush()\n\n await recalculateProposalStatus(em, freshAction.proposalId, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })\n\n if (ctx.eventBus) {\n await ctx.eventBus.emit('inbox_ops.action.failed', {\n actionId: freshAction.id,\n proposalId: freshAction.proposalId,\n actionType: freshAction.actionType,\n error: freshAction.executionError,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n }\n\n return { success: false, error: freshAction.executionError || 'Unknown error', statusCode }\n }\n}\n\nexport async function rejectAction(\n action: InboxProposalAction,\n ctx: ExecutionContext,\n): Promise<void> {\n const em = ctx.em.fork()\n const rejectedAt = new Date()\n const claimed = await em.nativeUpdate(\n InboxProposalAction,\n {\n id: action.id,\n proposalId: action.proposalId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: { $in: ACTION_EXECUTABLE_STATUSES },\n deletedAt: null,\n },\n {\n status: 'rejected',\n executedAt: rejectedAt,\n executedByUserId: ctx.userId,\n },\n )\n if (claimed === 0) return\n\n const freshAction = await findOneWithDecryption(\n em,\n InboxProposalAction,\n { id: action.id, deletedAt: null },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!freshAction) return\n\n const encScope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n await resolveActionDiscrepancies(em, freshAction.id, encScope)\n await recalculateProposalStatus(em, freshAction.proposalId, encScope)\n\n if (ctx.eventBus) {\n await ctx.eventBus.emit('inbox_ops.action.rejected', {\n actionId: freshAction.id,\n proposalId: freshAction.proposalId,\n actionType: freshAction.actionType,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n }\n}\n\nexport async function rejectProposal(\n proposalId: string,\n ctx: ExecutionContext,\n): Promise<void> {\n const em = ctx.em.fork()\n const rejectedAt = new Date()\n\n await em.nativeUpdate(\n InboxProposalAction,\n {\n proposalId,\n status: { $in: ACTION_EXECUTABLE_STATUSES },\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n {\n status: 'rejected',\n executedAt: rejectedAt,\n executedByUserId: ctx.userId,\n },\n )\n\n await em.nativeUpdate(\n InboxDiscrepancy,\n {\n proposalId,\n resolved: false,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n },\n { resolved: true },\n )\n\n await recalculateProposalStatus(em, proposalId, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })\n\n if (ctx.eventBus) {\n await ctx.eventBus.emit('inbox_ops.proposal.rejected', {\n proposalId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n }\n}\n\nexport async function acceptAllActions(\n proposalId: string,\n ctx: ExecutionContext,\n): Promise<{ results: ExecutionResult[]; stoppedOnFailure: boolean }> {\n const em = ctx.em.fork()\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId,\n status: 'pending',\n deletedAt: null,\n },\n { orderBy: { sortOrder: 'ASC' } },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n\n const results: ExecutionResult[] = []\n let stoppedOnFailure = false\n\n for (const action of actions) {\n const result = await executeAction(action, ctx)\n results.push(result)\n\n if (!result.success) {\n stoppedOnFailure = true\n break\n }\n }\n\n return { results, stoppedOnFailure }\n}\n\n/**\n * Normalize common LLM payload issues before action-specific normalization.\n */\nfunction normalizeCommonPayloadFields(\n payload: Record<string, unknown>,\n actionType: string,\n): Record<string, unknown> {\n // Lowercase contact type fields (LLM often outputs \"Person\" / \"Company\")\n if (typeof payload.type === 'string') {\n payload.type = payload.type.toLowerCase()\n }\n if (typeof payload.contactType === 'string') {\n payload.contactType = payload.contactType.toLowerCase()\n }\n\n // Normalize link_contact field names (LLM may use various alternatives)\n if (actionType === 'link_contact') {\n if (!payload.emailAddress) {\n const alt = payload.email ?? payload.contactEmail\n if (typeof alt === 'string') payload.emailAddress = alt\n }\n if (!payload.contactId) {\n const alt = payload.id ?? payload.matchedId ?? payload.matchedContactId\n if (typeof alt === 'string') payload.contactId = alt\n }\n if (!payload.contactType) {\n const alt = payload.type ?? payload.kind ?? payload.matchedType ?? payload.matchedContactType\n if (typeof alt === 'string') payload.contactType = alt.toLowerCase()\n }\n if (!payload.contactName) {\n const alt = payload.name ?? payload.displayName\n if (typeof alt === 'string') payload.contactName = alt\n }\n }\n\n return payload\n}\n\n/**\n * Adapt the internal ExecutionContext to the shared InboxActionExecutionContext\n * for use by registered action handlers.\n */\nfunction adaptContext(ctx: ExecutionContext): InboxActionExecutionContext {\n return {\n ...ctx,\n executeCommand: <TInput, TResult>(commandId: string, input: TInput) =>\n executeCommand<TInput, TResult>(ctx, commandId, input),\n resolveEntityClass: <T>(key: string) =>\n resolveEntityClassInternal(ctx, key) as (new (...args: unknown[]) => T) | null,\n }\n}\n\nasync function executeByType(\n action: InboxProposalAction,\n ctx: ExecutionContext,\n): Promise<TypeExecutionResult> {\n // Lazy-load the generated registry to avoid circular imports at module load time\n const { getInboxAction } = await import('@/.mercato/generated/inbox-actions.generated')\n const definition = getInboxAction(action.actionType)\n if (!definition) {\n throw new ExecutionError(`Unknown action type: ${action.actionType}`, 400)\n }\n\n let payload = { ...(action.payload as Record<string, unknown>) }\n\n // Common normalization (lowercase enums, field aliases)\n payload = normalizeCommonPayloadFields(payload, action.actionType)\n\n // Action-specific normalization from the registered handler\n const actionCtx = adaptContext(ctx)\n clearLatestCommandLogEntry(actionCtx)\n if (definition.normalizePayload) {\n payload = await definition.normalizePayload(payload, actionCtx)\n }\n\n const parsed = definition.payloadSchema.safeParse(payload)\n if (!parsed.success) {\n throw new ExecutionError(\n `Invalid ${action.actionType} payload: ${formatZodErrors(parsed.error)}`,\n 400,\n )\n }\n\n const result = await definition.execute(\n { id: action.id, proposalId: action.proposalId, payload: parsed.data },\n actionCtx,\n )\n\n return {\n ...result,\n operationLogEntry: readLatestCommandLogEntry(actionCtx),\n }\n}\n\nasync function resolveUnknownContactDiscrepanciesInProposal(\n em: EntityManager,\n proposalId: string,\n contactEmail: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<void> {\n if (!contactEmail) return\n\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n {\n proposalId,\n type: 'unknown_contact',\n resolved: false,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n undefined,\n scope,\n )\n\n const normalizedEmail = contactEmail.trim().toLowerCase()\n const matching = discrepancies.filter((d) => {\n const foundValue = (d.foundValue || '').trim().toLowerCase()\n return foundValue === normalizedEmail\n })\n\n for (const d of matching) {\n d.resolved = true\n }\n\n if (matching.length > 0) {\n await em.flush()\n }\n}\n\nasync function ensureUserCanExecuteAction(action: InboxProposalAction, ctx: ExecutionContext): Promise<void> {\n const requiredFeature = getRequiredFeatureForAction(action)\n if (!requiredFeature || requiredFeature.trim().length === 0) {\n throw new ExecutionError(\n `No required feature is mapped for action type \"${action.actionType}\"; refusing to execute`,\n 403,\n )\n }\n\n const rbacService = ctx.container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId: string },\n ) => Promise<boolean>\n }\n\n if (!rbacService || typeof rbacService.userHasAllFeatures !== 'function') {\n throw new ExecutionError('Unable to verify permissions for action execution', 503)\n }\n\n const hasFeature = await rbacService.userHasAllFeatures(\n ctx.userId,\n [requiredFeature],\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n\n if (!hasFeature) {\n throw new ExecutionError(`Insufficient permissions: ${requiredFeature} required`, 403)\n }\n}\n\nfunction resolveEntityClassInternal(\n ctx: ExecutionContext,\n key: string,\n): unknown {\n const fromEntities = (ctx.entities as Record<string, unknown> | undefined)?.[key]\n if (fromEntities) return fromEntities\n try { return ctx.container.resolve(key) } catch { return null }\n}\n\n// Re-export splitPersonName for backward compat\nexport { splitPersonName } from './contactValidation'\n\nasync function resolveActionDiscrepancies(\n em: EntityManager,\n actionId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<void> {\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n { actionId, resolved: false },\n undefined,\n scope,\n )\n for (const discrepancy of discrepancies) {\n discrepancy.resolved = true\n }\n if (discrepancies.length > 0) {\n await em.flush()\n }\n}\n\nexport async function recalculateProposalStatus(\n em: EntityManager,\n proposalId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<void> {\n const proposal = await findOneWithDecryption(\n em,\n InboxProposal,\n { id: proposalId, deletedAt: null },\n undefined,\n scope,\n )\n if (!proposal) return\n\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n { proposalId, deletedAt: null },\n undefined,\n scope,\n )\n\n if (actions.length === 0) {\n proposal.status = 'pending'\n await em.flush()\n return\n }\n\n const statuses = actions.map((action) => action.status)\n const allAcceptedOrExecuted = statuses.every((status) => status === 'accepted' || status === 'executed')\n const allRejected = statuses.every((status) => status === 'rejected')\n const allPending = statuses.every((status) => status === 'pending')\n\n let newStatus: InboxProposalStatus\n if (allAcceptedOrExecuted) {\n newStatus = 'accepted'\n } else if (allRejected) {\n newStatus = 'rejected'\n } else if (allPending) {\n newStatus = 'pending'\n } else {\n newStatus = 'partial'\n }\n\n if (proposal.status !== newStatus) {\n proposal.status = newStatus\n await em.flush()\n }\n}\n\nexport function getRequiredFeature(actionType: InboxActionType): string {\n return REQUIRED_FEATURES_MAP[actionType]\n}\n\nfunction getRequiredFeatureForAction(action: InboxProposalAction): string {\n if (action.actionType === 'create_contact') {\n const payload = action.payload as Record<string, unknown> | null\n if (payload?.type === 'company') {\n return 'customers.companies.manage'\n }\n }\n return REQUIRED_FEATURES_MAP[action.actionType]\n}\n"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EntityClass } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport type { EventBus } from '@open-mercato/events/types'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'\nimport { InboxProposal, InboxProposalAction, InboxDiscrepancy } from '../data/entities'\nimport type { InboxActionStatus, InboxActionType, InboxProposalStatus } from '../data/entities'\nimport { REQUIRED_FEATURES_MAP } from './constants'\nimport { formatZodErrors } from './validation'\nimport {\n clearLatestCommandLogEntry,\n ExecutionError,\n executeCommand,\n readLatestCommandLogEntry,\n} from './executionHelpers'\n\ninterface CommonEntityFields {\n tenantId?: string\n organizationId?: string\n deletedAt?: Date | null\n createdAt?: Date\n}\n\nexport interface CrossModuleEntities {\n CustomerEntity: EntityClass<CommonEntityFields & { id: string; kind: string; displayName: string; primaryEmail?: string | null }>\n SalesOrder: EntityClass<CommonEntityFields & { id: string; orderNumber: string; currencyCode: string; comments?: string | null; customerReference?: string | null }>\n SalesShipment: EntityClass<CommonEntityFields & { id: string; order: unknown }>\n SalesChannel: EntityClass<CommonEntityFields & { id: string; name: string; metadata?: Record<string, unknown> | null }>\n Dictionary: EntityClass<CommonEntityFields & { id: string; key: string }>\n DictionaryEntry: EntityClass<CommonEntityFields & { id: string; label: string; value: string; normalizedValue?: string | null; dictionary: unknown }>\n}\n\ninterface ExecutionContext {\n em: EntityManager\n userId: string\n tenantId: string\n organizationId: string\n eventBus?: EventBus | null\n container: AwilixContainer\n auth?: AuthContext\n entities?: CrossModuleEntities\n}\n\ninterface ExecutionResult {\n success: boolean\n createdEntityId?: string | null\n createdEntityType?: string | null\n operationLogEntry?: {\n id?: string | null\n undoToken?: string | null\n commandId?: string | null\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n createdAt?: Date | string | null\n } | null\n error?: string\n statusCode?: number\n}\n\ninterface TypeExecutionResult {\n createdEntityId?: string | null\n createdEntityType?: string | null\n matchedEntityId?: string | null\n matchedEntityType?: string | null\n operationLogEntry?: ExecutionResult['operationLogEntry']\n}\n\nconst ACTION_EXECUTABLE_STATUSES: InboxActionStatus[] = ['pending', 'failed']\n\nexport async function executeAction(\n action: InboxProposalAction,\n ctx: ExecutionContext,\n): Promise<ExecutionResult> {\n const em = ctx.em.fork()\n\n try {\n await ensureUserCanExecuteAction(action, ctx)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unable to verify permissions'\n const statusCode = err instanceof ExecutionError ? err.statusCode : 503\n return { success: false, error: message, statusCode }\n }\n\n const claimed = await em.nativeUpdate(\n InboxProposalAction,\n {\n id: action.id,\n proposalId: action.proposalId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: { $in: ACTION_EXECUTABLE_STATUSES },\n deletedAt: null,\n },\n {\n status: 'processing',\n executionError: null,\n },\n )\n\n if (claimed === 0) {\n return { success: false, error: 'Action already processed', statusCode: 409 }\n }\n\n const freshAction = await findOneWithDecryption(\n em,\n InboxProposalAction,\n { id: action.id, deletedAt: null },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!freshAction) {\n return { success: false, error: 'Action not found', statusCode: 404 }\n }\n\n try {\n const result = await executeByType(freshAction, ctx)\n\n freshAction.status = 'executed'\n freshAction.executedAt = new Date()\n freshAction.executedByUserId = ctx.userId\n freshAction.createdEntityId = result.createdEntityId || null\n freshAction.createdEntityType = result.createdEntityType || null\n if (result.matchedEntityId !== undefined) {\n freshAction.matchedEntityId = result.matchedEntityId\n }\n if (result.matchedEntityType !== undefined) {\n freshAction.matchedEntityType = result.matchedEntityType\n }\n freshAction.executionError = null\n\n await em.flush()\n const encScope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n await resolveActionDiscrepancies(em, freshAction.id, encScope)\n\n // After create_contact or link_contact, resolve unknown_contact discrepancies\n // on ALL other actions in the same proposal that reference the same email\n if (freshAction.actionType === 'create_contact' || freshAction.actionType === 'link_contact') {\n const payload = freshAction.payload as Record<string, unknown> | null\n const contactEmail =\n typeof payload?.email === 'string' ? payload.email\n : typeof payload?.emailAddress === 'string' ? payload.emailAddress\n : null\n if (contactEmail) {\n await resolveUnknownContactDiscrepanciesInProposal(\n em, freshAction.proposalId, contactEmail, encScope,\n )\n }\n }\n\n await recalculateProposalStatus(em, freshAction.proposalId, encScope)\n\n if (ctx.eventBus) {\n await ctx.eventBus.emit('inbox_ops.action.executed', {\n actionId: freshAction.id,\n proposalId: freshAction.proposalId,\n actionType: freshAction.actionType,\n createdEntityId: result.createdEntityId || null,\n createdEntityType: result.createdEntityType || null,\n executedByUserId: ctx.userId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n }\n\n return { success: true, ...result, statusCode: 200 }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n const statusCode = err instanceof ExecutionError ? err.statusCode : 500\n\n freshAction.status = 'failed'\n freshAction.executionError = message\n freshAction.executedAt = new Date()\n freshAction.executedByUserId = ctx.userId\n await em.flush()\n\n await recalculateProposalStatus(em, freshAction.proposalId, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })\n\n if (ctx.eventBus) {\n await ctx.eventBus.emit('inbox_ops.action.failed', {\n actionId: freshAction.id,\n proposalId: freshAction.proposalId,\n actionType: freshAction.actionType,\n error: freshAction.executionError,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n }\n\n return { success: false, error: freshAction.executionError || 'Unknown error', statusCode }\n }\n}\n\nexport async function rejectAction(\n action: InboxProposalAction,\n ctx: ExecutionContext,\n): Promise<void> {\n const em = ctx.em.fork()\n const rejectedAt = new Date()\n const claimed = await em.nativeUpdate(\n InboxProposalAction,\n {\n id: action.id,\n proposalId: action.proposalId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: { $in: ACTION_EXECUTABLE_STATUSES },\n deletedAt: null,\n },\n {\n status: 'rejected',\n executedAt: rejectedAt,\n executedByUserId: ctx.userId,\n },\n )\n if (claimed === 0) return\n\n const freshAction = await findOneWithDecryption(\n em,\n InboxProposalAction,\n { id: action.id, deletedAt: null },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!freshAction) return\n\n const encScope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n await resolveActionDiscrepancies(em, freshAction.id, encScope)\n await recalculateProposalStatus(em, freshAction.proposalId, encScope)\n\n if (ctx.eventBus) {\n await ctx.eventBus.emit('inbox_ops.action.rejected', {\n actionId: freshAction.id,\n proposalId: freshAction.proposalId,\n actionType: freshAction.actionType,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n }\n}\n\nexport async function rejectProposal(\n proposalId: string,\n ctx: ExecutionContext,\n): Promise<void> {\n const em = ctx.em.fork()\n const rejectedAt = new Date()\n\n await em.nativeUpdate(\n InboxProposalAction,\n {\n proposalId,\n status: { $in: ACTION_EXECUTABLE_STATUSES },\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n {\n status: 'rejected',\n executedAt: rejectedAt,\n executedByUserId: ctx.userId,\n },\n )\n\n await em.nativeUpdate(\n InboxDiscrepancy,\n {\n proposalId,\n resolved: false,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n },\n { resolved: true },\n )\n\n await recalculateProposalStatus(em, proposalId, { tenantId: ctx.tenantId, organizationId: ctx.organizationId })\n\n if (ctx.eventBus) {\n await ctx.eventBus.emit('inbox_ops.proposal.rejected', {\n proposalId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n }\n}\n\nexport async function acceptAllActions(\n proposalId: string,\n ctx: ExecutionContext,\n): Promise<{ results: ExecutionResult[]; stoppedOnFailure: boolean }> {\n const em = ctx.em.fork()\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId,\n status: 'pending',\n deletedAt: null,\n },\n { orderBy: { sortOrder: 'ASC' } },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n\n const results: ExecutionResult[] = []\n let stoppedOnFailure = false\n\n for (const action of actions) {\n const result = await executeAction(action, ctx)\n results.push(result)\n\n if (!result.success) {\n stoppedOnFailure = true\n break\n }\n }\n\n return { results, stoppedOnFailure }\n}\n\n/**\n * Normalize common LLM payload issues before action-specific normalization.\n */\nfunction normalizeCommonPayloadFields(\n payload: Record<string, unknown>,\n actionType: string,\n): Record<string, unknown> {\n // Lowercase contact type fields (LLM often outputs \"Person\" / \"Company\")\n if (typeof payload.type === 'string') {\n payload.type = payload.type.toLowerCase()\n }\n if (typeof payload.contactType === 'string') {\n payload.contactType = payload.contactType.toLowerCase()\n }\n\n // Normalize link_contact field names (LLM may use various alternatives)\n if (actionType === 'link_contact') {\n if (!payload.emailAddress) {\n const alt = payload.email ?? payload.contactEmail\n if (typeof alt === 'string') payload.emailAddress = alt\n }\n if (!payload.contactId) {\n const alt = payload.id ?? payload.matchedId ?? payload.matchedContactId\n if (typeof alt === 'string') payload.contactId = alt\n }\n if (!payload.contactType) {\n const alt = payload.type ?? payload.kind ?? payload.matchedType ?? payload.matchedContactType\n if (typeof alt === 'string') payload.contactType = alt.toLowerCase()\n }\n if (!payload.contactName) {\n const alt = payload.name ?? payload.displayName\n if (typeof alt === 'string') payload.contactName = alt\n }\n }\n\n return payload\n}\n\n/**\n * Adapt the internal ExecutionContext to the shared InboxActionExecutionContext\n * for use by registered action handlers.\n */\nfunction adaptContext(ctx: ExecutionContext): InboxActionExecutionContext {\n return {\n ...ctx,\n executeCommand: <TInput, TResult>(commandId: string, input: TInput) =>\n executeCommand<TInput, TResult>(ctx, commandId, input),\n resolveEntityClass: <T>(key: string) =>\n resolveEntityClassInternal(ctx, key) as (new (...args: unknown[]) => T) | null,\n }\n}\n\nasync function executeByType(\n action: InboxProposalAction,\n ctx: ExecutionContext,\n): Promise<TypeExecutionResult> {\n // Lazy-load the generated registry to avoid circular imports at module load time\n const { getInboxAction } = await import('@/.mercato/generated/inbox-actions.generated')\n const definition = getInboxAction(action.actionType)\n if (!definition) {\n throw new ExecutionError(`Unknown action type: ${action.actionType}`, 400)\n }\n\n let payload = { ...(action.payload as Record<string, unknown>) }\n\n // Common normalization (lowercase enums, field aliases)\n payload = normalizeCommonPayloadFields(payload, action.actionType)\n\n // Action-specific normalization from the registered handler\n const actionCtx = adaptContext(ctx)\n clearLatestCommandLogEntry(actionCtx)\n if (definition.normalizePayload) {\n payload = await definition.normalizePayload(payload, actionCtx)\n }\n\n const parsed = definition.payloadSchema.safeParse(payload)\n if (!parsed.success) {\n throw new ExecutionError(\n `Invalid ${action.actionType} payload: ${formatZodErrors(parsed.error)}`,\n 400,\n )\n }\n\n const result = await definition.execute(\n { id: action.id, proposalId: action.proposalId, payload: parsed.data },\n actionCtx,\n )\n\n return {\n ...result,\n operationLogEntry: readLatestCommandLogEntry(actionCtx),\n }\n}\n\nasync function resolveUnknownContactDiscrepanciesInProposal(\n em: EntityManager,\n proposalId: string,\n contactEmail: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<void> {\n if (!contactEmail) return\n\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n {\n proposalId,\n type: 'unknown_contact',\n resolved: false,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n undefined,\n scope,\n )\n\n const normalizedEmail = contactEmail.trim().toLowerCase()\n const matching = discrepancies.filter((d) => {\n const foundValue = (d.foundValue || '').trim().toLowerCase()\n return foundValue === normalizedEmail\n })\n\n for (const d of matching) {\n d.resolved = true\n }\n\n if (matching.length > 0) {\n await em.flush()\n }\n}\n\nasync function ensureUserCanExecuteAction(action: InboxProposalAction, ctx: ExecutionContext): Promise<void> {\n const requiredFeature = getRequiredFeatureForAction(action)\n if (!requiredFeature || requiredFeature.trim().length === 0) {\n throw new ExecutionError(\n `No required feature is mapped for action type \"${action.actionType}\"; refusing to execute`,\n 403,\n )\n }\n\n const rbacService = ctx.container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId: string },\n ) => Promise<boolean>\n }\n\n if (!rbacService || typeof rbacService.userHasAllFeatures !== 'function') {\n throw new ExecutionError('Unable to verify permissions for action execution', 503)\n }\n\n const hasFeature = await rbacService.userHasAllFeatures(\n ctx.userId,\n [requiredFeature],\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n\n if (!hasFeature) {\n throw new ExecutionError(`Insufficient permissions: ${requiredFeature} required`, 403)\n }\n}\n\nfunction resolveEntityClassInternal(\n ctx: ExecutionContext,\n key: string,\n): unknown {\n const fromEntities = (ctx.entities as Record<string, unknown> | undefined)?.[key]\n if (fromEntities) return fromEntities\n try { return ctx.container.resolve(key) } catch { return null }\n}\n\n// Re-export splitPersonName for backward compat\nexport { splitPersonName } from './contactValidation'\n\nasync function resolveActionDiscrepancies(\n em: EntityManager,\n actionId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<void> {\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n { actionId, resolved: false },\n undefined,\n scope,\n )\n for (const discrepancy of discrepancies) {\n discrepancy.resolved = true\n }\n if (discrepancies.length > 0) {\n await em.flush()\n }\n}\n\nexport async function recalculateProposalStatus(\n em: EntityManager,\n proposalId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<void> {\n const proposal = await findOneWithDecryption(\n em,\n InboxProposal,\n { id: proposalId, deletedAt: null },\n undefined,\n scope,\n )\n if (!proposal) return\n\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n { proposalId, deletedAt: null },\n undefined,\n scope,\n )\n\n if (actions.length === 0) {\n proposal.status = 'pending'\n await em.flush()\n return\n }\n\n const statuses = actions.map((action) => action.status)\n const allAcceptedOrExecuted = statuses.every((status) => status === 'accepted' || status === 'executed')\n const allRejected = statuses.every((status) => status === 'rejected')\n const allPending = statuses.every((status) => status === 'pending')\n\n let newStatus: InboxProposalStatus\n if (allAcceptedOrExecuted) {\n newStatus = 'accepted'\n } else if (allRejected) {\n newStatus = 'rejected'\n } else if (allPending) {\n newStatus = 'pending'\n } else {\n newStatus = 'partial'\n }\n\n if (proposal.status !== newStatus) {\n proposal.status = newStatus\n await em.flush()\n }\n}\n\nexport function getRequiredFeature(actionType: InboxActionType): string {\n return REQUIRED_FEATURES_MAP[actionType]\n}\n\nfunction getRequiredFeatureForAction(action: InboxProposalAction): string {\n if (action.actionType === 'create_contact') {\n const payload = action.payload as Record<string, unknown> | null\n if (payload?.type === 'company') {\n return 'customers.companies.manage'\n }\n }\n return REQUIRED_FEATURES_MAP[action.actionType]\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -132,28 +132,6 @@ async function resolveFirstChannelId(ctx) {
|
|
|
132
132
|
return null;
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
-
async function resolveChannelCurrency(ctx, channelId) {
|
|
136
|
-
const SalesChannelClass = resolveEntityClass(ctx, "SalesChannel");
|
|
137
|
-
if (!SalesChannelClass) return null;
|
|
138
|
-
try {
|
|
139
|
-
const where = {
|
|
140
|
-
tenantId: ctx.tenantId,
|
|
141
|
-
organizationId: ctx.organizationId,
|
|
142
|
-
deletedAt: null
|
|
143
|
-
};
|
|
144
|
-
if (channelId) where.id = channelId;
|
|
145
|
-
const channel = await findOneWithDecryption(
|
|
146
|
-
ctx.em,
|
|
147
|
-
SalesChannelClass,
|
|
148
|
-
where,
|
|
149
|
-
channelId ? void 0 : { orderBy: { name: "ASC" } },
|
|
150
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId }
|
|
151
|
-
);
|
|
152
|
-
return channel?.currencyCode ?? null;
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
135
|
async function resolveEffectiveDocumentKind(ctx, channelId) {
|
|
158
136
|
const SalesChannelClass = resolveEntityClass(ctx, "SalesChannel");
|
|
159
137
|
if (!SalesChannelClass) return "order";
|
|
@@ -390,7 +368,6 @@ export {
|
|
|
390
368
|
parseDateToken,
|
|
391
369
|
parseNumberToken,
|
|
392
370
|
readLatestCommandLogEntry,
|
|
393
|
-
resolveChannelCurrency,
|
|
394
371
|
resolveContactIdByNameAndType,
|
|
395
372
|
resolveCustomerEntityIdByEmail,
|
|
396
373
|
resolveEffectiveDocumentKind,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/inbox_ops/lib/executionHelpers.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EntityClass } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport type { EventBus } from '@open-mercato/events/types'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'\nimport type { CrossModuleEntities } from './executionEngine'\nexport { formatZodErrors } from './validation'\n\n// ---------------------------------------------------------------------------\n// Context type used by helper functions (concrete types for ORM/DI access)\n// ---------------------------------------------------------------------------\n\nexport interface ExecutionHelperContext {\n em: EntityManager\n userId: string\n tenantId: string\n organizationId: string\n eventBus?: EventBus | null\n container: AwilixContainer\n auth?: AuthContext\n entities?: CrossModuleEntities\n}\n\ntype OperationLogEntryLike = {\n id?: string | null\n undoToken?: string | null\n commandId?: string | null\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n createdAt?: Date | string | null\n}\n\nconst LAST_OPERATION_LOG_ENTRY = Symbol('inboxOps:lastOperationLogEntry')\n\ntype OperationLogCarrier = {\n [LAST_OPERATION_LOG_ENTRY]?: OperationLogEntryLike | null\n}\n\n/**\n * Cast InboxActionExecutionContext (from shared) to the concrete helper context.\n * The inbox-actions.ts handlers receive InboxActionExecutionContext but helpers\n * need concrete EntityManager / AwilixContainer types.\n */\nexport function asHelperContext(ctx: InboxActionExecutionContext): ExecutionHelperContext {\n return ctx as unknown as ExecutionHelperContext\n}\n\nexport function clearLatestCommandLogEntry(ctx: object) {\n ;(ctx as OperationLogCarrier)[LAST_OPERATION_LOG_ENTRY] = null\n}\n\nexport function readLatestCommandLogEntry(ctx: object): OperationLogEntryLike | null {\n return (ctx as OperationLogCarrier)[LAST_OPERATION_LOG_ENTRY] ?? null\n}\n\ninterface FeatureCheckingRbacService {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId: string },\n ) => Promise<boolean>\n}\n\n// Super-admin status is the immutable `isSuperAdmin` flag derived from the ACL\n// `is_super_admin` column at session resolution. Never fall back to comparing the\n// auth context's `roles` against a literal like 'superadmin' \u2014 role names are\n// tenant-mutable and trivially spoofable, so a role-name fallback would let a tenant\n// admin mint a `superadmin`-named role to bypass every inbox-ops feature gate\n// (#2699). Mirrors the scheduler module's hardened gate.\nfunction hasSuperAdminAccess(auth: ExecutionHelperContext['auth']): boolean {\n if (!auth || typeof auth !== 'object') return false\n return (auth as Record<string, unknown>).isSuperAdmin === true\n}\n\n// ---------------------------------------------------------------------------\n// Error\n// ---------------------------------------------------------------------------\n\nexport class ExecutionError extends Error {\n statusCode: number\n\n constructor(message: string, statusCode = 400) {\n super(message)\n this.statusCode = statusCode\n }\n}\n\n// ---------------------------------------------------------------------------\n// Command execution\n// ---------------------------------------------------------------------------\n\nexport async function executeCommand<TInput, TResult>(\n ctx: ExecutionHelperContext,\n commandId: string,\n input: TInput,\n): Promise<TResult> {\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n if (!commandBus || typeof commandBus.execute !== 'function') {\n throw new ExecutionError('Command bus is not available', 503)\n }\n\n const auth =\n ctx.auth ??\n ({\n sub: ctx.userId,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n isSuperAdmin: false,\n } satisfies Exclude<AuthContext, null>)\n\n const commandContext: CommandRuntimeContext = {\n container: ctx.container,\n auth,\n organizationScope: null,\n selectedOrganizationId: ctx.organizationId,\n organizationIds: [ctx.organizationId],\n }\n\n const { result, logEntry } = await commandBus.execute<TInput, TResult>(commandId, {\n input,\n ctx: commandContext,\n })\n\n if (logEntry?.undoToken && logEntry.id && logEntry.commandId) {\n ;(ctx as ExecutionHelperContext & OperationLogCarrier)[LAST_OPERATION_LOG_ENTRY] = logEntry\n }\n\n return result\n}\n\nexport async function userHasFeature(\n ctx: ExecutionHelperContext,\n feature: string,\n): Promise<boolean> {\n if (!feature || feature.trim().length === 0) return false\n if (hasSuperAdminAccess(ctx.auth)) return true\n\n try {\n const rbacService = ctx.container.resolve('rbacService') as FeatureCheckingRbacService\n if (!rbacService || typeof rbacService.userHasAllFeatures !== 'function') {\n return false\n }\n return rbacService.userHasAllFeatures(\n ctx.userId,\n [feature],\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n } catch {\n return false\n }\n}\n\n// ---------------------------------------------------------------------------\n// Entity resolution\n// ---------------------------------------------------------------------------\n\nexport function resolveEntityClass<K extends keyof CrossModuleEntities>(\n ctx: ExecutionHelperContext,\n key: K,\n): CrossModuleEntities[K] | null {\n const fromEntities = ctx.entities?.[key]\n if (fromEntities) return fromEntities\n try { return ctx.container.resolve(key) } catch { return null }\n}\n\n// ---------------------------------------------------------------------------\n// Source metadata\n// ---------------------------------------------------------------------------\n\nexport function buildSourceMetadata(actionId: string, proposalId: string): Record<string, unknown> {\n return {\n source: 'inbox_ops',\n inboxOpsActionId: actionId,\n inboxOpsProposalId: proposalId,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Order resolution\n// ---------------------------------------------------------------------------\n\nexport async function resolveOrderByReference(\n ctx: ExecutionHelperContext,\n orderId?: string,\n orderNumber?: string,\n): Promise<{ id: string; orderNumber: string; currencyCode: string; comments?: string | null }> {\n const SalesOrderClass = resolveEntityClass(ctx, 'SalesOrder')\n if (!SalesOrderClass) {\n throw new ExecutionError('Sales module entities not available', 503)\n }\n\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n }\n if (orderId) {\n where.id = orderId\n } else if (orderNumber && orderNumber.trim().length > 0) {\n where.orderNumber = orderNumber.trim()\n } else {\n throw new ExecutionError('Order reference is required', 400)\n }\n\n const order = await findOneWithDecryption(\n ctx.em,\n SalesOrderClass,\n where,\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!order) {\n throw new ExecutionError('Referenced order not found', 404)\n }\n return order\n}\n\n// ---------------------------------------------------------------------------\n// Channel resolution\n// ---------------------------------------------------------------------------\n\nexport async function resolveFirstChannelId(ctx: ExecutionHelperContext): Promise<string | null> {\n const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')\n if (!SalesChannelClass) return null\n\n try {\n const channel = await findOneWithDecryption(\n ctx.em,\n SalesChannelClass,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n { orderBy: { name: 'ASC' } },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n return channel?.id ?? null\n } catch {\n return null\n }\n}\n\nexport async function resolveChannelCurrency(\n ctx: ExecutionHelperContext,\n channelId: string | null,\n): Promise<string | null> {\n const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')\n if (!SalesChannelClass) return null\n\n try {\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n }\n if (channelId) where.id = channelId\n const channel = await findOneWithDecryption(\n ctx.em,\n SalesChannelClass,\n where,\n channelId ? undefined : { orderBy: { name: 'ASC' } },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n return channel?.currencyCode ?? null\n } catch {\n return null\n }\n}\n\nexport async function resolveEffectiveDocumentKind(\n ctx: ExecutionHelperContext,\n channelId: string,\n): Promise<'order' | 'quote'> {\n const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')\n if (!SalesChannelClass) return 'order'\n\n const channel = await findOneWithDecryption(\n ctx.em,\n SalesChannelClass,\n {\n id: channelId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!channel) return 'order'\n\n const metadata = channel.metadata as Record<string, unknown> | null\n if (metadata?.quotesRequired === true) {\n return 'quote'\n }\n return 'order'\n}\n\n// ---------------------------------------------------------------------------\n// Shipment status resolution\n// ---------------------------------------------------------------------------\n\nconst SALES_SHIPMENT_STATUS_DICTIONARY_KEY = 'sales.shipment_status'\n\nexport async function resolveShipmentStatusEntryId(\n ctx: ExecutionHelperContext,\n statusLabel: string,\n): Promise<string | null> {\n const DictionaryClass = resolveEntityClass(ctx, 'Dictionary')\n const DictionaryEntryClass = resolveEntityClass(ctx, 'DictionaryEntry')\n if (!DictionaryClass || !DictionaryEntryClass) return null\n\n const encryptionScope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n\n const dictionary = await findOneWithDecryption(\n ctx.em,\n DictionaryClass,\n {\n key: SALES_SHIPMENT_STATUS_DICTIONARY_KEY,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n undefined,\n encryptionScope,\n )\n if (!dictionary) return null\n\n const entries = await findWithDecryption(\n ctx.em,\n DictionaryEntryClass,\n {\n dictionary: dictionary.id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n },\n undefined,\n encryptionScope,\n )\n if (!entries.length) return null\n\n const normalizedTarget = normalizeDictionaryToken(statusLabel)\n const loweredTarget = statusLabel.trim().toLowerCase()\n\n const match = entries.find((entry) => {\n const label = entry.label.trim().toLowerCase()\n const value = entry.value.trim().toLowerCase()\n return (\n entry.normalizedValue === normalizedTarget ||\n label === loweredTarget ||\n value === loweredTarget\n )\n })\n\n return match?.id ?? null\n}\n\n// ---------------------------------------------------------------------------\n// Customer / contact resolution\n// ---------------------------------------------------------------------------\n\nexport async function resolveCustomerEntityIdByEmail(\n ctx: ExecutionHelperContext,\n email: string,\n): Promise<string | null> {\n const normalized = email.trim().toLowerCase()\n if (!normalized) return null\n\n const CustomerEntityClass = resolveEntityClass(ctx, 'CustomerEntity')\n if (!CustomerEntityClass) return null\n\n const entity = await findOneWithDecryption(\n ctx.em,\n CustomerEntityClass,\n {\n primaryEmail: normalized,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (entity) return entity.id\n\n const candidates = await findWithDecryption(\n ctx.em,\n CustomerEntityClass,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n { limit: 100, orderBy: { createdAt: 'DESC' } },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n const match = candidates.find(\n (e) => e.primaryEmail && e.primaryEmail.toLowerCase() === normalized,\n )\n return match?.id ?? null\n}\n\nexport async function resolveContactIdByNameAndType(\n ctx: ExecutionHelperContext,\n contactName: string,\n contactType: string,\n): Promise<string | null> {\n const CustomerEntityClass = resolveEntityClass(ctx, 'CustomerEntity')\n if (!CustomerEntityClass) return null\n\n const normalized = contactName.trim()\n if (!normalized) return null\n\n const entity = await findOneWithDecryption(\n ctx.em,\n CustomerEntityClass,\n {\n displayName: normalized,\n kind: contactType,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n\n return entity?.id ?? null\n}\n\n// ---------------------------------------------------------------------------\n// Order line items\n// ---------------------------------------------------------------------------\n\nexport interface OrderLineItem {\n id: string\n name?: string | null\n}\n\nexport async function loadOrderLineItems(\n ctx: ExecutionHelperContext,\n orderId: string,\n): Promise<OrderLineItem[]> {\n try {\n const result = await executeCommand<Record<string, unknown>, { lines?: OrderLineItem[] }>(\n ctx,\n 'sales.orders.lines.list',\n { orderId, organizationId: ctx.organizationId, tenantId: ctx.tenantId },\n )\n return result.lines ?? []\n } catch {\n return []\n }\n}\n\nexport function matchLineItemByName(\n orderLines: OrderLineItem[],\n lineItemName: string,\n): string | null {\n const target = lineItemName.trim().toLowerCase()\n if (!target) return null\n\n const exact = orderLines.find((l) => (l.name || '').trim().toLowerCase() === target)\n if (exact) return exact.id\n\n const partial = orderLines.find((l) => {\n const name = (l.name || '').trim().toLowerCase()\n return name.includes(target) || target.includes(name)\n })\n return partial?.id ?? null\n}\n\n// ---------------------------------------------------------------------------\n// Data normalization utilities\n// ---------------------------------------------------------------------------\n\nexport function normalizeAddressSnapshot(\n address: Record<string, unknown>,\n): Record<string, unknown> {\n return {\n addressLine1: address.line1 ?? address.addressLine1 ?? '',\n addressLine2: address.line2 ?? address.addressLine2 ?? null,\n companyName: address.company ?? address.companyName ?? null,\n name: address.contactName ?? address.name ?? null,\n city: address.city ?? null,\n region: address.state ?? address.region ?? null,\n postalCode: address.postalCode ?? null,\n country: address.country ?? null,\n }\n}\n\nexport function parseDateToken(value?: string | null): Date | undefined {\n if (!value) return undefined\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return undefined\n return parsed\n}\n\nexport function parseNumberToken(value: string, fieldName: string): number {\n const parsed = Number(value)\n if (!Number.isFinite(parsed)) {\n throw new ExecutionError(`Invalid numeric value for ${fieldName}`, 400)\n }\n return parsed\n}\n\nexport function normalizeDictionaryToken(value: string): string {\n return value.trim().toLowerCase().replace(/[\\s-]+/g, '_')\n}\n\n// ---------------------------------------------------------------------------\n// Product discrepancy resolution (used by catalog inbox action handler)\n// ---------------------------------------------------------------------------\n\nexport async function resolveProductDiscrepanciesInProposal(\n em: EntityManager,\n proposalId: string,\n productTitle: string,\n productId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<void> {\n const { InboxDiscrepancy, InboxProposalAction } = await import('../data/entities')\n\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n {\n proposalId,\n type: 'product_not_found',\n resolved: false,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n undefined,\n scope,\n )\n\n const normalizedTitle = productTitle.toLowerCase().trim()\n const matchingDiscrepancies = discrepancies.filter((d) => {\n const foundValue = (d.foundValue || '').toLowerCase().trim()\n return foundValue === normalizedTitle\n })\n\n if (matchingDiscrepancies.length === 0) return\n\n // Phase 1: flush scalar mutations before any queries to avoid UoW tracking loss (SPEC-018)\n for (const discrepancy of matchingDiscrepancies) {\n discrepancy.resolved = true\n }\n await em.flush()\n\n // Phase 2: update line item product IDs (involves findOneWithDecryption queries)\n const actionIds = matchingDiscrepancies\n .map((d) => d.actionId)\n .filter((id): id is string => !!id)\n\n for (const actionId of actionIds) {\n const action = await findOneWithDecryption(\n em,\n InboxProposalAction,\n { id: actionId, deletedAt: null },\n undefined,\n scope,\n )\n if (!action) continue\n\n const payload = action.payload as Record<string, unknown>\n const lineItems = Array.isArray(payload?.lineItems)\n ? (payload.lineItems as Record<string, unknown>[])\n : []\n\n let updated = false\n for (const item of lineItems) {\n if (item.productId) continue\n const itemName = (typeof item.productName === 'string' ? item.productName : '').toLowerCase().trim()\n if (itemName === normalizedTitle) {\n item.productId = productId\n updated = true\n break\n }\n }\n\n if (updated) {\n action.payload = { ...payload, lineItems }\n }\n }\n\n if (actionIds.length > 0) {\n await em.flush()\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EntityClass } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport type { EventBus } from '@open-mercato/events/types'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { InboxActionExecutionContext } from '@open-mercato/shared/modules/inbox-actions'\nimport type { CrossModuleEntities } from './executionEngine'\nexport { formatZodErrors } from './validation'\n\n// ---------------------------------------------------------------------------\n// Context type used by helper functions (concrete types for ORM/DI access)\n// ---------------------------------------------------------------------------\n\nexport interface ExecutionHelperContext {\n em: EntityManager\n userId: string\n tenantId: string\n organizationId: string\n eventBus?: EventBus | null\n container: AwilixContainer\n auth?: AuthContext\n entities?: CrossModuleEntities\n}\n\ntype OperationLogEntryLike = {\n id?: string | null\n undoToken?: string | null\n commandId?: string | null\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n createdAt?: Date | string | null\n}\n\nconst LAST_OPERATION_LOG_ENTRY = Symbol('inboxOps:lastOperationLogEntry')\n\ntype OperationLogCarrier = {\n [LAST_OPERATION_LOG_ENTRY]?: OperationLogEntryLike | null\n}\n\n/**\n * Cast InboxActionExecutionContext (from shared) to the concrete helper context.\n * The inbox-actions.ts handlers receive InboxActionExecutionContext but helpers\n * need concrete EntityManager / AwilixContainer types.\n */\nexport function asHelperContext(ctx: InboxActionExecutionContext): ExecutionHelperContext {\n return ctx as unknown as ExecutionHelperContext\n}\n\nexport function clearLatestCommandLogEntry(ctx: object) {\n ;(ctx as OperationLogCarrier)[LAST_OPERATION_LOG_ENTRY] = null\n}\n\nexport function readLatestCommandLogEntry(ctx: object): OperationLogEntryLike | null {\n return (ctx as OperationLogCarrier)[LAST_OPERATION_LOG_ENTRY] ?? null\n}\n\ninterface FeatureCheckingRbacService {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId: string },\n ) => Promise<boolean>\n}\n\n// Super-admin status is the immutable `isSuperAdmin` flag derived from the ACL\n// `is_super_admin` column at session resolution. Never fall back to comparing the\n// auth context's `roles` against a literal like 'superadmin' \u2014 role names are\n// tenant-mutable and trivially spoofable, so a role-name fallback would let a tenant\n// admin mint a `superadmin`-named role to bypass every inbox-ops feature gate\n// (#2699). Mirrors the scheduler module's hardened gate.\nfunction hasSuperAdminAccess(auth: ExecutionHelperContext['auth']): boolean {\n if (!auth || typeof auth !== 'object') return false\n return (auth as Record<string, unknown>).isSuperAdmin === true\n}\n\n// ---------------------------------------------------------------------------\n// Error\n// ---------------------------------------------------------------------------\n\nexport class ExecutionError extends Error {\n statusCode: number\n\n constructor(message: string, statusCode = 400) {\n super(message)\n this.statusCode = statusCode\n }\n}\n\n// ---------------------------------------------------------------------------\n// Command execution\n// ---------------------------------------------------------------------------\n\nexport async function executeCommand<TInput, TResult>(\n ctx: ExecutionHelperContext,\n commandId: string,\n input: TInput,\n): Promise<TResult> {\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n if (!commandBus || typeof commandBus.execute !== 'function') {\n throw new ExecutionError('Command bus is not available', 503)\n }\n\n const auth =\n ctx.auth ??\n ({\n sub: ctx.userId,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n isSuperAdmin: false,\n } satisfies Exclude<AuthContext, null>)\n\n const commandContext: CommandRuntimeContext = {\n container: ctx.container,\n auth,\n organizationScope: null,\n selectedOrganizationId: ctx.organizationId,\n organizationIds: [ctx.organizationId],\n }\n\n const { result, logEntry } = await commandBus.execute<TInput, TResult>(commandId, {\n input,\n ctx: commandContext,\n })\n\n if (logEntry?.undoToken && logEntry.id && logEntry.commandId) {\n ;(ctx as ExecutionHelperContext & OperationLogCarrier)[LAST_OPERATION_LOG_ENTRY] = logEntry\n }\n\n return result\n}\n\nexport async function userHasFeature(\n ctx: ExecutionHelperContext,\n feature: string,\n): Promise<boolean> {\n if (!feature || feature.trim().length === 0) return false\n if (hasSuperAdminAccess(ctx.auth)) return true\n\n try {\n const rbacService = ctx.container.resolve('rbacService') as FeatureCheckingRbacService\n if (!rbacService || typeof rbacService.userHasAllFeatures !== 'function') {\n return false\n }\n return rbacService.userHasAllFeatures(\n ctx.userId,\n [feature],\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n } catch {\n return false\n }\n}\n\n// ---------------------------------------------------------------------------\n// Entity resolution\n// ---------------------------------------------------------------------------\n\nexport function resolveEntityClass<K extends keyof CrossModuleEntities>(\n ctx: ExecutionHelperContext,\n key: K,\n): CrossModuleEntities[K] | null {\n const fromEntities = ctx.entities?.[key]\n if (fromEntities) return fromEntities\n try { return ctx.container.resolve(key) } catch { return null }\n}\n\n// ---------------------------------------------------------------------------\n// Source metadata\n// ---------------------------------------------------------------------------\n\nexport function buildSourceMetadata(actionId: string, proposalId: string): Record<string, unknown> {\n return {\n source: 'inbox_ops',\n inboxOpsActionId: actionId,\n inboxOpsProposalId: proposalId,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Order resolution\n// ---------------------------------------------------------------------------\n\nexport async function resolveOrderByReference(\n ctx: ExecutionHelperContext,\n orderId?: string,\n orderNumber?: string,\n): Promise<{ id: string; orderNumber: string; currencyCode: string; comments?: string | null }> {\n const SalesOrderClass = resolveEntityClass(ctx, 'SalesOrder')\n if (!SalesOrderClass) {\n throw new ExecutionError('Sales module entities not available', 503)\n }\n\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n }\n if (orderId) {\n where.id = orderId\n } else if (orderNumber && orderNumber.trim().length > 0) {\n where.orderNumber = orderNumber.trim()\n } else {\n throw new ExecutionError('Order reference is required', 400)\n }\n\n const order = await findOneWithDecryption(\n ctx.em,\n SalesOrderClass,\n where,\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!order) {\n throw new ExecutionError('Referenced order not found', 404)\n }\n return order\n}\n\n// ---------------------------------------------------------------------------\n// Channel resolution\n// ---------------------------------------------------------------------------\n\nexport async function resolveFirstChannelId(ctx: ExecutionHelperContext): Promise<string | null> {\n const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')\n if (!SalesChannelClass) return null\n\n try {\n const channel = await findOneWithDecryption(\n ctx.em,\n SalesChannelClass,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n { orderBy: { name: 'ASC' } },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n return channel?.id ?? null\n } catch {\n return null\n }\n}\n\nexport async function resolveEffectiveDocumentKind(\n ctx: ExecutionHelperContext,\n channelId: string,\n): Promise<'order' | 'quote'> {\n const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')\n if (!SalesChannelClass) return 'order'\n\n const channel = await findOneWithDecryption(\n ctx.em,\n SalesChannelClass,\n {\n id: channelId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!channel) return 'order'\n\n const metadata = channel.metadata as Record<string, unknown> | null\n if (metadata?.quotesRequired === true) {\n return 'quote'\n }\n return 'order'\n}\n\n// ---------------------------------------------------------------------------\n// Shipment status resolution\n// ---------------------------------------------------------------------------\n\nconst SALES_SHIPMENT_STATUS_DICTIONARY_KEY = 'sales.shipment_status'\n\nexport async function resolveShipmentStatusEntryId(\n ctx: ExecutionHelperContext,\n statusLabel: string,\n): Promise<string | null> {\n const DictionaryClass = resolveEntityClass(ctx, 'Dictionary')\n const DictionaryEntryClass = resolveEntityClass(ctx, 'DictionaryEntry')\n if (!DictionaryClass || !DictionaryEntryClass) return null\n\n const encryptionScope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n\n const dictionary = await findOneWithDecryption(\n ctx.em,\n DictionaryClass,\n {\n key: SALES_SHIPMENT_STATUS_DICTIONARY_KEY,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n undefined,\n encryptionScope,\n )\n if (!dictionary) return null\n\n const entries = await findWithDecryption(\n ctx.em,\n DictionaryEntryClass,\n {\n dictionary: dictionary.id,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n },\n undefined,\n encryptionScope,\n )\n if (!entries.length) return null\n\n const normalizedTarget = normalizeDictionaryToken(statusLabel)\n const loweredTarget = statusLabel.trim().toLowerCase()\n\n const match = entries.find((entry) => {\n const label = entry.label.trim().toLowerCase()\n const value = entry.value.trim().toLowerCase()\n return (\n entry.normalizedValue === normalizedTarget ||\n label === loweredTarget ||\n value === loweredTarget\n )\n })\n\n return match?.id ?? null\n}\n\n// ---------------------------------------------------------------------------\n// Customer / contact resolution\n// ---------------------------------------------------------------------------\n\nexport async function resolveCustomerEntityIdByEmail(\n ctx: ExecutionHelperContext,\n email: string,\n): Promise<string | null> {\n const normalized = email.trim().toLowerCase()\n if (!normalized) return null\n\n const CustomerEntityClass = resolveEntityClass(ctx, 'CustomerEntity')\n if (!CustomerEntityClass) return null\n\n const entity = await findOneWithDecryption(\n ctx.em,\n CustomerEntityClass,\n {\n primaryEmail: normalized,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (entity) return entity.id\n\n const candidates = await findWithDecryption(\n ctx.em,\n CustomerEntityClass,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n { limit: 100, orderBy: { createdAt: 'DESC' } },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n const match = candidates.find(\n (e) => e.primaryEmail && e.primaryEmail.toLowerCase() === normalized,\n )\n return match?.id ?? null\n}\n\nexport async function resolveContactIdByNameAndType(\n ctx: ExecutionHelperContext,\n contactName: string,\n contactType: string,\n): Promise<string | null> {\n const CustomerEntityClass = resolveEntityClass(ctx, 'CustomerEntity')\n if (!CustomerEntityClass) return null\n\n const normalized = contactName.trim()\n if (!normalized) return null\n\n const entity = await findOneWithDecryption(\n ctx.em,\n CustomerEntityClass,\n {\n displayName: normalized,\n kind: contactType,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n\n return entity?.id ?? null\n}\n\n// ---------------------------------------------------------------------------\n// Order line items\n// ---------------------------------------------------------------------------\n\nexport interface OrderLineItem {\n id: string\n name?: string | null\n}\n\nexport async function loadOrderLineItems(\n ctx: ExecutionHelperContext,\n orderId: string,\n): Promise<OrderLineItem[]> {\n try {\n const result = await executeCommand<Record<string, unknown>, { lines?: OrderLineItem[] }>(\n ctx,\n 'sales.orders.lines.list',\n { orderId, organizationId: ctx.organizationId, tenantId: ctx.tenantId },\n )\n return result.lines ?? []\n } catch {\n return []\n }\n}\n\nexport function matchLineItemByName(\n orderLines: OrderLineItem[],\n lineItemName: string,\n): string | null {\n const target = lineItemName.trim().toLowerCase()\n if (!target) return null\n\n const exact = orderLines.find((l) => (l.name || '').trim().toLowerCase() === target)\n if (exact) return exact.id\n\n const partial = orderLines.find((l) => {\n const name = (l.name || '').trim().toLowerCase()\n return name.includes(target) || target.includes(name)\n })\n return partial?.id ?? null\n}\n\n// ---------------------------------------------------------------------------\n// Data normalization utilities\n// ---------------------------------------------------------------------------\n\nexport function normalizeAddressSnapshot(\n address: Record<string, unknown>,\n): Record<string, unknown> {\n return {\n addressLine1: address.line1 ?? address.addressLine1 ?? '',\n addressLine2: address.line2 ?? address.addressLine2 ?? null,\n companyName: address.company ?? address.companyName ?? null,\n name: address.contactName ?? address.name ?? null,\n city: address.city ?? null,\n region: address.state ?? address.region ?? null,\n postalCode: address.postalCode ?? null,\n country: address.country ?? null,\n }\n}\n\nexport function parseDateToken(value?: string | null): Date | undefined {\n if (!value) return undefined\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return undefined\n return parsed\n}\n\nexport function parseNumberToken(value: string, fieldName: string): number {\n const parsed = Number(value)\n if (!Number.isFinite(parsed)) {\n throw new ExecutionError(`Invalid numeric value for ${fieldName}`, 400)\n }\n return parsed\n}\n\nexport function normalizeDictionaryToken(value: string): string {\n return value.trim().toLowerCase().replace(/[\\s-]+/g, '_')\n}\n\n// ---------------------------------------------------------------------------\n// Product discrepancy resolution (used by catalog inbox action handler)\n// ---------------------------------------------------------------------------\n\nexport async function resolveProductDiscrepanciesInProposal(\n em: EntityManager,\n proposalId: string,\n productTitle: string,\n productId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<void> {\n const { InboxDiscrepancy, InboxProposalAction } = await import('../data/entities')\n\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n {\n proposalId,\n type: 'product_not_found',\n resolved: false,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n undefined,\n scope,\n )\n\n const normalizedTitle = productTitle.toLowerCase().trim()\n const matchingDiscrepancies = discrepancies.filter((d) => {\n const foundValue = (d.foundValue || '').toLowerCase().trim()\n return foundValue === normalizedTitle\n })\n\n if (matchingDiscrepancies.length === 0) return\n\n // Phase 1: flush scalar mutations before any queries to avoid UoW tracking loss (SPEC-018)\n for (const discrepancy of matchingDiscrepancies) {\n discrepancy.resolved = true\n }\n await em.flush()\n\n // Phase 2: update line item product IDs (involves findOneWithDecryption queries)\n const actionIds = matchingDiscrepancies\n .map((d) => d.actionId)\n .filter((id): id is string => !!id)\n\n for (const actionId of actionIds) {\n const action = await findOneWithDecryption(\n em,\n InboxProposalAction,\n { id: actionId, deletedAt: null },\n undefined,\n scope,\n )\n if (!action) continue\n\n const payload = action.payload as Record<string, unknown>\n const lineItems = Array.isArray(payload?.lineItems)\n ? (payload.lineItems as Record<string, unknown>[])\n : []\n\n let updated = false\n for (const item of lineItems) {\n if (item.productId) continue\n const itemName = (typeof item.productName === 'string' ? item.productName : '').toLowerCase().trim()\n if (itemName === normalizedTitle) {\n item.productId = productId\n updated = true\n break\n }\n }\n\n if (updated) {\n action.payload = { ...payload, lineItems }\n }\n }\n\n if (actionIds.length > 0) {\n await em.flush()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|