@open-mercato/core 0.6.2-develop.3461.1.605f31c2c9 → 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 (173) 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/frontend/login.js +35 -0
  31. package/dist/modules/auth/frontend/login.js.map +2 -2
  32. package/dist/modules/auth/lib/grantChecks.js +97 -2
  33. package/dist/modules/auth/lib/grantChecks.js.map +2 -2
  34. package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
  35. package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
  36. package/dist/modules/business_rules/lib/rule-evaluator.js +3 -3
  37. package/dist/modules/business_rules/lib/rule-evaluator.js.map +2 -2
  38. package/dist/modules/customers/ai-agents.js +10 -1
  39. package/dist/modules/customers/ai-agents.js.map +2 -2
  40. package/dist/modules/customers/ai-tools/settings-pack.js +55 -1
  41. package/dist/modules/customers/ai-tools/settings-pack.js.map +2 -2
  42. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +13 -5
  43. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  44. package/dist/modules/customers/lib/personCompanyLinkTable.js +4 -30
  45. package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
  46. package/dist/modules/customers/migrations/Migration20260521175027.js +12 -0
  47. package/dist/modules/customers/migrations/Migration20260521175027.js.map +7 -0
  48. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +41 -12
  49. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  50. package/dist/modules/data_sync/api/options.js +2 -0
  51. package/dist/modules/data_sync/api/options.js.map +2 -2
  52. package/dist/modules/data_sync/api/run.js +77 -58
  53. package/dist/modules/data_sync/api/run.js.map +2 -2
  54. package/dist/modules/data_sync/backend/data-sync/page.js +3 -2
  55. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  56. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +8 -3
  57. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  58. package/dist/modules/integrations/backend/integrations/[id]/page.js +15 -13
  59. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  60. package/dist/modules/integrations/data/validators.js +9 -6
  61. package/dist/modules/integrations/data/validators.js.map +2 -2
  62. package/dist/modules/messages/api/route.js +2 -2
  63. package/dist/modules/messages/api/route.js.map +2 -2
  64. package/dist/modules/messages/commands/messages.js +42 -2
  65. package/dist/modules/messages/commands/messages.js.map +2 -2
  66. package/dist/modules/messages/components/MessagesInboxPageClient.js +70 -89
  67. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  68. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +8 -6
  69. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +2 -2
  70. package/dist/modules/messages/components/inboxFilters.js +133 -0
  71. package/dist/modules/messages/components/inboxFilters.js.map +7 -0
  72. package/dist/modules/messages/components/messageListLabels.js +14 -0
  73. package/dist/modules/messages/components/messageListLabels.js.map +7 -0
  74. package/dist/modules/messages/components/useMessagesInboxBulkActions.js +47 -23
  75. package/dist/modules/messages/components/useMessagesInboxBulkActions.js.map +2 -2
  76. package/dist/modules/messages/data/validators.js +8 -6
  77. package/dist/modules/messages/data/validators.js.map +2 -2
  78. package/dist/modules/progress/api/jobs/route.js +26 -16
  79. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  80. package/dist/modules/query_index/di.js +8 -6
  81. package/dist/modules/query_index/di.js.map +2 -2
  82. package/dist/modules/query_index/lib/indexer.js +12 -10
  83. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  84. package/dist/modules/query_index/subscribers/upsert_one.js +7 -1
  85. package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
  86. package/dist/modules/sales/api/order-adjustments/route.js.map +2 -2
  87. package/dist/modules/sales/api/quote-adjustments/route.js.map +2 -2
  88. package/dist/modules/sales/commands/documents.js +104 -0
  89. package/dist/modules/sales/commands/documents.js.map +2 -2
  90. package/dist/modules/sales/data/validators.js +20 -1
  91. package/dist/modules/sales/data/validators.js.map +2 -2
  92. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
  93. package/dist/modules/sync_excel/api/import/route.js +126 -116
  94. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  95. package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
  96. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  97. package/dist/modules/translations/components/TranslationManager.js +9 -4
  98. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  99. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  100. package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
  101. package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
  102. package/jest.config.cjs +4 -1
  103. package/package.json +15 -16
  104. package/src/helpers/integration/queue-runner.ts +21 -2
  105. package/src/helpers/integration/queue.ts +1 -1
  106. package/src/helpers/integration/salesUi.ts +26 -1
  107. package/src/modules/auth/api/roles/acl/route.ts +39 -1
  108. package/src/modules/auth/api/roles/route.ts +38 -2
  109. package/src/modules/auth/api/users/acl/route.ts +41 -1
  110. package/src/modules/auth/api/users/route.ts +105 -12
  111. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +35 -13
  112. package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
  113. package/src/modules/auth/backend/users/create/page.tsx +1 -1
  114. package/src/modules/auth/backend/users/roleOptions.ts +2 -1
  115. package/src/modules/auth/commands/roles.ts +8 -0
  116. package/src/modules/auth/commands/users.ts +3 -0
  117. package/src/modules/auth/components/AclEditor.tsx +50 -24
  118. package/src/modules/auth/frontend/login.tsx +40 -0
  119. package/src/modules/auth/lib/grantChecks.ts +116 -2
  120. package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
  121. package/src/modules/business_rules/lib/rule-evaluator.ts +3 -3
  122. package/src/modules/customers/ai-agents.ts +10 -1
  123. package/src/modules/customers/ai-tools/settings-pack.ts +62 -1
  124. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +18 -7
  125. package/src/modules/customers/lib/personCompanyLinkTable.ts +33 -41
  126. package/src/modules/customers/migrations/Migration20260521175027.ts +39 -0
  127. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +38 -12
  128. package/src/modules/data_sync/api/options.ts +2 -0
  129. package/src/modules/data_sync/api/run.ts +80 -61
  130. package/src/modules/data_sync/backend/data-sync/page.tsx +10 -0
  131. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +17 -3
  132. package/src/modules/data_sync/i18n/de.json +3 -0
  133. package/src/modules/data_sync/i18n/en.json +3 -0
  134. package/src/modules/data_sync/i18n/es.json +3 -0
  135. package/src/modules/data_sync/i18n/pl.json +3 -0
  136. package/src/modules/data_sync/lib/adapter.ts +9 -0
  137. package/src/modules/integrations/backend/integrations/[id]/page.tsx +15 -13
  138. package/src/modules/integrations/data/validators.ts +5 -4
  139. package/src/modules/messages/api/route.ts +2 -2
  140. package/src/modules/messages/commands/messages.ts +54 -2
  141. package/src/modules/messages/components/MessagesInboxPageClient.tsx +80 -92
  142. package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +9 -5
  143. package/src/modules/messages/components/inboxFilters.ts +162 -0
  144. package/src/modules/messages/components/messageListLabels.ts +29 -0
  145. package/src/modules/messages/components/useMessagesInboxBulkActions.ts +58 -26
  146. package/src/modules/messages/data/validators.ts +7 -5
  147. package/src/modules/messages/i18n/de.json +7 -4
  148. package/src/modules/messages/i18n/en.json +7 -4
  149. package/src/modules/messages/i18n/es.json +7 -4
  150. package/src/modules/messages/i18n/pl.json +7 -4
  151. package/src/modules/progress/api/jobs/route.ts +30 -20
  152. package/src/modules/query_index/di.ts +9 -7
  153. package/src/modules/query_index/lib/indexer.ts +13 -11
  154. package/src/modules/query_index/subscribers/upsert_one.ts +9 -1
  155. package/src/modules/sales/api/order-adjustments/route.ts +2 -1
  156. package/src/modules/sales/api/quote-adjustments/route.ts +2 -1
  157. package/src/modules/sales/commands/documents.ts +116 -0
  158. package/src/modules/sales/data/validators.ts +38 -0
  159. package/src/modules/sales/i18n/de.json +2 -0
  160. package/src/modules/sales/i18n/en.json +2 -0
  161. package/src/modules/sales/i18n/es.json +2 -0
  162. package/src/modules/sales/i18n/pl.json +2 -0
  163. package/src/modules/sales/lib/makeSalesLineRoute.ts +2 -1
  164. package/src/modules/staff/i18n/de.json +1 -0
  165. package/src/modules/staff/i18n/en.json +1 -0
  166. package/src/modules/staff/i18n/es.json +1 -0
  167. package/src/modules/staff/i18n/pl.json +1 -0
  168. package/src/modules/sync_excel/api/import/route.ts +141 -131
  169. package/src/modules/sync_excel/lib/adapters/customers.ts +1 -0
  170. package/src/modules/translations/components/TranslationManager.tsx +9 -4
  171. package/src/modules/workflows/lib/activity-executor.ts +4 -3
  172. package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
  173. package/tsconfig.json +1 -0
@@ -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.",
@@ -159,6 +160,8 @@
159
160
  "messages.forwardNote": "Nota opcional",
160
161
  "messages.includeAttachments": "Incluir adjuntos",
161
162
  "messages.includeAttachmentsHint": "Conservar adjuntos del mensaje original.",
163
+ "messages.list.noRecipient": "(Sin destinatario)",
164
+ "messages.list.noSubject": "(Sin asunto)",
162
165
  "messages.loading.attachments": "Cargando adjuntos...",
163
166
  "messages.loading.detail": "Cargando mensaje...",
164
167
  "messages.loading.objectTypes": "Cargando tipos de objeto...",
@@ -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.",
@@ -159,6 +160,8 @@
159
160
  "messages.forwardNote": "Notatka opcjonalna",
160
161
  "messages.includeAttachments": "Dołącz załączniki",
161
162
  "messages.includeAttachmentsHint": "Przenieś załączniki z oryginalnej wiadomości.",
163
+ "messages.list.noRecipient": "(Brak odbiorcy)",
164
+ "messages.list.noSubject": "(Brak tematu)",
162
165
  "messages.loading.attachments": "Ładowanie załączników...",
163
166
  "messages.loading.detail": "Ładowanie wiadomości...",
164
167
  "messages.loading.objectTypes": "Ładowanie typów obiektów...",
@@ -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",
@@ -4,6 +4,8 @@
4
4
  "audit_logs.resource_kind.sales.payment": "Payment",
5
5
  "audit_logs.resource_kind.sales.shipment": "Shipment",
6
6
  "customers~sales.nav.group": "Sales",
7
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "Return adjustment amountGross exceeds the remaining grand total. Reduce the amount or remove existing returns first.",
8
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "Return adjustment amountNet exceeds the remaining grand total. Reduce the amount or remove existing returns first.",
7
9
  "sales.audit.channels.create": "Create sales channel",
8
10
  "sales.audit.channels.delete": "Delete sales channel",
9
11
  "sales.audit.channels.update": "Update sales channel",
@@ -4,6 +4,8 @@
4
4
  "audit_logs.resource_kind.sales.payment": "Pago",
5
5
  "audit_logs.resource_kind.sales.shipment": "Envío",
6
6
  "customers~sales.nav.group": "Ventas",
7
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "El importe bruto del ajuste de devolución supera el total restante. Reduzca el importe o elimine las devoluciones existentes.",
8
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "El importe neto del ajuste de devolución supera el total restante. Reduzca el importe o elimine las devoluciones existentes.",
7
9
  "sales.audit.channels.create": "Crear canal de ventas",
8
10
  "sales.audit.channels.delete": "Eliminar canal de ventas",
9
11
  "sales.audit.channels.update": "Actualizar canal de ventas",
@@ -4,6 +4,8 @@
4
4
  "audit_logs.resource_kind.sales.payment": "Płatność",
5
5
  "audit_logs.resource_kind.sales.shipment": "Wysyłka",
6
6
  "customers~sales.nav.group": "Sprzedaż",
7
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "Kwota brutto korekty zwrotu przekracza pozostałą wartość zamówienia. Zmniejsz kwotę lub usuń istniejące zwroty.",
8
+ "sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "Kwota netto korekty zwrotu przekracza pozostałą wartość zamówienia. Zmniejsz kwotę lub usuń istniejące zwroty.",
7
9
  "sales.audit.channels.create": "Utwórz kanał sprzedaży",
8
10
  "sales.audit.channels.delete": "Usuń kanał sprzedaży",
9
11
  "sales.audit.channels.update": "Zaktualizuj kanał sprzedaży",
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/core'
2
3
  import { makeCrudRoute, type CrudCtx } 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'
@@ -210,7 +211,7 @@ export function makeSalesLineRoute(config: SalesLineRouteConfig) {
210
211
  if (query.id) filters.id = { $eq: query.id }
211
212
  if (query[parentFkParam]) filters[parentFkColumn] = { $eq: query[parentFkParam] }
212
213
  try {
213
- const em = ctx.container.resolve('em')
214
+ const em = ctx.container.resolve<EntityManager>('em')
214
215
  const cfFilters = await buildCustomFieldFiltersFromQuery({
215
216
  entityId,
216
217
  query,
@@ -280,6 +280,7 @@
280
280
  "staff.messageObjects.notFound": "Nicht gefunden",
281
281
  "staff.messageObjects.team.title": "Team",
282
282
  "staff.messageObjects.teamMember.title": "Teammitglied",
283
+ "staff.messages.leaveRequestApproval": "Urlaubsantrag – Genehmigung",
283
284
  "staff.myAvailability.empty.profileRequired": "Erstelle dein Teamprofil, um Verfügbarkeit zu verwalten.",
284
285
  "staff.myAvailability.errors.load": "Verfügbarkeit konnte nicht geladen werden.",
285
286
  "staff.myAvailability.loading": "Verfügbarkeit wird geladen...",
@@ -280,6 +280,7 @@
280
280
  "staff.messageObjects.notFound": "Not found",
281
281
  "staff.messageObjects.team.title": "Team",
282
282
  "staff.messageObjects.teamMember.title": "Team member",
283
+ "staff.messages.leaveRequestApproval": "Leave request approval",
283
284
  "staff.myAvailability.empty.profileRequired": "Create your team member profile to manage availability.",
284
285
  "staff.myAvailability.errors.load": "Failed to load availability.",
285
286
  "staff.myAvailability.loading": "Loading availability...",
@@ -280,6 +280,7 @@
280
280
  "staff.messageObjects.notFound": "No encontrado",
281
281
  "staff.messageObjects.team.title": "Equipo",
282
282
  "staff.messageObjects.teamMember.title": "Miembro del equipo",
283
+ "staff.messages.leaveRequestApproval": "Solicitud de permiso: aprobación",
283
284
  "staff.myAvailability.empty.profileRequired": "Crea tu perfil de miembro para gestionar la disponibilidad.",
284
285
  "staff.myAvailability.errors.load": "No se pudo cargar la disponibilidad.",
285
286
  "staff.myAvailability.loading": "Cargando disponibilidad...",
@@ -280,6 +280,7 @@
280
280
  "staff.messageObjects.notFound": "Nie znaleziono",
281
281
  "staff.messageObjects.team.title": "Zespół",
282
282
  "staff.messageObjects.teamMember.title": "Członek zespołu",
283
+ "staff.messages.leaveRequestApproval": "Wniosek urlopowy – zatwierdzenie",
283
284
  "staff.myAvailability.empty.profileRequired": "Utwórz profil pracownika, aby zarządzać dostępnością.",
284
285
  "staff.myAvailability.errors.load": "Nie udało się załadować dostępności.",
285
286
  "staff.myAvailability.loading": "Ładowanie dostępności...",