@open-mercato/core 0.6.6-develop.6257.1.6d0af84d26 → 0.6.6-develop.6286.1.d985c5c374

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 (208) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/carrier_shipment_idempotency_key/index.js +19 -0
  3. package/dist/generated/entities/carrier_shipment_idempotency_key/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +2 -1
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +10 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/lib/productResolution.js +45 -0
  9. package/dist/modules/catalog/lib/productResolution.js.map +7 -0
  10. package/dist/modules/communication_channels/workers/channel-import-history.js +1 -1
  11. package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
  12. package/dist/modules/currencies/api/fetch-configs/route.js +34 -0
  13. package/dist/modules/currencies/api/fetch-configs/route.js.map +2 -2
  14. package/dist/modules/currencies/components/CurrencyFetchingConfig.js +17 -15
  15. package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
  16. package/dist/modules/customers/api/companies/[id]/route.js +176 -161
  17. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  18. package/dist/modules/customers/api/people/[id]/route.js +75 -59
  19. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  20. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +17 -10
  21. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
  22. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +6 -4
  23. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +2 -2
  24. package/dist/modules/customers/components/detail/AiActionChips.js +5 -2
  25. package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
  26. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +19 -21
  27. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
  28. package/dist/modules/data_sync/api/runs/[id]/cancel.js +1 -1
  29. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  30. package/dist/modules/data_sync/backend/data-sync/page.js +9 -45
  31. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  32. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +11 -17
  33. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  34. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +12 -10
  35. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  36. package/dist/modules/data_sync/lib/id-mapping.js +46 -1
  37. package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
  38. package/dist/modules/data_sync/lib/sync-engine.js +2 -2
  39. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  40. package/dist/modules/data_sync/lib/syncRunStatus.js +30 -0
  41. package/dist/modules/data_sync/lib/syncRunStatus.js.map +7 -0
  42. package/dist/modules/entities/api/records.js +6 -0
  43. package/dist/modules/entities/api/records.js.map +2 -2
  44. package/dist/modules/feature_toggles/api/global/route.js +17 -15
  45. package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
  46. package/dist/modules/feature_toggles/api/openapi.js +2 -2
  47. package/dist/modules/feature_toggles/api/openapi.js.map +2 -2
  48. package/dist/modules/feature_toggles/commands/global.js +14 -0
  49. package/dist/modules/feature_toggles/commands/global.js.map +2 -2
  50. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +6 -0
  51. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  52. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +3 -1
  53. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
  54. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +12 -5
  55. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  56. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js +10 -1
  57. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js.map +2 -2
  58. package/dist/modules/payment_gateways/api/cancel/route.js +5 -0
  59. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  60. package/dist/modules/payment_gateways/api/capture/route.js +5 -0
  61. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  62. package/dist/modules/payment_gateways/api/refund/route.js +5 -0
  63. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  64. package/dist/modules/payment_gateways/lib/gateway-service.js +52 -25
  65. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  66. package/dist/modules/payment_gateways/lib/status-machine.js +13 -0
  67. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  68. package/dist/modules/perspectives/api/[tableId]/route.js +8 -2
  69. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  70. package/dist/modules/perspectives/data/entities.js +33 -3
  71. package/dist/modules/perspectives/data/entities.js.map +2 -2
  72. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js +63 -0
  73. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js.map +7 -0
  74. package/dist/modules/perspectives/services/perspectiveService.js +17 -0
  75. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  76. package/dist/modules/planner/commands/availability-weekly.js +33 -1
  77. package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
  78. package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -2
  79. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  80. package/dist/modules/progress/api/active/route.js +1 -1
  81. package/dist/modules/progress/api/active/route.js.map +2 -2
  82. package/dist/modules/progress/lib/progressService.js.map +1 -1
  83. package/dist/modules/progress/lib/progressServiceImpl.js +33 -8
  84. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  85. package/dist/modules/query_index/api/status.js +19 -24
  86. package/dist/modules/query_index/api/status.js.map +3 -3
  87. package/dist/modules/query_index/lib/coverage.js +35 -0
  88. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  89. package/dist/modules/query_index/lib/engine.js +11 -6
  90. package/dist/modules/query_index/lib/engine.js.map +2 -2
  91. package/dist/modules/resources/api/resource-types.js +5 -37
  92. package/dist/modules/resources/api/resource-types.js.map +2 -2
  93. package/dist/modules/resources/api/resourceTypeCounts.js +35 -0
  94. package/dist/modules/resources/api/resourceTypeCounts.js.map +7 -0
  95. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  96. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +1 -1
  97. package/dist/modules/resources/backend/resources/resource-types/page.js +2 -1
  98. package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
  99. package/dist/modules/sales/components/documents/ReturnDialog.js +4 -1
  100. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  101. package/dist/modules/sales/components/documents/ShipmentDialog.js +17 -2
  102. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  103. package/dist/modules/sales/di.js +4 -0
  104. package/dist/modules/sales/di.js.map +2 -2
  105. package/dist/modules/sales/services/salesOrderService.js +26 -0
  106. package/dist/modules/sales/services/salesOrderService.js.map +7 -0
  107. package/dist/modules/shipping_carriers/api/shipments/route.js +8 -0
  108. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  109. package/dist/modules/shipping_carriers/data/entities.js +38 -0
  110. package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
  111. package/dist/modules/shipping_carriers/data/validators.js +2 -1
  112. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  113. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js +76 -0
  114. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js.map +7 -0
  115. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +2 -1
  116. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +2 -2
  117. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +20 -2
  118. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  119. package/dist/modules/shipping_carriers/lib/shipping-service.js +82 -43
  120. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  121. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js +14 -0
  122. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js.map +7 -0
  123. package/dist/modules/staff/lib/timesheets-ui/ListView.js +15 -6
  124. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
  125. package/dist/modules/translations/components/TranslationManager.js +9 -18
  126. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  127. package/generated/entities/carrier_shipment_idempotency_key/index.ts +8 -0
  128. package/generated/entities.ids.generated.ts +2 -1
  129. package/generated/entity-fields-registry.ts +10 -0
  130. package/package.json +9 -8
  131. package/src/modules/catalog/lib/productResolution.ts +75 -0
  132. package/src/modules/communication_channels/workers/channel-import-history.ts +1 -1
  133. package/src/modules/currencies/api/fetch-configs/route.ts +36 -0
  134. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +20 -17
  135. package/src/modules/currencies/i18n/de.json +2 -1
  136. package/src/modules/currencies/i18n/en.json +2 -1
  137. package/src/modules/currencies/i18n/es.json +2 -1
  138. package/src/modules/currencies/i18n/pl.json +2 -1
  139. package/src/modules/customers/api/companies/[id]/route.ts +214 -189
  140. package/src/modules/customers/api/people/[id]/route.ts +106 -80
  141. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +19 -12
  142. package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +8 -6
  143. package/src/modules/customers/components/detail/AiActionChips.tsx +6 -3
  144. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +16 -18
  145. package/src/modules/data_sync/api/runs/[id]/cancel.ts +1 -1
  146. package/src/modules/data_sync/backend/data-sync/page.tsx +11 -58
  147. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +13 -20
  148. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +10 -8
  149. package/src/modules/data_sync/lib/id-mapping.ts +58 -1
  150. package/src/modules/data_sync/lib/sync-engine.ts +2 -2
  151. package/src/modules/data_sync/lib/syncRunStatus.ts +39 -0
  152. package/src/modules/entities/api/records.ts +14 -0
  153. package/src/modules/feature_toggles/api/global/route.ts +16 -14
  154. package/src/modules/feature_toggles/api/openapi.ts +2 -2
  155. package/src/modules/feature_toggles/commands/global.ts +15 -0
  156. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +7 -0
  157. package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +2 -0
  158. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +14 -4
  159. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.ts +15 -1
  160. package/src/modules/payment_gateways/api/cancel/route.ts +5 -0
  161. package/src/modules/payment_gateways/api/capture/route.ts +5 -0
  162. package/src/modules/payment_gateways/api/refund/route.ts +5 -0
  163. package/src/modules/payment_gateways/lib/gateway-service.ts +58 -25
  164. package/src/modules/payment_gateways/lib/status-machine.ts +16 -0
  165. package/src/modules/perspectives/api/[tableId]/route.ts +8 -2
  166. package/src/modules/perspectives/data/entities.ts +43 -3
  167. package/src/modules/perspectives/migrations/.snapshot-open-mercato.json +69 -25
  168. package/src/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.ts +65 -0
  169. package/src/modules/perspectives/services/perspectiveService.ts +17 -0
  170. package/src/modules/planner/commands/availability-weekly.ts +48 -1
  171. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +31 -10
  172. package/src/modules/progress/api/active/route.ts +1 -1
  173. package/src/modules/progress/lib/progressService.ts +2 -2
  174. package/src/modules/progress/lib/progressServiceImpl.ts +33 -8
  175. package/src/modules/query_index/api/status.ts +28 -32
  176. package/src/modules/query_index/lib/coverage.ts +63 -0
  177. package/src/modules/query_index/lib/engine.ts +37 -24
  178. package/src/modules/resources/api/resource-types.ts +4 -44
  179. package/src/modules/resources/api/resourceTypeCounts.ts +74 -0
  180. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  181. package/src/modules/resources/backend/resources/resource-types/page.tsx +1 -0
  182. package/src/modules/sales/components/documents/ReturnDialog.tsx +4 -1
  183. package/src/modules/sales/components/documents/ShipmentDialog.tsx +20 -2
  184. package/src/modules/sales/di.ts +6 -0
  185. package/src/modules/sales/services/salesOrderService.ts +38 -0
  186. package/src/modules/shipping_carriers/api/shipments/route.ts +8 -0
  187. package/src/modules/shipping_carriers/data/entities.ts +33 -0
  188. package/src/modules/shipping_carriers/data/validators.ts +1 -0
  189. package/src/modules/shipping_carriers/i18n/de.json +1 -0
  190. package/src/modules/shipping_carriers/i18n/en.json +1 -0
  191. package/src/modules/shipping_carriers/i18n/es.json +1 -0
  192. package/src/modules/shipping_carriers/i18n/pl.json +1 -0
  193. package/src/modules/shipping_carriers/lib/shipment-idempotency.ts +119 -0
  194. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +3 -1
  195. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +24 -2
  196. package/src/modules/shipping_carriers/lib/shipping-service.ts +87 -43
  197. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +197 -0
  198. package/src/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.ts +14 -0
  199. package/src/modules/staff/i18n/de.json +1 -0
  200. package/src/modules/staff/i18n/en.json +1 -0
  201. package/src/modules/staff/i18n/es.json +1 -0
  202. package/src/modules/staff/i18n/pl.json +1 -0
  203. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +15 -7
  204. package/src/modules/translations/components/TranslationManager.tsx +17 -24
  205. package/src/modules/translations/i18n/de.json +1 -0
  206. package/src/modules/translations/i18n/en.json +1 -0
  207. package/src/modules/translations/i18n/es.json +1 -0
  208. package/src/modules/translations/i18n/pl.json +1 -0
@@ -16,11 +16,11 @@ export interface ProgressService {
16
16
  failJob(jobId: string, input: FailJobInput, ctx: ProgressServiceContext): Promise<ProgressJob>
17
17
  cancelJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>
18
18
  markCancelled(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>
19
- isCancellationRequested(jobId: string, tenantId: string): Promise<boolean>
19
+ isCancellationRequested(jobId: string, tenantId: string, organizationId?: string | null): Promise<boolean>
20
20
  getActiveJobs(ctx: ProgressServiceContext): Promise<ProgressJob[]>
21
21
  getRecentlyCompletedJobs(ctx: ProgressServiceContext, sinceSeconds?: number): Promise<ProgressJob[]>
22
22
  getJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob | null>
23
- markStaleJobsFailed(tenantId: string, timeoutSeconds?: number): Promise<number>
23
+ markStaleJobsFailed(tenantId: string, timeoutSeconds?: number, organizationId?: string | null): Promise<number>
24
24
  }
25
25
 
26
26
  export const HEARTBEAT_INTERVAL_MS = 5000
@@ -54,7 +54,11 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
54
54
  },
55
55
 
56
56
  async startJob(jobId, ctx) {
57
- const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
57
+ const job = await em.findOneOrFail(ProgressJob, {
58
+ id: jobId,
59
+ tenantId: ctx.tenantId,
60
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
61
+ })
58
62
  if (job.status === 'cancelled') {
59
63
  return job
60
64
  }
@@ -120,7 +124,11 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
120
124
  },
121
125
 
122
126
  async incrementProgress(jobId, delta, ctx) {
123
- const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
127
+ const job = await em.findOneOrFail(ProgressJob, {
128
+ id: jobId,
129
+ tenantId: ctx.tenantId,
130
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
131
+ })
124
132
  if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
125
133
  return job
126
134
  }
@@ -147,7 +155,11 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
147
155
  },
148
156
 
149
157
  async completeJob(jobId, input, ctx) {
150
- const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
158
+ const job = await em.findOne(ProgressJob, {
159
+ id: jobId,
160
+ tenantId: ctx.tenantId,
161
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
162
+ })
151
163
  if (!job) throw new Error(`Job ${jobId} not found`)
152
164
  if (job.status === 'cancelled') {
153
165
  return job
@@ -174,7 +186,11 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
174
186
  },
175
187
 
176
188
  async failJob(jobId, input, ctx) {
177
- const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
189
+ const job = await em.findOne(ProgressJob, {
190
+ id: jobId,
191
+ tenantId: ctx.tenantId,
192
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
193
+ })
178
194
  if (!job) throw new Error(`Job ${jobId} not found`)
179
195
  if (job.status === 'cancelled') {
180
196
  return job
@@ -226,7 +242,11 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
226
242
  },
227
243
 
228
244
  async markCancelled(jobId, ctx) {
229
- const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
245
+ const job = await em.findOne(ProgressJob, {
246
+ id: jobId,
247
+ tenantId: ctx.tenantId,
248
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
249
+ })
230
250
  if (!job) throw new Error(`Job ${jobId} not found`)
231
251
  if (job.status === 'cancelled') {
232
252
  return job
@@ -249,8 +269,12 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
249
269
  return job
250
270
  },
251
271
 
252
- async isCancellationRequested(jobId, tenantId) {
253
- const job = await findOneWithDecryption(em, ProgressJob, { id: jobId, tenantId })
272
+ async isCancellationRequested(jobId, tenantId, organizationId) {
273
+ const job = await findOneWithDecryption(em, ProgressJob, {
274
+ id: jobId,
275
+ tenantId,
276
+ ...(organizationId ? { organizationId } : {}),
277
+ })
254
278
  return job?.cancelRequestedAt != null
255
279
  },
256
280
 
@@ -288,11 +312,12 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
288
312
  })
289
313
  },
290
314
 
291
- async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS) {
315
+ async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS, organizationId?: string | null) {
292
316
  const cutoff = new Date(Date.now() - timeoutSeconds * 1000)
293
317
 
294
318
  const staleJobs = await em.find(ProgressJob, {
295
319
  tenantId,
320
+ ...(organizationId ? { organizationId } : {}),
296
321
  status: 'running',
297
322
  $or: [
298
323
  { heartbeatAt: { $lt: cutoff } },
@@ -4,7 +4,8 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'
5
5
  import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import { sql } from 'kysely'
7
- import { readCoverageSnapshot, refreshCoverageSnapshot } from '../lib/coverage'
7
+ import { readCoverageSnapshots, refreshCoverageSnapshot } from '../lib/coverage'
8
+ import { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'
8
9
  import type { FullTextSearchStrategy } from '@open-mercato/search/strategies'
9
10
  import type { SearchModuleConfig } from '@open-mercato/shared/modules/search'
10
11
  import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
@@ -146,6 +147,7 @@ export async function GET(req: Request) {
146
147
 
147
148
  const HEARTBEAT_STALE_MS = 60_000
148
149
  const COVERAGE_STALE_MS = 60_000
150
+ const COVERAGE_REFRESH_CONCURRENCY = 8
149
151
 
150
152
  async function fetchJobSummary(entityType: string, tenantIdParam: string | null, organizationIdParam: string | null) {
151
153
  try {
@@ -288,40 +290,31 @@ export async function GET(req: Request) {
288
290
  return Number.isFinite(parsed) ? parsed : null
289
291
  }
290
292
 
291
- const coverageSnapshots: Array<Awaited<ReturnType<typeof readCoverageSnapshot>>> = []
293
+ const coverageScope = {
294
+ tenantId: tenantId ?? null,
295
+ organizationId,
296
+ withDeleted: false,
297
+ } as const
292
298
  const entitiesNeedingRefresh = new Set<string>()
293
- for (const entityId of entityIds) {
294
- const scope = {
295
- entityType: entityId,
296
- tenantId: tenantId ?? null,
297
- organizationId,
298
- withDeleted: false,
299
- } as const
300
- const ensureSnapshot = async () => {
301
- let snapshot = await readCoverageSnapshot(db, scope)
302
- const refreshedAt = snapshot?.refreshed_at instanceof Date
303
- ? snapshot.refreshed_at
304
- : snapshot?.refreshed_at
305
- ? new Date(snapshot.refreshed_at)
306
- : null
307
- const stale = !snapshot || !refreshedAt || (Date.now() - refreshedAt.getTime() > COVERAGE_STALE_MS)
308
- if (forceRefresh || stale) {
309
- await refreshCoverageSnapshot(em, scope).catch(() => undefined)
310
- snapshot = await readCoverageSnapshot(db, scope)
311
- }
312
- const finalRefreshed = snapshot?.refreshed_at instanceof Date
313
- ? snapshot.refreshed_at
314
- : snapshot?.refreshed_at
315
- ? new Date(snapshot.refreshed_at)
316
- : null
317
- if (!snapshot || !finalRefreshed || (Date.now() - finalRefreshed.getTime() > COVERAGE_STALE_MS)) {
318
- entitiesNeedingRefresh.add(entityId)
319
- }
320
- return snapshot
321
- }
322
- coverageSnapshots.push(await ensureSnapshot())
299
+
300
+ // Read every entity's coverage snapshot in a single batched query. This endpoint is
301
+ // polled by the status table every few seconds, so the poll path must stay read-cheap:
302
+ // stale snapshots are refreshed asynchronously via the query_index.coverage.refresh
303
+ // event emitted below, never inline per entity.
304
+ const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })
305
+
306
+ // An explicit refresh action (?refresh) is allowed to block: recompute every entity's
307
+ // coverage with bounded concurrency, then re-read the freshly written snapshots.
308
+ if (forceRefresh && entityIds.length > 0) {
309
+ await mapWithConcurrency(entityIds, COVERAGE_REFRESH_CONCURRENCY, (entityId) =>
310
+ refreshCoverageSnapshot(em, { entityType: entityId, ...coverageScope }).catch(() => undefined),
311
+ )
312
+ const refreshed = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })
313
+ for (const [entityId, snapshot] of refreshed) snapshotByEntity.set(entityId, snapshot)
323
314
  }
324
315
 
316
+ const coverageSnapshots = entityIds.map((entityId) => snapshotByEntity.get(entityId) ?? null)
317
+
325
318
  const jobs = await Promise.all(entityIds.map((eid) => fetchJobSummary(eid, tenantId, organizationId)))
326
319
 
327
320
  const items: any[] = []
@@ -467,6 +460,9 @@ export async function GET(req: Request) {
467
460
 
468
461
  const response = NextResponse.json({ items, errors, logs })
469
462
  const partial = items.find((item) => {
463
+ // Coverage not computed yet (no snapshot) — pending an async refresh, not a partial
464
+ // index. Do not raise the partial-index warning while counts are still unknown.
465
+ if (item.baseCount == null && item.indexCount == null) return false
470
466
  if (item.baseCount == null || item.indexCount == null) return true
471
467
  return item.baseCount !== item.indexCount
472
468
  })
@@ -16,6 +16,19 @@ type CoverageRow = {
16
16
  refreshed_at: Date | string | null
17
17
  }
18
18
 
19
+ export type CoverageSnapshot = CoverageRow & {
20
+ baseCount: number
21
+ indexedCount: number
22
+ vectorIndexedCount: number
23
+ }
24
+
25
+ export type CoverageBatchScope = {
26
+ entityTypes: readonly string[]
27
+ tenantId?: string | null
28
+ organizationId?: string | null
29
+ withDeleted?: boolean
30
+ }
31
+
19
32
  export type CoverageAdjustment = {
20
33
  entityType: string
21
34
  tenantId: string | null
@@ -186,6 +199,56 @@ export async function readCoverageSnapshot(
186
199
  }
187
200
  }
188
201
 
202
+ export async function readCoverageSnapshots(
203
+ db: Kysely<any>,
204
+ batch: CoverageBatchScope
205
+ ): Promise<Map<string, CoverageSnapshot>> {
206
+ const entityTypes = Array.from(
207
+ new Set((batch.entityTypes ?? []).map((id) => String(id || '')).filter((id) => id.length > 0))
208
+ )
209
+ const result = new Map<string, CoverageSnapshot>()
210
+ if (entityTypes.length === 0) return result
211
+
212
+ const withDeleted = batch.withDeleted === true
213
+ let query = db
214
+ .selectFrom('entity_index_coverage' as any)
215
+ .select([
216
+ 'entity_type' as any,
217
+ 'base_count' as any,
218
+ 'indexed_count' as any,
219
+ 'vector_indexed_count' as any,
220
+ 'refreshed_at' as any,
221
+ 'organization_id' as any,
222
+ ])
223
+ .where('entity_type' as any, 'in', entityTypes)
224
+ .where('with_deleted' as any, '=', withDeleted)
225
+ .orderBy('refreshed_at' as any, 'desc')
226
+ query = batch.tenantId == null
227
+ ? query.where('tenant_id' as any, 'is', null as any)
228
+ : query.where('tenant_id' as any, '=', batch.tenantId)
229
+ query = applyOrganizationCondition(query as any, 'organization_id', batch.organizationId ?? null)
230
+
231
+ const rows = await query.execute() as Array<CoverageRow & { entity_type: string }>
232
+ for (const row of rows ?? []) {
233
+ const entityType = String(row.entity_type || '')
234
+ // Rows are ordered by refreshed_at desc, so the first row seen per entity is the latest.
235
+ if (!entityType || result.has(entityType)) continue
236
+ const refreshedAt = row.refreshed_at instanceof Date
237
+ ? row.refreshed_at
238
+ : (row.refreshed_at ? new Date(row.refreshed_at) : null)
239
+ result.set(entityType, {
240
+ base_count: row.base_count,
241
+ indexed_count: row.indexed_count,
242
+ vector_indexed_count: row.vector_indexed_count,
243
+ refreshed_at: refreshedAt ?? null,
244
+ baseCount: toCount(row.base_count),
245
+ indexedCount: toCount(row.indexed_count),
246
+ vectorIndexedCount: toCount(row.vector_indexed_count),
247
+ })
248
+ }
249
+ return result
250
+ }
251
+
189
252
  export async function applyCoverageAdjustments(
190
253
  em: EntityManager,
191
254
  adjustments: CoverageAdjustment[]
@@ -3,6 +3,7 @@ import { SortDir } from '@open-mercato/shared/lib/query/types'
3
3
  import type { EntityId } from '@open-mercato/shared/modules/entities'
4
4
  import type { EntityManager } from '@mikro-orm/postgresql'
5
5
  import { BasicQueryEngine, resolveEntityTableName, resolveRegisteredEntityTableName } from '@open-mercato/shared/lib/query/engine'
6
+ import { isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
6
7
  import { type Kysely, sql, type RawBuilder } from 'kysely'
7
8
  import type { EventBus } from '@open-mercato/events'
8
9
  import { readCoverageSnapshot, refreshCoverageSnapshot } from './coverage'
@@ -1064,32 +1065,44 @@ export class HybridQueryEngine implements QueryEngine {
1064
1065
  if (cached !== undefined) return cached
1065
1066
  let result = false
1066
1067
  try {
1067
- const db = this.getDb() as any
1068
- const row = await db
1069
- .selectFrom('custom_entities')
1070
- .select('id')
1071
- .where('entity_id', '=', entity)
1072
- .where('is_active', '=', true)
1073
- .executeTakeFirst()
1074
- if (row) {
1075
- result = true
1076
- } else if (resolveRegisteredEntityTableName(this.em, entity) !== null) {
1077
- // An id backed by a registered ORM table is never doc-storage-backed by
1078
- // inference: stray `custom_entities_storage` rows for such an id (e.g. written
1079
- // through the generic entities data engine) must not hijack every list/detail
1080
- // read for the whole entity type away from its base table (#2939). Surfaces
1081
- // that intentionally read doc records for a dual-declared id pass
1082
- // `forceCustomEntityStorage` in QueryOptions instead.
1068
+ if (isOrmBackedSystemEntityId(this.em, entity)) {
1069
+ // An id backed by a registered ORM table is never doc-storage-backed. Classify it
1070
+ // as its base table BEFORE probing `custom_entities`, so neither a stray
1071
+ // `custom_entities_storage` row nor an active `custom_entities` row (e.g. a
1072
+ // presentation-metadata overlay for a system entity) can hijack every list/detail
1073
+ // read for the whole entity type away from its base table (#2939). This mirrors the
1074
+ // ORM-backed-first ordering already used by the records API (`classifyRecordsEntity`)
1075
+ // and the doc-storage write guard (`assertCustomEntityStorageEntityId`); without it
1076
+ // the read classifier alone trusted an active registration row over ORM-table
1077
+ // resolution. Surfaces that intentionally read doc records for a dual-declared id
1078
+ // pass `forceCustomEntityStorage` in QueryOptions instead.
1083
1079
  result = false
1084
1080
  } else {
1085
- // Read/write symmetry. Records written through the entities data engine
1086
- // (`de.createCustomEntityRecord`) always land in `custom_entities_storage`,
1087
- // even for module-declared custom entities whose id is also a frozen system
1088
- // id — those are NEVER registered in `custom_entities` (install treats a
1089
- // system id as non-registrable). Without this fallback the query routes to
1090
- // the empty ORM/index path and those records are write-only (created with
1091
- // 200 but unreadable on the edit form).
1092
- result = await this.hasCustomEntityStorageRows(entity)
1081
+ const db = this.getDb() as any
1082
+ const row = await db
1083
+ .selectFrom('custom_entities')
1084
+ .select('id')
1085
+ .where('entity_id', '=', entity)
1086
+ .where('is_active', '=', true)
1087
+ .executeTakeFirst()
1088
+ if (row) {
1089
+ result = true
1090
+ } else if (resolveRegisteredEntityTableName(this.em, entity) !== null) {
1091
+ // A non-registry id whose entity segment collides with an ORM class name (e.g.
1092
+ // `user:todo` vs the example module's `Todo`) resolves to a table but is NOT an
1093
+ // ORM-backed system entity, so it is not short-circuited above. Stray
1094
+ // `custom_entities_storage` rows for such an id must not hijack reads either.
1095
+ result = false
1096
+ } else {
1097
+ // Read/write symmetry. Records written through the entities data engine
1098
+ // (`de.createCustomEntityRecord`) always land in `custom_entities_storage`,
1099
+ // even for module-declared custom entities whose id is also a frozen system
1100
+ // id — those are NEVER registered in `custom_entities` (install treats a
1101
+ // system id as non-registrable). Without this fallback the query routes to
1102
+ // the empty ORM/index path and those records are write-only (created with
1103
+ // 200 but unreadable on the edit form).
1104
+ result = await this.hasCustomEntityStorageRows(entity)
1105
+ }
1093
1106
  }
1094
1107
  } catch {
1095
1108
  result = false
@@ -3,11 +3,10 @@ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
3
3
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
4
  import { resolveCrudRecordId, parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
5
5
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
6
- import type { EntityManager } from '@mikro-orm/postgresql'
7
- import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
8
- import { ResourcesResource, ResourcesResourceType } from '../data/entities'
6
+ import { ResourcesResourceType } from '../data/entities'
9
7
  import { resourcesResourceTypeCreateSchema, resourcesResourceTypeUpdateSchema } from '../data/validators'
10
8
  import { sanitizeSearchTerm } from './helpers'
9
+ import { attachResourceTypeCounts } from './resourceTypeCounts'
11
10
  import { E } from '#generated/entities.ids.generated'
12
11
  import { createResourcesCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from './openapi'
13
12
 
@@ -47,6 +46,7 @@ const listSchema = z
47
46
  ids: z.string().optional(),
48
47
  sortField: z.string().optional(),
49
48
  sortDir: z.enum(['asc', 'desc']).optional(),
49
+ withResourceCounts: z.string().optional(),
50
50
  })
51
51
  .passthrough()
52
52
 
@@ -131,47 +131,7 @@ const crud = makeCrudRoute({
131
131
  },
132
132
  },
133
133
  hooks: {
134
- afterList: async (payload, ctx) => {
135
- const items: Array<Record<string, unknown>> = Array.isArray(payload?.items)
136
- ? (payload.items as Array<Record<string, unknown>>)
137
- : []
138
- if (!items.length) return
139
- const typeIds = items
140
- .map((item) => (typeof item.id === 'string' ? item.id : null))
141
- .filter((id): id is string => typeof id === 'string' && id.length > 0)
142
- if (!typeIds.length) return
143
- const em = (ctx.container.resolve('em') as EntityManager).fork()
144
- const tenantId = ctx.organizationScope?.tenantId ?? ctx.auth?.tenantId ?? null
145
- const orgIds = ctx.organizationIds ?? ctx.organizationScope?.filterIds ?? null
146
- const orgFilter =
147
- Array.isArray(orgIds) && orgIds.length > 0
148
- ? { $in: orgIds }
149
- : ctx.selectedOrganizationId ?? ctx.organizationScope?.selectedId ?? null
150
- const scope = { tenantId, organizationId: ctx.selectedOrganizationId ?? ctx.organizationScope?.selectedId ?? null }
151
- const resources = await findWithDecryption(
152
- em,
153
- ResourcesResource,
154
- {
155
- resourceTypeId: { $in: typeIds },
156
- deletedAt: null,
157
- ...(tenantId ? { tenantId } : {}),
158
- ...(orgFilter ? { organizationId: orgFilter } : {}),
159
- },
160
- { fields: ['id', 'resourceTypeId'] },
161
- scope,
162
- )
163
- const countMap = new Map<string, number>()
164
- resources.forEach((resource) => {
165
- const typeId = resource.resourceTypeId ?? null
166
- if (!typeId) return
167
- countMap.set(typeId, (countMap.get(typeId) ?? 0) + 1)
168
- })
169
- items.forEach((item) => {
170
- const id = typeof item.id === 'string' ? item.id : null
171
- if (!id) return
172
- item.resourceCount = countMap.get(id) ?? 0
173
- })
174
- },
134
+ afterList: (payload, ctx) => attachResourceTypeCounts(payload, ctx),
175
135
  },
176
136
  })
177
137
 
@@ -0,0 +1,74 @@
1
+ import type { Kysely } from 'kysely'
2
+ import { sql } from 'kysely'
3
+ import { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'
4
+ import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
5
+
6
+ type ResourceCountDatabase = {
7
+ resources_resources: {
8
+ resource_type_id: string | null
9
+ deleted_at: Date | null
10
+ tenant_id: string | null
11
+ organization_id: string | null
12
+ }
13
+ }
14
+
15
+ type KyselyCapableEntityManager = { getKysely: () => Kysely<ResourceCountDatabase> }
16
+
17
+ type ResourceTypeCountContext = Pick<
18
+ CrudCtx,
19
+ 'container' | 'auth' | 'organizationScope' | 'organizationIds' | 'selectedOrganizationId'
20
+ > & { query?: { withResourceCounts?: unknown } }
21
+
22
+ type ResourceTypeListPayload = { items?: unknown } | null | undefined
23
+
24
+ type ResourceCountRow = { resource_type_id: string | null; count: string | number }
25
+
26
+ export async function attachResourceTypeCounts(
27
+ payload: ResourceTypeListPayload,
28
+ ctx: ResourceTypeCountContext,
29
+ ): Promise<void> {
30
+ // Resource counts are opt-in: only callers that render the count (the
31
+ // resource-types admin table and its delete gate) pass `withResourceCounts`.
32
+ // Select/filter callers (resource dropdowns, group labels) skip the scan
33
+ // entirely so ordinary type lookups no longer pay an O(resources) cost.
34
+ if (!parseBooleanFromUnknown(ctx.query?.withResourceCounts)) return
35
+
36
+ const items = Array.isArray(payload?.items)
37
+ ? (payload!.items as Array<Record<string, unknown>>)
38
+ : []
39
+ if (!items.length) return
40
+
41
+ const typeIds = items
42
+ .map((item) => (typeof item.id === 'string' ? item.id : null))
43
+ .filter((id): id is string => typeof id === 'string' && id.length > 0)
44
+ if (!typeIds.length) return
45
+
46
+ const tenantId = ctx.organizationScope?.tenantId ?? ctx.auth?.tenantId ?? null
47
+ const orgIds = ctx.organizationIds ?? ctx.organizationScope?.filterIds ?? null
48
+ const singleOrgId = ctx.selectedOrganizationId ?? ctx.organizationScope?.selectedId ?? null
49
+
50
+ const db = (ctx.container.resolve('em') as unknown as KyselyCapableEntityManager).getKysely()
51
+ let query = db
52
+ .selectFrom('resources_resources')
53
+ .select(['resource_type_id', sql<string>`count(*)`.as('count')])
54
+ .where('resource_type_id', 'in', typeIds)
55
+ .where('deleted_at', 'is', null)
56
+ if (tenantId) query = query.where('tenant_id', '=', tenantId)
57
+ if (Array.isArray(orgIds) && orgIds.length > 0) {
58
+ query = query.where('organization_id', 'in', orgIds)
59
+ } else if (singleOrgId) {
60
+ query = query.where('organization_id', '=', singleOrgId)
61
+ }
62
+ const rows = (await query.groupBy('resource_type_id').execute()) as ResourceCountRow[]
63
+
64
+ const countMap = new Map<string, number>()
65
+ for (const row of rows) {
66
+ if (typeof row.resource_type_id !== 'string') continue
67
+ const count = typeof row.count === 'string' ? Number.parseInt(row.count, 10) : Number(row.count)
68
+ countMap.set(row.resource_type_id, Number.isFinite(count) ? count : 0)
69
+ }
70
+ for (const item of items) {
71
+ if (typeof item.id !== 'string') continue
72
+ item.resourceCount = countMap.get(item.id) ?? 0
73
+ }
74
+ }
@@ -34,7 +34,7 @@ export default function ResourcesResourceTypeEditPage({ params }: { params?: { i
34
34
  setIsNotFound(false)
35
35
  try {
36
36
  const payload = await readApiResultOrThrow<ResourceTypesResponse>(
37
- `/api/resources/resource-types?ids=${encodeURIComponent(resourceTypeId)}&page=1&pageSize=1`,
37
+ `/api/resources/resource-types?ids=${encodeURIComponent(resourceTypeId)}&page=1&pageSize=1&withResourceCounts=true`,
38
38
  undefined,
39
39
  { errorMessage: t('resources.resourceTypes.errors.load', 'Failed to load resource types.') },
40
40
  )
@@ -208,6 +208,7 @@ export default function ResourcesResourceTypesPage() {
208
208
  const params = new URLSearchParams({
209
209
  page: String(page),
210
210
  pageSize: String(PAGE_SIZE),
211
+ withResourceCounts: 'true',
211
212
  })
212
213
  const sort = sorting[0]
213
214
  if (sort?.id) {
@@ -11,6 +11,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
11
11
  import { apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
12
12
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
13
13
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
+ import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
14
15
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
15
16
  import { computeAvailableReturnQuantity } from '@open-mercato/core/modules/sales/lib/returnQuantity'
16
17
  import { handleSectionMutationError } from './optimisticLock'
@@ -136,7 +137,9 @@ export function ReturnDialog({ open, orderId, lines, documentUpdatedAt, onClose,
136
137
  onClose()
137
138
  return
138
139
  }
139
- flash(t('sales.returns.errors.create', 'Failed to create return.'), 'error')
140
+ const normalized = normalizeCrudServerError(err)
141
+ const fallback = t('sales.returns.errors.create', 'Failed to create return.')
142
+ flash(normalized.message || fallback, 'error')
140
143
  } finally {
141
144
  setSaving(false)
142
145
  }
@@ -255,6 +255,20 @@ const extractShipmentAddressSnapshot = (
255
255
  return normalizeAddressSnapshot(raw as Record<string, unknown>)
256
256
  }
257
257
 
258
+ const addressOptionsSignature = (options: ShipmentAddressOption[]): string =>
259
+ options.map((option) => `${snapshotKey(option.snapshot) ?? ''}::${option.id}`).join('||')
260
+
261
+ function useStableAddressOptions(options: ShipmentAddressOption[]): ShipmentAddressOption[] {
262
+ const signature = addressOptionsSignature(options)
263
+ const stableRef = React.useRef(options)
264
+ const signatureRef = React.useRef(signature)
265
+ if (signatureRef.current !== signature) {
266
+ signatureRef.current = signature
267
+ stableRef.current = options
268
+ }
269
+ return stableRef.current
270
+ }
271
+
258
272
  export function ShipmentDialog({
259
273
  open,
260
274
  mode,
@@ -323,7 +337,7 @@ export function ShipmentDialog({
323
337
  [shipmentAddressSnapshot, t],
324
338
  )
325
339
 
326
- const baseAddressOptions = React.useMemo(
340
+ const computedBaseAddressOptions = React.useMemo(
327
341
  () =>
328
342
  dedupeAddressOptions(
329
343
  [shippingAddressOption, shipmentAddressOption].filter(
@@ -332,6 +346,7 @@ export function ShipmentDialog({
332
346
  ),
333
347
  [shipmentAddressOption, shippingAddressOption],
334
348
  )
349
+ const baseAddressOptions = useStableAddressOptions(computedBaseAddressOptions)
335
350
 
336
351
  const preferredAddressId = React.useMemo(() => {
337
352
  const shipmentKey = snapshotKey(shipmentAddressSnapshot)
@@ -452,7 +467,10 @@ export function ShipmentDialog({
452
467
 
453
468
  const mergeAddressOptions = React.useCallback(
454
469
  (options: ShipmentAddressOption[]) =>
455
- setAddressOptions((prev) => dedupeAddressOptions([...prev, ...options])),
470
+ setAddressOptions((prev) => {
471
+ const next = dedupeAddressOptions([...prev, ...options])
472
+ return next.length === prev.length ? prev : next
473
+ }),
456
474
  [],
457
475
  )
458
476
 
@@ -7,6 +7,7 @@ import { registerOptimisticLockReaders } from '@open-mercato/shared/lib/crud/opt
7
7
  import { DefaultSalesCalculationService } from './services/salesCalculationService'
8
8
  import { DefaultTaxCalculationService } from './services/taxCalculationService'
9
9
  import { SalesDocumentNumberGenerator } from './services/salesDocumentNumberGenerator'
10
+ import { DefaultSalesOrderService } from './services/salesOrderService'
10
11
  import {
11
12
  SalesOrder,
12
13
  SalesOrderLine,
@@ -85,6 +86,11 @@ export function register(container: AppContainer) {
85
86
  })
86
87
  .singleton()
87
88
  .proxy(),
89
+ salesOrderService: asFunction(({ em }: AppCradle) => {
90
+ return new DefaultSalesOrderService(em)
91
+ })
92
+ .singleton()
93
+ .proxy(),
88
94
  SalesOrder: asValue(SalesOrder),
89
95
  SalesOrderLine: asValue(SalesOrderLine),
90
96
  SalesOrderAdjustment: asValue(SalesOrderAdjustment),
@@ -0,0 +1,38 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
+ import { SalesOrder } from '../data/entities'
4
+
5
+ export type SalesOrderLookupScope = {
6
+ organizationId: string
7
+ tenantId: string
8
+ }
9
+
10
+ export interface SalesOrderService {
11
+ findByExternalReference(
12
+ externalReference: string,
13
+ scope: SalesOrderLookupScope,
14
+ ): Promise<SalesOrder | null>
15
+ }
16
+
17
+ export class DefaultSalesOrderService implements SalesOrderService {
18
+ constructor(private readonly em: EntityManager) {}
19
+
20
+ async findByExternalReference(
21
+ externalReference: string,
22
+ scope: SalesOrderLookupScope,
23
+ ): Promise<SalesOrder | null> {
24
+ if (!externalReference) return null
25
+ return findOneWithDecryption(
26
+ this.em,
27
+ SalesOrder,
28
+ {
29
+ externalReference,
30
+ organizationId: scope.organizationId,
31
+ tenantId: scope.tenantId,
32
+ deletedAt: null,
33
+ },
34
+ undefined,
35
+ { tenantId: scope.tenantId, organizationId: scope.organizationId },
36
+ )
37
+ }
38
+ }