@open-mercato/core 0.6.6-develop.6309.1.983aeec27a → 0.6.6-develop.6313.1.618a49a6b4

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sales/components/documents/ReturnsSection.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Undo2, Plus } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { ErrorMessage, LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n emitSalesDocumentTotalsRefresh,\n subscribeSalesDocumentTotalsRefresh,\n} from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'\nimport { formatMoney, normalizeNumber } from './lineItemUtils'\nimport { ReturnDialog, type ReturnOrderLine } from './ReturnDialog'\nimport { ReturnEditDialog, type ReturnEditRecord } from './ReturnEditDialog'\nimport { handleSectionMutationError, readRowUpdatedAt, rowOptimisticVersion } from './optimisticLock'\n\ntype ReturnRow = {\n id: string\n returnNumber: string\n status: string | null\n reason: string | null\n notes: string | null\n returnedAt: string | null\n updatedAt: string | null\n totalNetAmount: number | null\n totalGrossAmount: number | null\n}\n\ntype SalesReturnsSectionProps = {\n orderId: string\n currencyCode?: string | null\n documentUpdatedAt?: string | null\n}\n\nfunction formatDisplayDate(value: string | null | undefined): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)\n}\n\nexport function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }: SalesReturnsSectionProps) {\n const t = useT()\n const { organizationId, tenantId } = useOrganizationScopeDetail()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [returns, setReturns] = React.useState<ReturnRow[]>([])\n const [lines, setLines] = React.useState<ReturnOrderLine[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [dialogOpen, setDialogOpen] = React.useState(false)\n const [editRecord, setEditRecord] = React.useState<ReturnEditRecord | null>(null)\n\n const loadLines = React.useCallback(async () => {\n const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })\n const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/sales/order-lines?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []\n const mapped: ReturnOrderLine[] = items\n .map((item) => {\n const map = item as Record<string, unknown>\n const id = typeof map.id === 'string' ? map.id : null\n if (!id) return null\n const snapshot = map['catalog_snapshot']\n const snapshotName =\n snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)\n ? (snapshot as Record<string, unknown>)['name']\n : null\n const name =\n typeof map.name === 'string'\n ? map.name\n : typeof snapshotName === 'string'\n ? snapshotName\n : null\n const lineNumber =\n typeof map['line_number'] === 'number'\n ? (map['line_number'] as number)\n : typeof map.lineNumber === 'number'\n ? (map.lineNumber as number)\n : null\n const quantity =\n typeof map.quantity === 'number'\n ? (map.quantity as number)\n : typeof map.quantity === 'string'\n ? Number(map.quantity)\n : 0\n const returnedQuantity =\n typeof map['returned_quantity'] === 'number'\n ? (map['returned_quantity'] as number)\n : typeof map.returnedQuantity === 'number'\n ? (map.returnedQuantity as number)\n : typeof map['returned_quantity'] === 'string'\n ? Number(map['returned_quantity'])\n : typeof map.returnedQuantity === 'string'\n ? Number(map.returnedQuantity)\n : 0\n return {\n id,\n title: name ?? id,\n lineNumber,\n quantity: Number.isFinite(quantity) ? quantity : 0,\n returnedQuantity: Number.isFinite(returnedQuantity) ? returnedQuantity : 0,\n }\n })\n .filter((entry): entry is ReturnOrderLine => Boolean(entry?.id))\n setLines(mapped)\n }, [orderId])\n\n const loadReturns = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })\n const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/sales/returns?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []\n const mapped: ReturnRow[] = items\n .map((item) => {\n const map = item as Record<string, unknown>\n const id = typeof map.id === 'string' ? map.id : null\n if (!id) return null\n const returnNumber =\n typeof map['return_number'] === 'string'\n ? (map['return_number'] as string)\n : typeof map.returnNumber === 'string'\n ? (map.returnNumber as string)\n : id\n const returnedAt =\n typeof map['returned_at'] === 'string'\n ? (map['returned_at'] as string)\n : typeof map.returnedAt === 'string'\n ? (map.returnedAt as string)\n : null\n const totalNetAmount =\n typeof map['total_net_amount'] === 'number'\n ? (map['total_net_amount'] as number)\n : typeof map.totalNetAmount === 'number'\n ? (map.totalNetAmount as number)\n : null\n const totalGrossAmount =\n typeof map['total_gross_amount'] === 'number'\n ? (map['total_gross_amount'] as number)\n : typeof map.totalGrossAmount === 'number'\n ? (map.totalGrossAmount as number)\n : null\n const reason =\n typeof map.reason === 'string' ? (map.reason as string) : null\n const notes =\n typeof map.notes === 'string' ? (map.notes as string) : null\n return {\n id,\n returnNumber,\n status: typeof map.status === 'string' ? (map.status as string) : null,\n reason,\n notes,\n returnedAt,\n updatedAt: readRowUpdatedAt(map),\n totalNetAmount,\n totalGrossAmount,\n }\n })\n .filter((entry): entry is ReturnRow => Boolean(entry?.id))\n setReturns(mapped)\n await loadLines()\n } catch {\n setError(t('sales.returns.errors.load', 'Failed to load returns.'))\n } finally {\n setLoading(false)\n }\n }, [loadLines, orderId, t])\n\n React.useEffect(() => {\n loadReturns()\n }, [loadReturns])\n\n React.useEffect(() => {\n return subscribeSalesDocumentTotalsRefresh((detail) => {\n if (detail.documentId !== orderId) return\n loadReturns()\n })\n }, [loadReturns, orderId])\n\n const emptyState = React.useMemo(\n () => ({\n title: t('sales.returns.empty.title', 'No returns yet.'),\n description: t('sales.returns.empty.description', 'Create a return to generate credit adjustments for returned items.'),\n }),\n [t],\n )\n\n const rows = React.useMemo(() => {\n return returns.map((ret) => {\n const total = normalizeNumber(ret.totalGrossAmount ?? ret.totalNetAmount ?? 0, 0)\n return {\n ...ret,\n total,\n }\n })\n }, [returns])\n\n const handleEdit = React.useCallback((row: ReturnRow) => {\n setEditRecord({\n id: row.id,\n reason: row.reason,\n notes: row.notes,\n returnedAt: row.returnedAt,\n updatedAt: row.updatedAt,\n })\n }, [])\n\n const handleDelete = React.useCallback(\n async (row: ReturnRow) => {\n const confirmed = await confirm({\n title: t('sales.returns.confirmDelete', 'Delete this return?'),\n description: t(\n 'sales.returns.confirmDelete.description',\n 'This reverses the returned quantities and the related credit adjustments.',\n ),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n const result = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(rowOptimisticVersion(row)),\n () =>\n deleteCrud('sales/returns', {\n body: {\n id: row.id,\n orderId,\n ...(organizationId ? { organizationId } : {}),\n ...(tenantId ? { tenantId } : {}),\n },\n errorMessage: t('sales.returns.errors.delete', 'Failed to delete return.'),\n }),\n )\n if (result.ok) {\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n await loadReturns()\n }\n } catch (err) {\n if (handleSectionMutationError(err, t, () => void loadReturns())) return\n flash(t('sales.returns.errors.delete', 'Failed to delete return.'), 'error')\n }\n },\n [confirm, loadReturns, orderId, organizationId, tenantId, t],\n )\n\n if (loading) return <LoadingMessage label={t('sales.returns.loading', 'Loading returns\u2026')} />\n if (error) return <ErrorMessage label={error} />\n\n if (!rows.length) {\n return (\n <div className=\"space-y-4\">\n <TabEmptyState\n title={emptyState.title}\n description={emptyState.description}\n action={{\n label: t('sales.returns.create', 'Create return'),\n icon: <Plus className=\"mr-2 h-4 w-4\" aria-hidden />,\n onClick: () => setDialogOpen(true),\n }}\n />\n <ReturnDialog\n open={dialogOpen}\n orderId={orderId}\n lines={lines}\n documentUpdatedAt={documentUpdatedAt}\n onClose={() => setDialogOpen(false)}\n onSaved={async () => {\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n await loadReturns()\n }}\n />\n </div>\n )\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-end\">\n <Button type=\"button\" onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" aria-hidden />\n {t('sales.returns.create', 'Create return')}\n </Button>\n </div>\n\n <div className=\"overflow-hidden rounded-md border\">\n <div className=\"grid grid-cols-[1fr_auto_auto_auto] gap-3 border-b bg-muted/30 px-4 py-2 text-xs font-medium text-muted-foreground\">\n <div>{t('sales.returns.returnNumber', 'Return')}</div>\n <div className=\"text-right\">{t('sales.returns.returnedAt', 'Returned at')}</div>\n <div className=\"text-right\">{t('sales.returns.total', 'Total')}</div>\n <div className=\"sr-only\">{t('sales.returns.actions', 'Actions')}</div>\n </div>\n <div className=\"divide-y\">\n {rows.map((ret) => (\n <div key={ret.id} className=\"grid grid-cols-[1fr_auto_auto_auto] items-center gap-3 px-4 py-3\">\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <Undo2 className=\"h-4 w-4 text-muted-foreground\" aria-hidden />\n <div className=\"truncate text-sm font-medium\">{ret.returnNumber}</div>\n {ret.status ? <Badge variant=\"secondary\">{ret.status}</Badge> : null}\n </div>\n </div>\n <div className=\"whitespace-nowrap text-right text-sm text-muted-foreground\">\n {formatDisplayDate(ret.returnedAt) ?? t('sales.returns.notSet', 'Not set')}\n </div>\n <div className=\"whitespace-nowrap text-right text-sm font-medium\">\n {formatMoney(ret.total, currencyCode ?? null)}\n </div>\n <div className=\"flex justify-end\">\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('ui.actions.edit', 'Edit'),\n onSelect: () => handleEdit(ret),\n },\n {\n id: 'delete',\n label: t('ui.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => void handleDelete(ret),\n },\n ]}\n />\n </div>\n </div>\n ))}\n </div>\n </div>\n\n <ReturnDialog\n open={dialogOpen}\n orderId={orderId}\n lines={lines}\n documentUpdatedAt={documentUpdatedAt}\n onClose={() => setDialogOpen(false)}\n onSaved={async () => {\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n await loadReturns()\n }}\n />\n\n <ReturnEditDialog\n open={editRecord !== null}\n returnRecord={editRecord}\n orderId={orderId}\n organizationId={organizationId ?? null}\n tenantId={tenantId ?? null}\n onClose={() => setEditRecord(null)}\n onSaved={async () => {\n setEditRecord(null)\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n await loadReturns()\n }}\n />\n\n {ConfirmDialogElement}\n </div>\n )\n}\n\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Undo2, Plus } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { ErrorMessage, LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n emitSalesDocumentTotalsRefresh,\n subscribeSalesDocumentTotalsRefresh,\n} from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'\nimport { sumShippedQuantityByLine } from '@open-mercato/core/modules/sales/lib/returnQuantity'\nimport { formatMoney, normalizeNumber } from './lineItemUtils'\nimport { ReturnDialog, type ReturnOrderLine } from './ReturnDialog'\nimport { ReturnEditDialog, type ReturnEditRecord } from './ReturnEditDialog'\nimport { handleSectionMutationError, readRowUpdatedAt, rowOptimisticVersion } from './optimisticLock'\n\ntype ReturnRow = {\n id: string\n returnNumber: string\n status: string | null\n reason: string | null\n notes: string | null\n returnedAt: string | null\n updatedAt: string | null\n totalNetAmount: number | null\n totalGrossAmount: number | null\n}\n\ntype SalesReturnsSectionProps = {\n orderId: string\n currencyCode?: string | null\n documentUpdatedAt?: string | null\n}\n\nfunction formatDisplayDate(value: string | null | undefined): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)\n}\n\nexport function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }: SalesReturnsSectionProps) {\n const t = useT()\n const { organizationId, tenantId } = useOrganizationScopeDetail()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [returns, setReturns] = React.useState<ReturnRow[]>([])\n const [lines, setLines] = React.useState<ReturnOrderLine[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [dialogOpen, setDialogOpen] = React.useState(false)\n const [editRecord, setEditRecord] = React.useState<ReturnEditRecord | null>(null)\n\n const loadLines = React.useCallback(async () => {\n const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })\n const shipmentParams = new URLSearchParams({ page: '1', pageSize: '200', orderId })\n const [response, shipmentsResponse] = await Promise.all([\n apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/sales/order-lines?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n ),\n apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/sales/shipments?${shipmentParams.toString()}`,\n undefined,\n { fallback: { items: [] } },\n ),\n ])\n const shippedByLine = sumShippedQuantityByLine(\n Array.isArray(shipmentsResponse.result?.items) ? shipmentsResponse.result?.items : [],\n )\n const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []\n const mapped: ReturnOrderLine[] = items\n .map((item): ReturnOrderLine | null => {\n const map = item as Record<string, unknown>\n const id = typeof map.id === 'string' ? map.id : null\n if (!id) return null\n const snapshot = map['catalog_snapshot']\n const snapshotName =\n snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)\n ? (snapshot as Record<string, unknown>)['name']\n : null\n const name =\n typeof map.name === 'string'\n ? map.name\n : typeof snapshotName === 'string'\n ? snapshotName\n : null\n const lineNumber =\n typeof map['line_number'] === 'number'\n ? (map['line_number'] as number)\n : typeof map.lineNumber === 'number'\n ? (map.lineNumber as number)\n : null\n const quantity =\n typeof map.quantity === 'number'\n ? (map.quantity as number)\n : typeof map.quantity === 'string'\n ? Number(map.quantity)\n : 0\n const returnedQuantity =\n typeof map['returned_quantity'] === 'number'\n ? (map['returned_quantity'] as number)\n : typeof map.returnedQuantity === 'number'\n ? (map.returnedQuantity as number)\n : typeof map['returned_quantity'] === 'string'\n ? Number(map['returned_quantity'])\n : typeof map.returnedQuantity === 'string'\n ? Number(map.returnedQuantity)\n : 0\n return {\n id,\n title: name ?? id,\n lineNumber,\n quantity: Number.isFinite(quantity) ? quantity : 0,\n returnedQuantity: Number.isFinite(returnedQuantity) ? returnedQuantity : 0,\n shippedQuantity: shippedByLine.get(id) ?? 0,\n }\n })\n .filter((entry): entry is ReturnOrderLine => Boolean(entry?.id))\n setLines(mapped)\n }, [orderId])\n\n const loadReturns = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })\n const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/sales/returns?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []\n const mapped: ReturnRow[] = items\n .map((item) => {\n const map = item as Record<string, unknown>\n const id = typeof map.id === 'string' ? map.id : null\n if (!id) return null\n const returnNumber =\n typeof map['return_number'] === 'string'\n ? (map['return_number'] as string)\n : typeof map.returnNumber === 'string'\n ? (map.returnNumber as string)\n : id\n const returnedAt =\n typeof map['returned_at'] === 'string'\n ? (map['returned_at'] as string)\n : typeof map.returnedAt === 'string'\n ? (map.returnedAt as string)\n : null\n const totalNetAmount =\n typeof map['total_net_amount'] === 'number'\n ? (map['total_net_amount'] as number)\n : typeof map.totalNetAmount === 'number'\n ? (map.totalNetAmount as number)\n : null\n const totalGrossAmount =\n typeof map['total_gross_amount'] === 'number'\n ? (map['total_gross_amount'] as number)\n : typeof map.totalGrossAmount === 'number'\n ? (map.totalGrossAmount as number)\n : null\n const reason =\n typeof map.reason === 'string' ? (map.reason as string) : null\n const notes =\n typeof map.notes === 'string' ? (map.notes as string) : null\n return {\n id,\n returnNumber,\n status: typeof map.status === 'string' ? (map.status as string) : null,\n reason,\n notes,\n returnedAt,\n updatedAt: readRowUpdatedAt(map),\n totalNetAmount,\n totalGrossAmount,\n }\n })\n .filter((entry): entry is ReturnRow => Boolean(entry?.id))\n setReturns(mapped)\n await loadLines()\n } catch {\n setError(t('sales.returns.errors.load', 'Failed to load returns.'))\n } finally {\n setLoading(false)\n }\n }, [loadLines, orderId, t])\n\n React.useEffect(() => {\n loadReturns()\n }, [loadReturns])\n\n React.useEffect(() => {\n return subscribeSalesDocumentTotalsRefresh((detail) => {\n if (detail.documentId !== orderId) return\n loadReturns()\n })\n }, [loadReturns, orderId])\n\n const emptyState = React.useMemo(\n () => ({\n title: t('sales.returns.empty.title', 'No returns yet.'),\n description: t('sales.returns.empty.description', 'Create a return to generate credit adjustments for returned items.'),\n }),\n [t],\n )\n\n const rows = React.useMemo(() => {\n return returns.map((ret) => {\n const total = normalizeNumber(ret.totalGrossAmount ?? ret.totalNetAmount ?? 0, 0)\n return {\n ...ret,\n total,\n }\n })\n }, [returns])\n\n const handleEdit = React.useCallback((row: ReturnRow) => {\n setEditRecord({\n id: row.id,\n reason: row.reason,\n notes: row.notes,\n returnedAt: row.returnedAt,\n updatedAt: row.updatedAt,\n })\n }, [])\n\n const handleDelete = React.useCallback(\n async (row: ReturnRow) => {\n const confirmed = await confirm({\n title: t('sales.returns.confirmDelete', 'Delete this return?'),\n description: t(\n 'sales.returns.confirmDelete.description',\n 'This reverses the returned quantities and the related credit adjustments.',\n ),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n const result = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(rowOptimisticVersion(row)),\n () =>\n deleteCrud('sales/returns', {\n body: {\n id: row.id,\n orderId,\n ...(organizationId ? { organizationId } : {}),\n ...(tenantId ? { tenantId } : {}),\n },\n errorMessage: t('sales.returns.errors.delete', 'Failed to delete return.'),\n }),\n )\n if (result.ok) {\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n await loadReturns()\n }\n } catch (err) {\n if (handleSectionMutationError(err, t, () => void loadReturns())) return\n flash(t('sales.returns.errors.delete', 'Failed to delete return.'), 'error')\n }\n },\n [confirm, loadReturns, orderId, organizationId, tenantId, t],\n )\n\n if (loading) return <LoadingMessage label={t('sales.returns.loading', 'Loading returns\u2026')} />\n if (error) return <ErrorMessage label={error} />\n\n if (!rows.length) {\n return (\n <div className=\"space-y-4\">\n <TabEmptyState\n title={emptyState.title}\n description={emptyState.description}\n action={{\n label: t('sales.returns.create', 'Create return'),\n icon: <Plus className=\"mr-2 h-4 w-4\" aria-hidden />,\n onClick: () => setDialogOpen(true),\n }}\n />\n <ReturnDialog\n open={dialogOpen}\n orderId={orderId}\n lines={lines}\n documentUpdatedAt={documentUpdatedAt}\n onClose={() => setDialogOpen(false)}\n onSaved={async () => {\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n await loadReturns()\n }}\n />\n </div>\n )\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-end\">\n <Button type=\"button\" onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" aria-hidden />\n {t('sales.returns.create', 'Create return')}\n </Button>\n </div>\n\n <div className=\"overflow-hidden rounded-md border\">\n <div className=\"grid grid-cols-[1fr_auto_auto_auto] gap-3 border-b bg-muted/30 px-4 py-2 text-xs font-medium text-muted-foreground\">\n <div>{t('sales.returns.returnNumber', 'Return')}</div>\n <div className=\"text-right\">{t('sales.returns.returnedAt', 'Returned at')}</div>\n <div className=\"text-right\">{t('sales.returns.total', 'Total')}</div>\n <div className=\"sr-only\">{t('sales.returns.actions', 'Actions')}</div>\n </div>\n <div className=\"divide-y\">\n {rows.map((ret) => (\n <div key={ret.id} className=\"grid grid-cols-[1fr_auto_auto_auto] items-center gap-3 px-4 py-3\">\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <Undo2 className=\"h-4 w-4 text-muted-foreground\" aria-hidden />\n <div className=\"truncate text-sm font-medium\">{ret.returnNumber}</div>\n {ret.status ? <Badge variant=\"secondary\">{ret.status}</Badge> : null}\n </div>\n </div>\n <div className=\"whitespace-nowrap text-right text-sm text-muted-foreground\">\n {formatDisplayDate(ret.returnedAt) ?? t('sales.returns.notSet', 'Not set')}\n </div>\n <div className=\"whitespace-nowrap text-right text-sm font-medium\">\n {formatMoney(ret.total, currencyCode ?? null)}\n </div>\n <div className=\"flex justify-end\">\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('ui.actions.edit', 'Edit'),\n onSelect: () => handleEdit(ret),\n },\n {\n id: 'delete',\n label: t('ui.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => void handleDelete(ret),\n },\n ]}\n />\n </div>\n </div>\n ))}\n </div>\n </div>\n\n <ReturnDialog\n open={dialogOpen}\n orderId={orderId}\n lines={lines}\n documentUpdatedAt={documentUpdatedAt}\n onClose={() => setDialogOpen(false)}\n onSaved={async () => {\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n await loadReturns()\n }}\n />\n\n <ReturnEditDialog\n open={editRecord !== null}\n returnRecord={editRecord}\n orderId={orderId}\n organizationId={organizationId ?? null}\n tenantId={tenantId ?? null}\n onClose={() => setEditRecord(null)}\n onSaved={async () => {\n setEditRecord(null)\n emitSalesDocumentTotalsRefresh({ documentId: orderId, kind: 'order' })\n await loadReturns()\n }}\n />\n\n {ConfirmDialogElement}\n </div>\n )\n}\n\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,36 @@
1
1
  function computeAvailableReturnQuantity(line) {
2
- const raw = line.quantity - line.returnedQuantity;
2
+ const cap = typeof line.shippedQuantity === "number" && Number.isFinite(line.shippedQuantity) ? Math.min(line.quantity, line.shippedQuantity) : line.quantity;
3
+ const raw = cap - line.returnedQuantity;
3
4
  if (!Number.isFinite(raw) || raw <= 0) return 0;
4
5
  return Math.max(0, Math.floor(raw + 1e-6));
5
6
  }
7
+ function coerceQuantity(value) {
8
+ if (typeof value === "number" && Number.isFinite(value)) return value;
9
+ if (typeof value === "string" && value.trim().length) {
10
+ const parsed = Number(value);
11
+ if (Number.isFinite(parsed)) return parsed;
12
+ }
13
+ return 0;
14
+ }
15
+ function sumShippedQuantityByLine(shipments) {
16
+ const shippedByLine = /* @__PURE__ */ new Map();
17
+ if (!Array.isArray(shipments)) return shippedByLine;
18
+ for (const shipment of shipments) {
19
+ if (!shipment || typeof shipment !== "object") continue;
20
+ const lineItems = shipment.items;
21
+ if (!Array.isArray(lineItems)) continue;
22
+ for (const lineItem of lineItems) {
23
+ if (!lineItem || typeof lineItem !== "object") continue;
24
+ const record = lineItem;
25
+ const orderLineId = typeof record.orderLineId === "string" ? record.orderLineId : typeof record.order_line_id === "string" ? record.order_line_id : null;
26
+ if (!orderLineId) continue;
27
+ shippedByLine.set(orderLineId, (shippedByLine.get(orderLineId) ?? 0) + coerceQuantity(record.quantity));
28
+ }
29
+ }
30
+ return shippedByLine;
31
+ }
6
32
  export {
7
- computeAvailableReturnQuantity
33
+ computeAvailableReturnQuantity,
34
+ sumShippedQuantityByLine
8
35
  };
9
36
  //# sourceMappingURL=returnQuantity.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sales/lib/returnQuantity.ts"],
4
- "sourcesContent": ["export type ReturnAvailableInput = {\n quantity: number\n returnedQuantity: number\n}\n\nexport function computeAvailableReturnQuantity(line: ReturnAvailableInput): number {\n const raw = line.quantity - line.returnedQuantity\n if (!Number.isFinite(raw) || raw <= 0) return 0\n return Math.max(0, Math.floor(raw + 1e-6))\n}\n"],
5
- "mappings": "AAKO,SAAS,+BAA+B,MAAoC;AACjF,QAAM,MAAM,KAAK,WAAW,KAAK;AACjC,MAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,EAAG,QAAO;AAC9C,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,IAAI,CAAC;AAC3C;",
4
+ "sourcesContent": ["export type ReturnAvailableInput = {\n quantity: number\n returnedQuantity: number\n shippedQuantity?: number\n}\n\nexport function computeAvailableReturnQuantity(line: ReturnAvailableInput): number {\n const cap =\n typeof line.shippedQuantity === 'number' && Number.isFinite(line.shippedQuantity)\n ? Math.min(line.quantity, line.shippedQuantity)\n : line.quantity\n const raw = cap - line.returnedQuantity\n if (!Number.isFinite(raw) || raw <= 0) return 0\n return Math.max(0, Math.floor(raw + 1e-6))\n}\n\nfunction coerceQuantity(value: unknown): number {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim().length) {\n const parsed = Number(value)\n if (Number.isFinite(parsed)) return parsed\n }\n return 0\n}\n\n/**\n * Aggregate shipped quantity per order line from the `/api/sales/shipments`\n * list payload. Mirrors the server-side `loadShippedQuantityByLine` cap so the\n * Return dialog's \"Available\" figure matches what `sales.returns.create`\n * enforces. Each shipment exposes its lines under `items` (either the\n * persisted snapshot or DB-grouped rows), both carrying `orderLineId` and\n * `quantity`.\n */\nexport function sumShippedQuantityByLine(\n shipments: ReadonlyArray<Record<string, unknown>> | null | undefined,\n): Map<string, number> {\n const shippedByLine = new Map<string, number>()\n if (!Array.isArray(shipments)) return shippedByLine\n for (const shipment of shipments) {\n if (!shipment || typeof shipment !== 'object') continue\n const lineItems = (shipment as Record<string, unknown>).items\n if (!Array.isArray(lineItems)) continue\n for (const lineItem of lineItems) {\n if (!lineItem || typeof lineItem !== 'object') continue\n const record = lineItem as Record<string, unknown>\n const orderLineId =\n typeof record.orderLineId === 'string'\n ? record.orderLineId\n : typeof record.order_line_id === 'string'\n ? record.order_line_id\n : null\n if (!orderLineId) continue\n shippedByLine.set(orderLineId, (shippedByLine.get(orderLineId) ?? 0) + coerceQuantity(record.quantity))\n }\n }\n return shippedByLine\n}\n"],
5
+ "mappings": "AAMO,SAAS,+BAA+B,MAAoC;AACjF,QAAM,MACJ,OAAO,KAAK,oBAAoB,YAAY,OAAO,SAAS,KAAK,eAAe,IAC5E,KAAK,IAAI,KAAK,UAAU,KAAK,eAAe,IAC5C,KAAK;AACX,QAAM,MAAM,MAAM,KAAK;AACvB,MAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,EAAG,QAAO;AAC9C,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,IAAI,CAAC;AAC3C;AAEA,SAAS,eAAe,OAAwB;AAC9C,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,EAAG,QAAO;AAChE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,QAAQ;AACpD,UAAM,SAAS,OAAO,KAAK;AAC3B,QAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AAAA,EACtC;AACA,SAAO;AACT;AAUO,SAAS,yBACd,WACqB;AACrB,QAAM,gBAAgB,oBAAI,IAAoB;AAC9C,MAAI,CAAC,MAAM,QAAQ,SAAS,EAAG,QAAO;AACtC,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,YAAY,OAAO,aAAa,SAAU;AAC/C,UAAM,YAAa,SAAqC;AACxD,QAAI,CAAC,MAAM,QAAQ,SAAS,EAAG;AAC/B,eAAW,YAAY,WAAW;AAChC,UAAI,CAAC,YAAY,OAAO,aAAa,SAAU;AAC/C,YAAM,SAAS;AACf,YAAM,cACJ,OAAO,OAAO,gBAAgB,WAC1B,OAAO,cACP,OAAO,OAAO,kBAAkB,WAC9B,OAAO,gBACP;AACR,UAAI,CAAC,YAAa;AAClB,oBAAc,IAAI,cAAc,cAAc,IAAI,WAAW,KAAK,KAAK,eAAe,OAAO,QAAQ,CAAC;AAAA,IACxG;AAAA,EACF;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/workflows/lib/event-trigger-service.ts"],
4
- "sourcesContent": ["/**\n * Workflows Module - Event Trigger Service\n *\n * Core service for evaluating and processing workflow event triggers.\n * Handles pattern matching, filter evaluation, context mapping, and workflow starting.\n */\n\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CacheService } from '@open-mercato/cache'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'\nimport {\n WorkflowEventTrigger,\n WorkflowDefinition,\n WorkflowInstance,\n type TriggerFilterCondition,\n type TriggerContextMapping,\n type WorkflowEventTriggerConfig,\n type WorkflowDefinitionTrigger,\n} from '../data/entities'\nimport { startWorkflow, executeWorkflow } from './workflow-executor'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface EventTriggerContext {\n eventName: string\n payload: Record<string, unknown>\n tenantId: string\n organizationId: string\n}\n\nexport interface ProcessTriggersResult {\n triggered: number\n skipped: number\n errors: Array<{ triggerId: string; error: string }>\n instances: Array<{ triggerId: string; instanceId: string }>\n}\n\n/**\n * Unified trigger interface for both legacy (entity) and embedded (definition) triggers\n */\nexport interface UnifiedTrigger {\n id: string // For legacy: entity ID, for embedded: `${definitionId}:${triggerId}`\n triggerId: string\n name: string\n description?: string | null\n eventPattern: string\n config: WorkflowEventTriggerConfig | null\n enabled: boolean\n priority: number\n workflowDefinitionId: string\n workflowId: string\n workflowVersion: number\n source: 'legacy' | 'embedded'\n tenantId: string\n organizationId: string\n}\n\ninterface CachedTriggers {\n triggers: UnifiedTrigger[]\n cachedAt: number\n}\n\n// Cache TTL: 5 minutes\nconst TRIGGER_CACHE_TTL = 5 * 60 * 1000\n\n// ============================================================================\n// Pattern Matching\n// ============================================================================\n\n// ============================================================================\n// Filter Evaluation\n// ============================================================================\n\nconst MAX_WORKFLOW_REGEX_PATTERN_LENGTH = 200\nconst MAX_WORKFLOW_REGEX_INPUT_LENGTH = 10_000\n\n/**\n * Get a nested value from an object using dot notation.\n */\nfunction getNestedValue(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined\n\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) return undefined\n if (typeof current !== 'object') return undefined\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\nfunction getQuantifierEnd(pattern: string, index: number): number | null {\n const char = pattern[index]\n if (char === '*' || char === '+' || char === '?') return index\n if (char !== '{') return null\n\n const closeIndex = pattern.indexOf('}', index + 1)\n if (closeIndex === -1) return null\n\n const body = pattern.slice(index + 1, closeIndex)\n return /^[0-9]+(?:,[0-9]*)?$/.test(body) ? closeIndex : null\n}\n\ninterface RegexGroupFrame {\n hasAlternation: boolean\n hasQuantifier: boolean\n}\n\nfunction isSafeWorkflowRegexPattern(pattern: string): boolean {\n if (pattern.length > MAX_WORKFLOW_REGEX_PATTERN_LENGTH) return false\n\n const groupStack: RegexGroupFrame[] = [{ hasAlternation: false, hasQuantifier: false }]\n let inCharClass = false\n let lastClosedGroup: RegexGroupFrame | null = null\n let lastAtomWasQuantified = false\n\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index]\n\n if (char === '\\\\') {\n const next = pattern[index + 1]\n if (!inCharClass && (/[1-9]/.test(next ?? '') || (next === 'k' && pattern[index + 2] === '<'))) {\n return false\n }\n index += 1\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (inCharClass) {\n if (char === ']') {\n inCharClass = false\n lastAtomWasQuantified = false\n }\n lastClosedGroup = null\n continue\n }\n\n if (char === '[') {\n inCharClass = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '(') {\n if (pattern[index + 1] === '?') {\n if (pattern[index + 2] !== ':') return false\n index += 2\n }\n\n groupStack.push({ hasAlternation: false, hasQuantifier: false })\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === ')') {\n if (groupStack.length === 1) return false\n lastClosedGroup = groupStack.pop()!\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '|') {\n groupStack[groupStack.length - 1].hasAlternation = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n const quantifierEnd = getQuantifierEnd(pattern, index)\n if (quantifierEnd !== null) {\n if (lastAtomWasQuantified) return false\n if (lastClosedGroup?.hasAlternation || lastClosedGroup?.hasQuantifier) return false\n\n groupStack[groupStack.length - 1].hasQuantifier = true\n lastClosedGroup = null\n lastAtomWasQuantified = true\n index = quantifierEnd\n\n if (pattern[index + 1] === '?') {\n index += 1\n }\n\n continue\n }\n\n lastClosedGroup = null\n lastAtomWasQuantified = false\n }\n\n return groupStack.length === 1 && !inCharClass\n}\n\n/**\n * Evaluate a single filter condition against the event payload.\n */\nfunction evaluateCondition(condition: TriggerFilterCondition, payload: Record<string, unknown>): boolean {\n const value = getNestedValue(payload, condition.field)\n const expected = condition.value\n\n switch (condition.operator) {\n case 'eq':\n return value === expected\n\n case 'neq':\n return value !== expected\n\n case 'gt':\n return typeof value === 'number' && typeof expected === 'number' && value > expected\n\n case 'gte':\n return typeof value === 'number' && typeof expected === 'number' && value >= expected\n\n case 'lt':\n return typeof value === 'number' && typeof expected === 'number' && value < expected\n\n case 'lte':\n return typeof value === 'number' && typeof expected === 'number' && value <= expected\n\n case 'contains':\n if (typeof value === 'string' && typeof expected === 'string') {\n return value.includes(expected)\n }\n if (Array.isArray(value)) {\n return value.includes(expected)\n }\n return false\n\n case 'startsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.startsWith(expected)\n\n case 'endsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.endsWith(expected)\n\n case 'in':\n return Array.isArray(expected) && expected.includes(value)\n\n case 'notIn':\n return Array.isArray(expected) && !expected.includes(value)\n\n case 'exists':\n return value !== undefined && value !== null\n\n case 'notExists':\n return value === undefined || value === null\n\n case 'regex':\n if (typeof value !== 'string' || typeof expected !== 'string') return false\n if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false\n if (!isSafeWorkflowRegexPattern(expected)) return false\n {\n const result = testLinearRegex(expected, value, {\n maxPatternLength: MAX_WORKFLOW_REGEX_PATTERN_LENGTH,\n maxInputLength: MAX_WORKFLOW_REGEX_INPUT_LENGTH,\n })\n return result.ok ? result.matched : false\n }\n\n default:\n return false\n }\n}\n\n/**\n * Evaluate all filter conditions against the event payload.\n * All conditions must pass (AND logic).\n */\nexport function evaluateFilterConditions(\n conditions: TriggerFilterCondition[] | undefined,\n payload: Record<string, unknown>\n): boolean {\n if (!conditions || conditions.length === 0) return true\n\n return conditions.every(condition => evaluateCondition(condition, payload))\n}\n\n// ============================================================================\n// Context Mapping\n// ============================================================================\n\n/**\n * Map event payload to workflow initial context.\n */\nexport function mapEventToContext(\n mapping: TriggerContextMapping[] | undefined,\n payload: Record<string, unknown>\n): Record<string, unknown> {\n const context: Record<string, unknown> = {}\n\n if (!mapping || mapping.length === 0) return context\n\n for (const item of mapping) {\n const value = getNestedValue(payload, item.sourceExpression)\n context[item.targetKey] = value !== undefined ? value : item.defaultValue\n }\n\n return context\n}\n\n// ============================================================================\n// Trigger Loading with Caching\n// ============================================================================\n\n// In-memory cache for triggers (per tenant/org).\n// Park the Map on globalThis so the same compiled module loaded under two\n// paths (a Next.js server chunk vs. a worker resolving the file through a\n// different import root) shares one cache. Without this,\n// `invalidateTriggerCache(...)` called from the PUT /api/workflows/definitions\n// route clears its own copy while the wildcard event-trigger subscriber keeps\n// reading a stale copy populated before the trigger was added \u2014 newly added\n// triggers stay invisible for up to TRIGGER_CACHE_TTL. Mirrors the same\n// workaround used by the modules registry and `getDiRegistrars()`.\nconst GLOBAL_TRIGGER_CACHE_KEY = '__openMercatoWorkflowTriggerCache__'\n\nfunction getTriggerCache(): Map<string, CachedTriggers> {\n const existing = (globalThis as any)[GLOBAL_TRIGGER_CACHE_KEY] as\n | Map<string, CachedTriggers>\n | undefined\n if (existing) return existing\n const created = new Map<string, CachedTriggers>()\n ;(globalThis as any)[GLOBAL_TRIGGER_CACHE_KEY] = created\n return created\n}\n\nfunction getCacheKey(tenantId: string, organizationId: string): string {\n return `${tenantId}:${organizationId}`\n}\n\n/**\n * Load legacy triggers from WorkflowEventTrigger entity table.\n * For backward compatibility with existing triggers.\n */\nasync function loadLegacyTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const postgresEm = em as unknown as PostgreSqlEntityManager\n const legacyTriggers = await findWithDecryption(\n postgresEm,\n WorkflowEventTrigger,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {\n orderBy: { priority: 'DESC', createdAt: 'ASC' },\n },\n { tenantId, organizationId },\n )\n\n // Get definitions for these triggers to get workflowId\n const definitionIds = [...new Set(legacyTriggers.map(t => t.workflowDefinitionId))]\n const definitions = definitionIds.length > 0 ? await findWithDecryption(postgresEm, WorkflowDefinition, {\n id: { $in: definitionIds },\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n }, {}, { tenantId, organizationId }) : []\n const definitionMap = new Map(definitions.map(d => [d.id, d]))\n\n return legacyTriggers\n .filter(t => definitionMap.has(t.workflowDefinitionId))\n .map(t => {\n const def = definitionMap.get(t.workflowDefinitionId)!\n return {\n id: t.id,\n triggerId: t.id,\n name: t.name,\n description: t.description,\n eventPattern: t.eventPattern,\n config: t.config ?? null,\n enabled: t.enabled,\n priority: t.priority,\n workflowDefinitionId: t.workflowDefinitionId,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'legacy' as const,\n tenantId: t.tenantId,\n organizationId: t.organizationId,\n }\n })\n}\n\n/**\n * Load embedded triggers from workflow definitions.\n * New triggers are embedded directly in the definition JSONB.\n */\nasync function loadEmbeddedTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const postgresEm = em as unknown as PostgreSqlEntityManager\n // Load all enabled definitions that may have triggers\n const definitions = await findWithDecryption(\n postgresEm,\n WorkflowDefinition,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {},\n { tenantId, organizationId },\n )\n\n const triggers: UnifiedTrigger[] = []\n\n for (const def of definitions) {\n const embeddedTriggers = def.definition?.triggers as WorkflowDefinitionTrigger[] | undefined\n if (!embeddedTriggers || embeddedTriggers.length === 0) continue\n\n for (const trigger of embeddedTriggers) {\n if (!trigger.enabled) continue\n\n triggers.push({\n id: `${def.id}:${trigger.triggerId}`,\n triggerId: trigger.triggerId,\n name: trigger.name,\n description: trigger.description ?? null,\n eventPattern: trigger.eventPattern,\n config: trigger.config ?? null,\n enabled: trigger.enabled,\n priority: trigger.priority,\n workflowDefinitionId: def.id,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'embedded' as const,\n tenantId,\n organizationId,\n })\n }\n }\n\n return triggers\n}\n\n/**\n * Load all enabled triggers for a tenant/organization with caching.\n * Merges both legacy (entity) triggers and embedded (definition) triggers.\n */\nexport async function loadTriggersForTenant(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n cacheService?: CacheService\n): Promise<UnifiedTrigger[]> {\n const cacheKey = getCacheKey(tenantId, organizationId)\n const cache = getTriggerCache()\n\n // Check in-memory cache\n const cached = cache.get(cacheKey)\n if (cached && Date.now() - cached.cachedAt < TRIGGER_CACHE_TTL) {\n return cached.triggers\n }\n\n // Load from both sources\n const [legacyTriggers, embeddedTriggers] = await Promise.all([\n loadLegacyTriggers(em, tenantId, organizationId),\n loadEmbeddedTriggers(em, tenantId, organizationId),\n ])\n\n // Merge and sort by priority (higher first)\n const allTriggers = [...legacyTriggers, ...embeddedTriggers]\n .sort((a, b) => b.priority - a.priority)\n\n // Update cache\n cache.set(cacheKey, {\n triggers: allTriggers,\n cachedAt: Date.now(),\n })\n\n return allTriggers\n}\n\n/**\n * Invalidate trigger cache for a tenant/organization.\n * Call this when:\n * - Legacy triggers are created/updated/deleted\n * - Workflow definitions with embedded triggers are created/updated/deleted\n */\nexport function invalidateTriggerCache(tenantId: string, organizationId?: string): void {\n const cache = getTriggerCache()\n if (organizationId) {\n // Invalidate specific org\n const cacheKey = getCacheKey(tenantId, organizationId)\n cache.delete(cacheKey)\n } else {\n // Invalidate all orgs for tenant\n for (const key of cache.keys()) {\n if (key.startsWith(`${tenantId}:`)) {\n cache.delete(key)\n }\n }\n }\n}\n\n// ============================================================================\n// Trigger Matching\n// ============================================================================\n\n/**\n * Find all triggers that match an event.\n */\nexport async function findMatchingTriggers(\n em: EntityManager,\n context: EventTriggerContext\n): Promise<UnifiedTrigger[]> {\n const triggers = await loadTriggersForTenant(\n em,\n context.tenantId,\n context.organizationId\n )\n\n return triggers.filter(trigger => {\n // Check event pattern\n if (!matchEventPattern(context.eventName, trigger.eventPattern)) {\n return false\n }\n\n // Check filter conditions\n if (!evaluateFilterConditions(trigger.config?.filterConditions, context.payload)) {\n return false\n }\n\n return true\n })\n}\n\n// ============================================================================\n// Trigger Processing\n// ============================================================================\n\n/**\n * Check if max concurrent instances limit is reached.\n */\nasync function checkConcurrencyLimit(\n em: EntityManager,\n trigger: UnifiedTrigger\n): Promise<boolean> {\n const maxInstances = trigger.config?.maxConcurrentInstances\n\n if (!maxInstances) return true // No limit\n\n // Count running instances for this trigger's workflow definition\n const runningCount = await em.count(WorkflowInstance, {\n definitionId: trigger.workflowDefinitionId,\n status: { $in: ['RUNNING', 'WAITING_FOR_ACTIVITIES'] },\n tenantId: trigger.tenantId,\n organizationId: trigger.organizationId,\n deletedAt: null,\n })\n\n return runningCount < maxInstances\n}\n\n/**\n * Process all matching triggers for an event and start workflows.\n */\nexport async function processEventTriggers(\n em: EntityManager,\n container: AwilixContainer,\n context: EventTriggerContext\n): Promise<ProcessTriggersResult> {\n const result: ProcessTriggersResult = {\n triggered: 0,\n skipped: 0,\n errors: [],\n instances: [],\n }\n\n // Find matching triggers\n const triggers = await findMatchingTriggers(em, context)\n\n if (triggers.length === 0) {\n return result\n }\n\n // Process each trigger (definitions already validated during loading)\n for (const trigger of triggers) {\n try {\n // Check concurrency limit\n const canStart = await checkConcurrencyLimit(em, trigger)\n if (!canStart) {\n console.log(`[workflow-trigger] Skipping trigger \"${trigger.name}\": max concurrent instances reached`)\n result.skipped++\n continue\n }\n\n // Map event payload to workflow context\n const mappedContext = mapEventToContext(trigger.config?.contextMapping, context.payload)\n\n // Extract entity info from payload for metadata\n const payloadId = context.payload?.id as string | undefined\n const payloadEntityType = context.payload?.entityType as string | undefined\n\n // Include event metadata and payload in context\n const initialContext = {\n // Include raw event payload fields (e.g., id, organizationId, tenantId)\n ...context.payload,\n // Override with explicit mappings if provided\n ...mappedContext,\n __trigger: {\n triggerId: trigger.id,\n triggerName: trigger.name,\n eventName: context.eventName,\n eventPayload: context.payload,\n triggeredAt: new Date().toISOString(),\n source: trigger.source,\n },\n }\n\n // Start workflow\n const instance = await startWorkflow(em, {\n workflowId: trigger.workflowId,\n version: trigger.workflowVersion,\n initialContext,\n metadata: {\n initiatedBy: `trigger:${trigger.id}`,\n // Include entityId and entityType for widget discovery\n entityId: payloadId,\n entityType: payloadEntityType || trigger.config?.entityType,\n labels: {\n trigger_id: trigger.id,\n trigger_name: trigger.name,\n event_name: context.eventName,\n trigger_source: trigger.source,\n },\n },\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n })\n\n result.triggered++\n result.instances.push({\n triggerId: trigger.id,\n instanceId: instance.id,\n })\n\n // Execute workflow asynchronously (don't wait)\n executeWorkflow(em.fork(), container, instance.id).catch(err => {\n console.error(`[workflow-trigger] Error executing workflow ${instance.id}:`, err)\n })\n\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n console.error(`[workflow-trigger] Error processing trigger \"${trigger.name}\":`, error)\n result.errors.push({\n triggerId: trigger.id,\n error: errorMessage,\n })\n }\n }\n\n return result\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Workflows Module - Event Trigger Service\n *\n * Core service for evaluating and processing workflow event triggers.\n * Handles pattern matching, filter evaluation, context mapping, and workflow starting.\n */\n\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CacheService } from '@open-mercato/cache'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'\nimport {\n WorkflowEventTrigger,\n WorkflowDefinition,\n WorkflowInstance,\n type TriggerFilterCondition,\n type TriggerContextMapping,\n type WorkflowEventTriggerConfig,\n type WorkflowDefinitionTrigger,\n} from '../data/entities'\nimport { startWorkflow, executeWorkflow } from './workflow-executor'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface EventTriggerContext {\n eventName: string\n payload: Record<string, unknown>\n tenantId: string\n organizationId: string\n}\n\nexport interface ProcessTriggersResult {\n triggered: number\n skipped: number\n errors: Array<{ triggerId: string; error: string }>\n instances: Array<{ triggerId: string; instanceId: string }>\n}\n\n/**\n * Unified trigger interface for both legacy (entity) and embedded (definition) triggers\n */\nexport interface UnifiedTrigger {\n id: string // For legacy: entity ID, for embedded: `${definitionId}:${triggerId}`\n triggerId: string\n name: string\n description?: string | null\n eventPattern: string\n config: WorkflowEventTriggerConfig | null\n enabled: boolean\n priority: number\n workflowDefinitionId: string\n workflowId: string\n workflowVersion: number\n source: 'legacy' | 'embedded'\n tenantId: string\n organizationId: string\n}\n\ninterface CachedTriggers {\n triggers: UnifiedTrigger[]\n cachedAt: number\n}\n\n// Cache TTL: 5 minutes\nconst TRIGGER_CACHE_TTL = 5 * 60 * 1000\n\n// ============================================================================\n// Pattern Matching\n// ============================================================================\n\n// ============================================================================\n// Filter Evaluation\n// ============================================================================\n\nconst MAX_WORKFLOW_REGEX_PATTERN_LENGTH = 200\nconst MAX_WORKFLOW_REGEX_INPUT_LENGTH = 10_000\n\n/**\n * Get a nested value from an object using dot notation.\n */\nfunction getNestedValue(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined\n\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) return undefined\n if (typeof current !== 'object') return undefined\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\nfunction getQuantifierEnd(pattern: string, index: number): number | null {\n const char = pattern[index]\n if (char === '*' || char === '+' || char === '?') return index\n if (char !== '{') return null\n\n const closeIndex = pattern.indexOf('}', index + 1)\n if (closeIndex === -1) return null\n\n const body = pattern.slice(index + 1, closeIndex)\n return /^[0-9]+(?:,[0-9]*)?$/.test(body) ? closeIndex : null\n}\n\ninterface RegexGroupFrame {\n hasAlternation: boolean\n hasQuantifier: boolean\n}\n\nfunction isSafeWorkflowRegexPattern(pattern: string): boolean {\n if (pattern.length > MAX_WORKFLOW_REGEX_PATTERN_LENGTH) return false\n\n const groupStack: RegexGroupFrame[] = [{ hasAlternation: false, hasQuantifier: false }]\n let inCharClass = false\n let lastClosedGroup: RegexGroupFrame | null = null\n let lastAtomWasQuantified = false\n\n // Hand-written single-pass lexer: `index` is intentionally advanced inside the\n // loop body (escape pairs, `(?:` prefixes, multi-char quantifiers, lazy `?`) on\n // top of the `index += 1` update clause. Do not \"simplify\" these in-body writes.\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index]\n\n if (char === '\\\\') {\n const next = pattern[index + 1]\n if (!inCharClass && (/[1-9]/.test(next ?? '') || (next === 'k' && pattern[index + 2] === '<'))) {\n return false\n }\n index += 1\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (inCharClass) {\n if (char === ']') {\n inCharClass = false\n lastAtomWasQuantified = false\n }\n lastClosedGroup = null\n continue\n }\n\n if (char === '[') {\n inCharClass = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '(') {\n if (pattern[index + 1] === '?') {\n if (pattern[index + 2] !== ':') return false\n index += 2\n }\n\n groupStack.push({ hasAlternation: false, hasQuantifier: false })\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === ')') {\n if (groupStack.length === 1) return false\n lastClosedGroup = groupStack.pop()!\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '|') {\n groupStack[groupStack.length - 1].hasAlternation = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n const quantifierEnd = getQuantifierEnd(pattern, index)\n if (quantifierEnd !== null) {\n if (lastAtomWasQuantified) return false\n if (lastClosedGroup?.hasAlternation || lastClosedGroup?.hasQuantifier) return false\n\n groupStack[groupStack.length - 1].hasQuantifier = true\n lastClosedGroup = null\n lastAtomWasQuantified = true\n index = quantifierEnd\n\n if (pattern[index + 1] === '?') {\n index += 1\n }\n\n continue\n }\n\n lastClosedGroup = null\n lastAtomWasQuantified = false\n }\n\n return groupStack.length === 1 && !inCharClass\n}\n\n/**\n * Evaluate a single filter condition against the event payload.\n */\nfunction evaluateCondition(condition: TriggerFilterCondition, payload: Record<string, unknown>): boolean {\n const value = getNestedValue(payload, condition.field)\n const expected = condition.value\n\n switch (condition.operator) {\n case 'eq':\n return value === expected\n\n case 'neq':\n return value !== expected\n\n case 'gt':\n return typeof value === 'number' && typeof expected === 'number' && value > expected\n\n case 'gte':\n return typeof value === 'number' && typeof expected === 'number' && value >= expected\n\n case 'lt':\n return typeof value === 'number' && typeof expected === 'number' && value < expected\n\n case 'lte':\n return typeof value === 'number' && typeof expected === 'number' && value <= expected\n\n case 'contains':\n if (typeof value === 'string' && typeof expected === 'string') {\n return value.includes(expected)\n }\n if (Array.isArray(value)) {\n return value.includes(expected)\n }\n return false\n\n case 'startsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.startsWith(expected)\n\n case 'endsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.endsWith(expected)\n\n case 'in':\n return Array.isArray(expected) && expected.includes(value)\n\n case 'notIn':\n return Array.isArray(expected) && !expected.includes(value)\n\n case 'exists':\n return value !== undefined && value !== null\n\n case 'notExists':\n return value === undefined || value === null\n\n case 'regex':\n if (typeof value !== 'string' || typeof expected !== 'string') return false\n if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false\n if (!isSafeWorkflowRegexPattern(expected)) return false\n {\n const result = testLinearRegex(expected, value, {\n maxPatternLength: MAX_WORKFLOW_REGEX_PATTERN_LENGTH,\n maxInputLength: MAX_WORKFLOW_REGEX_INPUT_LENGTH,\n })\n return result.ok ? result.matched : false\n }\n\n default:\n return false\n }\n}\n\n/**\n * Evaluate all filter conditions against the event payload.\n * All conditions must pass (AND logic).\n */\nexport function evaluateFilterConditions(\n conditions: TriggerFilterCondition[] | undefined,\n payload: Record<string, unknown>\n): boolean {\n if (!conditions || conditions.length === 0) return true\n\n return conditions.every(condition => evaluateCondition(condition, payload))\n}\n\n// ============================================================================\n// Context Mapping\n// ============================================================================\n\n/**\n * Map event payload to workflow initial context.\n */\nexport function mapEventToContext(\n mapping: TriggerContextMapping[] | undefined,\n payload: Record<string, unknown>\n): Record<string, unknown> {\n const context: Record<string, unknown> = {}\n\n if (!mapping || mapping.length === 0) return context\n\n for (const item of mapping) {\n const value = getNestedValue(payload, item.sourceExpression)\n context[item.targetKey] = value !== undefined ? value : item.defaultValue\n }\n\n return context\n}\n\n// ============================================================================\n// Trigger Loading with Caching\n// ============================================================================\n\n// In-memory cache for triggers (per tenant/org).\n// Park the Map on globalThis so the same compiled module loaded under two\n// paths (a Next.js server chunk vs. a worker resolving the file through a\n// different import root) shares one cache. Without this,\n// `invalidateTriggerCache(...)` called from the PUT /api/workflows/definitions\n// route clears its own copy while the wildcard event-trigger subscriber keeps\n// reading a stale copy populated before the trigger was added \u2014 newly added\n// triggers stay invisible for up to TRIGGER_CACHE_TTL. Mirrors the same\n// workaround used by the modules registry and `getDiRegistrars()`.\nconst GLOBAL_TRIGGER_CACHE_KEY = '__openMercatoWorkflowTriggerCache__'\n\nfunction getTriggerCache(): Map<string, CachedTriggers> {\n const existing = (globalThis as any)[GLOBAL_TRIGGER_CACHE_KEY] as\n | Map<string, CachedTriggers>\n | undefined\n if (existing) return existing\n const created = new Map<string, CachedTriggers>()\n ;(globalThis as any)[GLOBAL_TRIGGER_CACHE_KEY] = created\n return created\n}\n\nfunction getCacheKey(tenantId: string, organizationId: string): string {\n return `${tenantId}:${organizationId}`\n}\n\n/**\n * Load legacy triggers from WorkflowEventTrigger entity table.\n * For backward compatibility with existing triggers.\n */\nasync function loadLegacyTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const postgresEm = em as unknown as PostgreSqlEntityManager\n const legacyTriggers = await findWithDecryption(\n postgresEm,\n WorkflowEventTrigger,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {\n orderBy: { priority: 'DESC', createdAt: 'ASC' },\n },\n { tenantId, organizationId },\n )\n\n // Get definitions for these triggers to get workflowId\n const definitionIds = [...new Set(legacyTriggers.map(t => t.workflowDefinitionId))]\n const definitions = definitionIds.length > 0 ? await findWithDecryption(postgresEm, WorkflowDefinition, {\n id: { $in: definitionIds },\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n }, {}, { tenantId, organizationId }) : []\n const definitionMap = new Map(definitions.map(d => [d.id, d]))\n\n return legacyTriggers\n .filter(t => definitionMap.has(t.workflowDefinitionId))\n .map(t => {\n const def = definitionMap.get(t.workflowDefinitionId)!\n return {\n id: t.id,\n triggerId: t.id,\n name: t.name,\n description: t.description,\n eventPattern: t.eventPattern,\n config: t.config ?? null,\n enabled: t.enabled,\n priority: t.priority,\n workflowDefinitionId: t.workflowDefinitionId,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'legacy' as const,\n tenantId: t.tenantId,\n organizationId: t.organizationId,\n }\n })\n}\n\n/**\n * Load embedded triggers from workflow definitions.\n * New triggers are embedded directly in the definition JSONB.\n */\nasync function loadEmbeddedTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const postgresEm = em as unknown as PostgreSqlEntityManager\n // Load all enabled definitions that may have triggers\n const definitions = await findWithDecryption(\n postgresEm,\n WorkflowDefinition,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {},\n { tenantId, organizationId },\n )\n\n const triggers: UnifiedTrigger[] = []\n\n for (const def of definitions) {\n const embeddedTriggers = def.definition?.triggers as WorkflowDefinitionTrigger[] | undefined\n if (!embeddedTriggers || embeddedTriggers.length === 0) continue\n\n for (const trigger of embeddedTriggers) {\n if (!trigger.enabled) continue\n\n triggers.push({\n id: `${def.id}:${trigger.triggerId}`,\n triggerId: trigger.triggerId,\n name: trigger.name,\n description: trigger.description ?? null,\n eventPattern: trigger.eventPattern,\n config: trigger.config ?? null,\n enabled: trigger.enabled,\n priority: trigger.priority,\n workflowDefinitionId: def.id,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'embedded' as const,\n tenantId,\n organizationId,\n })\n }\n }\n\n return triggers\n}\n\n/**\n * Load all enabled triggers for a tenant/organization with caching.\n * Merges both legacy (entity) triggers and embedded (definition) triggers.\n */\nexport async function loadTriggersForTenant(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n cacheService?: CacheService\n): Promise<UnifiedTrigger[]> {\n const cacheKey = getCacheKey(tenantId, organizationId)\n const cache = getTriggerCache()\n\n // Check in-memory cache\n const cached = cache.get(cacheKey)\n if (cached && Date.now() - cached.cachedAt < TRIGGER_CACHE_TTL) {\n return cached.triggers\n }\n\n // Load from both sources\n const [legacyTriggers, embeddedTriggers] = await Promise.all([\n loadLegacyTriggers(em, tenantId, organizationId),\n loadEmbeddedTriggers(em, tenantId, organizationId),\n ])\n\n // Merge and sort by priority (higher first)\n const allTriggers = [...legacyTriggers, ...embeddedTriggers]\n .sort((a, b) => b.priority - a.priority)\n\n // Update cache\n cache.set(cacheKey, {\n triggers: allTriggers,\n cachedAt: Date.now(),\n })\n\n return allTriggers\n}\n\n/**\n * Invalidate trigger cache for a tenant/organization.\n * Call this when:\n * - Legacy triggers are created/updated/deleted\n * - Workflow definitions with embedded triggers are created/updated/deleted\n */\nexport function invalidateTriggerCache(tenantId: string, organizationId?: string): void {\n const cache = getTriggerCache()\n if (organizationId) {\n // Invalidate specific org\n const cacheKey = getCacheKey(tenantId, organizationId)\n cache.delete(cacheKey)\n } else {\n // Invalidate all orgs for tenant\n for (const key of cache.keys()) {\n if (key.startsWith(`${tenantId}:`)) {\n cache.delete(key)\n }\n }\n }\n}\n\n// ============================================================================\n// Trigger Matching\n// ============================================================================\n\n/**\n * Find all triggers that match an event.\n */\nexport async function findMatchingTriggers(\n em: EntityManager,\n context: EventTriggerContext\n): Promise<UnifiedTrigger[]> {\n const triggers = await loadTriggersForTenant(\n em,\n context.tenantId,\n context.organizationId\n )\n\n return triggers.filter(trigger => {\n // Check event pattern\n if (!matchEventPattern(context.eventName, trigger.eventPattern)) {\n return false\n }\n\n // Check filter conditions\n if (!evaluateFilterConditions(trigger.config?.filterConditions, context.payload)) {\n return false\n }\n\n return true\n })\n}\n\n// ============================================================================\n// Trigger Processing\n// ============================================================================\n\n/**\n * Check if max concurrent instances limit is reached.\n */\nasync function checkConcurrencyLimit(\n em: EntityManager,\n trigger: UnifiedTrigger\n): Promise<boolean> {\n const maxInstances = trigger.config?.maxConcurrentInstances\n\n if (!maxInstances) return true // No limit\n\n // Count running instances for this trigger's workflow definition\n const runningCount = await em.count(WorkflowInstance, {\n definitionId: trigger.workflowDefinitionId,\n status: { $in: ['RUNNING', 'WAITING_FOR_ACTIVITIES'] },\n tenantId: trigger.tenantId,\n organizationId: trigger.organizationId,\n deletedAt: null,\n })\n\n return runningCount < maxInstances\n}\n\n/**\n * Process all matching triggers for an event and start workflows.\n */\nexport async function processEventTriggers(\n em: EntityManager,\n container: AwilixContainer,\n context: EventTriggerContext\n): Promise<ProcessTriggersResult> {\n const result: ProcessTriggersResult = {\n triggered: 0,\n skipped: 0,\n errors: [],\n instances: [],\n }\n\n // Find matching triggers\n const triggers = await findMatchingTriggers(em, context)\n\n if (triggers.length === 0) {\n return result\n }\n\n // Process each trigger (definitions already validated during loading)\n for (const trigger of triggers) {\n try {\n // Check concurrency limit\n const canStart = await checkConcurrencyLimit(em, trigger)\n if (!canStart) {\n console.log(`[workflow-trigger] Skipping trigger \"${trigger.name}\": max concurrent instances reached`)\n result.skipped++\n continue\n }\n\n // Map event payload to workflow context\n const mappedContext = mapEventToContext(trigger.config?.contextMapping, context.payload)\n\n // Extract entity info from payload for metadata\n const payloadId = context.payload?.id as string | undefined\n const payloadEntityType = context.payload?.entityType as string | undefined\n\n // Include event metadata and payload in context\n const initialContext = {\n // Include raw event payload fields (e.g., id, organizationId, tenantId)\n ...context.payload,\n // Override with explicit mappings if provided\n ...mappedContext,\n __trigger: {\n triggerId: trigger.id,\n triggerName: trigger.name,\n eventName: context.eventName,\n eventPayload: context.payload,\n triggeredAt: new Date().toISOString(),\n source: trigger.source,\n },\n }\n\n // Start workflow\n const instance = await startWorkflow(em, {\n workflowId: trigger.workflowId,\n version: trigger.workflowVersion,\n initialContext,\n metadata: {\n initiatedBy: `trigger:${trigger.id}`,\n // Include entityId and entityType for widget discovery\n entityId: payloadId,\n entityType: payloadEntityType || trigger.config?.entityType,\n labels: {\n trigger_id: trigger.id,\n trigger_name: trigger.name,\n event_name: context.eventName,\n trigger_source: trigger.source,\n },\n },\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n })\n\n result.triggered++\n result.instances.push({\n triggerId: trigger.id,\n instanceId: instance.id,\n })\n\n // Execute workflow asynchronously (don't wait)\n executeWorkflow(em.fork(), container, instance.id).catch(err => {\n console.error(`[workflow-trigger] Error executing workflow ${instance.id}:`, err)\n })\n\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n console.error(`[workflow-trigger] Error processing trigger \"${trigger.name}\":`, error)\n result.errors.push({\n triggerId: trigger.id,\n error: errorMessage,\n })\n }\n }\n\n return result\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6309.1.983aeec27a",
3
+ "version": "0.6.6-develop.6313.1.618a49a6b4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -246,16 +246,16 @@
246
246
  "zod": "^4.4.3"
247
247
  },
248
248
  "peerDependencies": {
249
- "@open-mercato/ai-assistant": "0.6.6-develop.6309.1.983aeec27a",
250
- "@open-mercato/shared": "0.6.6-develop.6309.1.983aeec27a",
251
- "@open-mercato/ui": "0.6.6-develop.6309.1.983aeec27a",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6313.1.618a49a6b4",
250
+ "@open-mercato/shared": "0.6.6-develop.6313.1.618a49a6b4",
251
+ "@open-mercato/ui": "0.6.6-develop.6313.1.618a49a6b4",
252
252
  "react": "^19.0.0",
253
253
  "react-dom": "^19.0.0"
254
254
  },
255
255
  "devDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6309.1.983aeec27a",
257
- "@open-mercato/shared": "0.6.6-develop.6309.1.983aeec27a",
258
- "@open-mercato/ui": "0.6.6-develop.6309.1.983aeec27a",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6313.1.618a49a6b4",
257
+ "@open-mercato/shared": "0.6.6-develop.6313.1.618a49a6b4",
258
+ "@open-mercato/ui": "0.6.6-develop.6313.1.618a49a6b4",
259
259
  "@testing-library/dom": "^10.4.1",
260
260
  "@testing-library/jest-dom": "^6.9.1",
261
261
  "@testing-library/react": "^16.3.1",
@@ -73,6 +73,27 @@ export async function createOrderLineFixture(
73
73
  );
74
74
  }
75
75
 
76
+ /**
77
+ * Ship one or more order lines so a subsequent return passes the
78
+ * shipped-quantity guard (issue #3034). A return can only be created for
79
+ * quantities that were physically shipped, so any spec that creates a return
80
+ * must ship the relevant line(s) first. Returns the created shipment id.
81
+ */
82
+ export async function createShipmentFixture(
83
+ request: APIRequestContext,
84
+ token: string,
85
+ orderId: string,
86
+ items: Array<{ orderLineId: string; quantity: number }>,
87
+ ): Promise<string> {
88
+ return createEntity(
89
+ request,
90
+ token,
91
+ '/api/sales/shipments',
92
+ { orderId, items },
93
+ ['id', 'shipmentId'],
94
+ );
95
+ }
96
+
76
97
  /**
77
98
  * Probe whether the authenticated principal can create a sales order on the
78
99
  * current tenant (i.e. holds `sales.orders.manage`). Sales-write integration
@@ -14,7 +14,9 @@ import type { SalesCalculationService } from '../services/salesCalculationServic
14
14
  import type { SalesAdjustmentDraft, SalesLineSnapshot, SalesDocumentCalculationResult } from '../lib/types'
15
15
  import { cloneJson, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, SALES_RESOURCE_KIND_ORDER, SALES_RESOURCE_KIND_RETURN } from './shared'
16
16
  import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
17
- import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine } from '../data/entities'
17
+ import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine, SalesShipment, SalesShipmentItem } from '../data/entities'
18
+ import { coerceShipmentQuantity } from '../lib/shipments/snapshots'
19
+ import { computeAvailableReturnQuantity } from '../lib/returnQuantity'
18
20
  import {
19
21
  returnCreateSchema,
20
22
  returnUpdateSchema,
@@ -541,6 +543,36 @@ async function restoreReturnEffects(
541
543
  return createdLines
542
544
  }
543
545
 
546
+ async function loadShippedQuantityByLine(
547
+ em: EntityManager,
548
+ orderId: string,
549
+ scope: { tenantId: string; organizationId: string },
550
+ ): Promise<Map<string, number>> {
551
+ const shipments = await findWithDecryption(
552
+ em,
553
+ SalesShipment,
554
+ { order: orderId, deletedAt: null },
555
+ {},
556
+ scope,
557
+ )
558
+ const shippedByLine = new Map<string, number>()
559
+ if (!shipments.length) return shippedByLine
560
+ const items = await findWithDecryption(
561
+ em,
562
+ SalesShipmentItem,
563
+ { shipment: { $in: shipments.map((shipment) => shipment.id) } },
564
+ {},
565
+ scope,
566
+ )
567
+ items.forEach((item) => {
568
+ const orderLineId = typeof item.orderLine === 'string' ? item.orderLine : item.orderLine?.id ?? null
569
+ if (!orderLineId) return
570
+ const next = (shippedByLine.get(orderLineId) ?? 0) + coerceShipmentQuantity(item.quantity)
571
+ shippedByLine.set(orderLineId, next)
572
+ })
573
+ return shippedByLine
574
+ }
575
+
544
576
  function normalizeLinesInput(lines: ReturnCreateInput['lines']): ReturnLineInput[] {
545
577
  const seen = new Set<string>()
546
578
  const result: ReturnLineInput[] = []
@@ -594,14 +626,23 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
594
626
  )
595
627
  const lineMap = new Map(orderLines.map((line) => [line.id, line]))
596
628
 
629
+ const shippedByLine = await loadShippedQuantityByLine(tx, order.id, {
630
+ tenantId: input.tenantId,
631
+ organizationId: input.organizationId,
632
+ })
633
+
597
634
  requested.forEach(({ orderLineId, quantity }) => {
598
635
  const line = lineMap.get(orderLineId)
599
636
  if (!line) {
600
637
  throw new CrudHttpError(404, { error: translate('sales.returns.lineMissing', 'Order line not found.') })
601
638
  }
602
- const available = toNumeric(line.quantity) - toNumeric(line.returnedQuantity)
639
+ const available = computeAvailableReturnQuantity({
640
+ quantity: toNumeric(line.quantity),
641
+ returnedQuantity: toNumeric(line.returnedQuantity),
642
+ shippedQuantity: shippedByLine.get(orderLineId) ?? 0,
643
+ })
603
644
  if (quantity - 1e-6 > available) {
604
- throw new CrudHttpError(400, { error: translate('sales.returns.quantityExceeded', 'Cannot return more than the remaining quantity.') })
645
+ throw new CrudHttpError(400, { error: translate('sales.returns.quantityExceedsShipped', 'Cannot return more than the shipped quantity. Ship the items before recording a return.') })
605
646
  }
606
647
  })
607
648
 
@@ -22,6 +22,7 @@ export type ReturnOrderLine = {
22
22
  lineNumber: number | null
23
23
  quantity: number
24
24
  returnedQuantity: number
25
+ shippedQuantity?: number
25
26
  thumbnail?: string | null
26
27
  }
27
28
 
@@ -17,6 +17,7 @@ import {
17
17
  emitSalesDocumentTotalsRefresh,
18
18
  subscribeSalesDocumentTotalsRefresh,
19
19
  } from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'
20
+ import { sumShippedQuantityByLine } from '@open-mercato/core/modules/sales/lib/returnQuantity'
20
21
  import { formatMoney, normalizeNumber } from './lineItemUtils'
21
22
  import { ReturnDialog, type ReturnOrderLine } from './ReturnDialog'
22
23
  import { ReturnEditDialog, type ReturnEditRecord } from './ReturnEditDialog'
@@ -60,14 +61,25 @@ export function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }
60
61
 
61
62
  const loadLines = React.useCallback(async () => {
62
63
  const params = new URLSearchParams({ page: '1', pageSize: '100', orderId })
63
- const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(
64
- `/api/sales/order-lines?${params.toString()}`,
65
- undefined,
66
- { fallback: { items: [] } },
64
+ const shipmentParams = new URLSearchParams({ page: '1', pageSize: '200', orderId })
65
+ const [response, shipmentsResponse] = await Promise.all([
66
+ apiCall<{ items?: Array<Record<string, unknown>> }>(
67
+ `/api/sales/order-lines?${params.toString()}`,
68
+ undefined,
69
+ { fallback: { items: [] } },
70
+ ),
71
+ apiCall<{ items?: Array<Record<string, unknown>> }>(
72
+ `/api/sales/shipments?${shipmentParams.toString()}`,
73
+ undefined,
74
+ { fallback: { items: [] } },
75
+ ),
76
+ ])
77
+ const shippedByLine = sumShippedQuantityByLine(
78
+ Array.isArray(shipmentsResponse.result?.items) ? shipmentsResponse.result?.items : [],
67
79
  )
68
80
  const items = Array.isArray(response.result?.items) ? response.result?.items ?? [] : []
69
81
  const mapped: ReturnOrderLine[] = items
70
- .map((item) => {
82
+ .map((item): ReturnOrderLine | null => {
71
83
  const map = item as Record<string, unknown>
72
84
  const id = typeof map.id === 'string' ? map.id : null
73
85
  if (!id) return null
@@ -110,6 +122,7 @@ export function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }
110
122
  lineNumber,
111
123
  quantity: Number.isFinite(quantity) ? quantity : 0,
112
124
  returnedQuantity: Number.isFinite(returnedQuantity) ? returnedQuantity : 0,
125
+ shippedQuantity: shippedByLine.get(id) ?? 0,
113
126
  }
114
127
  })
115
128
  .filter((entry): entry is ReturnOrderLine => Boolean(entry?.id))
@@ -1326,6 +1326,7 @@
1326
1326
  "sales.returns.qty.increase": "Menge erhöhen",
1327
1327
  "sales.returns.quantity": "Menge",
1328
1328
  "sales.returns.quantityExceeded": "Es kann nicht mehr als die verbleibende Menge zurückgegeben werden.",
1329
+ "sales.returns.quantityExceedsShipped": "Es kann nicht mehr als die versandte Menge zurückgegeben werden. Versenden Sie die Artikel, bevor Sie eine Rücksendung erfassen.",
1329
1330
  "sales.returns.reason": "Grund",
1330
1331
  "sales.returns.reason.placeholder": "Optional",
1331
1332
  "sales.returns.returnNumber": "Rückgabe",
@@ -1326,6 +1326,7 @@
1326
1326
  "sales.returns.qty.increase": "Increase quantity",
1327
1327
  "sales.returns.quantity": "Quantity",
1328
1328
  "sales.returns.quantityExceeded": "Cannot return more than the remaining quantity.",
1329
+ "sales.returns.quantityExceedsShipped": "Cannot return more than the shipped quantity. Ship the items before recording a return.",
1329
1330
  "sales.returns.reason": "Reason",
1330
1331
  "sales.returns.reason.placeholder": "Optional",
1331
1332
  "sales.returns.returnNumber": "Return",
@@ -1326,6 +1326,7 @@
1326
1326
  "sales.returns.qty.increase": "Aumentar cantidad",
1327
1327
  "sales.returns.quantity": "Cantidad",
1328
1328
  "sales.returns.quantityExceeded": "No se puede devolver más que la cantidad restante.",
1329
+ "sales.returns.quantityExceedsShipped": "No se puede devolver más que la cantidad enviada. Envíe los artículos antes de registrar una devolución.",
1329
1330
  "sales.returns.reason": "Motivo",
1330
1331
  "sales.returns.reason.placeholder": "Opcional",
1331
1332
  "sales.returns.returnNumber": "Devolución",
@@ -1326,6 +1326,7 @@
1326
1326
  "sales.returns.qty.increase": "Zwiększ ilość",
1327
1327
  "sales.returns.quantity": "Ilość",
1328
1328
  "sales.returns.quantityExceeded": "Nie można zwrócić więcej niż pozostała ilość.",
1329
+ "sales.returns.quantityExceedsShipped": "Nie można zwrócić więcej niż wysłana ilość. Wyślij artykuły, zanim zarejestrujesz zwrot.",
1329
1330
  "sales.returns.reason": "Powód",
1330
1331
  "sales.returns.reason.placeholder": "Opcjonalny",
1331
1332
  "sales.returns.returnNumber": "Zwrot",
@@ -1,10 +1,57 @@
1
1
  export type ReturnAvailableInput = {
2
2
  quantity: number
3
3
  returnedQuantity: number
4
+ shippedQuantity?: number
4
5
  }
5
6
 
6
7
  export function computeAvailableReturnQuantity(line: ReturnAvailableInput): number {
7
- const raw = line.quantity - line.returnedQuantity
8
+ const cap =
9
+ typeof line.shippedQuantity === 'number' && Number.isFinite(line.shippedQuantity)
10
+ ? Math.min(line.quantity, line.shippedQuantity)
11
+ : line.quantity
12
+ const raw = cap - line.returnedQuantity
8
13
  if (!Number.isFinite(raw) || raw <= 0) return 0
9
14
  return Math.max(0, Math.floor(raw + 1e-6))
10
15
  }
16
+
17
+ function coerceQuantity(value: unknown): number {
18
+ if (typeof value === 'number' && Number.isFinite(value)) return value
19
+ if (typeof value === 'string' && value.trim().length) {
20
+ const parsed = Number(value)
21
+ if (Number.isFinite(parsed)) return parsed
22
+ }
23
+ return 0
24
+ }
25
+
26
+ /**
27
+ * Aggregate shipped quantity per order line from the `/api/sales/shipments`
28
+ * list payload. Mirrors the server-side `loadShippedQuantityByLine` cap so the
29
+ * Return dialog's "Available" figure matches what `sales.returns.create`
30
+ * enforces. Each shipment exposes its lines under `items` (either the
31
+ * persisted snapshot or DB-grouped rows), both carrying `orderLineId` and
32
+ * `quantity`.
33
+ */
34
+ export function sumShippedQuantityByLine(
35
+ shipments: ReadonlyArray<Record<string, unknown>> | null | undefined,
36
+ ): Map<string, number> {
37
+ const shippedByLine = new Map<string, number>()
38
+ if (!Array.isArray(shipments)) return shippedByLine
39
+ for (const shipment of shipments) {
40
+ if (!shipment || typeof shipment !== 'object') continue
41
+ const lineItems = (shipment as Record<string, unknown>).items
42
+ if (!Array.isArray(lineItems)) continue
43
+ for (const lineItem of lineItems) {
44
+ if (!lineItem || typeof lineItem !== 'object') continue
45
+ const record = lineItem as Record<string, unknown>
46
+ const orderLineId =
47
+ typeof record.orderLineId === 'string'
48
+ ? record.orderLineId
49
+ : typeof record.order_line_id === 'string'
50
+ ? record.order_line_id
51
+ : null
52
+ if (!orderLineId) continue
53
+ shippedByLine.set(orderLineId, (shippedByLine.get(orderLineId) ?? 0) + coerceQuantity(record.quantity))
54
+ }
55
+ }
56
+ return shippedByLine
57
+ }