@open-mercato/core 0.6.7-develop.6851.1.c1680b00d8 → 0.6.7-develop.6860.1.f796b22e80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/bootstrap.js +16 -1
  3. package/dist/bootstrap.js.map +2 -2
  4. package/dist/generated/entities/attachment_quota_reservation/index.js +33 -0
  5. package/dist/generated/entities/attachment_quota_reservation/index.js.map +7 -0
  6. package/dist/generated/entities.ids.generated.js +2 -1
  7. package/dist/generated/entities.ids.generated.js.map +2 -2
  8. package/dist/generated/entity-fields-registry.js +17 -0
  9. package/dist/generated/entity-fields-registry.js.map +2 -2
  10. package/dist/modules/attachments/api/route.js +104 -8
  11. package/dist/modules/attachments/api/route.js.map +2 -2
  12. package/dist/modules/attachments/data/entities.js +67 -1
  13. package/dist/modules/attachments/data/entities.js.map +2 -2
  14. package/dist/modules/attachments/di.js +7 -1
  15. package/dist/modules/attachments/di.js.map +2 -2
  16. package/dist/modules/attachments/lib/drivers/index.js.map +1 -1
  17. package/dist/modules/attachments/lib/drivers/localDriver.js +19 -6
  18. package/dist/modules/attachments/lib/drivers/localDriver.js.map +2 -2
  19. package/dist/modules/attachments/lib/quota-recovery-queue.js +9 -0
  20. package/dist/modules/attachments/lib/quota-recovery-queue.js.map +7 -0
  21. package/dist/modules/attachments/lib/quota-service.js +224 -0
  22. package/dist/modules/attachments/lib/quota-service.js.map +7 -0
  23. package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js +13 -0
  24. package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js.map +7 -0
  25. package/dist/modules/attachments/workers/quota-recovery.js +45 -0
  26. package/dist/modules/attachments/workers/quota-recovery.js.map +7 -0
  27. package/dist/modules/auth/lib/setup-app.js +16 -2
  28. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  29. package/dist/modules/dashboards/services/widgetDataService.js +3 -4
  30. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  31. package/dist/modules/entities/cli.js +193 -19
  32. package/dist/modules/entities/cli.js.map +2 -2
  33. package/dist/modules/messages/api/[id]/route.js +15 -0
  34. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  35. package/dist/modules/query_index/lib/subscriber-scope.js +2 -3
  36. package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
  37. package/dist/modules/sales/commands/documents.js +110 -22
  38. package/dist/modules/sales/commands/documents.js.map +2 -2
  39. package/dist/modules/sales/workflows.js +10 -7
  40. package/dist/modules/sales/workflows.js.map +2 -2
  41. package/dist/modules/workflows/lib/activity-executor.js +27 -0
  42. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  43. package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js +10 -0
  44. package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js.map +7 -0
  45. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +10 -6
  46. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
  47. package/generated/entities/attachment_quota_reservation/index.ts +15 -0
  48. package/generated/entities.ids.generated.ts +2 -1
  49. package/generated/entity-fields-registry.ts +17 -0
  50. package/package.json +7 -7
  51. package/src/bootstrap.ts +17 -2
  52. package/src/modules/attachments/api/route.ts +114 -7
  53. package/src/modules/attachments/data/entities.ts +59 -0
  54. package/src/modules/attachments/di.ts +9 -1
  55. package/src/modules/attachments/i18n/de.json +3 -0
  56. package/src/modules/attachments/i18n/en.json +3 -0
  57. package/src/modules/attachments/i18n/es.json +3 -0
  58. package/src/modules/attachments/i18n/ko.json +3 -0
  59. package/src/modules/attachments/i18n/pl.json +3 -0
  60. package/src/modules/attachments/lib/drivers/index.ts +1 -1
  61. package/src/modules/attachments/lib/drivers/localDriver.ts +23 -8
  62. package/src/modules/attachments/lib/drivers/types.ts +5 -0
  63. package/src/modules/attachments/lib/quota-recovery-queue.ts +16 -0
  64. package/src/modules/attachments/lib/quota-service.ts +442 -0
  65. package/src/modules/attachments/migrations/.snapshot-open-mercato.json +648 -120
  66. package/src/modules/attachments/migrations/Migration20260709234741_attachments.ts +12 -0
  67. package/src/modules/attachments/workers/quota-recovery.ts +68 -0
  68. package/src/modules/auth/lib/setup-app.ts +20 -2
  69. package/src/modules/dashboards/services/widgetDataService.ts +3 -6
  70. package/src/modules/entities/cli.ts +238 -21
  71. package/src/modules/messages/api/[id]/route.ts +21 -0
  72. package/src/modules/query_index/lib/subscriber-scope.ts +2 -7
  73. package/src/modules/sales/commands/documents.ts +188 -29
  74. package/src/modules/sales/workflows.ts +10 -7
  75. package/src/modules/workflows/i18n/de.json +1 -0
  76. package/src/modules/workflows/i18n/en.json +1 -0
  77. package/src/modules/workflows/i18n/es.json +1 -0
  78. package/src/modules/workflows/i18n/ko.json +1 -0
  79. package/src/modules/workflows/i18n/pl.json +1 -0
  80. package/src/modules/workflows/lib/activity-executor.ts +40 -1
  81. package/src/modules/workflows/widgets/injection/order-approval/context-keys.ts +18 -0
  82. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +15 -6
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/workflows/widgets/injection/order-approval/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { emitSalesDocumentDataRefresh } from '@open-mercato/core/modules/sales/lib/frontend/documentDataEvents'\nimport { fetchAllDictionaryEntries } from '@open-mercato/core/modules/dictionaries/lib/fetchAllEntries'\n\ntype OrderRecord = {\n id: string\n orderNumber?: string\n status?: string\n statusEntryId?: string\n}\n\ntype WorkflowInstance = {\n id: string\n workflowId: string\n status: 'RUNNING' | 'PAUSED' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'WAITING_FOR_ACTIVITIES'\n currentStepId: string\n context: Record<string, any>\n}\n\ntype UserTask = {\n id: string\n taskName: string\n status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'\n workflowInstanceId: string\n claimedBy: string | null\n}\n\ntype DictionaryEntry = {\n id: string\n value: string\n label: string\n color?: string\n icon?: string\n}\n\nconst WORKFLOW_ID = 'sales.order-approval'\n\nexport default function OrderApprovalWidget({ data }: InjectionWidgetComponentProps<unknown, OrderRecord>) {\n const t = useT()\n const queryClient = useQueryClient()\n const orderId = data?.id\n\n const [decision, setDecision] = React.useState<'approve' | 'reject' | ''>('')\n const [comments, setComments] = React.useState('')\n const [error, setError] = React.useState<string | null>(null)\n\n // Track if we're waiting for workflow to process (for polling)\n const [isWaitingForProcessing, setIsWaitingForProcessing] = React.useState(false)\n\n // Fetch current order status directly (to detect when workflow updates it)\n // Use list endpoint with ID filter since single-item endpoint doesn't exist\n const { data: orderData } = useQuery({\n queryKey: ['order-status', orderId],\n queryFn: async () => {\n if (!orderId) return null\n const result = await apiCall<{ items: Array<{ id: string; status?: string | null }> }>(\n `/api/sales/orders?id=${orderId}&pageSize=1`\n )\n return result.ok && result.result?.items?.[0] ? result.result.items[0] : null\n },\n enabled: Boolean(orderId),\n staleTime: 5_000,\n // Poll when waiting for processing to detect status change\n refetchInterval: isWaitingForProcessing ? 2_000 : false,\n })\n\n // Use fresh order status from query, fallback to prop data\n // The API returns status as a string value (e.g., \"approved\", \"pending_approval\")\n const currentOrderStatus = orderData?.status || data?.status\n\n // Fetch active workflow instances for this order\n const { data: instancesData, isLoading: instancesLoading } = useQuery({\n queryKey: ['workflow-instances', orderId],\n queryFn: async () => {\n if (!orderId) return { data: [] }\n const result = await apiCall<{ data: WorkflowInstance[] }>(\n `/api/workflows/instances?entityId=${orderId}&status=RUNNING,PAUSED,WAITING_FOR_ACTIVITIES`\n )\n return result.ok ? result.result : { data: [] }\n },\n enabled: Boolean(orderId),\n staleTime: 5_000,\n // Poll every 2 seconds when waiting for processing\n refetchInterval: isWaitingForProcessing ? 2_000 : false,\n })\n\n const activeInstance = instancesData?.data?.find(\n (inst) => inst.workflowId === WORKFLOW_ID && !['COMPLETED', 'FAILED', 'CANCELLED'].includes(inst.status)\n )\n\n // Fetch pending user tasks for active instance\n const { data: tasksData, isLoading: tasksLoading } = useQuery({\n queryKey: ['workflow-tasks', activeInstance?.id],\n queryFn: async () => {\n if (!activeInstance?.id) return { data: [] }\n const result = await apiCall<{ data: UserTask[] }>(\n `/api/workflows/tasks?workflowInstanceId=${activeInstance.id}&status=PENDING,IN_PROGRESS`\n )\n return result.ok ? result.result : { data: [] }\n },\n enabled: Boolean(activeInstance?.id),\n staleTime: 5_000,\n // Poll every 2 seconds when waiting for processing\n refetchInterval: isWaitingForProcessing ? 2_000 : false,\n })\n\n const pendingTask = tasksData?.data?.[0]\n\n // Auto-detect when workflow is processing (active instance but no pending task)\n const isProcessing = activeInstance && !pendingTask && !['COMPLETED', 'FAILED', 'CANCELLED'].includes(activeInstance.status)\n\n // Track previous status to detect changes\n const prevStatusRef = React.useRef<string | undefined>(data?.status)\n\n // Update polling state when processing state changes\n React.useEffect(() => {\n if (isProcessing) {\n setIsWaitingForProcessing(true)\n } else if (!activeInstance || activeInstance.status === 'COMPLETED') {\n setIsWaitingForProcessing(false)\n }\n }, [isProcessing, activeInstance])\n\n // When order status changes, emit refresh event to update the page\n React.useEffect(() => {\n const newStatus = orderData?.status\n const oldStatus = prevStatusRef.current\n\n if (newStatus && oldStatus && newStatus !== oldStatus) {\n // Status changed - emit document refresh event to reload the page data\n if (orderId) {\n emitSalesDocumentDataRefresh({ documentId: orderId, kind: 'order' })\n }\n }\n\n prevStatusRef.current = newStatus || oldStatus\n }, [orderData?.status, orderId])\n\n // First fetch dictionaries to find the order_status dictionary ID\n const { data: dictionariesData } = useQuery({\n queryKey: ['dictionaries'],\n queryFn: async () => {\n const result = await apiCall<{ items: Array<{ id: string; key: string }> }>(\n '/api/dictionaries'\n )\n return result.ok ? result.result : { items: [] }\n },\n staleTime: 60_000,\n })\n\n const orderStatusDictionaryId = React.useMemo(() => {\n const dict = dictionariesData?.items?.find(d => d.key === 'sales.order_status')\n return dict?.id\n }, [dictionariesData])\n\n // Fetch order status dictionary entries using the dictionary ID\n const { data: statusEntriesData } = useQuery({\n queryKey: ['dictionary-entries', orderStatusDictionaryId],\n queryFn: async (): Promise<{ items: DictionaryEntry[] }> => {\n if (!orderStatusDictionaryId) return { items: [] }\n const result = await fetchAllDictionaryEntries(orderStatusDictionaryId)\n if (!result.ok) return { items: [] }\n const items = result.items.flatMap((entry) => {\n const id = typeof entry.id === 'string' ? entry.id : null\n const value = typeof entry.value === 'string' ? entry.value : null\n if (!id || !value) return []\n const label = typeof entry.label === 'string' && entry.label.trim().length ? entry.label : value\n return [{ id, value, label }]\n })\n return { items }\n },\n enabled: Boolean(orderStatusDictionaryId),\n staleTime: 60_000,\n })\n\n const findStatusId = React.useCallback((code: string) => {\n const entries = statusEntriesData?.items || []\n // The API returns 'value' as the code/key for dictionary entries\n const entry = entries.find(\n (e) => e.value === code || e.label?.toLowerCase() === code.toLowerCase()\n )\n return entry?.id\n }, [statusEntriesData])\n\n // Start workflow mutation\n const startWorkflowMutation = useMutation({\n mutationFn: async () => {\n const pendingApprovalStatusId = findStatusId('pending_approval')\n const approvedStatusId = findStatusId('approved')\n const rejectedStatusId = findStatusId('rejected')\n\n if (!pendingApprovalStatusId || !approvedStatusId || !rejectedStatusId) {\n throw new Error(t('workflows.orderApproval.missingStatuses', 'Missing order status entries. Please ensure pending_approval, approved, and rejected statuses exist in the sales.order_status dictionary.'))\n }\n\n const result = await apiCall<{ data: WorkflowInstance }>('/api/workflows/instances', {\n method: 'POST',\n body: JSON.stringify({\n workflowId: WORKFLOW_ID,\n initialContext: {\n orderId,\n pendingApprovalStatusId,\n approvedStatusId,\n rejectedStatusId,\n },\n metadata: {\n entityType: 'SalesOrder',\n entityId: orderId,\n },\n }),\n })\n\n if (!result.ok) {\n const errorResult = result.result as { error?: string } | null\n throw new Error(errorResult?.error || t('workflows.orderApproval.startError', 'Failed to start approval workflow'))\n }\n\n return result.result\n },\n onSuccess: () => {\n setError(null)\n queryClient.invalidateQueries({ queryKey: ['workflow-instances', orderId] })\n },\n onError: (err: Error) => {\n setError(err.message)\n },\n })\n\n // Complete task mutation\n const completeTaskMutation = useMutation({\n mutationFn: async ({ taskId, formData }: { taskId: string; formData: { decision: string; comments?: string } }) => {\n const result = await apiCall(`/api/workflows/tasks/${taskId}/complete`, {\n method: 'POST',\n body: JSON.stringify({ formData }),\n })\n\n if (!result.ok) {\n const errorResult = result.result as { error?: string } | null\n throw new Error(errorResult?.error || t('workflows.orderApproval.completeError', 'Failed to complete approval task'))\n }\n\n return result.result\n },\n onSuccess: () => {\n setError(null)\n setDecision('')\n setComments('')\n // Start polling immediately after submitting decision\n setIsWaitingForProcessing(true)\n queryClient.invalidateQueries({ queryKey: ['workflow-instances', orderId] })\n queryClient.invalidateQueries({ queryKey: ['workflow-tasks', activeInstance?.id] })\n // Also refresh the order data\n queryClient.invalidateQueries({ queryKey: ['sales-order', orderId] })\n },\n onError: (err: Error) => {\n setError(err.message)\n },\n })\n\n const handleStartWorkflow = () => {\n // Start polling after starting workflow\n setIsWaitingForProcessing(true)\n startWorkflowMutation.mutate()\n }\n\n const handleCompleteTask = () => {\n if (!pendingTask || !decision) return\n completeTaskMutation.mutate({\n taskId: pendingTask.id,\n formData: { decision, comments },\n })\n }\n\n // Handle keyboard shortcuts (Cmd/Ctrl+Enter to submit)\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n if (pendingTask && decision && !isSubmitting) {\n handleCompleteTask()\n }\n }\n }\n\n const isLoading = instancesLoading || tasksLoading\n const isSubmitting = startWorkflowMutation.isPending || completeTaskMutation.isPending\n\n // Don't render if no orderId\n if (!orderId) return null\n\n // Only show widget when order status is pending_approval\n const orderStatus = currentOrderStatus?.toLowerCase()\n if (orderStatus !== 'pending_approval') {\n return null\n }\n\n // Loading state\n if (isLoading) {\n return (\n <div className=\"flex items-center justify-center p-4\">\n <Spinner size=\"sm\" />\n <span className=\"ml-2 text-sm text-muted-foreground\">{t('common.loading', 'Loading...')}</span>\n </div>\n )\n }\n\n // Show workflow status badge\n const getStatusBadge = () => {\n if (!activeInstance) return null\n\n const statusVariants: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {\n RUNNING: 'default',\n PAUSED: 'secondary',\n WAITING_FOR_ACTIVITIES: 'secondary',\n COMPLETED: 'default',\n FAILED: 'destructive',\n CANCELLED: 'outline',\n }\n\n return (\n <Badge variant={statusVariants[activeInstance.status] || 'outline'}>\n {t(`workflows.instances.statuses.${activeInstance.status}`, activeInstance.status)}\n </Badge>\n )\n }\n\n return (\n <div className=\"space-y-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <div className=\"text-sm font-semibold text-foreground\">\n {t('workflows.orderApproval.groupLabel', 'Order Approval')}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t('workflows.orderApproval.groupDescription', 'Review and approve or reject this order')}\n </p>\n </div>\n {getStatusBadge()}\n </div>\n\n {error && (\n <div className=\"rounded-md border border-red-200 bg-red-50 p-3 text-xs text-red-800\">\n {error}\n </div>\n )}\n\n {/* No active workflow - show request approval button */}\n {!activeInstance && (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.orderApproval.noWorkflowActive', 'No approval workflow is active for this order.')}\n </p>\n <Button\n onClick={handleStartWorkflow}\n disabled={isSubmitting}\n variant=\"default\"\n size=\"sm\"\n >\n {isSubmitting && <Spinner size=\"sm\" className=\"mr-2\" />}\n {t('workflows.orderApproval.requestApproval', 'Request Approval')}\n </Button>\n </div>\n )}\n\n {/* Active workflow with pending task - show approve/reject UI */}\n {activeInstance && pendingTask && (\n <div className=\"space-y-3\" onKeyDown={handleKeyDown}>\n <div className=\"rounded-md border border-amber-200 bg-amber-50 p-3\">\n <p className=\"text-sm font-medium text-amber-800\">\n {t('workflows.orderApproval.pendingTitle', 'Pending Approval')}\n </p>\n <p className=\"text-xs text-amber-700 mt-1\">\n {t('workflows.orderApproval.pendingDescription', 'This order requires approval before processing.')}\n </p>\n </div>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {t('workflows.orderApproval.decisionLabel', 'Decision')}\n </label>\n <div className=\"flex gap-2\">\n <Button\n variant={decision === 'approve' ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => setDecision('approve')}\n className={decision === 'approve' ? 'bg-emerald-600 hover:bg-emerald-700' : ''}\n >\n {t('workflows.orderApproval.approveButton', 'Approve')}\n </Button>\n <Button\n variant={decision === 'reject' ? 'destructive' : 'outline'}\n size=\"sm\"\n onClick={() => setDecision('reject')}\n >\n {t('workflows.orderApproval.rejectButton', 'Reject')}\n </Button>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {t('workflows.orderApproval.commentsLabel', 'Comments')} <span className=\"text-muted-foreground font-normal\">({t('common.optional', 'optional')})</span>\n </label>\n <Textarea\n value={comments}\n onChange={(e) => setComments(e.target.value)}\n placeholder={t('workflows.orderApproval.commentsPlaceholder', 'Add optional comments...')}\n rows={2}\n />\n </div>\n\n <Button\n onClick={handleCompleteTask}\n disabled={!decision || isSubmitting}\n variant=\"default\"\n size=\"sm\"\n className=\"w-full\"\n >\n {isSubmitting && <Spinner size=\"sm\" className=\"mr-2\" />}\n {t('workflows.orderApproval.submitDecision', 'Submit Decision')}\n </Button>\n </div>\n )}\n\n {/* Active workflow but no pending task (processing) */}\n {activeInstance && !pendingTask && activeInstance.status !== 'COMPLETED' && (\n <div className=\"rounded-md border border-blue-200 bg-blue-50 p-3\">\n <p className=\"text-sm text-blue-800\">\n {t('workflows.orderApproval.processing', 'Workflow is processing...')}\n </p>\n </div>\n )}\n\n {/* Completed workflow */}\n {activeInstance && activeInstance.status === 'COMPLETED' && (\n <div className=\"rounded-md border border-emerald-200 bg-emerald-50 p-3\">\n <p className=\"text-sm text-emerald-800\">\n {t('workflows.orderApproval.completed', 'Approval workflow completed.')}\n </p>\n </div>\n )}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { emitSalesDocumentDataRefresh } from '@open-mercato/core/modules/sales/lib/frontend/documentDataEvents'\nimport { fetchAllDictionaryEntries } from '@open-mercato/core/modules/dictionaries/lib/fetchAllEntries'\nimport type { OrderApprovalInitialContext } from './context-keys'\n\ntype OrderRecord = {\n id: string\n orderNumber?: string\n status?: string\n statusEntryId?: string\n}\n\ntype WorkflowInstance = {\n id: string\n workflowId: string\n status: 'RUNNING' | 'PAUSED' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'WAITING_FOR_ACTIVITIES'\n currentStepId: string\n context: Record<string, any>\n}\n\ntype UserTask = {\n id: string\n taskName: string\n status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'\n workflowInstanceId: string\n claimedBy: string | null\n}\n\ntype DictionaryEntry = {\n id: string\n value: string\n label: string\n color?: string\n icon?: string\n}\n\nconst WORKFLOW_ID = 'sales.order-approval'\n\nexport default function OrderApprovalWidget({ data }: InjectionWidgetComponentProps<unknown, OrderRecord>) {\n const t = useT()\n const queryClient = useQueryClient()\n const orderId = data?.id\n\n const [decision, setDecision] = React.useState<'approve' | 'reject' | ''>('')\n const [comments, setComments] = React.useState('')\n const [error, setError] = React.useState<string | null>(null)\n\n // Track if we're waiting for workflow to process (for polling)\n const [isWaitingForProcessing, setIsWaitingForProcessing] = React.useState(false)\n\n // Fetch current order status directly (to detect when workflow updates it)\n // Use list endpoint with ID filter since single-item endpoint doesn't exist\n const { data: orderData } = useQuery({\n queryKey: ['order-status', orderId],\n queryFn: async () => {\n if (!orderId) return null\n const result = await apiCall<{ items: Array<{ id: string; status?: string | null }> }>(\n `/api/sales/orders?id=${orderId}&pageSize=1`\n )\n return result.ok && result.result?.items?.[0] ? result.result.items[0] : null\n },\n enabled: Boolean(orderId),\n staleTime: 5_000,\n // Poll when waiting for processing to detect status change\n refetchInterval: isWaitingForProcessing ? 2_000 : false,\n })\n\n // Use fresh order status from query, fallback to prop data\n // The API returns status as a string value (e.g., \"approved\", \"pending_approval\")\n const currentOrderStatus = orderData?.status || data?.status\n\n // Fetch active workflow instances for this order\n const { data: instancesData, isLoading: instancesLoading } = useQuery({\n queryKey: ['workflow-instances', orderId],\n queryFn: async () => {\n if (!orderId) return { data: [] }\n const result = await apiCall<{ data: WorkflowInstance[] }>(\n `/api/workflows/instances?entityId=${orderId}&status=RUNNING,PAUSED,WAITING_FOR_ACTIVITIES`\n )\n return result.ok ? result.result : { data: [] }\n },\n enabled: Boolean(orderId),\n staleTime: 5_000,\n // Poll every 2 seconds when waiting for processing\n refetchInterval: isWaitingForProcessing ? 2_000 : false,\n })\n\n const activeInstance = instancesData?.data?.find(\n (inst) => inst.workflowId === WORKFLOW_ID && !['COMPLETED', 'FAILED', 'CANCELLED'].includes(inst.status)\n )\n\n // Fetch pending user tasks for active instance\n const { data: tasksData, isLoading: tasksLoading } = useQuery({\n queryKey: ['workflow-tasks', activeInstance?.id],\n queryFn: async () => {\n if (!activeInstance?.id) return { data: [] }\n const result = await apiCall<{ data: UserTask[] }>(\n `/api/workflows/tasks?workflowInstanceId=${activeInstance.id}&status=PENDING,IN_PROGRESS`\n )\n return result.ok ? result.result : { data: [] }\n },\n enabled: Boolean(activeInstance?.id),\n staleTime: 5_000,\n // Poll every 2 seconds when waiting for processing\n refetchInterval: isWaitingForProcessing ? 2_000 : false,\n })\n\n const pendingTask = tasksData?.data?.[0]\n\n // Auto-detect when workflow is processing (active instance but no pending task)\n const isProcessing = activeInstance && !pendingTask && !['COMPLETED', 'FAILED', 'CANCELLED'].includes(activeInstance.status)\n\n // Track previous status to detect changes\n const prevStatusRef = React.useRef<string | undefined>(data?.status)\n\n // Update polling state when processing state changes\n React.useEffect(() => {\n if (isProcessing) {\n setIsWaitingForProcessing(true)\n } else if (!activeInstance || activeInstance.status === 'COMPLETED') {\n setIsWaitingForProcessing(false)\n }\n }, [isProcessing, activeInstance])\n\n // When order status changes, emit refresh event to update the page\n React.useEffect(() => {\n const newStatus = orderData?.status\n const oldStatus = prevStatusRef.current\n\n if (newStatus && oldStatus && newStatus !== oldStatus) {\n // Status changed - emit document refresh event to reload the page data\n if (orderId) {\n emitSalesDocumentDataRefresh({ documentId: orderId, kind: 'order' })\n }\n }\n\n prevStatusRef.current = newStatus || oldStatus\n }, [orderData?.status, orderId])\n\n // First fetch dictionaries to find the order_status dictionary ID\n const { data: dictionariesData } = useQuery({\n queryKey: ['dictionaries'],\n queryFn: async () => {\n const result = await apiCall<{ items: Array<{ id: string; key: string }> }>(\n '/api/dictionaries'\n )\n return result.ok ? result.result : { items: [] }\n },\n staleTime: 60_000,\n })\n\n const orderStatusDictionaryId = React.useMemo(() => {\n const dict = dictionariesData?.items?.find(d => d.key === 'sales.order_status')\n return dict?.id\n }, [dictionariesData])\n\n // Fetch order status dictionary entries using the dictionary ID\n const { data: statusEntriesData } = useQuery({\n queryKey: ['dictionary-entries', orderStatusDictionaryId],\n queryFn: async (): Promise<{ items: DictionaryEntry[] }> => {\n if (!orderStatusDictionaryId) return { items: [] }\n const result = await fetchAllDictionaryEntries(orderStatusDictionaryId)\n if (!result.ok) return { items: [] }\n const items = result.items.flatMap((entry) => {\n const id = typeof entry.id === 'string' ? entry.id : null\n const value = typeof entry.value === 'string' ? entry.value : null\n if (!id || !value) return []\n const label = typeof entry.label === 'string' && entry.label.trim().length ? entry.label : value\n return [{ id, value, label }]\n })\n return { items }\n },\n enabled: Boolean(orderStatusDictionaryId),\n staleTime: 60_000,\n })\n\n const findStatusId = React.useCallback((code: string) => {\n const entries = statusEntriesData?.items || []\n // The API returns 'value' as the code/key for dictionary entries\n const entry = entries.find(\n (e) => e.value === code || e.label?.toLowerCase() === code.toLowerCase()\n )\n return entry?.id\n }, [statusEntriesData])\n\n // Start workflow mutation\n const startWorkflowMutation = useMutation({\n mutationFn: async () => {\n const pendingApprovalStatusId = findStatusId('pending_approval')\n const approvedStatusId = findStatusId('approved')\n const rejectedStatusId = findStatusId('rejected')\n\n if (!orderId) {\n throw new Error(t('workflows.orderApproval.missingOrderId', 'Cannot start the approval workflow before the order has loaded.'))\n }\n\n if (!pendingApprovalStatusId || !approvedStatusId || !rejectedStatusId) {\n throw new Error(t('workflows.orderApproval.missingStatuses', 'Missing order status entries. Please ensure pending_approval, approved, and rejected statuses exist in the sales.order_status dictionary.'))\n }\n\n // Typed against the shared key list so a definition that references a new\n // {{context.*}} key cannot compile until this seed provides it.\n const initialContext: OrderApprovalInitialContext = {\n orderId,\n pendingApprovalStatusId,\n approvedStatusId,\n rejectedStatusId,\n }\n\n const result = await apiCall<{ data: WorkflowInstance }>('/api/workflows/instances', {\n method: 'POST',\n body: JSON.stringify({\n workflowId: WORKFLOW_ID,\n initialContext,\n metadata: {\n entityType: 'SalesOrder',\n entityId: orderId,\n },\n }),\n })\n\n if (!result.ok) {\n const errorResult = result.result as { error?: string } | null\n throw new Error(errorResult?.error || t('workflows.orderApproval.startError', 'Failed to start approval workflow'))\n }\n\n return result.result\n },\n onSuccess: () => {\n setError(null)\n queryClient.invalidateQueries({ queryKey: ['workflow-instances', orderId] })\n },\n onError: (err: Error) => {\n setError(err.message)\n },\n })\n\n // Complete task mutation\n const completeTaskMutation = useMutation({\n mutationFn: async ({ taskId, formData }: { taskId: string; formData: { decision: string; comments?: string } }) => {\n const result = await apiCall(`/api/workflows/tasks/${taskId}/complete`, {\n method: 'POST',\n body: JSON.stringify({ formData }),\n })\n\n if (!result.ok) {\n const errorResult = result.result as { error?: string } | null\n throw new Error(errorResult?.error || t('workflows.orderApproval.completeError', 'Failed to complete approval task'))\n }\n\n return result.result\n },\n onSuccess: () => {\n setError(null)\n setDecision('')\n setComments('')\n // Start polling immediately after submitting decision\n setIsWaitingForProcessing(true)\n queryClient.invalidateQueries({ queryKey: ['workflow-instances', orderId] })\n queryClient.invalidateQueries({ queryKey: ['workflow-tasks', activeInstance?.id] })\n // Also refresh the order data\n queryClient.invalidateQueries({ queryKey: ['sales-order', orderId] })\n },\n onError: (err: Error) => {\n setError(err.message)\n },\n })\n\n const handleStartWorkflow = () => {\n // Start polling after starting workflow\n setIsWaitingForProcessing(true)\n startWorkflowMutation.mutate()\n }\n\n const handleCompleteTask = () => {\n if (!pendingTask || !decision) return\n completeTaskMutation.mutate({\n taskId: pendingTask.id,\n formData: { decision, comments },\n })\n }\n\n // Handle keyboard shortcuts (Cmd/Ctrl+Enter to submit)\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n if (pendingTask && decision && !isSubmitting) {\n handleCompleteTask()\n }\n }\n }\n\n const isLoading = instancesLoading || tasksLoading\n const isSubmitting = startWorkflowMutation.isPending || completeTaskMutation.isPending\n\n // Don't render if no orderId\n if (!orderId) return null\n\n // Only show widget when order status is pending_approval\n const orderStatus = currentOrderStatus?.toLowerCase()\n if (orderStatus !== 'pending_approval') {\n return null\n }\n\n // Loading state\n if (isLoading) {\n return (\n <div className=\"flex items-center justify-center p-4\">\n <Spinner size=\"sm\" />\n <span className=\"ml-2 text-sm text-muted-foreground\">{t('common.loading', 'Loading...')}</span>\n </div>\n )\n }\n\n // Show workflow status badge\n const getStatusBadge = () => {\n if (!activeInstance) return null\n\n const statusVariants: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {\n RUNNING: 'default',\n PAUSED: 'secondary',\n WAITING_FOR_ACTIVITIES: 'secondary',\n COMPLETED: 'default',\n FAILED: 'destructive',\n CANCELLED: 'outline',\n }\n\n return (\n <Badge variant={statusVariants[activeInstance.status] || 'outline'}>\n {t(`workflows.instances.statuses.${activeInstance.status}`, activeInstance.status)}\n </Badge>\n )\n }\n\n return (\n <div className=\"space-y-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <div className=\"text-sm font-semibold text-foreground\">\n {t('workflows.orderApproval.groupLabel', 'Order Approval')}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t('workflows.orderApproval.groupDescription', 'Review and approve or reject this order')}\n </p>\n </div>\n {getStatusBadge()}\n </div>\n\n {error && (\n <div className=\"rounded-md border border-red-200 bg-red-50 p-3 text-xs text-red-800\">\n {error}\n </div>\n )}\n\n {/* No active workflow - show request approval button */}\n {!activeInstance && (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.orderApproval.noWorkflowActive', 'No approval workflow is active for this order.')}\n </p>\n <Button\n onClick={handleStartWorkflow}\n disabled={isSubmitting}\n variant=\"default\"\n size=\"sm\"\n >\n {isSubmitting && <Spinner size=\"sm\" className=\"mr-2\" />}\n {t('workflows.orderApproval.requestApproval', 'Request Approval')}\n </Button>\n </div>\n )}\n\n {/* Active workflow with pending task - show approve/reject UI */}\n {activeInstance && pendingTask && (\n <div className=\"space-y-3\" onKeyDown={handleKeyDown}>\n <div className=\"rounded-md border border-amber-200 bg-amber-50 p-3\">\n <p className=\"text-sm font-medium text-amber-800\">\n {t('workflows.orderApproval.pendingTitle', 'Pending Approval')}\n </p>\n <p className=\"text-xs text-amber-700 mt-1\">\n {t('workflows.orderApproval.pendingDescription', 'This order requires approval before processing.')}\n </p>\n </div>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {t('workflows.orderApproval.decisionLabel', 'Decision')}\n </label>\n <div className=\"flex gap-2\">\n <Button\n variant={decision === 'approve' ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => setDecision('approve')}\n className={decision === 'approve' ? 'bg-emerald-600 hover:bg-emerald-700' : ''}\n >\n {t('workflows.orderApproval.approveButton', 'Approve')}\n </Button>\n <Button\n variant={decision === 'reject' ? 'destructive' : 'outline'}\n size=\"sm\"\n onClick={() => setDecision('reject')}\n >\n {t('workflows.orderApproval.rejectButton', 'Reject')}\n </Button>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\">\n {t('workflows.orderApproval.commentsLabel', 'Comments')} <span className=\"text-muted-foreground font-normal\">({t('common.optional', 'optional')})</span>\n </label>\n <Textarea\n value={comments}\n onChange={(e) => setComments(e.target.value)}\n placeholder={t('workflows.orderApproval.commentsPlaceholder', 'Add optional comments...')}\n rows={2}\n />\n </div>\n\n <Button\n onClick={handleCompleteTask}\n disabled={!decision || isSubmitting}\n variant=\"default\"\n size=\"sm\"\n className=\"w-full\"\n >\n {isSubmitting && <Spinner size=\"sm\" className=\"mr-2\" />}\n {t('workflows.orderApproval.submitDecision', 'Submit Decision')}\n </Button>\n </div>\n )}\n\n {/* Active workflow but no pending task (processing) */}\n {activeInstance && !pendingTask && activeInstance.status !== 'COMPLETED' && (\n <div className=\"rounded-md border border-blue-200 bg-blue-50 p-3\">\n <p className=\"text-sm text-blue-800\">\n {t('workflows.orderApproval.processing', 'Workflow is processing...')}\n </p>\n </div>\n )}\n\n {/* Completed workflow */}\n {activeInstance && activeInstance.status === 'COMPLETED' && (\n <div className=\"rounded-md border border-emerald-200 bg-emerald-50 p-3\">\n <p className=\"text-sm text-emerald-800\">\n {t('workflows.orderApproval.completed', 'Approval workflow completed.')}\n </p>\n </div>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,15 @@
1
+ export const id = "id";
2
+ export const tenant_id = "tenant_id";
3
+ export const organization_id = "organization_id";
4
+ export const reserved_bytes = "reserved_bytes";
5
+ export const actual_bytes = "actual_bytes";
6
+ export const status = "status";
7
+ export const source = "source";
8
+ export const storage_driver = "storage_driver";
9
+ export const partition_code = "partition_code";
10
+ export const storage_path = "storage_path";
11
+ export const lease_token = "lease_token";
12
+ export const upload_token_hash = "upload_token_hash";
13
+ export const expires_at = "expires_at";
14
+ export const created_at = "created_at";
15
+ export const updated_at = "updated_at";
@@ -117,7 +117,8 @@ export const M = {
117
117
  },
118
118
  "attachments": {
119
119
  "attachment_partition": "attachments:attachment_partition",
120
- "attachment": "attachments:attachment"
120
+ "attachment": "attachments:attachment",
121
+ "attachment_quota_reservation": "attachments:attachment_quota_reservation"
121
122
  },
122
123
  "catalog": {
123
124
  "catalog_option_schema_template": "catalog:catalog_option_schema_template",
@@ -94,6 +94,23 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
94
94
  "created_at": "created_at",
95
95
  "updated_at": "updated_at"
96
96
  },
97
+ "attachment_quota_reservation": {
98
+ "id": "id",
99
+ "tenant_id": "tenant_id",
100
+ "organization_id": "organization_id",
101
+ "reserved_bytes": "reserved_bytes",
102
+ "actual_bytes": "actual_bytes",
103
+ "status": "status",
104
+ "source": "source",
105
+ "storage_driver": "storage_driver",
106
+ "partition_code": "partition_code",
107
+ "storage_path": "storage_path",
108
+ "lease_token": "lease_token",
109
+ "upload_token_hash": "upload_token_hash",
110
+ "expires_at": "expires_at",
111
+ "created_at": "created_at",
112
+ "updated_at": "updated_at"
113
+ },
97
114
  "business_rule": {
98
115
  "id": "id",
99
116
  "rule_id": "rule_id",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.7-develop.6851.1.c1680b00d8",
3
+ "version": "0.6.7-develop.6860.1.f796b22e80",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -254,16 +254,16 @@
254
254
  "zod": "^4.4.3"
255
255
  },
256
256
  "peerDependencies": {
257
- "@open-mercato/ai-assistant": "0.6.7-develop.6851.1.c1680b00d8",
258
- "@open-mercato/shared": "0.6.7-develop.6851.1.c1680b00d8",
259
- "@open-mercato/ui": "0.6.7-develop.6851.1.c1680b00d8",
257
+ "@open-mercato/ai-assistant": "0.6.7-develop.6860.1.f796b22e80",
258
+ "@open-mercato/shared": "0.6.7-develop.6860.1.f796b22e80",
259
+ "@open-mercato/ui": "0.6.7-develop.6860.1.f796b22e80",
260
260
  "react": "^19.0.0",
261
261
  "react-dom": "^19.0.0"
262
262
  },
263
263
  "devDependencies": {
264
- "@open-mercato/ai-assistant": "0.6.7-develop.6851.1.c1680b00d8",
265
- "@open-mercato/shared": "0.6.7-develop.6851.1.c1680b00d8",
266
- "@open-mercato/ui": "0.6.7-develop.6851.1.c1680b00d8",
264
+ "@open-mercato/ai-assistant": "0.6.7-develop.6860.1.f796b22e80",
265
+ "@open-mercato/shared": "0.6.7-develop.6860.1.f796b22e80",
266
+ "@open-mercato/ui": "0.6.7-develop.6860.1.f796b22e80",
267
267
  "@testing-library/dom": "^10.4.1",
268
268
  "@testing-library/jest-dom": "^7.0.0",
269
269
  "@testing-library/react": "^16.3.1",
package/src/bootstrap.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { AwilixContainer } from 'awilix'
2
2
  import { asValue } from 'awilix'
3
- import type { ModuleSubscriber } from '@open-mercato/shared/modules/registry'
3
+ import { getDefaultEncryptionMaps, type ModuleSubscriber } from '@open-mercato/shared/modules/registry'
4
+ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
4
5
  import { createEventBus } from '@open-mercato/events/index'
5
6
  import { setGlobalEventBus } from '@open-mercato/shared/modules/events'
6
7
  import { createCacheService } from '@open-mercato/cache'
@@ -184,12 +185,26 @@ export async function bootstrap(container: AwilixContainer) {
184
185
  // KMS + tenant encryption
185
186
  const kmsService = createKmsService()
186
187
  container.register({ kmsService: asValue(kmsService) })
188
+ let defaultEncryptionMaps: ModuleEncryptionMap[] = []
189
+ if (isTenantDataEncryptionEnabled()) {
190
+ try {
191
+ const { getModules } = await import('@open-mercato/shared/lib/i18n/server')
192
+ defaultEncryptionMaps = getDefaultEncryptionMaps(getModules())
193
+ } catch (err) {
194
+ logger.error('Failed to load default encryption maps', { component: 'encryption', err })
195
+ throw err
196
+ }
197
+ }
187
198
  try {
188
199
  const em = container.resolve('em') as EntityManager
189
200
  const cacheService = (() => {
190
201
  try { return container.resolve('cache') as any } catch { return null }
191
202
  })()
192
- const tenantEncryptionService = new TenantDataEncryptionService(em, { cache: cacheService, kms: kmsService })
203
+ const tenantEncryptionService = new TenantDataEncryptionService(em, {
204
+ cache: cacheService,
205
+ kms: kmsService,
206
+ defaultEncryptionMaps,
207
+ })
193
208
  container.register({ tenantEncryptionService: asValue(tenantEncryptionService) })
194
209
  if (isTenantDataEncryptionEnabled() && kmsService.isHealthy()) {
195
210
  try {
@@ -45,6 +45,7 @@ import {
45
45
  } from '../lib/upload-limits'
46
46
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
47
47
  import { createLogger } from '@open-mercato/shared/lib/logger'
48
+ import type { AttachmentQuotaService } from '../lib/quota-service'
48
49
 
49
50
  const logger = createLogger('attachments')
50
51
 
@@ -319,6 +320,20 @@ export async function POST(req: Request) {
319
320
  const container = await createRequestContainer()
320
321
  const em = container.resolve('em') as EntityManager
321
322
  const dataEngine = container.resolve('dataEngine')
323
+ let attachmentQuotaService: AttachmentQuotaService | null = null
324
+ let attachmentQuotaRecoveryScheduler: ((
325
+ payload: { reservationId: string; tenantId: string; organizationId: string },
326
+ delayMs: number,
327
+ ) => Promise<void>) | null = null
328
+ try {
329
+ attachmentQuotaService = container.resolve('attachmentQuotaService') as AttachmentQuotaService
330
+ attachmentQuotaRecoveryScheduler = container.resolve('attachmentQuotaRecoveryScheduler') as (
331
+ payload: { reservationId: string; tenantId: string; organizationId: string },
332
+ delayMs: number,
333
+ ) => Promise<void>
334
+ } catch {
335
+ // Legacy fallback below when the quota service is not registered in this container.
336
+ }
322
337
  const storageDriverFactory =
323
338
  (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
324
339
  const orgId = await resolveAttachmentOrganizationId(container, auth, req)
@@ -363,11 +378,20 @@ export async function POST(req: Request) {
363
378
  error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),
364
379
  }, { status: 413 })
365
380
  }
366
- const tenantUsageBytes = await readTenantAttachmentUsageBytes(em, tenantId)
367
- if (willExceedAttachmentTenantQuota(tenantUsageBytes, file.size)) {
368
- return NextResponse.json({
369
- error: t('attachments.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.'),
370
- }, { status: 413 })
381
+ if (!attachmentQuotaService) {
382
+ try {
383
+ const tenantUsageBytes = await readTenantAttachmentUsageBytes(em, tenantId)
384
+ if (willExceedAttachmentTenantQuota(tenantUsageBytes, file.size)) {
385
+ return NextResponse.json({
386
+ error: t('attachments.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.'),
387
+ }, { status: 413 })
388
+ }
389
+ } catch (error) {
390
+ logger.error('Attachment quota accounting failed', { err: error })
391
+ return NextResponse.json({
392
+ error: t('attachments.errors.quotaUnavailable', 'Storage quota accounting is unavailable.'),
393
+ }, { status: 500 })
394
+ }
371
395
  }
372
396
  const buf = Buffer.from(await file.arrayBuffer())
373
397
  const safeName = sanitizeUploadedFileName(file.name)
@@ -409,6 +433,62 @@ export async function POST(req: Request) {
409
433
  return NextResponse.json({ error: t('attachments.errors.publicPartitionBlocked', 'Public storage partitions cannot be selected explicitly for this upload.') }, { status: 403 })
410
434
  }
411
435
  const uploadDriver = await storageDriverFactory.resolveForPartition(partition.code, { tenantId, organizationId: orgId })
436
+ const preparedStoragePath = attachmentQuotaService
437
+ ? uploadDriver.prepareStoragePath?.({
438
+ partitionCode: partition.code,
439
+ orgId,
440
+ tenantId,
441
+ fileName: safeName,
442
+ })
443
+ : undefined
444
+ if (attachmentQuotaService && !preparedStoragePath) {
445
+ return NextResponse.json({
446
+ error: t('attachments.errors.quotaRecoveryUnsupported', 'Storage driver cannot participate in quota recovery.'),
447
+ }, { status: 500 })
448
+ }
449
+ let quotaReservation: { id: string; leaseToken: string; expiresAt: Date } | null = null
450
+ if (attachmentQuotaService) {
451
+ try {
452
+ quotaReservation = await attachmentQuotaService.reserve({
453
+ tenantId,
454
+ organizationId: orgId,
455
+ bytes: buf.length,
456
+ source: 'attachment',
457
+ storageDriver: uploadDriver.key,
458
+ storagePath: preparedStoragePath!,
459
+ partitionCode: partition.code,
460
+ })
461
+ if (!attachmentQuotaRecoveryScheduler) throw new Error('Attachment quota recovery is unavailable.')
462
+ await attachmentQuotaRecoveryScheduler({
463
+ reservationId: quotaReservation.id,
464
+ tenantId,
465
+ organizationId: orgId,
466
+ }, Math.max(1_000, quotaReservation.expiresAt.getTime() - Date.now()))
467
+ if (typeof attachmentQuotaService.beginStorage === 'function') {
468
+ await attachmentQuotaService.beginStorage(quotaReservation.id, quotaReservation.leaseToken)
469
+ }
470
+ } catch (error) {
471
+ if (quotaReservation) {
472
+ await attachmentQuotaService.release(quotaReservation.id, quotaReservation.leaseToken).catch(() => {})
473
+ quotaReservation = null
474
+ }
475
+ const code = (error as { code?: unknown })?.code
476
+ if (code === 'quota_exceeded') {
477
+ return NextResponse.json({
478
+ error: t('attachments.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.'),
479
+ }, { status: 413 })
480
+ }
481
+ if (code === 'quota_target_exists') {
482
+ return NextResponse.json({
483
+ error: t('attachments.errors.storagePathExists', 'The target storage path already exists.'),
484
+ }, { status: 409 })
485
+ }
486
+ logger.error('Attachment quota reservation failed', { err: error })
487
+ return NextResponse.json({
488
+ error: t('attachments.errors.quotaUnavailable', 'Storage quota accounting is unavailable.'),
489
+ }, { status: 500 })
490
+ }
491
+ }
412
492
  let storedPath: string
413
493
  try {
414
494
  const stored = await uploadDriver.store({
@@ -417,10 +497,25 @@ export async function POST(req: Request) {
417
497
  tenantId,
418
498
  fileName: safeName,
419
499
  buffer: buf,
500
+ storagePath: preparedStoragePath,
420
501
  })
421
502
  storedPath = stored.storagePath
503
+ if (quotaReservation) {
504
+ await attachmentQuotaService!.markStored(quotaReservation.id, quotaReservation.leaseToken)
505
+ }
422
506
  } catch (error) {
423
507
  logger.error('Failed to persist file', { err: error })
508
+ if (preparedStoragePath) {
509
+ try {
510
+ await (uploadDriver.deleteStrict?.(partition.code, preparedStoragePath)
511
+ ?? uploadDriver.delete(partition.code, preparedStoragePath))
512
+ if (quotaReservation) {
513
+ await attachmentQuotaService!.release(quotaReservation.id, quotaReservation.leaseToken)
514
+ }
515
+ } catch (cleanupError) {
516
+ logger.error('Failed to compensate attachment storage', { err: cleanupError })
517
+ }
518
+ }
424
519
  return NextResponse.json({ error: 'Failed to persist attachment.' }, { status: 500 })
425
520
  }
426
521
 
@@ -485,9 +580,21 @@ export async function POST(req: Request) {
485
580
  values: customFieldValues,
486
581
  })
487
582
  }
583
+ if (quotaReservation) {
584
+ await attachmentQuotaService!.completeAttachment(quotaReservation.id, quotaReservation.leaseToken, tx)
585
+ }
488
586
  })
489
587
  } catch (error) {
490
588
  logger.error('Failed to persist attachment with custom attributes', { err: error })
589
+ try {
590
+ await (uploadDriver.deleteStrict?.(partition.code, storedPath)
591
+ ?? uploadDriver.delete(partition.code, storedPath))
592
+ if (quotaReservation) {
593
+ await attachmentQuotaService!.release(quotaReservation.id, quotaReservation.leaseToken)
594
+ }
595
+ } catch (cleanupError) {
596
+ logger.error('Failed to compensate attachment persistence', { err: cleanupError })
597
+ }
491
598
  return NextResponse.json({ error: 'Failed to save attachment attributes.' }, { status: 500 })
492
599
  }
493
600
 
@@ -551,8 +658,8 @@ async function readTenantAttachmentUsageBytes(em: EntityManager, tenantId: strin
551
658
  return Number.isFinite(parsed) ? parsed : 0
552
659
  }
553
660
  return 0
554
- } catch {
555
- return 0
661
+ } catch (error) {
662
+ throw new Error('Attachment quota accounting is unavailable.', { cause: error })
556
663
  }
557
664
  }
558
665
 
@@ -99,3 +99,62 @@ export class Attachment {
99
99
  @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
100
100
  createdAt: Date = new Date()
101
101
  }
102
+
103
+ @Entity({ tableName: 'attachment_quota_reservations' })
104
+ @Unique({
105
+ name: 'attachment_quota_reservations_scope_path_unique',
106
+ properties: ['tenantId', 'storageDriver', 'storagePath'],
107
+ })
108
+ @Index({
109
+ name: 'attachment_quota_reservations_tenant_status_idx',
110
+ properties: ['tenantId', 'status'],
111
+ })
112
+ export class AttachmentQuotaReservation {
113
+ [OptionalProps]?: 'actualBytes' | 'createdAt' | 'status' | 'updatedAt' | 'uploadTokenHash'
114
+
115
+ @PrimaryKey({ type: 'uuid' })
116
+ id!: string
117
+
118
+ @Property({ name: 'tenant_id', type: 'uuid' })
119
+ tenantId!: string
120
+
121
+ @Property({ name: 'organization_id', type: 'uuid' })
122
+ organizationId!: string
123
+
124
+ @Property({ name: 'reserved_bytes', type: 'bigint' })
125
+ reservedBytes!: number
126
+
127
+ @Property({ name: 'actual_bytes', type: 'bigint', nullable: true })
128
+ actualBytes?: number | null
129
+
130
+ @Property({ type: 'text', default: 'reserved' })
131
+ status: 'reserved' | 'storing' | 'stored' | 'recovering' | 'committed' = 'reserved'
132
+
133
+ @Property({ type: 'text' })
134
+ source!: string
135
+
136
+ @Property({ name: 'storage_driver', type: 'text' })
137
+ storageDriver!: string
138
+
139
+ @Property({ name: 'partition_code', type: 'text', nullable: true })
140
+ partitionCode?: string | null
141
+
142
+ @Property({ name: 'storage_path', type: 'text' })
143
+ storagePath!: string
144
+
145
+ @Property({ name: 'lease_token', type: 'uuid' })
146
+ leaseToken!: string
147
+
148
+ @Property({ name: 'upload_token_hash', type: 'text', nullable: true })
149
+ uploadTokenHash?: string | null
150
+
151
+ @Property({ name: 'expires_at', type: Date, nullable: true })
152
+ @Index({ name: 'attachment_quota_reservations_expires_idx' })
153
+ expiresAt?: Date | null
154
+
155
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
156
+ createdAt: Date = new Date()
157
+
158
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
159
+ updatedAt: Date = new Date()
160
+ }
@@ -1,9 +1,17 @@
1
- import { asFunction } from 'awilix'
1
+ import { asFunction, asValue } from 'awilix'
2
2
  import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
3
  import { StorageDriverFactory } from './lib/drivers/driverFactory'
4
+ import { createAttachmentQuotaService } from './lib/quota-service'
5
+ import { scheduleAttachmentQuotaRecovery } from './lib/quota-recovery-queue'
4
6
 
5
7
  export function register(container: AppContainer) {
6
8
  container.register({
9
+ attachmentQuotaRecoveryScheduler: asValue(scheduleAttachmentQuotaRecovery),
10
+ attachmentQuotaService: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>
11
+ createAttachmentQuotaService(em),
12
+ )
13
+ .scoped()
14
+ .proxy(),
7
15
  storageDriverFactory: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>
8
16
  new StorageDriverFactory(em),
9
17
  )
@@ -4,7 +4,10 @@
4
4
  "attachments.errors.maxUploadSize": "Der Anhang überschreitet die maximal zulässige Upload-Größe.",
5
5
  "attachments.errors.publicPartitionBlocked": "Öffentliche Speicherpartitionen können für diesen Upload nicht explizit ausgewählt werden.",
6
6
  "attachments.errors.quotaExceeded": "Das Speicherlimit für Anhänge dieses Tenants wurde überschritten.",
7
+ "attachments.errors.quotaRecoveryUnsupported": "Der Speichertreiber kann nicht an der Kontingent-Wiederherstellung teilnehmen.",
8
+ "attachments.errors.quotaUnavailable": "Die Speicherkontingent-Abrechnung ist nicht verfügbar.",
7
9
  "attachments.errors.selectOrganization": "Wählen Sie eine bestimmte Organisation aus, bevor Sie einen Anhang hochladen.",
10
+ "attachments.errors.storagePathExists": "Der Zielspeicherpfad existiert bereits.",
8
11
  "attachments.library.actions.copied": "Link kopiert.",
9
12
  "attachments.library.actions.copyError": "Link konnte nicht kopiert werden.",
10
13
  "attachments.library.actions.copyUrl": "URL kopieren",
@@ -4,7 +4,10 @@
4
4
  "attachments.errors.maxUploadSize": "Attachment exceeds the maximum upload size.",
5
5
  "attachments.errors.publicPartitionBlocked": "Public storage partitions cannot be selected explicitly for this upload.",
6
6
  "attachments.errors.quotaExceeded": "Attachment storage quota exceeded for this tenant.",
7
+ "attachments.errors.quotaRecoveryUnsupported": "Storage driver cannot participate in quota recovery.",
8
+ "attachments.errors.quotaUnavailable": "Storage quota accounting is unavailable.",
7
9
  "attachments.errors.selectOrganization": "Select a specific organization before uploading an attachment.",
10
+ "attachments.errors.storagePathExists": "The target storage path already exists.",
8
11
  "attachments.library.actions.copied": "Link copied.",
9
12
  "attachments.library.actions.copyError": "Unable to copy link.",
10
13
  "attachments.library.actions.copyUrl": "Copy URL",
@@ -4,7 +4,10 @@
4
4
  "attachments.errors.maxUploadSize": "El archivo adjunto supera el tamaño máximo de carga permitido.",
5
5
  "attachments.errors.publicPartitionBlocked": "No se pueden seleccionar explícitamente particiones de almacenamiento públicas para esta carga.",
6
6
  "attachments.errors.quotaExceeded": "Se superó la cuota de almacenamiento de adjuntos para este tenant.",
7
+ "attachments.errors.quotaRecoveryUnsupported": "El controlador de almacenamiento no puede participar en la recuperación de cuota.",
8
+ "attachments.errors.quotaUnavailable": "La contabilidad de la cuota de almacenamiento no está disponible.",
7
9
  "attachments.errors.selectOrganization": "Selecciona una organización específica antes de subir un adjunto.",
10
+ "attachments.errors.storagePathExists": "La ruta de almacenamiento de destino ya existe.",
8
11
  "attachments.library.actions.copied": "Enlace copiado.",
9
12
  "attachments.library.actions.copyError": "No se pudo copiar el enlace.",
10
13
  "attachments.library.actions.copyUrl": "Copiar URL",
@@ -4,7 +4,10 @@
4
4
  "attachments.errors.maxUploadSize": "첨부 파일이 최대 업로드 크기를 초과합니다.",
5
5
  "attachments.errors.publicPartitionBlocked": "이 업로드에는 공개 저장소 파티션을 명시적으로 선택할 수 없습니다.",
6
6
  "attachments.errors.quotaExceeded": "이 테넌트의 첨부 파일 저장소 할당량을 초과했습니다.",
7
+ "attachments.errors.quotaRecoveryUnsupported": "저장 용량 복구가 지원되지 않습니다.",
8
+ "attachments.errors.quotaUnavailable": "첨부파일 저장 용량 정보를 확인할 수 없습니다.",
7
9
  "attachments.errors.selectOrganization": "첨부파일을 업로드하기 전에 특정 조직을 선택하세요.",
10
+ "attachments.errors.storagePathExists": "대상 저장 경로가 이미 존재합니다.",
8
11
  "attachments.library.actions.copied": "링크가 복사되었습니다.",
9
12
  "attachments.library.actions.copyError": "링크를 복사할 수 없습니다.",
10
13
  "attachments.library.actions.copyUrl": "URL 복사",
@@ -4,7 +4,10 @@
4
4
  "attachments.errors.maxUploadSize": "Załącznik przekracza maksymalny dopuszczalny rozmiar przesyłania.",
5
5
  "attachments.errors.publicPartitionBlocked": "Nie można jawnie wybrać publicznej partycji do tego przesyłania.",
6
6
  "attachments.errors.quotaExceeded": "Przekroczono limit miejsca na załączniki dla tego tenantu.",
7
+ "attachments.errors.quotaRecoveryUnsupported": "Sterownik przechowywania nie może uczestniczyć w odzyskiwaniu limitu.",
8
+ "attachments.errors.quotaUnavailable": "Rozliczanie limitu przestrzeni dyskowej jest niedostępne.",
7
9
  "attachments.errors.selectOrganization": "Wybierz konkretną organizację przed przesłaniem załącznika.",
10
+ "attachments.errors.storagePathExists": "Docelowa ścieżka przechowywania już istnieje.",
8
11
  "attachments.library.actions.copied": "Link skopiowany.",
9
12
  "attachments.library.actions.copyError": "Nie można skopiować linku.",
10
13
  "attachments.library.actions.copyUrl": "Kopiuj URL",
@@ -1,4 +1,4 @@
1
- export type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'
1
+ export type { PrepareFilePayload, StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'
2
2
  export { LocalStorageDriver } from './localDriver'
3
3
  export { LegacyPublicStorageDriver } from './legacyPublicDriver'
4
4
  export { StorageDriverFactory, registerExternalStorageDriver, registerExternalCredentialEnhancer } from './driverFactory'