@open-mercato/core 0.6.2-develop.3467.1.2a1818709d → 0.6.2

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 (158) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/queue-runner.js +15 -3
  3. package/dist/helpers/integration/queue-runner.js.map +2 -2
  4. package/dist/helpers/integration/queue.js +1 -1
  5. package/dist/helpers/integration/queue.js.map +2 -2
  6. package/dist/helpers/integration/salesUi.js +24 -1
  7. package/dist/helpers/integration/salesUi.js.map +2 -2
  8. package/dist/modules/auth/api/roles/acl/route.js +37 -1
  9. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  10. package/dist/modules/auth/api/roles/route.js +36 -2
  11. package/dist/modules/auth/api/roles/route.js.map +2 -2
  12. package/dist/modules/auth/api/users/acl/route.js +36 -1
  13. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  14. package/dist/modules/auth/api/users/route.js +101 -13
  15. package/dist/modules/auth/api/users/route.js.map +2 -2
  16. package/dist/modules/auth/backend/roles/[id]/edit/page.js +32 -14
  17. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  18. package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
  19. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  20. package/dist/modules/auth/backend/users/create/page.js +1 -1
  21. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  22. package/dist/modules/auth/backend/users/roleOptions.js +1 -1
  23. package/dist/modules/auth/backend/users/roleOptions.js.map +2 -2
  24. package/dist/modules/auth/commands/roles.js +8 -0
  25. package/dist/modules/auth/commands/roles.js.map +2 -2
  26. package/dist/modules/auth/commands/users.js +3 -0
  27. package/dist/modules/auth/commands/users.js.map +2 -2
  28. package/dist/modules/auth/components/AclEditor.js +53 -26
  29. package/dist/modules/auth/components/AclEditor.js.map +2 -2
  30. package/dist/modules/auth/lib/grantChecks.js +97 -2
  31. package/dist/modules/auth/lib/grantChecks.js.map +2 -2
  32. package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
  33. package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
  34. package/dist/modules/customers/ai-agents.js +10 -1
  35. package/dist/modules/customers/ai-agents.js.map +2 -2
  36. package/dist/modules/customers/ai-tools/settings-pack.js +55 -1
  37. package/dist/modules/customers/ai-tools/settings-pack.js.map +2 -2
  38. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +13 -5
  39. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  40. package/dist/modules/customers/lib/personCompanyLinkTable.js +4 -30
  41. package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
  42. package/dist/modules/customers/migrations/Migration20260521175027.js +12 -0
  43. package/dist/modules/customers/migrations/Migration20260521175027.js.map +7 -0
  44. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +41 -12
  45. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  46. package/dist/modules/data_sync/api/options.js +2 -0
  47. package/dist/modules/data_sync/api/options.js.map +2 -2
  48. package/dist/modules/data_sync/api/run.js +77 -58
  49. package/dist/modules/data_sync/api/run.js.map +2 -2
  50. package/dist/modules/data_sync/backend/data-sync/page.js +3 -2
  51. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  52. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +8 -3
  53. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  54. package/dist/modules/integrations/backend/integrations/[id]/page.js +15 -13
  55. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  56. package/dist/modules/integrations/data/validators.js +9 -6
  57. package/dist/modules/integrations/data/validators.js.map +2 -2
  58. package/dist/modules/messages/api/route.js +2 -2
  59. package/dist/modules/messages/api/route.js.map +2 -2
  60. package/dist/modules/messages/commands/messages.js +42 -2
  61. package/dist/modules/messages/commands/messages.js.map +2 -2
  62. package/dist/modules/messages/components/MessagesInboxPageClient.js +66 -87
  63. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  64. package/dist/modules/messages/components/inboxFilters.js +133 -0
  65. package/dist/modules/messages/components/inboxFilters.js.map +7 -0
  66. package/dist/modules/messages/data/validators.js +8 -6
  67. package/dist/modules/messages/data/validators.js.map +2 -2
  68. package/dist/modules/progress/api/jobs/route.js +26 -16
  69. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  70. package/dist/modules/query_index/di.js +8 -6
  71. package/dist/modules/query_index/di.js.map +2 -2
  72. package/dist/modules/query_index/lib/indexer.js +12 -10
  73. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  74. package/dist/modules/query_index/subscribers/upsert_one.js +7 -1
  75. package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
  76. package/dist/modules/sales/api/order-adjustments/route.js.map +2 -2
  77. package/dist/modules/sales/api/quote-adjustments/route.js.map +2 -2
  78. package/dist/modules/sales/commands/documents.js +104 -0
  79. package/dist/modules/sales/commands/documents.js.map +2 -2
  80. package/dist/modules/sales/data/validators.js +20 -1
  81. package/dist/modules/sales/data/validators.js.map +2 -2
  82. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
  83. package/dist/modules/sync_excel/api/import/route.js +126 -116
  84. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  85. package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
  86. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  87. package/dist/modules/translations/components/TranslationManager.js +9 -4
  88. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  89. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  90. package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
  91. package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
  92. package/jest.config.cjs +4 -1
  93. package/package.json +15 -16
  94. package/src/helpers/integration/queue-runner.ts +21 -2
  95. package/src/helpers/integration/queue.ts +1 -1
  96. package/src/helpers/integration/salesUi.ts +26 -1
  97. package/src/modules/auth/api/roles/acl/route.ts +39 -1
  98. package/src/modules/auth/api/roles/route.ts +38 -2
  99. package/src/modules/auth/api/users/acl/route.ts +41 -1
  100. package/src/modules/auth/api/users/route.ts +105 -12
  101. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +35 -13
  102. package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
  103. package/src/modules/auth/backend/users/create/page.tsx +1 -1
  104. package/src/modules/auth/backend/users/roleOptions.ts +2 -1
  105. package/src/modules/auth/commands/roles.ts +8 -0
  106. package/src/modules/auth/commands/users.ts +3 -0
  107. package/src/modules/auth/components/AclEditor.tsx +50 -24
  108. package/src/modules/auth/lib/grantChecks.ts +116 -2
  109. package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
  110. package/src/modules/customers/ai-agents.ts +10 -1
  111. package/src/modules/customers/ai-tools/settings-pack.ts +62 -1
  112. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +18 -7
  113. package/src/modules/customers/lib/personCompanyLinkTable.ts +33 -41
  114. package/src/modules/customers/migrations/Migration20260521175027.ts +39 -0
  115. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +38 -12
  116. package/src/modules/data_sync/api/options.ts +2 -0
  117. package/src/modules/data_sync/api/run.ts +80 -61
  118. package/src/modules/data_sync/backend/data-sync/page.tsx +10 -0
  119. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +17 -3
  120. package/src/modules/data_sync/i18n/de.json +3 -0
  121. package/src/modules/data_sync/i18n/en.json +3 -0
  122. package/src/modules/data_sync/i18n/es.json +3 -0
  123. package/src/modules/data_sync/i18n/pl.json +3 -0
  124. package/src/modules/data_sync/lib/adapter.ts +9 -0
  125. package/src/modules/integrations/backend/integrations/[id]/page.tsx +15 -13
  126. package/src/modules/integrations/data/validators.ts +5 -4
  127. package/src/modules/messages/api/route.ts +2 -2
  128. package/src/modules/messages/commands/messages.ts +54 -2
  129. package/src/modules/messages/components/MessagesInboxPageClient.tsx +75 -90
  130. package/src/modules/messages/components/inboxFilters.ts +162 -0
  131. package/src/modules/messages/data/validators.ts +7 -5
  132. package/src/modules/messages/i18n/de.json +5 -4
  133. package/src/modules/messages/i18n/en.json +5 -4
  134. package/src/modules/messages/i18n/es.json +5 -4
  135. package/src/modules/messages/i18n/pl.json +5 -4
  136. package/src/modules/progress/api/jobs/route.ts +30 -20
  137. package/src/modules/query_index/di.ts +9 -7
  138. package/src/modules/query_index/lib/indexer.ts +13 -11
  139. package/src/modules/query_index/subscribers/upsert_one.ts +9 -1
  140. package/src/modules/sales/api/order-adjustments/route.ts +2 -1
  141. package/src/modules/sales/api/quote-adjustments/route.ts +2 -1
  142. package/src/modules/sales/commands/documents.ts +116 -0
  143. package/src/modules/sales/data/validators.ts +38 -0
  144. package/src/modules/sales/i18n/de.json +2 -0
  145. package/src/modules/sales/i18n/en.json +2 -0
  146. package/src/modules/sales/i18n/es.json +2 -0
  147. package/src/modules/sales/i18n/pl.json +2 -0
  148. package/src/modules/sales/lib/makeSalesLineRoute.ts +2 -1
  149. package/src/modules/staff/i18n/de.json +1 -0
  150. package/src/modules/staff/i18n/en.json +1 -0
  151. package/src/modules/staff/i18n/es.json +1 -0
  152. package/src/modules/staff/i18n/pl.json +1 -0
  153. package/src/modules/sync_excel/api/import/route.ts +141 -131
  154. package/src/modules/sync_excel/lib/adapters/customers.ts +1 -0
  155. package/src/modules/translations/components/TranslationManager.tsx +9 -4
  156. package/src/modules/workflows/lib/activity-executor.ts +4 -3
  157. package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
  158. package/tsconfig.json +1 -0
@@ -0,0 +1,162 @@
1
+ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
2
+
3
+ type Translator = (key: string, fallback?: string) => string
4
+
5
+ export type SenderOption = {
6
+ value: string
7
+ label: string
8
+ description?: string | null
9
+ }
10
+
11
+ export type MessagesInboxFilterContext = {
12
+ t: Translator
13
+ typeOptions: { value: string; label: string }[]
14
+ senderOptions: SenderOption[]
15
+ loadSenderOptions: (query?: string) => Promise<SenderOption[]>
16
+ }
17
+
18
+ export function buildMessagesInboxFilters({
19
+ t,
20
+ typeOptions,
21
+ senderOptions,
22
+ loadSenderOptions,
23
+ }: MessagesInboxFilterContext): FilterDef[] {
24
+ const senderLabel = (val: string): string => {
25
+ const match = senderOptions.find((opt) => opt.value === val)
26
+ return match?.label ?? val
27
+ }
28
+
29
+ return [
30
+ {
31
+ id: 'status',
32
+ label: t('messages.filters.status', 'Status'),
33
+ type: 'select',
34
+ options: [
35
+ { value: '', label: t('messages.filters.all', 'All') },
36
+ { value: 'unread', label: t('messages.status.unread', 'Unread') },
37
+ { value: 'read', label: t('messages.status.read', 'Read') },
38
+ { value: 'archived', label: t('messages.status.archived', 'Archived') },
39
+ ],
40
+ },
41
+ {
42
+ id: 'type',
43
+ label: t('messages.filters.type', 'Type'),
44
+ type: 'select',
45
+ options: [{ value: '', label: t('messages.filters.all', 'All') }, ...typeOptions],
46
+ },
47
+ {
48
+ id: 'hasObjects',
49
+ label: t('messages.filters.hasObjects', 'Has objects'),
50
+ type: 'select',
51
+ options: [
52
+ { value: '', label: t('messages.filters.all', 'All') },
53
+ { value: 'true', label: t('common.yes', 'Yes') },
54
+ { value: 'false', label: t('common.no', 'No') },
55
+ ],
56
+ },
57
+ {
58
+ id: 'hasAttachments',
59
+ label: t('messages.filters.hasAttachments', 'Has attachments'),
60
+ type: 'select',
61
+ options: [
62
+ { value: '', label: t('messages.filters.all', 'All') },
63
+ { value: 'true', label: t('common.yes', 'Yes') },
64
+ { value: 'false', label: t('common.no', 'No') },
65
+ ],
66
+ },
67
+ {
68
+ id: 'hasActions',
69
+ label: t('messages.filters.hasActions', 'Has actions'),
70
+ type: 'select',
71
+ options: [
72
+ { value: '', label: t('messages.filters.all', 'All') },
73
+ { value: 'true', label: t('common.yes', 'Yes') },
74
+ { value: 'false', label: t('common.no', 'No') },
75
+ ],
76
+ },
77
+ {
78
+ id: 'senderId',
79
+ label: t('messages.filters.sender', 'Sender'),
80
+ type: 'combobox',
81
+ placeholder: t('messages.filters.senderEmpty', 'Type to search users'),
82
+ options: senderOptions,
83
+ loadOptions: loadSenderOptions,
84
+ formatValue: senderLabel,
85
+ formatDescription: (val: string) =>
86
+ senderOptions.find((opt) => opt.value === val)?.description ?? null,
87
+ },
88
+ {
89
+ id: 'since',
90
+ label: t('messages.filters.since', 'Sent after'),
91
+ type: 'text',
92
+ placeholder: t('messages.filters.sincePlaceholder', 'YYYY-MM-DD'),
93
+ },
94
+ ]
95
+ }
96
+
97
+ const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/
98
+
99
+ export function normalizeMessagesSinceValue(input: string): string | null {
100
+ const trimmed = input.trim()
101
+ if (!trimmed) return null
102
+ if (DATE_ONLY_PATTERN.test(trimmed)) {
103
+ const parsed = new Date(`${trimmed}T00:00:00.000Z`)
104
+ if (Number.isNaN(parsed.getTime())) return null
105
+ if (parsed.toISOString().slice(0, 10) !== trimmed) return null
106
+ return parsed.toISOString()
107
+ }
108
+ const parsed = new Date(trimmed)
109
+ if (Number.isNaN(parsed.getTime())) return null
110
+ return parsed.toISOString()
111
+ }
112
+
113
+ export type BuildMessagesListParamsOptions = {
114
+ folder: string
115
+ page: number
116
+ pageSize: number
117
+ search: string
118
+ filterValues: FilterValues
119
+ }
120
+
121
+ export function buildMessagesListParams({
122
+ folder,
123
+ page,
124
+ pageSize,
125
+ search,
126
+ filterValues,
127
+ }: BuildMessagesListParamsOptions): URLSearchParams {
128
+ const params = new URLSearchParams()
129
+ params.set('folder', folder)
130
+ params.set('page', String(page))
131
+ params.set('pageSize', String(pageSize))
132
+
133
+ if (search.trim()) {
134
+ params.set('search', search.trim())
135
+ }
136
+
137
+ const stringValue = (id: string): string => {
138
+ const raw = filterValues[id]
139
+ return typeof raw === 'string' ? raw.trim() : ''
140
+ }
141
+
142
+ const status = stringValue('status')
143
+ const type = stringValue('type')
144
+ const hasObjects = stringValue('hasObjects')
145
+ const hasAttachments = stringValue('hasAttachments')
146
+ const hasActions = stringValue('hasActions')
147
+ const senderId = stringValue('senderId')
148
+ const sinceRaw = stringValue('since')
149
+
150
+ if (status) params.set('status', status)
151
+ if (type) params.set('type', type)
152
+ if (hasObjects) params.set('hasObjects', hasObjects)
153
+ if (hasAttachments) params.set('hasAttachments', hasAttachments)
154
+ if (hasActions) params.set('hasActions', hasActions)
155
+ if (senderId) params.set('senderId', senderId)
156
+ if (sinceRaw) {
157
+ const normalized = normalizeMessagesSinceValue(sinceRaw)
158
+ if (normalized) params.set('since', normalized)
159
+ }
160
+
161
+ return params
162
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import { parseBooleanFlag } from '@open-mercato/shared/lib/boolean'
2
3
 
3
4
  function collectDuplicateRecipientIds(
4
5
  recipients: Array<{ userId: string }>,
@@ -178,14 +179,15 @@ export const updateDraftSchema = z.object({
178
179
  externalEmail: z.string().email().optional(),
179
180
  externalName: z.string().min(1).max(255).optional(),
180
181
  recipients: z.array(messageRecipientSchema).optional(),
181
- subject: z.string().min(1).max(500).optional(),
182
- body: z.string().min(1).max(50000).optional(),
182
+ subject: z.string().max(500).optional(),
183
+ body: z.string().max(50000).optional(),
183
184
  bodyFormat: z.enum(['text', 'markdown']).optional(),
184
185
  priority: z.enum(['low', 'normal', 'high', 'urgent']).optional(),
185
186
  objects: z.array(messageObjectSchema).optional(),
186
187
  attachmentIds: z.array(z.string().uuid()).optional(),
187
188
  actionData: messageActionDataSchema.optional(),
188
189
  sendViaEmail: z.boolean().optional(),
190
+ isDraft: z.literal(false).optional(),
189
191
  }).superRefine((value, ctx) => {
190
192
  if (value.recipients) {
191
193
  const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)
@@ -209,9 +211,9 @@ export const listMessagesSchema = z.object({
209
211
  sourceEntityType: z.string().optional(),
210
212
  sourceEntityId: z.string().uuid().optional(),
211
213
  externalEmail: z.string().email().optional(),
212
- hasObjects: z.coerce.boolean().optional(),
213
- hasAttachments: z.coerce.boolean().optional(),
214
- hasActions: z.coerce.boolean().optional(),
214
+ hasObjects: z.string().transform(parseBooleanFlag).optional(),
215
+ hasAttachments: z.string().transform(parseBooleanFlag).optional(),
216
+ hasActions: z.string().transform(parseBooleanFlag).optional(),
215
217
  senderId: z.string().uuid().optional(),
216
218
  search: z.string().max(200).optional(),
217
219
  since: z.string().datetime().optional(),
@@ -131,12 +131,13 @@
131
131
  "messages.externalName": "Externer Name",
132
132
  "messages.fields.type": "Nachrichtentyp",
133
133
  "messages.filters.all": "Alle",
134
- "messages.filters.hasActions": "Aktionen",
135
- "messages.filters.hasAttachments": "Anhänge",
136
- "messages.filters.hasObjects": "Objekte",
134
+ "messages.filters.hasActions": "Hat Aktionen",
135
+ "messages.filters.hasAttachments": "Hat Anhänge",
136
+ "messages.filters.hasObjects": "Hat Objekte",
137
137
  "messages.filters.sender": "Absender",
138
+ "messages.filters.senderEmpty": "Tippe, um Benutzer zu suchen",
138
139
  "messages.filters.since": "Gesendet nach",
139
- "messages.filters.sincePlaceholder": "YYYY-MM-DDTHH:mm:ssZ",
140
+ "messages.filters.sincePlaceholder": "YYYY-MM-DD",
140
141
  "messages.filters.status": "Status",
141
142
  "messages.filters.type": "Typ",
142
143
  "messages.flash.actionSuccess": "Aktion abgeschlossen.",
@@ -131,12 +131,13 @@
131
131
  "messages.externalName": "External name",
132
132
  "messages.fields.type": "Message type",
133
133
  "messages.filters.all": "All",
134
- "messages.filters.hasActions": "Actions",
135
- "messages.filters.hasAttachments": "Attachments",
136
- "messages.filters.hasObjects": "Objects",
134
+ "messages.filters.hasActions": "Has actions",
135
+ "messages.filters.hasAttachments": "Has attachments",
136
+ "messages.filters.hasObjects": "Has objects",
137
137
  "messages.filters.sender": "Sender",
138
+ "messages.filters.senderEmpty": "Type to search users",
138
139
  "messages.filters.since": "Sent after",
139
- "messages.filters.sincePlaceholder": "YYYY-MM-DDTHH:mm:ssZ",
140
+ "messages.filters.sincePlaceholder": "YYYY-MM-DD",
140
141
  "messages.filters.status": "Status",
141
142
  "messages.filters.type": "Type",
142
143
  "messages.flash.actionSuccess": "Action completed.",
@@ -131,12 +131,13 @@
131
131
  "messages.externalName": "Nombre externo",
132
132
  "messages.fields.type": "Tipo de mensaje",
133
133
  "messages.filters.all": "Todos",
134
- "messages.filters.hasActions": "Acciones",
135
- "messages.filters.hasAttachments": "Adjuntos",
136
- "messages.filters.hasObjects": "Objetos",
134
+ "messages.filters.hasActions": "Tiene acciones",
135
+ "messages.filters.hasAttachments": "Tiene adjuntos",
136
+ "messages.filters.hasObjects": "Tiene objetos",
137
137
  "messages.filters.sender": "Remitente",
138
+ "messages.filters.senderEmpty": "Escribe para buscar usuarios",
138
139
  "messages.filters.since": "Enviado después de",
139
- "messages.filters.sincePlaceholder": "YYYY-MM-DDTHH:mm:ssZ",
140
+ "messages.filters.sincePlaceholder": "YYYY-MM-DD",
140
141
  "messages.filters.status": "Estado",
141
142
  "messages.filters.type": "Tipo",
142
143
  "messages.flash.actionSuccess": "Acción completada.",
@@ -131,12 +131,13 @@
131
131
  "messages.externalName": "Nazwa zewnętrzna",
132
132
  "messages.fields.type": "Typ wiadomości",
133
133
  "messages.filters.all": "Wszystkie",
134
- "messages.filters.hasActions": "Akcje",
135
- "messages.filters.hasAttachments": "Załączniki",
136
- "messages.filters.hasObjects": "Obiekty",
134
+ "messages.filters.hasActions": "Ma akcje",
135
+ "messages.filters.hasAttachments": "Ma załączniki",
136
+ "messages.filters.hasObjects": "Ma obiekty",
137
137
  "messages.filters.sender": "Nadawca",
138
+ "messages.filters.senderEmpty": "Wpisz, aby wyszukać użytkowników",
138
139
  "messages.filters.since": "Wysłane po",
139
- "messages.filters.sincePlaceholder": "YYYY-MM-DDTHH:mm:ssZ",
140
+ "messages.filters.sincePlaceholder": "RRRR-MM-DD",
140
141
  "messages.filters.status": "Status",
141
142
  "messages.filters.type": "Typ",
142
143
  "messages.flash.actionSuccess": "Akcja wykonana.",
@@ -138,27 +138,37 @@ export async function GET(req: Request) {
138
138
  }
139
139
 
140
140
  export async function POST(req: Request) {
141
- const auth = await getAuthFromRequest(req)
142
- if (!auth || !auth.tenantId) {
143
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
144
- }
145
-
146
- const body = await req.json()
147
- const parsed = createProgressJobSchema.safeParse(body)
148
- if (!parsed.success) {
149
- return NextResponse.json({ error: 'Invalid input', details: parsed.error.flatten() }, { status: 400 })
141
+ try {
142
+ const auth = await getAuthFromRequest(req)
143
+ if (!auth || !auth.tenantId) {
144
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
145
+ }
146
+
147
+ const body = await req.json()
148
+ const parsed = createProgressJobSchema.safeParse(body)
149
+ if (!parsed.success) {
150
+ return NextResponse.json({ error: 'Invalid input', details: parsed.error.flatten() }, { status: 400 })
151
+ }
152
+
153
+ const container = await createRequestContainer()
154
+ const progressService = container.resolve('progressService') as import('../../lib/progressService').ProgressService
155
+
156
+ const job = await progressService.createJob(parsed.data, {
157
+ tenantId: auth.tenantId,
158
+ organizationId: auth.orgId,
159
+ userId: auth.sub,
160
+ })
161
+
162
+ return NextResponse.json({ id: job.id }, { status: 201 })
163
+ } catch (error) {
164
+ const message = error instanceof Error ? error.message : String(error)
165
+ const stack = error instanceof Error ? error.stack : undefined
166
+ console.error('[progress.jobs.create] unhandled error', { message, stack })
167
+ return NextResponse.json(
168
+ { error: 'Failed to create progress job.', message, stack },
169
+ { status: 500 },
170
+ )
150
171
  }
151
-
152
- const container = await createRequestContainer()
153
- const progressService = container.resolve('progressService') as import('../../lib/progressService').ProgressService
154
-
155
- const job = await progressService.createJob(parsed.data, {
156
- tenantId: auth.tenantId,
157
- organizationId: auth.orgId,
158
- userId: auth.sub,
159
- })
160
-
161
- return NextResponse.json({ id: job.id }, { status: 201 })
162
172
  }
163
173
 
164
174
  const jobListItemSchema = z.object({
@@ -66,8 +66,8 @@ export function register(container: AppContainer) {
66
66
  const makeUpsertHandler = (entityType: string) => async (payload: any, ctx: any) => {
67
67
  try {
68
68
  const em = ctx.resolve('em')
69
- let orgId = payload?.organizationId || payload?.orgId || null
70
- let tenantId = payload?.tenantId || null
69
+ let orgId = payload?.organizationId || payload?.orgId || ctx?.organizationId || null
70
+ let tenantId = payload?.tenantId || ctx?.tenantId || null
71
71
  const id = String(payload?.id || payload?.recordId || '')
72
72
  if (!id) return
73
73
  if (!orgId || !tenantId) {
@@ -119,24 +119,26 @@ export function register(container: AppContainer) {
119
119
  const makeDeleteHandler = (entityType: string) => async (payload: any, ctx: any) => {
120
120
  try {
121
121
  const em = ctx.resolve('em')
122
- let orgId = payload?.organizationId || payload?.orgId || null
122
+ let orgId = payload?.organizationId || payload?.orgId || ctx?.organizationId || null
123
+ let tenantId = payload?.tenantId || ctx?.tenantId || null
123
124
  const id = String(payload?.id || payload?.recordId || '')
124
125
  if (!id) return
125
- if (!orgId) {
126
+ if (!orgId || !tenantId) {
126
127
  try {
127
128
  const db = (em as any).getKysely()
128
129
  const table = resolveEntityTableName(em, entityType)
129
130
  const row = await db
130
131
  .selectFrom(table as any)
131
- .select(['organization_id' as any])
132
+ .select(['organization_id' as any, 'tenant_id' as any])
132
133
  .where('id' as any, '=', id)
133
- .executeTakeFirst() as { organization_id: string | null } | undefined
134
+ .executeTakeFirst() as { organization_id: string | null; tenant_id: string | null } | undefined
134
135
  orgId = row?.organization_id ?? orgId
136
+ tenantId = row?.tenant_id ?? tenantId
135
137
  } catch {}
136
138
  }
137
139
  try {
138
140
  const bus = ctx.resolve('eventBus') as any
139
- await bus.emitEvent('query_index.delete_one', { entityType, recordId: id, organizationId: orgId })
141
+ await bus.emitEvent('query_index.delete_one', { entityType, recordId: id, organizationId: orgId, tenantId })
140
142
  } catch {}
141
143
  } catch {}
142
144
  }
@@ -158,7 +158,7 @@ function scopeEntityIndexes<QB extends { where: (...args: any[]) => QB }>(
158
158
 
159
159
  export async function upsertIndexRow(
160
160
  em: EntityManager,
161
- args: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null }
161
+ args: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null; searchTokenDoc?: Record<string, unknown> | null }
162
162
  ): Promise<UpsertIndexResult> {
163
163
  const db = (em as any).getKysely()
164
164
 
@@ -234,21 +234,23 @@ export async function upsertIndexRow(
234
234
  const created = !existed
235
235
  const revived = existed && wasDeleted
236
236
  try {
237
- const encryption = resolveTenantEncryptionService(em as any)
238
- const dekKeyCache = new Map<string | null, string | null>()
239
- const tokenDoc = await decryptIndexDocForSearch(
240
- args.entityType,
241
- doc,
242
- { tenantId: args.tenantId ?? null, organizationId: args.organizationId ?? null },
243
- encryption,
244
- dekKeyCache,
245
- )
237
+ const tokenDoc = args.searchTokenDoc ?? (() => {
238
+ const encryption = resolveTenantEncryptionService(em as any)
239
+ const dekKeyCache = new Map<string | null, string | null>()
240
+ return decryptIndexDocForSearch(
241
+ args.entityType,
242
+ doc,
243
+ { tenantId: args.tenantId ?? null, organizationId: args.organizationId ?? null },
244
+ encryption,
245
+ dekKeyCache,
246
+ )
247
+ })()
246
248
  await replaceSearchTokensForRecord(db, {
247
249
  entityType: args.entityType,
248
250
  recordId: args.recordId,
249
251
  organizationId: args.organizationId ?? null,
250
252
  tenantId: args.tenantId ?? null,
251
- doc: tokenDoc,
253
+ doc: await tokenDoc,
252
254
  })
253
255
  } catch {}
254
256
  return { doc, existed, wasDeleted, created, revived }
@@ -28,7 +28,15 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
28
28
  organizationId = resolvedScope.organizationId
29
29
  tenantId = resolvedScope.tenantId
30
30
 
31
- const result = await upsertIndexRow(em, { entityType, recordId, organizationId, tenantId })
31
+ const result = await upsertIndexRow(em, {
32
+ entityType,
33
+ recordId,
34
+ organizationId,
35
+ tenantId,
36
+ searchTokenDoc: typeof payload?.searchTokenDoc === 'object' && payload.searchTokenDoc && !Array.isArray(payload.searchTokenDoc)
37
+ ? payload.searchTokenDoc
38
+ : null,
39
+ })
32
40
  if (!suppressCoverage) {
33
41
  const doc = result.doc
34
42
  const isActive = !!doc && (doc.deleted_at == null || doc.deleted_at === null)
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/core'
2
3
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
3
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
5
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
@@ -85,7 +86,7 @@ const crud = makeCrudRoute({
85
86
  const filters: Record<string, unknown> = {}
86
87
  if (query.orderId) filters.order_id = { $eq: query.orderId }
87
88
  try {
88
- const em = ctx.container.resolve('em')
89
+ const em = ctx.container.resolve<EntityManager>('em')
89
90
  const cfFilters = await buildCustomFieldFiltersFromQuery({
90
91
  entityId: E.sales.sales_order_adjustment,
91
92
  query,
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/core'
2
3
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
3
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
5
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
@@ -82,7 +83,7 @@ const crud = makeCrudRoute({
82
83
  const filters: Record<string, unknown> = {}
83
84
  if (query.quoteId) filters.quote_id = { $eq: query.quoteId }
84
85
  try {
85
- const em = ctx.container.resolve('em')
86
+ const em = ctx.container.resolve<EntityManager>('em')
86
87
  const cfFilters = await buildCustomFieldFiltersFromQuery({
87
88
  entityId: E.sales.sales_quote_adjustment,
88
89
  query,
@@ -67,6 +67,9 @@ import {
67
67
  } from "../../customers/data/entities";
68
68
  import {
69
69
  enforceAdjustmentSign,
70
+ validateReturnAdjustmentWithinRemaining,
71
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
72
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
70
73
  quoteCreateSchema,
71
74
  quoteLineCreateSchema,
72
75
  quoteAdjustmentCreateSchema,
@@ -7384,6 +7387,63 @@ const orderAdjustmentUpsertCommand: CommandHandler<
7384
7387
  shippingMethodCode: order.shippingMethodCode ?? null,
7385
7388
  paymentMethodCode: order.paymentMethodCode ?? null,
7386
7389
  });
7390
+ const effectiveAdjustment = nextAdjustments.find(
7391
+ (adj) => adj.id === adjustmentId,
7392
+ );
7393
+ if (effectiveAdjustment?.kind === "return") {
7394
+ const baselineAdjustments = nextAdjustments.filter(
7395
+ (adj) => adj.id !== adjustmentId,
7396
+ );
7397
+ const baselineCalculation =
7398
+ await salesCalculationService.calculateDocumentTotals({
7399
+ documentKind: "order",
7400
+ lines: calcLines,
7401
+ adjustments: baselineAdjustments,
7402
+ context: calculationContext,
7403
+ existingTotals: resolveExistingPaymentTotals(order),
7404
+ });
7405
+ const issues = validateReturnAdjustmentWithinRemaining({
7406
+ kind: "return",
7407
+ amountNet:
7408
+ effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
7409
+ amountGross:
7410
+ effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
7411
+ remainingNet: Number(
7412
+ baselineCalculation.totals?.grandTotalNetAmount ?? 0,
7413
+ ),
7414
+ remainingGross: Number(
7415
+ baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
7416
+ ),
7417
+ });
7418
+ if (issues.length > 0) {
7419
+ const { translate } = await resolveTranslations();
7420
+ const grossIssue = issues.find((issue) => issue.path === "amountGross");
7421
+ const netIssue = issues.find((issue) => issue.path === "amountNet");
7422
+ const fieldErrors: Record<string, string> = {};
7423
+ if (grossIssue) {
7424
+ fieldErrors.amountGross = translate(
7425
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
7426
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
7427
+ );
7428
+ }
7429
+ if (netIssue) {
7430
+ fieldErrors.amountNet = translate(
7431
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
7432
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
7433
+ );
7434
+ }
7435
+ throw new CrudHttpError(400, {
7436
+ error:
7437
+ fieldErrors.amountGross ??
7438
+ fieldErrors.amountNet ??
7439
+ translate(
7440
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
7441
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
7442
+ ),
7443
+ fieldErrors,
7444
+ });
7445
+ }
7446
+ }
7387
7447
  const calculation = await salesCalculationService.calculateDocumentTotals({
7388
7448
  documentKind: "order",
7389
7449
  lines: calcLines,
@@ -7766,6 +7826,62 @@ const quoteAdjustmentUpsertCommand: CommandHandler<
7766
7826
  shippingMethodCode: quote.shippingMethodCode ?? null,
7767
7827
  paymentMethodCode: quote.paymentMethodCode ?? null,
7768
7828
  });
7829
+ const effectiveAdjustment = nextAdjustments.find(
7830
+ (adj) => adj.id === adjustmentId,
7831
+ );
7832
+ if (effectiveAdjustment?.kind === "return") {
7833
+ const baselineAdjustments = nextAdjustments.filter(
7834
+ (adj) => adj.id !== adjustmentId,
7835
+ );
7836
+ const baselineCalculation =
7837
+ await salesCalculationService.calculateDocumentTotals({
7838
+ documentKind: "quote",
7839
+ lines: calcLines,
7840
+ adjustments: baselineAdjustments,
7841
+ context: calculationContext,
7842
+ });
7843
+ const issues = validateReturnAdjustmentWithinRemaining({
7844
+ kind: "return",
7845
+ amountNet:
7846
+ effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
7847
+ amountGross:
7848
+ effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
7849
+ remainingNet: Number(
7850
+ baselineCalculation.totals?.grandTotalNetAmount ?? 0,
7851
+ ),
7852
+ remainingGross: Number(
7853
+ baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
7854
+ ),
7855
+ });
7856
+ if (issues.length > 0) {
7857
+ const { translate } = await resolveTranslations();
7858
+ const grossIssue = issues.find((issue) => issue.path === "amountGross");
7859
+ const netIssue = issues.find((issue) => issue.path === "amountNet");
7860
+ const fieldErrors: Record<string, string> = {};
7861
+ if (grossIssue) {
7862
+ fieldErrors.amountGross = translate(
7863
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
7864
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
7865
+ );
7866
+ }
7867
+ if (netIssue) {
7868
+ fieldErrors.amountNet = translate(
7869
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
7870
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
7871
+ );
7872
+ }
7873
+ throw new CrudHttpError(400, {
7874
+ error:
7875
+ fieldErrors.amountGross ??
7876
+ fieldErrors.amountNet ??
7877
+ translate(
7878
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
7879
+ RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
7880
+ ),
7881
+ fieldErrors,
7882
+ });
7883
+ }
7884
+ }
7769
7885
  const calculation = await salesCalculationService.calculateDocumentTotals({
7770
7886
  documentKind: "quote",
7771
7887
  lines: calcLines,
@@ -523,6 +523,44 @@ export const enforceAdjustmentSign = (
523
523
  */
524
524
  export const enforceReturnAdjustmentSign = enforceAdjustmentSign
525
525
 
526
+ export const RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE =
527
+ 'Return adjustment amountNet exceeds the remaining grand total. Reduce the amount or remove existing returns first.'
528
+ export const RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE =
529
+ 'Return adjustment amountGross exceeds the remaining grand total. Reduce the amount or remove existing returns first.'
530
+
531
+ export type ReturnAdjustmentRemainingCheck = {
532
+ kind?: string | null
533
+ amountNet?: number | null
534
+ amountGross?: number | null
535
+ remainingNet: number
536
+ remainingGross: number
537
+ }
538
+
539
+ export type ReturnAdjustmentRemainingIssue = {
540
+ path: 'amountNet' | 'amountGross'
541
+ message: string
542
+ }
543
+
544
+ // Inclusive: abs(amount) === remaining is allowed. Tiny epsilon absorbs
545
+ // floating-point rounding from upstream tax/rate adjustments.
546
+ const RETURN_REMAINING_EPSILON = 0.005
547
+
548
+ export const validateReturnAdjustmentWithinRemaining = (
549
+ value: ReturnAdjustmentRemainingCheck
550
+ ): ReturnAdjustmentRemainingIssue[] => {
551
+ if (value.kind !== 'return') return []
552
+ const absNet = typeof value.amountNet === 'number' ? Math.abs(value.amountNet) : 0
553
+ const absGross = typeof value.amountGross === 'number' ? Math.abs(value.amountGross) : 0
554
+ const issues: ReturnAdjustmentRemainingIssue[] = []
555
+ if (absGross > value.remainingGross + RETURN_REMAINING_EPSILON) {
556
+ issues.push({ path: 'amountGross', message: RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE })
557
+ }
558
+ if (absNet > value.remainingNet + RETURN_REMAINING_EPSILON) {
559
+ issues.push({ path: 'amountNet', message: RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE })
560
+ }
561
+ return issues
562
+ }
563
+
526
564
  export const orderAdjustmentCreateSchema = scoped.extend({
527
565
  orderId: uuid(),
528
566
  orderLineId: uuid().optional(),
@@ -4,6 +4,8 @@
4
4
  "audit_logs.resource_kind.sales.payment": "Zahlung",
5
5
  "audit_logs.resource_kind.sales.shipment": "Versand",
6
6
  "customers~sales.nav.group": "Vertrieb",
7
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "Der Bruttobetrag der Rücksendeanpassung überschreitet den verbleibenden Gesamtbetrag. Bitte verringern Sie den Betrag oder entfernen Sie bestehende Rücksendungen.",
8
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "Der Nettobetrag der Rücksendeanpassung überschreitet den verbleibenden Gesamtbetrag. Bitte verringern Sie den Betrag oder entfernen Sie bestehende Rücksendungen.",
7
9
  "sales.audit.channels.create": "Vertriebskanal erstellen",
8
10
  "sales.audit.channels.delete": "Vertriebskanal löschen",
9
11
  "sales.audit.channels.update": "Vertriebskanal aktualisieren",