@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.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/carrier_shipment_idempotency_key/index.js +19 -0
- package/dist/generated/entities/carrier_shipment_idempotency_key/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +10 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/lib/productResolution.js +45 -0
- package/dist/modules/catalog/lib/productResolution.js.map +7 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js +1 -1
- package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
- package/dist/modules/currencies/api/fetch-configs/route.js +34 -0
- package/dist/modules/currencies/api/fetch-configs/route.js.map +2 -2
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js +17 -15
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +176 -161
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +75 -59
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +17 -10
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +6 -4
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +2 -2
- package/dist/modules/customers/components/detail/AiActionChips.js +5 -2
- package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +19 -21
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +1 -1
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +9 -45
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +11 -17
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +12 -10
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/data_sync/lib/id-mapping.js +46 -1
- package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/syncRunStatus.js +30 -0
- package/dist/modules/data_sync/lib/syncRunStatus.js.map +7 -0
- package/dist/modules/entities/api/records.js +6 -0
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/feature_toggles/api/global/route.js +17 -15
- package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
- package/dist/modules/feature_toggles/api/openapi.js +2 -2
- package/dist/modules/feature_toggles/api/openapi.js.map +2 -2
- package/dist/modules/feature_toggles/commands/global.js +14 -0
- package/dist/modules/feature_toggles/commands/global.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +6 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +3 -1
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +12 -5
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js +10 -1
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js.map +2 -2
- package/dist/modules/payment_gateways/api/cancel/route.js +5 -0
- package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/capture/route.js +5 -0
- package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/refund/route.js +5 -0
- package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +52 -25
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/status-machine.js +13 -0
- package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/route.js +8 -2
- package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
- package/dist/modules/perspectives/data/entities.js +33 -3
- package/dist/modules/perspectives/data/entities.js.map +2 -2
- package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js +63 -0
- package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js.map +7 -0
- package/dist/modules/perspectives/services/perspectiveService.js +17 -0
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/dist/modules/planner/commands/availability-weekly.js +33 -1
- package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/progress/api/active/route.js +1 -1
- package/dist/modules/progress/api/active/route.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js.map +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js +33 -8
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/query_index/api/status.js +19 -24
- package/dist/modules/query_index/api/status.js.map +3 -3
- package/dist/modules/query_index/lib/coverage.js +35 -0
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +11 -6
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/resources/api/resource-types.js +5 -37
- package/dist/modules/resources/api/resource-types.js.map +2 -2
- package/dist/modules/resources/api/resourceTypeCounts.js +35 -0
- package/dist/modules/resources/api/resourceTypeCounts.js.map +7 -0
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +1 -1
- package/dist/modules/resources/backend/resources/resource-types/page.js +2 -1
- package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnDialog.js +4 -1
- package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +17 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/di.js +4 -0
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/services/salesOrderService.js +26 -0
- package/dist/modules/sales/services/salesOrderService.js.map +7 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js +8 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/shipping_carriers/data/entities.js +38 -0
- package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
- package/dist/modules/shipping_carriers/data/validators.js +2 -1
- package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-idempotency.js +76 -0
- package/dist/modules/shipping_carriers/lib/shipment-idempotency.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +2 -1
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +20 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +82 -43
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js +14 -0
- package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-ui/ListView.js +15 -6
- package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +9 -18
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/generated/entities/carrier_shipment_idempotency_key/index.ts +8 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +10 -0
- package/package.json +9 -8
- package/src/modules/catalog/lib/productResolution.ts +75 -0
- package/src/modules/communication_channels/workers/channel-import-history.ts +1 -1
- package/src/modules/currencies/api/fetch-configs/route.ts +36 -0
- package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +20 -17
- package/src/modules/currencies/i18n/de.json +2 -1
- package/src/modules/currencies/i18n/en.json +2 -1
- package/src/modules/currencies/i18n/es.json +2 -1
- package/src/modules/currencies/i18n/pl.json +2 -1
- package/src/modules/customers/api/companies/[id]/route.ts +214 -189
- package/src/modules/customers/api/people/[id]/route.ts +106 -80
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +19 -12
- package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +8 -6
- package/src/modules/customers/components/detail/AiActionChips.tsx +6 -3
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +16 -18
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +1 -1
- package/src/modules/data_sync/backend/data-sync/page.tsx +11 -58
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +13 -20
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +10 -8
- package/src/modules/data_sync/lib/id-mapping.ts +58 -1
- package/src/modules/data_sync/lib/sync-engine.ts +2 -2
- package/src/modules/data_sync/lib/syncRunStatus.ts +39 -0
- package/src/modules/entities/api/records.ts +14 -0
- package/src/modules/feature_toggles/api/global/route.ts +16 -14
- package/src/modules/feature_toggles/api/openapi.ts +2 -2
- package/src/modules/feature_toggles/commands/global.ts +15 -0
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +7 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +2 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +14 -4
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.ts +15 -1
- package/src/modules/payment_gateways/api/cancel/route.ts +5 -0
- package/src/modules/payment_gateways/api/capture/route.ts +5 -0
- package/src/modules/payment_gateways/api/refund/route.ts +5 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +58 -25
- package/src/modules/payment_gateways/lib/status-machine.ts +16 -0
- package/src/modules/perspectives/api/[tableId]/route.ts +8 -2
- package/src/modules/perspectives/data/entities.ts +43 -3
- package/src/modules/perspectives/migrations/.snapshot-open-mercato.json +69 -25
- package/src/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.ts +65 -0
- package/src/modules/perspectives/services/perspectiveService.ts +17 -0
- package/src/modules/planner/commands/availability-weekly.ts +48 -1
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +31 -10
- package/src/modules/progress/api/active/route.ts +1 -1
- package/src/modules/progress/lib/progressService.ts +2 -2
- package/src/modules/progress/lib/progressServiceImpl.ts +33 -8
- package/src/modules/query_index/api/status.ts +28 -32
- package/src/modules/query_index/lib/coverage.ts +63 -0
- package/src/modules/query_index/lib/engine.ts +37 -24
- package/src/modules/resources/api/resource-types.ts +4 -44
- package/src/modules/resources/api/resourceTypeCounts.ts +74 -0
- package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
- package/src/modules/resources/backend/resources/resource-types/page.tsx +1 -0
- package/src/modules/sales/components/documents/ReturnDialog.tsx +4 -1
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +20 -2
- package/src/modules/sales/di.ts +6 -0
- package/src/modules/sales/services/salesOrderService.ts +38 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +8 -0
- package/src/modules/shipping_carriers/data/entities.ts +33 -0
- package/src/modules/shipping_carriers/data/validators.ts +1 -0
- package/src/modules/shipping_carriers/i18n/de.json +1 -0
- package/src/modules/shipping_carriers/i18n/en.json +1 -0
- package/src/modules/shipping_carriers/i18n/es.json +1 -0
- package/src/modules/shipping_carriers/i18n/pl.json +1 -0
- package/src/modules/shipping_carriers/lib/shipment-idempotency.ts +119 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +3 -1
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +24 -2
- package/src/modules/shipping_carriers/lib/shipping-service.ts +87 -43
- package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +197 -0
- package/src/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.ts +14 -0
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/lib/timesheets-ui/ListView.tsx +15 -7
- package/src/modules/translations/components/TranslationManager.tsx +17 -24
- package/src/modules/translations/i18n/de.json +1 -0
- package/src/modules/translations/i18n/en.json +1 -0
- package/src/modules/translations/i18n/es.json +1 -0
- 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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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 {
|
|
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
|
|
293
|
+
const coverageScope = {
|
|
294
|
+
tenantId: tenantId ?? null,
|
|
295
|
+
organizationId,
|
|
296
|
+
withDeleted: false,
|
|
297
|
+
} as const
|
|
292
298
|
const entitiesNeedingRefresh = new Set<string>()
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
.
|
|
1071
|
-
|
|
1072
|
-
.
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
//
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
|
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:
|
|
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
|
)
|
|
@@ -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
|
-
|
|
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
|
|
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) =>
|
|
470
|
+
setAddressOptions((prev) => {
|
|
471
|
+
const next = dedupeAddressOptions([...prev, ...options])
|
|
472
|
+
return next.length === prev.length ? prev : next
|
|
473
|
+
}),
|
|
456
474
|
[],
|
|
457
475
|
)
|
|
458
476
|
|
package/src/modules/sales/di.ts
CHANGED
|
@@ -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
|
+
}
|