@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.
Files changed (162) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +3 -3
  3. package/agentic/standalone-guide.md +13 -0
  4. package/dist/generated/entities/organization/index.js +2 -0
  5. package/dist/generated/entities/organization/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +1 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/auth/cli.js +1 -1
  9. package/dist/modules/auth/cli.js.map +2 -2
  10. package/dist/modules/auth/lib/backendChrome.js +4 -6
  11. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  12. package/dist/modules/auth/lib/setup-app.js +1 -0
  13. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  14. package/dist/modules/auth/services/rbacService.js +29 -44
  15. package/dist/modules/auth/services/rbacService.js.map +2 -2
  16. package/dist/modules/catalog/widgets/injection/product-seo/widget.js +1 -1
  17. package/dist/modules/catalog/widgets/injection/product-seo/widget.js.map +2 -2
  18. package/dist/modules/communication_channels/lib/access-control.js +2 -2
  19. package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
  20. package/dist/modules/customer_accounts/api/invitations/accept.js +1 -2
  21. package/dist/modules/customer_accounts/api/invitations/accept.js.map +2 -2
  22. package/dist/modules/customer_accounts/api/login.js +4 -2
  23. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  24. package/dist/modules/customer_accounts/api/magic-link/verify.js +4 -2
  25. package/dist/modules/customer_accounts/api/magic-link/verify.js.map +2 -2
  26. package/dist/modules/customer_accounts/api/portal/feature-check.js +5 -4
  27. package/dist/modules/customer_accounts/api/portal/feature-check.js.map +2 -2
  28. package/dist/modules/customer_accounts/api/portal/nav.js +6 -4
  29. package/dist/modules/customer_accounts/api/portal/nav.js.map +2 -2
  30. package/dist/modules/customer_accounts/api/portal/profile.js +5 -1
  31. package/dist/modules/customer_accounts/api/portal/profile.js.map +2 -2
  32. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js +3 -3
  33. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js.map +2 -2
  34. package/dist/modules/customer_accounts/lib/customerAuth.js +7 -2
  35. package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
  36. package/dist/modules/customer_accounts/lib/customerAuthServer.js +2 -1
  37. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  38. package/dist/modules/customer_accounts/services/customerRbacService.js +12 -3
  39. package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
  40. package/dist/modules/customer_accounts/setup.js +6 -0
  41. package/dist/modules/customer_accounts/setup.js.map +2 -2
  42. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +1 -1
  43. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +2 -2
  44. package/dist/modules/customers/lib/visibilityFilter.js +6 -4
  45. package/dist/modules/customers/lib/visibilityFilter.js.map +2 -2
  46. package/dist/modules/dashboards/api/layout/[itemId]/route.js +6 -3
  47. package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
  48. package/dist/modules/dashboards/api/layout/route.js +22 -9
  49. package/dist/modules/dashboards/api/layout/route.js.map +2 -2
  50. package/dist/modules/dashboards/api/roles/widgets/route.js +9 -3
  51. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  52. package/dist/modules/dashboards/api/users/widgets/route.js +9 -3
  53. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  54. package/dist/modules/dashboards/api/widgets/catalog.js +6 -3
  55. package/dist/modules/dashboards/api/widgets/catalog.js.map +2 -2
  56. package/dist/modules/dashboards/lib/access.js +5 -3
  57. package/dist/modules/dashboards/lib/access.js.map +2 -2
  58. package/dist/modules/directory/api/organization-branding/route.js +10 -3
  59. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  60. package/dist/modules/directory/api/organizations/route.js +7 -0
  61. package/dist/modules/directory/api/organizations/route.js.map +3 -3
  62. package/dist/modules/directory/backend/directory/branding/page.js +31 -8
  63. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  64. package/dist/modules/directory/commands/organizations.js +8 -1
  65. package/dist/modules/directory/commands/organizations.js.map +2 -2
  66. package/dist/modules/directory/data/entities.js +4 -0
  67. package/dist/modules/directory/data/entities.js.map +2 -2
  68. package/dist/modules/directory/data/validators.js +2 -0
  69. package/dist/modules/directory/data/validators.js.map +2 -2
  70. package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js +13 -0
  71. package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js.map +7 -0
  72. package/dist/modules/entities/lib/entityAcl.js +47 -15
  73. package/dist/modules/entities/lib/entityAcl.js.map +3 -3
  74. package/dist/modules/inbox_ops/ai-tools.js +5 -2
  75. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  76. package/dist/modules/messages/lib/routeHelpers.js +1 -3
  77. package/dist/modules/messages/lib/routeHelpers.js.map +2 -2
  78. package/dist/modules/notifications/lib/notificationRecipients.js +6 -7
  79. package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
  80. package/dist/modules/query_index/di.js +3 -0
  81. package/dist/modules/query_index/di.js.map +2 -2
  82. package/dist/modules/query_index/lib/coverage.js +2 -2
  83. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  84. package/dist/modules/query_index/lib/indexer.js +83 -17
  85. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  86. package/dist/modules/query_index/lib/search-tokens.js +13 -7
  87. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  88. package/dist/modules/query_index/subscribers/coverage_refresh.js +4 -0
  89. package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
  90. package/dist/modules/query_index/subscribers/delete_one.js +53 -1
  91. package/dist/modules/query_index/subscribers/delete_one.js.map +3 -3
  92. package/dist/modules/query_index/subscribers/upsert_one.js +60 -0
  93. package/dist/modules/query_index/subscribers/upsert_one.js.map +3 -3
  94. package/dist/modules/sales/commands/documents.js +17 -0
  95. package/dist/modules/sales/commands/documents.js.map +2 -2
  96. package/dist/modules/staff/api/interceptors.js +5 -3
  97. package/dist/modules/staff/api/interceptors.js.map +2 -2
  98. package/dist/modules/sync_excel/api/upload/route.js +15 -2
  99. package/dist/modules/sync_excel/api/upload/route.js.map +2 -2
  100. package/dist/modules/workflows/lib/activity-executor.js +7 -7
  101. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  102. package/generated/entities/organization/index.ts +1 -0
  103. package/generated/entity-fields-registry.ts +1 -0
  104. package/package.json +7 -7
  105. package/src/modules/auth/AGENTS.md +5 -3
  106. package/src/modules/auth/cli.ts +1 -1
  107. package/src/modules/auth/lib/backendChrome.tsx +5 -11
  108. package/src/modules/auth/lib/setup-app.ts +1 -0
  109. package/src/modules/auth/services/rbacService.ts +41 -46
  110. package/src/modules/catalog/widgets/injection/product-seo/widget.ts +1 -1
  111. package/src/modules/communication_channels/lib/access-control.ts +3 -3
  112. package/src/modules/customer_accounts/AGENTS.md +3 -3
  113. package/src/modules/customer_accounts/api/invitations/accept.ts +1 -2
  114. package/src/modules/customer_accounts/api/login.ts +4 -2
  115. package/src/modules/customer_accounts/api/magic-link/verify.ts +4 -2
  116. package/src/modules/customer_accounts/api/portal/feature-check.ts +5 -7
  117. package/src/modules/customer_accounts/api/portal/nav.ts +5 -3
  118. package/src/modules/customer_accounts/api/portal/profile.ts +5 -1
  119. package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +3 -3
  120. package/src/modules/customer_accounts/lib/customerAuth.ts +8 -2
  121. package/src/modules/customer_accounts/lib/customerAuthServer.ts +1 -0
  122. package/src/modules/customer_accounts/services/customerRbacService.ts +16 -3
  123. package/src/modules/customer_accounts/setup.ts +7 -0
  124. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +1 -3
  125. package/src/modules/customers/lib/visibilityFilter.ts +7 -5
  126. package/src/modules/dashboards/api/layout/[itemId]/route.ts +6 -3
  127. package/src/modules/dashboards/api/layout/route.ts +22 -9
  128. package/src/modules/dashboards/api/roles/widgets/route.ts +9 -3
  129. package/src/modules/dashboards/api/users/widgets/route.ts +9 -3
  130. package/src/modules/dashboards/api/widgets/catalog.ts +6 -3
  131. package/src/modules/dashboards/lib/access.ts +5 -3
  132. package/src/modules/directory/api/organization-branding/route.ts +12 -1
  133. package/src/modules/directory/api/organizations/route.ts +7 -0
  134. package/src/modules/directory/backend/directory/branding/page.tsx +34 -9
  135. package/src/modules/directory/commands/organizations.ts +9 -1
  136. package/src/modules/directory/data/entities.ts +3 -0
  137. package/src/modules/directory/data/validators.ts +2 -0
  138. package/src/modules/directory/i18n/de.json +3 -1
  139. package/src/modules/directory/i18n/en.json +3 -1
  140. package/src/modules/directory/i18n/es.json +3 -1
  141. package/src/modules/directory/i18n/pl.json +3 -1
  142. package/src/modules/directory/migrations/.snapshot-open-mercato.json +18 -1
  143. package/src/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.ts +13 -0
  144. package/src/modules/entities/lib/entityAcl.ts +47 -18
  145. package/src/modules/inbox_ops/ai-tools.ts +5 -3
  146. package/src/modules/messages/lib/routeHelpers.ts +4 -6
  147. package/src/modules/notifications/lib/notificationRecipients.ts +6 -8
  148. package/src/modules/query_index/di.ts +6 -0
  149. package/src/modules/query_index/lib/coverage.ts +9 -6
  150. package/src/modules/query_index/lib/indexer.ts +105 -19
  151. package/src/modules/query_index/lib/search-tokens.ts +19 -8
  152. package/src/modules/query_index/subscribers/coverage_refresh.ts +4 -0
  153. package/src/modules/query_index/subscribers/delete_one.ts +65 -1
  154. package/src/modules/query_index/subscribers/upsert_one.ts +68 -1
  155. package/src/modules/sales/commands/documents.ts +27 -0
  156. package/src/modules/sales/i18n/de.json +1 -0
  157. package/src/modules/sales/i18n/en.json +1 -0
  158. package/src/modules/sales/i18n/es.json +1 -0
  159. package/src/modules/sales/i18n/pl.json +1 -0
  160. package/src/modules/staff/api/interceptors.ts +6 -4
  161. package/src/modules/sync_excel/api/upload/route.ts +15 -2
  162. 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 { upsertIndexRow, reindexSearchTokensForRecord } from '../lib/indexer'
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 { hasFeature } from '@open-mercato/shared/security/features'
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
- // rbacService not available — fall through to deny
34
+ // Fall back to the already-loaded snapshot below.
36
35
  }
37
- return false
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 { isMultipartRequestWithinUploadLimit, resolveDefaultAttachmentMaxUploadBytes } from '../../../attachments/lib/upload-limits'
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
- const formData = await request.formData()
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
- getGrantedFeatures: (
165
+ userHasAllFeatures: (
167
166
  userId: string,
167
+ required: string[],
168
168
  opts: { tenantId: string | null; organizationId: string | null }
169
- ) => Promise<string[]>
169
+ ) => Promise<boolean>
170
170
  }
171
171
 
172
- async function resolveWorkflowUserFeatures(
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<string[]> {
178
+ ): Promise<boolean> {
178
179
  try {
179
180
  const rbac = container.resolve('rbacService') as RbacFeatureResolver | undefined
180
- if (rbac?.getGrantedFeatures) {
181
- return await rbac.getGrantedFeatures(userId, { tenantId, organizationId })
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 userFeatures = await resolveWorkflowUserFeatures(
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 (!hasAllFeatures(userFeatures, workflowSafeCommand.requiredFeatures)) {
692
+ if (!authorized) {
691
693
  throw new Error('UPDATE_ENTITY command is not authorized')
692
694
  }
693
695