@open-mercato/core 0.6.7-develop.6775.1.c2313bb8a3 → 0.6.7-develop.6784.1.f80b9afce5
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 +1 -1
- package/AGENTS.md +3 -3
- package/agentic/standalone-guide.md +13 -0
- package/dist/generated/entities/organization/index.js +2 -0
- package/dist/generated/entities/organization/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/cli.js +1 -1
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +4 -6
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +1 -0
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +29 -44
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/product-seo/widget.js +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.js.map +2 -2
- package/dist/modules/communication_channels/lib/access-control.js +2 -2
- package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
- package/dist/modules/customer_accounts/api/invitations/accept.js +1 -2
- package/dist/modules/customer_accounts/api/invitations/accept.js.map +2 -2
- package/dist/modules/customer_accounts/api/login.js +4 -2
- package/dist/modules/customer_accounts/api/login.js.map +2 -2
- package/dist/modules/customer_accounts/api/magic-link/verify.js +4 -2
- package/dist/modules/customer_accounts/api/magic-link/verify.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/feature-check.js +5 -4
- package/dist/modules/customer_accounts/api/portal/feature-check.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/nav.js +6 -4
- package/dist/modules/customer_accounts/api/portal/nav.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/profile.js +5 -1
- package/dist/modules/customer_accounts/api/portal/profile.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/sessions-refresh.js +3 -3
- package/dist/modules/customer_accounts/api/portal/sessions-refresh.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js +7 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuthServer.js +2 -1
- package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerRbacService.js +12 -3
- package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
- package/dist/modules/customer_accounts/setup.js +6 -0
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/customers/lib/visibilityFilter.js +6 -4
- package/dist/modules/customers/lib/visibilityFilter.js.map +2 -2
- package/dist/modules/dashboards/api/layout/[itemId]/route.js +6 -3
- package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
- package/dist/modules/dashboards/api/layout/route.js +22 -9
- package/dist/modules/dashboards/api/layout/route.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +9 -3
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +9 -3
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/catalog.js +6 -3
- package/dist/modules/dashboards/api/widgets/catalog.js.map +2 -2
- package/dist/modules/dashboards/lib/access.js +5 -3
- package/dist/modules/dashboards/lib/access.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +10 -3
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/api/organizations/route.js +7 -0
- package/dist/modules/directory/api/organizations/route.js.map +3 -3
- package/dist/modules/directory/backend/directory/branding/page.js +31 -8
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/directory/commands/organizations.js +8 -1
- package/dist/modules/directory/commands/organizations.js.map +2 -2
- package/dist/modules/directory/data/entities.js +4 -0
- package/dist/modules/directory/data/entities.js.map +2 -2
- package/dist/modules/directory/data/validators.js +2 -0
- package/dist/modules/directory/data/validators.js.map +2 -2
- package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js +13 -0
- package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js.map +7 -0
- package/dist/modules/entities/lib/entityAcl.js +47 -15
- package/dist/modules/entities/lib/entityAcl.js.map +3 -3
- package/dist/modules/inbox_ops/ai-tools.js +5 -2
- package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
- package/dist/modules/messages/lib/routeHelpers.js +1 -3
- package/dist/modules/messages/lib/routeHelpers.js.map +2 -2
- package/dist/modules/notifications/lib/notificationRecipients.js +6 -7
- package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
- package/dist/modules/query_index/di.js +3 -0
- package/dist/modules/query_index/di.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +2 -2
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +83 -17
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +13 -7
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/query_index/subscribers/coverage_refresh.js +4 -0
- package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
- package/dist/modules/query_index/subscribers/delete_one.js +53 -1
- package/dist/modules/query_index/subscribers/delete_one.js.map +3 -3
- package/dist/modules/query_index/subscribers/upsert_one.js +60 -0
- package/dist/modules/query_index/subscribers/upsert_one.js.map +3 -3
- package/dist/modules/sales/commands/documents.js +17 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/staff/api/interceptors.js +5 -3
- package/dist/modules/staff/api/interceptors.js.map +2 -2
- package/dist/modules/sync_excel/api/upload/route.js +15 -2
- package/dist/modules/sync_excel/api/upload/route.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +7 -7
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/generated/entities/organization/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/auth/AGENTS.md +5 -3
- package/src/modules/auth/cli.ts +1 -1
- package/src/modules/auth/lib/backendChrome.tsx +5 -11
- package/src/modules/auth/lib/setup-app.ts +1 -0
- package/src/modules/auth/services/rbacService.ts +41 -46
- package/src/modules/catalog/widgets/injection/product-seo/widget.ts +1 -1
- package/src/modules/communication_channels/lib/access-control.ts +3 -3
- package/src/modules/customer_accounts/AGENTS.md +3 -3
- package/src/modules/customer_accounts/api/invitations/accept.ts +1 -2
- package/src/modules/customer_accounts/api/login.ts +4 -2
- package/src/modules/customer_accounts/api/magic-link/verify.ts +4 -2
- package/src/modules/customer_accounts/api/portal/feature-check.ts +5 -7
- package/src/modules/customer_accounts/api/portal/nav.ts +5 -3
- package/src/modules/customer_accounts/api/portal/profile.ts +5 -1
- package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +3 -3
- package/src/modules/customer_accounts/lib/customerAuth.ts +8 -2
- package/src/modules/customer_accounts/lib/customerAuthServer.ts +1 -0
- package/src/modules/customer_accounts/services/customerRbacService.ts +16 -3
- package/src/modules/customer_accounts/setup.ts +7 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +1 -3
- package/src/modules/customers/lib/visibilityFilter.ts +7 -5
- package/src/modules/dashboards/api/layout/[itemId]/route.ts +6 -3
- package/src/modules/dashboards/api/layout/route.ts +22 -9
- package/src/modules/dashboards/api/roles/widgets/route.ts +9 -3
- package/src/modules/dashboards/api/users/widgets/route.ts +9 -3
- package/src/modules/dashboards/api/widgets/catalog.ts +6 -3
- package/src/modules/dashboards/lib/access.ts +5 -3
- package/src/modules/directory/api/organization-branding/route.ts +12 -1
- package/src/modules/directory/api/organizations/route.ts +7 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +34 -9
- package/src/modules/directory/commands/organizations.ts +9 -1
- package/src/modules/directory/data/entities.ts +3 -0
- package/src/modules/directory/data/validators.ts +2 -0
- package/src/modules/directory/i18n/de.json +3 -1
- package/src/modules/directory/i18n/en.json +3 -1
- package/src/modules/directory/i18n/es.json +3 -1
- package/src/modules/directory/i18n/pl.json +3 -1
- package/src/modules/directory/migrations/.snapshot-open-mercato.json +18 -1
- package/src/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.ts +13 -0
- package/src/modules/entities/lib/entityAcl.ts +47 -18
- package/src/modules/inbox_ops/ai-tools.ts +5 -3
- package/src/modules/messages/lib/routeHelpers.ts +4 -6
- package/src/modules/notifications/lib/notificationRecipients.ts +6 -8
- package/src/modules/query_index/di.ts +6 -0
- package/src/modules/query_index/lib/coverage.ts +9 -6
- package/src/modules/query_index/lib/indexer.ts +105 -19
- package/src/modules/query_index/lib/search-tokens.ts +19 -8
- package/src/modules/query_index/subscribers/coverage_refresh.ts +4 -0
- package/src/modules/query_index/subscribers/delete_one.ts +65 -1
- package/src/modules/query_index/subscribers/upsert_one.ts +68 -1
- package/src/modules/sales/commands/documents.ts +27 -0
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/staff/api/interceptors.ts +6 -4
- package/src/modules/sync_excel/api/upload/route.ts +15 -2
- package/src/modules/workflows/lib/activity-executor.ts +12 -10
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
|
|
2
|
+
import { isReadProjectionAlwaysConsistent } from '@open-mercato/shared/lib/data/consistency'
|
|
2
3
|
import { sql } from 'kysely'
|
|
3
4
|
import { markDeleted } from '../lib/indexer'
|
|
4
5
|
import { applyCoverageAdjustments, createCoverageAdjustments } from '../lib/coverage'
|
|
@@ -39,7 +40,9 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
|
|
|
39
40
|
if (!entityType || !recordId) return
|
|
40
41
|
let organizationId: string | null = payload?.organizationId ?? null
|
|
41
42
|
let tenantId: string | null = payload?.tenantId ?? null
|
|
43
|
+
const suppressCoverage = payload?.suppressCoverage === true
|
|
42
44
|
const coverageDelayMs = typeof payload?.coverageDelayMs === 'number' ? payload.coverageDelayMs : undefined
|
|
45
|
+
const alwaysConsistent = isReadProjectionAlwaysConsistent()
|
|
43
46
|
try {
|
|
44
47
|
const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'organizationId')
|
|
45
48
|
const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'tenantId')
|
|
@@ -55,6 +58,68 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
|
|
|
55
58
|
organizationId = resolvedScope.organizationId
|
|
56
59
|
tenantId = resolvedScope.tenantId
|
|
57
60
|
|
|
61
|
+
if (alwaysConsistent) {
|
|
62
|
+
const db = (em as any).getKysely()
|
|
63
|
+
await db.transaction().execute(async (trx: any) => {
|
|
64
|
+
const { wasActive } = await markDeleted(em, { entityType, recordId, organizationId, tenantId, trx })
|
|
65
|
+
|
|
66
|
+
let baseQuery = trx
|
|
67
|
+
.selectFrom(source.table as any)
|
|
68
|
+
.select(['deleted_at' as any])
|
|
69
|
+
.where('id' as any, '=', recordId)
|
|
70
|
+
if (source.organizationColumn) {
|
|
71
|
+
baseQuery = baseQuery.where(
|
|
72
|
+
source.organizationColumn as any,
|
|
73
|
+
organizationId === null ? 'is' : '=',
|
|
74
|
+
organizationId as any,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
if (source.tenantColumn) {
|
|
78
|
+
baseQuery = baseQuery.where(sql`${sql.ref(source.tenantColumn)} is not distinct from ${tenantId}`)
|
|
79
|
+
}
|
|
80
|
+
const row = await baseQuery.executeTakeFirst() as { deleted_at: Date | null } | undefined
|
|
81
|
+
const baseDelta = !row || row.deleted_at != null ? -1 : 0
|
|
82
|
+
|
|
83
|
+
const baseDeltaOverride =
|
|
84
|
+
typeof payload?.coverageBaseDelta === 'number' ? payload.coverageBaseDelta : undefined
|
|
85
|
+
const indexDeltaOverride =
|
|
86
|
+
typeof payload?.coverageIndexDelta === 'number' ? payload.coverageIndexDelta : undefined
|
|
87
|
+
let effectiveBaseDelta = baseDeltaOverride ?? baseDelta
|
|
88
|
+
let effectiveIndexDelta = indexDeltaOverride ?? (wasActive ? -1 : 0)
|
|
89
|
+
|
|
90
|
+
if (!Number.isFinite(effectiveBaseDelta)) effectiveBaseDelta = 0
|
|
91
|
+
if (!Number.isFinite(effectiveIndexDelta)) effectiveIndexDelta = 0
|
|
92
|
+
|
|
93
|
+
if (effectiveBaseDelta !== 0 || effectiveIndexDelta !== 0) {
|
|
94
|
+
const adjustments = createCoverageAdjustments({
|
|
95
|
+
entityType,
|
|
96
|
+
tenantId: tenantId ?? null,
|
|
97
|
+
organizationId: organizationId ?? null,
|
|
98
|
+
baseDelta: effectiveBaseDelta,
|
|
99
|
+
indexDelta: effectiveIndexDelta,
|
|
100
|
+
})
|
|
101
|
+
if (adjustments.length) {
|
|
102
|
+
await applyCoverageAdjustments(em, adjustments, { trx })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const bus = ctx.resolve<any>('eventBus')
|
|
108
|
+
const shouldRefreshCoverage =
|
|
109
|
+
!suppressCoverage && (coverageDelayMs === undefined || coverageDelayMs >= 0)
|
|
110
|
+
const coverageRefreshDelay = coverageDelayMs ?? 0
|
|
111
|
+
if (shouldRefreshCoverage) {
|
|
112
|
+
await bus.emitEvent('query_index.coverage.refresh', {
|
|
113
|
+
entityType,
|
|
114
|
+
tenantId: tenantId ?? null,
|
|
115
|
+
organizationId: organizationId ?? null,
|
|
116
|
+
delayMs: coverageRefreshDelay,
|
|
117
|
+
}, { rethrowHandlerErrors: true })
|
|
118
|
+
}
|
|
119
|
+
await bus.emitEvent('search.delete_record', { entityId: entityType, recordId, organizationId, tenantId }, { rethrowHandlerErrors: true })
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
58
123
|
const { wasActive } = await markDeleted(em, { entityType, recordId, organizationId, tenantId })
|
|
59
124
|
|
|
60
125
|
let baseDelta = 0
|
|
@@ -110,7 +175,6 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
|
|
|
110
175
|
// awaits this subscriber) so list reads are consistent immediately. The coverage
|
|
111
176
|
// recompute (a COUNT, run inline when delayMs is 0) and the fulltext delete are
|
|
112
177
|
// secondary, so defer them fire-and-forget to keep write/bulk-delete latency bounded.
|
|
113
|
-
const suppressCoverage = payload?.suppressCoverage === true
|
|
114
178
|
const explicitDelayRequested = typeof payload?.coverageDelayMs === 'number'
|
|
115
179
|
const shouldRefreshCoverage =
|
|
116
180
|
!suppressCoverage &&
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
|
|
2
|
-
import {
|
|
2
|
+
import { isReadProjectionAlwaysConsistent } from '@open-mercato/shared/lib/data/consistency'
|
|
3
|
+
import { upsertIndexRow, reindexSearchTokensForRecord, type UpsertIndexResult } from '../lib/indexer'
|
|
3
4
|
import { applyCoverageAdjustments, createCoverageAdjustments } from '../lib/coverage'
|
|
4
5
|
import {
|
|
5
6
|
loadQueryIndexRowScope,
|
|
@@ -26,6 +27,7 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
|
|
|
26
27
|
let tenantId: string | null = payload?.tenantId ?? null
|
|
27
28
|
const suppressCoverage = payload?.suppressCoverage === true
|
|
28
29
|
const coverageDelayMs = typeof payload?.coverageDelayMs === 'number' ? payload.coverageDelayMs : undefined
|
|
30
|
+
const alwaysConsistent = isReadProjectionAlwaysConsistent()
|
|
29
31
|
try {
|
|
30
32
|
const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'organizationId')
|
|
31
33
|
const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'tenantId')
|
|
@@ -44,6 +46,71 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
|
|
|
44
46
|
const searchTokenDoc = typeof payload?.searchTokenDoc === 'object' && payload.searchTokenDoc && !Array.isArray(payload.searchTokenDoc)
|
|
45
47
|
? (payload.searchTokenDoc as Record<string, unknown>)
|
|
46
48
|
: null
|
|
49
|
+
if (alwaysConsistent) {
|
|
50
|
+
const db = (em as any).getKysely()
|
|
51
|
+
let result: UpsertIndexResult | null = null
|
|
52
|
+
await db.transaction().execute(async (trx: any) => {
|
|
53
|
+
result = await upsertIndexRow(em, {
|
|
54
|
+
entityType,
|
|
55
|
+
recordId,
|
|
56
|
+
organizationId,
|
|
57
|
+
tenantId,
|
|
58
|
+
searchTokenDoc,
|
|
59
|
+
deferSearchTokens: false,
|
|
60
|
+
trx,
|
|
61
|
+
})
|
|
62
|
+
if (!suppressCoverage) {
|
|
63
|
+
const doc = result.doc
|
|
64
|
+
const isActive = !!doc && (doc.deleted_at == null || doc.deleted_at === null)
|
|
65
|
+
let baseDelta: number | undefined =
|
|
66
|
+
typeof payload?.coverageBaseDelta === 'number' ? payload.coverageBaseDelta : undefined
|
|
67
|
+
let indexDelta: number | undefined =
|
|
68
|
+
typeof payload?.coverageIndexDelta === 'number' ? payload.coverageIndexDelta : undefined
|
|
69
|
+
const crudAction = typeof payload?.crudAction === 'string' ? payload.crudAction : undefined
|
|
70
|
+
|
|
71
|
+
if (baseDelta === undefined) {
|
|
72
|
+
if (result.revived) baseDelta = 1
|
|
73
|
+
else if (crudAction === 'created') baseDelta = 1
|
|
74
|
+
else baseDelta = 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (indexDelta === undefined) {
|
|
78
|
+
if (isActive && (result.created || result.revived)) indexDelta = 1
|
|
79
|
+
else indexDelta = 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!isActive && baseDelta > 0) baseDelta = 0
|
|
83
|
+
if (!isActive && indexDelta > 0) indexDelta = 0
|
|
84
|
+
if (!Number.isFinite(baseDelta)) baseDelta = 0
|
|
85
|
+
if (!Number.isFinite(indexDelta)) indexDelta = 0
|
|
86
|
+
|
|
87
|
+
const adjustments = createCoverageAdjustments({
|
|
88
|
+
entityType,
|
|
89
|
+
tenantId: tenantId ?? null,
|
|
90
|
+
organizationId: organizationId ?? null,
|
|
91
|
+
baseDelta,
|
|
92
|
+
indexDelta,
|
|
93
|
+
})
|
|
94
|
+
if (adjustments.length) {
|
|
95
|
+
await applyCoverageAdjustments(em, adjustments, { trx })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const bus = ctx.resolve<any>('eventBus')
|
|
101
|
+
const eventScope = { entityType, recordId, organizationId, tenantId }
|
|
102
|
+
await bus.emitEvent('query_index.vectorize_one', eventScope, { rethrowHandlerErrors: true })
|
|
103
|
+
await bus.emitEvent('search.index_record', { entityId: entityType, recordId, organizationId, tenantId }, { rethrowHandlerErrors: true })
|
|
104
|
+
if (!suppressCoverage && coverageDelayMs !== undefined && coverageDelayMs >= 0) {
|
|
105
|
+
await bus.emitEvent('query_index.coverage.refresh', {
|
|
106
|
+
entityType,
|
|
107
|
+
tenantId: tenantId ?? null,
|
|
108
|
+
organizationId: organizationId ?? null,
|
|
109
|
+
delayMs: coverageDelayMs,
|
|
110
|
+
}, { rethrowHandlerErrors: true })
|
|
111
|
+
}
|
|
112
|
+
return
|
|
113
|
+
}
|
|
47
114
|
// Update the projection row synchronously so list reads (`customValues`) are
|
|
48
115
|
// consistent the moment the write returns; defer the heavy search-token rebuild.
|
|
49
116
|
const result = await upsertIndexRow(em, {
|
|
@@ -6686,6 +6686,32 @@ async function assertShippedOrderLineEditable(
|
|
|
6686
6686
|
}
|
|
6687
6687
|
}
|
|
6688
6688
|
|
|
6689
|
+
const FULFILLED_ORDER_STATUS = "fulfilled";
|
|
6690
|
+
|
|
6691
|
+
const isFulfilledOrder = (order: SalesOrder): boolean => {
|
|
6692
|
+
const normalize = (value: string | null | undefined): string =>
|
|
6693
|
+
(value ?? "").trim().toLowerCase();
|
|
6694
|
+
return (
|
|
6695
|
+
normalize(order.status) === FULFILLED_ORDER_STATUS ||
|
|
6696
|
+
normalize(order.fulfillmentStatus) === FULFILLED_ORDER_STATUS
|
|
6697
|
+
);
|
|
6698
|
+
};
|
|
6699
|
+
|
|
6700
|
+
async function assertOrderAcceptsNewLine(
|
|
6701
|
+
order: SalesOrder,
|
|
6702
|
+
existingSnapshot: SalesLineSnapshot | null,
|
|
6703
|
+
): Promise<void> {
|
|
6704
|
+
if (existingSnapshot) return;
|
|
6705
|
+
if (!isFulfilledOrder(order)) return;
|
|
6706
|
+
const { translate } = await resolveTranslations();
|
|
6707
|
+
throw new CrudHttpError(409, {
|
|
6708
|
+
error: translate(
|
|
6709
|
+
"sales.documents.items.errorAddToFulfilled",
|
|
6710
|
+
"You cannot add a new item to a fulfilled order. Change the order status first.",
|
|
6711
|
+
),
|
|
6712
|
+
});
|
|
6713
|
+
}
|
|
6714
|
+
|
|
6689
6715
|
const orderLineUpsertCommand: CommandHandler<
|
|
6690
6716
|
{ body?: Record<string, unknown>; query?: Record<string, unknown> },
|
|
6691
6717
|
{ orderId: string; lineId: string }
|
|
@@ -6730,6 +6756,7 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
6730
6756
|
const existingSnapshot = parsed.id
|
|
6731
6757
|
? (lineSnapshots.find((line) => line.id === parsed.id) ?? null)
|
|
6732
6758
|
: null;
|
|
6759
|
+
await assertOrderAcceptsNewLine(order, existingSnapshot);
|
|
6733
6760
|
await assertShippedOrderLineEditable(em, order, existingSnapshot, parsed);
|
|
6734
6761
|
const priceMode =
|
|
6735
6762
|
parsed.priceMode === "gross"
|
|
@@ -967,6 +967,7 @@
|
|
|
967
967
|
"sales.documents.items.deletedCatalogReference.title": "Dieses Katalogprodukt wurde gelöscht",
|
|
968
968
|
"sales.documents.items.editTitle": "Position bearbeiten",
|
|
969
969
|
"sales.documents.items.empty": "Noch keine Positionen.",
|
|
970
|
+
"sales.documents.items.errorAddToFulfilled": "Zu einem erfüllten Auftrag kann keine neue Position hinzugefügt werden. Ändere zuerst den Auftragsstatus.",
|
|
970
971
|
"sales.documents.items.errorCurrency": "Währung ist erforderlich.",
|
|
971
972
|
"sales.documents.items.errorDelete": "Position konnte nicht gelöscht werden.",
|
|
972
973
|
"sales.documents.items.errorDeleteLast": "Eine Bestellung muss mindestens eine Position enthalten.",
|
|
@@ -967,6 +967,7 @@
|
|
|
967
967
|
"sales.documents.items.deletedCatalogReference.title": "This catalog item was deleted",
|
|
968
968
|
"sales.documents.items.editTitle": "Edit line",
|
|
969
969
|
"sales.documents.items.empty": "No items yet.",
|
|
970
|
+
"sales.documents.items.errorAddToFulfilled": "You cannot add a new item to a fulfilled order. Change the order status first.",
|
|
970
971
|
"sales.documents.items.errorCurrency": "Currency is required.",
|
|
971
972
|
"sales.documents.items.errorDelete": "Failed to delete line.",
|
|
972
973
|
"sales.documents.items.errorDeleteLast": "An order must contain at least one line item.",
|
|
@@ -967,6 +967,7 @@
|
|
|
967
967
|
"sales.documents.items.deletedCatalogReference.title": "Este artículo del catálogo fue eliminado",
|
|
968
968
|
"sales.documents.items.editTitle": "Editar línea",
|
|
969
969
|
"sales.documents.items.empty": "Aún no hay artículos.",
|
|
970
|
+
"sales.documents.items.errorAddToFulfilled": "No puedes añadir un nuevo artículo a un pedido ya completado. Cambia primero el estado del pedido.",
|
|
970
971
|
"sales.documents.items.errorCurrency": "La moneda es obligatoria.",
|
|
971
972
|
"sales.documents.items.errorDelete": "No se pudo eliminar la línea.",
|
|
972
973
|
"sales.documents.items.errorDeleteLast": "Un pedido debe contener al menos una línea.",
|
|
@@ -967,6 +967,7 @@
|
|
|
967
967
|
"sales.documents.items.deletedCatalogReference.title": "Ten produkt katalogowy został usunięty",
|
|
968
968
|
"sales.documents.items.editTitle": "Edytuj pozycję",
|
|
969
969
|
"sales.documents.items.empty": "Brak pozycji.",
|
|
970
|
+
"sales.documents.items.errorAddToFulfilled": "Nie można dodać nowej pozycji do zrealizowanego zamówienia. Najpierw zmień status zamówienia.",
|
|
970
971
|
"sales.documents.items.errorCurrency": "Waluta jest wymagana.",
|
|
971
972
|
"sales.documents.items.errorDelete": "Nie udało się usunąć pozycji.",
|
|
972
973
|
"sales.documents.items.errorDeleteLast": "Zamówienie musi zawierać co najmniej jedną pozycję.",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ApiInterceptor, InterceptorContext } from '@open-mercato/shared/lib/crud/api-interceptor'
|
|
2
|
-
import {
|
|
2
|
+
import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
|
|
3
3
|
import { getStaffMemberByUserId } from '../lib/staffMemberResolver'
|
|
4
4
|
|
|
5
5
|
type RbacServiceLike = {
|
|
@@ -21,7 +21,6 @@ type RbacServiceLike = {
|
|
|
21
21
|
* from `rbacService.userHasAllFeatures`, which is cached and wildcard-aware.
|
|
22
22
|
*/
|
|
23
23
|
async function hasManageAllFeature(context: InterceptorContext): Promise<boolean> {
|
|
24
|
-
if (hasFeature(context.userFeatures, 'staff.timesheets.manage_all')) return true
|
|
25
24
|
try {
|
|
26
25
|
const rbac = context.container.resolve('rbacService') as RbacServiceLike | undefined
|
|
27
26
|
if (rbac?.userHasAllFeatures) {
|
|
@@ -32,9 +31,12 @@ async function hasManageAllFeature(context: InterceptorContext): Promise<boolean
|
|
|
32
31
|
)
|
|
33
32
|
}
|
|
34
33
|
} catch {
|
|
35
|
-
//
|
|
34
|
+
// Fall back to the already-loaded snapshot below.
|
|
36
35
|
}
|
|
37
|
-
return
|
|
36
|
+
return authorizeFeatures(
|
|
37
|
+
['staff.timesheets.manage_all'],
|
|
38
|
+
{ grantedFeatures: context.userFeatures ?? [] },
|
|
39
|
+
)
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export const interceptors: ApiInterceptor[] = [
|
|
@@ -10,7 +10,12 @@ import { buildSuggestedMapping } from '../../lib/mapping'
|
|
|
10
10
|
import { parseCsvPreview } from '../../lib/parser'
|
|
11
11
|
import { createSyncExcelUploadAttachment } from '../../lib/upload-storage'
|
|
12
12
|
import { resolveSyncExcelConcreteScope } from '../../lib/scope'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
isMultipartRequestWithinUploadLimit,
|
|
15
|
+
isMultipartUploadLimitError,
|
|
16
|
+
parseMultipartFormDataWithinUploadLimit,
|
|
17
|
+
resolveDefaultAttachmentMaxUploadBytes,
|
|
18
|
+
} from '../../../attachments/lib/upload-limits'
|
|
14
19
|
|
|
15
20
|
export const metadata = {
|
|
16
21
|
POST: { requireAuth: true, requireFeatures: ['sync_excel.run'] },
|
|
@@ -67,7 +72,15 @@ export async function POST(request: Request) {
|
|
|
67
72
|
return NextResponse.json({ error: 'CSV upload exceeds the maximum upload size.' }, { status: 413 })
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
let formData: FormData
|
|
76
|
+
try {
|
|
77
|
+
formData = await parseMultipartFormDataWithinUploadLimit(request)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (isMultipartUploadLimitError(error)) {
|
|
80
|
+
return NextResponse.json({ error: 'CSV upload exceeds the maximum upload size.' }, { status: 413 })
|
|
81
|
+
}
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
71
84
|
const parsedPayload = multipartSchema.safeParse({
|
|
72
85
|
entityType: typeof formData.get('entityType') === 'string' ? formData.get('entityType') : undefined,
|
|
73
86
|
})
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
type HostLookup,
|
|
23
23
|
} from '@open-mercato/shared/lib/url-safety'
|
|
24
24
|
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
25
|
-
import { hasAllFeatures } from '@open-mercato/shared/security/features'
|
|
26
25
|
import { callWebhookConfigSchema } from '../data/validators'
|
|
27
26
|
import { WorkflowActivityJob, WORKFLOW_ACTIVITIES_QUEUE_NAME } from './activity-queue-types'
|
|
28
27
|
import { logWorkflowEvent } from './event-logger'
|
|
@@ -163,27 +162,29 @@ export interface ActivityContext {
|
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
type RbacFeatureResolver = {
|
|
166
|
-
|
|
165
|
+
userHasAllFeatures: (
|
|
167
166
|
userId: string,
|
|
167
|
+
required: string[],
|
|
168
168
|
opts: { tenantId: string | null; organizationId: string | null }
|
|
169
|
-
) => Promise<
|
|
169
|
+
) => Promise<boolean>
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
async function
|
|
172
|
+
async function workflowUserHasAllFeatures(
|
|
173
173
|
container: AwilixContainer,
|
|
174
174
|
userId: string,
|
|
175
|
+
required: readonly string[],
|
|
175
176
|
tenantId: string | null,
|
|
176
177
|
organizationId: string | null
|
|
177
|
-
): Promise<
|
|
178
|
+
): Promise<boolean> {
|
|
178
179
|
try {
|
|
179
180
|
const rbac = container.resolve('rbacService') as RbacFeatureResolver | undefined
|
|
180
|
-
if (rbac?.
|
|
181
|
-
return await rbac.
|
|
181
|
+
if (rbac?.userHasAllFeatures) {
|
|
182
|
+
return await rbac.userHasAllFeatures(userId, [...required], { tenantId, organizationId })
|
|
182
183
|
}
|
|
183
184
|
} catch {
|
|
184
185
|
// Fail closed below when the workflow executor cannot prove the actor's grants.
|
|
185
186
|
}
|
|
186
|
-
return
|
|
187
|
+
return false
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
export interface ActivityExecutionResult {
|
|
@@ -681,13 +682,14 @@ export async function executeUpdateEntity(
|
|
|
681
682
|
throw new Error('UPDATE_ENTITY requires an authenticated workflow user')
|
|
682
683
|
}
|
|
683
684
|
|
|
684
|
-
const
|
|
685
|
+
const authorized = await workflowUserHasAllFeatures(
|
|
685
686
|
container,
|
|
686
687
|
actorUserId,
|
|
688
|
+
workflowSafeCommand.requiredFeatures,
|
|
687
689
|
context.workflowInstance.tenantId,
|
|
688
690
|
context.workflowInstance.organizationId
|
|
689
691
|
)
|
|
690
|
-
if (!
|
|
692
|
+
if (!authorized) {
|
|
691
693
|
throw new Error('UPDATE_ENTITY command is not authorized')
|
|
692
694
|
}
|
|
693
695
|
|