@open-mercato/core 0.6.5-develop.5337.1.534b781eac → 0.6.5-develop.5382.1.f542de69af

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 (119) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/modules/attachments/api/library/route.js +2 -2
  4. package/dist/modules/attachments/api/library/route.js.map +2 -2
  5. package/dist/modules/attachments/components/AttachmentContentPreview.js +9 -5
  6. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  7. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +3 -2
  8. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
  9. package/dist/modules/auth/commands/users.js +20 -14
  10. package/dist/modules/auth/commands/users.js.map +2 -2
  11. package/dist/modules/auth/data/entities.js +1 -1
  12. package/dist/modules/auth/data/entities.js.map +2 -2
  13. package/dist/modules/auth/migrations/Migration20260610120000.js +30 -0
  14. package/dist/modules/auth/migrations/Migration20260610120000.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +1 -1
  16. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +1 -1
  17. package/dist/modules/catalog/ai-tools/products-pack.js.map +1 -1
  18. package/dist/modules/catalog/ai-tools/variants-pack.js.map +1 -1
  19. package/dist/modules/communication_channels/data/entities.js.map +1 -1
  20. package/dist/modules/communication_channels/encryption.js.map +1 -1
  21. package/dist/modules/communication_channels/lib/thread-matcher.js.map +1 -1
  22. package/dist/modules/communication_channels/lib/thread-token.js.map +1 -1
  23. package/dist/modules/currencies/api/currencies/route.js +4 -3
  24. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  25. package/dist/modules/customer_accounts/api/admin/roles.js +2 -1
  26. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  27. package/dist/modules/customer_accounts/events.js +1 -1
  28. package/dist/modules/customer_accounts/events.js.map +1 -1
  29. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +1 -1
  30. package/dist/modules/customers/acl.js +1 -1
  31. package/dist/modules/customers/acl.js.map +1 -1
  32. package/dist/modules/customers/ai-tools/companies-pack.js.map +1 -1
  33. package/dist/modules/customers/ai-tools/deals-pack.js.map +1 -1
  34. package/dist/modules/customers/ai-tools/people-pack.js.map +1 -1
  35. package/dist/modules/customers/api/companies/route.js +4 -4
  36. package/dist/modules/customers/api/companies/route.js.map +2 -2
  37. package/dist/modules/customers/api/people/route.js +4 -4
  38. package/dist/modules/customers/api/people/route.js.map +2 -2
  39. package/dist/modules/customers/commands/addresses.js +5 -5
  40. package/dist/modules/customers/commands/addresses.js.map +2 -2
  41. package/dist/modules/customers/commands/comments.js +5 -5
  42. package/dist/modules/customers/commands/comments.js.map +2 -2
  43. package/dist/modules/customers/commands/deals.js +2 -2
  44. package/dist/modules/customers/commands/deals.js.map +2 -2
  45. package/dist/modules/customers/commands/entity-roles.js +2 -1
  46. package/dist/modules/customers/commands/entity-roles.js.map +2 -2
  47. package/dist/modules/customers/commands/interactions.js +8 -5
  48. package/dist/modules/customers/commands/interactions.js.map +2 -2
  49. package/dist/modules/customers/commands/shared.js +21 -6
  50. package/dist/modules/customers/commands/shared.js.map +2 -2
  51. package/dist/modules/customers/commands/tags.js +3 -3
  52. package/dist/modules/customers/commands/tags.js.map +2 -2
  53. package/dist/modules/customers/components/detail/assignableStaff.js +21 -8
  54. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  55. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +1 -1
  56. package/dist/modules/data_sync/api/run.js +1 -1
  57. package/dist/modules/data_sync/api/run.js.map +2 -2
  58. package/dist/modules/payment_gateways/api/transactions/route.js +2 -4
  59. package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
  60. package/dist/modules/progress/api/jobs/[id]/route.js +7 -2
  61. package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
  62. package/dist/modules/progress/api/jobs/route.js +1 -1
  63. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  64. package/dist/modules/progress/lib/progressServiceImpl.js +8 -2
  65. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  66. package/dist/modules/resources/api/resources.js +2 -3
  67. package/dist/modules/resources/api/resources.js.map +2 -2
  68. package/dist/modules/sales/api/documents/factory.js +2 -2
  69. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  70. package/dist/modules/sync_excel/api/import/route.js +1 -1
  71. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  72. package/dist/modules/workflows/api/definitions/route.js +3 -2
  73. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  74. package/package.json +7 -7
  75. package/src/modules/attachments/api/library/route.ts +2 -2
  76. package/src/modules/attachments/components/AttachmentContentPreview.tsx +6 -6
  77. package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +14 -2
  78. package/src/modules/auth/commands/users.ts +32 -15
  79. package/src/modules/auth/data/entities.ts +11 -1
  80. package/src/modules/auth/migrations/.snapshot-open-mercato.json +0 -10
  81. package/src/modules/auth/migrations/Migration20260610120000.ts +53 -0
  82. package/src/modules/catalog/ai-tools/configuration-pack.ts +1 -1
  83. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +1 -1
  84. package/src/modules/catalog/ai-tools/products-pack.ts +1 -1
  85. package/src/modules/catalog/ai-tools/variants-pack.ts +1 -1
  86. package/src/modules/communication_channels/data/entities.ts +2 -2
  87. package/src/modules/communication_channels/encryption.ts +1 -1
  88. package/src/modules/communication_channels/lib/adapter.ts +1 -1
  89. package/src/modules/communication_channels/lib/thread-matcher.ts +1 -1
  90. package/src/modules/communication_channels/lib/thread-token.ts +1 -1
  91. package/src/modules/currencies/api/currencies/route.ts +4 -3
  92. package/src/modules/customer_accounts/api/admin/roles.ts +2 -1
  93. package/src/modules/customer_accounts/events.ts +1 -1
  94. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +2 -2
  95. package/src/modules/customers/acl.ts +1 -1
  96. package/src/modules/customers/ai-tools/companies-pack.ts +1 -1
  97. package/src/modules/customers/ai-tools/deals-pack.ts +1 -1
  98. package/src/modules/customers/ai-tools/people-pack.ts +1 -1
  99. package/src/modules/customers/api/companies/route.ts +4 -4
  100. package/src/modules/customers/api/people/route.ts +4 -4
  101. package/src/modules/customers/commands/addresses.ts +5 -5
  102. package/src/modules/customers/commands/comments.ts +5 -5
  103. package/src/modules/customers/commands/deals.ts +2 -2
  104. package/src/modules/customers/commands/entity-roles.ts +2 -1
  105. package/src/modules/customers/commands/interactions.ts +8 -5
  106. package/src/modules/customers/commands/shared.ts +26 -4
  107. package/src/modules/customers/commands/tags.ts +3 -3
  108. package/src/modules/customers/components/detail/assignableStaff.ts +32 -8
  109. package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +1 -1
  110. package/src/modules/data_sync/api/run.ts +1 -1
  111. package/src/modules/payment_gateways/api/transactions/route.ts +2 -5
  112. package/src/modules/progress/api/jobs/[id]/route.ts +6 -1
  113. package/src/modules/progress/api/jobs/route.ts +1 -1
  114. package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
  115. package/src/modules/resources/api/resources.ts +2 -3
  116. package/src/modules/sales/api/documents/factory.ts +2 -2
  117. package/src/modules/staff/AGENTS.md +1 -1
  118. package/src/modules/sync_excel/api/import/route.ts +1 -1
  119. package/src/modules/workflows/api/definitions/route.ts +3 -2
@@ -89,7 +89,7 @@ export const features = [
89
89
  // privacy model is strict owner-only with NO admin bypass, so this feature is
90
90
  // declared but INERT — granting it does not unlock other users' private emails
91
91
  // (the visibility filter and the visibility-change gate ignore it). See
92
- // .ai/specs/2026-05-27-crm-email-integration.md (v1 strict owner-only).
92
+ // .ai/specs/implemented/2026-05-27-crm-email-integration.md (v1 strict owner-only).
93
93
  {
94
94
  id: 'customers.email.view_private',
95
95
  title: 'View other users\' private emails (reserved — inert in v1)',
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).
3
3
  *
4
- * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
4
+ * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
5
5
  * `customers.list_companies` is now an API-backed wrapper over
6
6
  * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and
7
7
  * output shape are unchanged.
@@ -2,7 +2,7 @@
2
2
  * `customers.list_deals` + `customers.get_deal` (Phase 1 WS-C, Step 3.9).
3
3
  * `customers.update_deal_stage` mutation tool (Phase 3 WS-C, Step 5.13).
4
4
  *
5
- * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
5
+ * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
6
6
  * `customers.list_deals` is now an API-backed wrapper over
7
7
  * `GET /api/customers/deals`. Tool name, schema, requiredFeatures, and output
8
8
  * shape are unchanged.
@@ -5,7 +5,7 @@
5
5
  * the existing customers query engine + encryption helpers. Mutation tools
6
6
  * are deferred to Step 5.13+ under the pending-action contract.
7
7
  *
8
- * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
8
+ * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
9
9
  * `customers.list_people` is now an API-backed wrapper over
10
10
  * `GET /api/customers/people`. The `companyId` AI input has no inclusion
11
11
  * equivalent on the route (the route exposes `excludeLinkedCompanyId` only)
@@ -23,7 +23,7 @@ import {
23
23
  extractAllCustomFieldEntries,
24
24
  splitCustomFieldPayload,
25
25
  } from '@open-mercato/shared/lib/crud/custom-fields'
26
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
26
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
27
27
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
28
28
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
29
29
  import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
@@ -178,7 +178,7 @@ const crud = makeCrudRoute({
178
178
  if (matchingIds !== null && matchingIds.length > 0) {
179
179
  applyEntityIdRestriction(filters, matchingIds)
180
180
  } else {
181
- const searchPattern = `%${escapeLikePattern(query.search)}%`
181
+ const searchPattern = buildIlikeTerm(query.search)
182
182
  filters.$or = [
183
183
  { display_name: { $ilike: searchPattern } },
184
184
  { primary_email: { $ilike: searchPattern } },
@@ -276,9 +276,9 @@ const crud = makeCrudRoute({
276
276
  if (email) {
277
277
  filters.primary_email = { $eq: email }
278
278
  } else if (emailStartsWith) {
279
- filters.primary_email = { $ilike: `${escapeLikePattern(emailStartsWith)}%` }
279
+ filters.primary_email = { $ilike: buildIlikeTerm(emailStartsWith, 'startsWith') }
280
280
  } else if (emailContains) {
281
- filters.primary_email = { $ilike: `%${escapeLikePattern(emailContains)}%` }
281
+ filters.primary_email = { $ilike: buildIlikeTerm(emailContains) }
282
282
  }
283
283
  const hasEmail = parseBooleanToken(query.hasEmail)
284
284
  if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {
@@ -19,7 +19,7 @@ import {
19
19
  withScopedPayload,
20
20
  } from '../utils'
21
21
  import { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries, splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'
22
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
22
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
23
23
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
24
24
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
25
25
  import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
@@ -173,7 +173,7 @@ const crud = makeCrudRoute({
173
173
  if (matchingIds !== null && matchingIds.length > 0) {
174
174
  applyEntityIdRestriction(filters, matchingIds)
175
175
  } else {
176
- const searchPattern = `%${escapeLikePattern(query.search)}%`
176
+ const searchPattern = buildIlikeTerm(query.search)
177
177
  filters.$or = [
178
178
  { display_name: { $ilike: searchPattern } },
179
179
  { primary_email: { $ilike: searchPattern } },
@@ -189,9 +189,9 @@ const crud = makeCrudRoute({
189
189
  if (email) {
190
190
  filters.primary_email = { $eq: email }
191
191
  } else if (emailStartsWith) {
192
- filters.primary_email = { $ilike: `${escapeLikePattern(emailStartsWith)}%` }
192
+ filters.primary_email = { $ilike: buildIlikeTerm(emailStartsWith, 'startsWith') }
193
193
  } else if (emailContains) {
194
- filters.primary_email = { $ilike: `%${escapeLikePattern(emailContains)}%` }
194
+ filters.primary_email = { $ilike: buildIlikeTerm(emailContains) }
195
195
  }
196
196
  if (query.status) {
197
197
  filters.status = { $eq: query.status }
@@ -109,7 +109,7 @@ const createAddressCommand: CommandHandler<AddressCreateInput, { addressId: stri
109
109
  ensureOrganizationScope(ctx, parsed.organizationId)
110
110
 
111
111
  const em = (ctx.container.resolve('em') as EntityManager).fork()
112
- const entity = await requireCustomerEntity(em, parsed.entityId, undefined, 'Customer not found')
112
+ const entity = await requireCustomerEntity(em, parsed.entityId, { tenantId: parsed.tenantId, organizationId: parsed.organizationId }, undefined, 'Customer not found')
113
113
  ensureSameScope(entity, parsed.organizationId, parsed.tenantId)
114
114
 
115
115
  const address = em.create(CustomerAddress, {
@@ -198,7 +198,7 @@ const createAddressCommand: CommandHandler<AddressCreateInput, { addressId: stri
198
198
  throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for address create' })
199
199
  }
200
200
  const em = (ctx.container.resolve('em') as EntityManager).fork()
201
- const entity = await requireCustomerEntity(em, after.entityId, undefined, 'Customer not found')
201
+ const entity = await requireCustomerEntity(em, after.entityId, { tenantId: after.tenantId, organizationId: after.organizationId }, undefined, 'Customer not found')
202
202
  let address = await findOneWithDecryption(
203
203
  em,
204
204
  CustomerAddress,
@@ -293,7 +293,7 @@ const updateAddressCommand: CommandHandler<AddressUpdateInput, { addressId: stri
293
293
  ensureOrganizationScope(ctx, address.organizationId)
294
294
 
295
295
  if (parsed.entityId !== undefined) {
296
- const entity = await requireCustomerEntity(em, parsed.entityId, undefined, 'Customer not found')
296
+ const entity = await requireCustomerEntity(em, parsed.entityId, { tenantId: address.tenantId, organizationId: address.organizationId }, undefined, 'Customer not found')
297
297
  ensureSameScope(entity, address.organizationId, address.tenantId)
298
298
  address.entity = entity
299
299
  }
@@ -396,7 +396,7 @@ const updateAddressCommand: CommandHandler<AddressUpdateInput, { addressId: stri
396
396
  if (!before) return
397
397
  const em = (ctx.container.resolve('em') as EntityManager).fork()
398
398
  let address = await em.findOne(CustomerAddress, { id: before.id })
399
- const entity = await requireCustomerEntity(em, before.entityId, undefined, 'Customer not found')
399
+ const entity = await requireCustomerEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId }, undefined, 'Customer not found')
400
400
  if (!address) {
401
401
  address = em.create(CustomerAddress, {
402
402
  id: before.id,
@@ -523,7 +523,7 @@ const deleteAddressCommand: CommandHandler<{ body?: Record<string, unknown>; que
523
523
  const before = payload?.before
524
524
  if (!before) return
525
525
  const em = (ctx.container.resolve('em') as EntityManager).fork()
526
- const entity = await requireCustomerEntity(em, before.entityId, undefined, 'Customer not found')
526
+ const entity = await requireCustomerEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId }, undefined, 'Customer not found')
527
527
  let address = await em.findOne(CustomerAddress, { id: before.id })
528
528
  if (!address) {
529
529
  address = em.create(CustomerAddress, {
@@ -84,7 +84,7 @@ const createCommentCommand: CommandHandler<CommentCreateInput, { commentId: stri
84
84
  const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId, ctx.auth)
85
85
 
86
86
  const em = (ctx.container.resolve('em') as EntityManager).fork()
87
- const entity = await requireTimelineParentEntity(em, parsed.entityId)
87
+ const entity = await requireTimelineParentEntity(em, parsed.entityId, { tenantId: parsed.tenantId, organizationId: parsed.organizationId })
88
88
  ensureSameScope(entity, parsed.organizationId, parsed.tenantId)
89
89
  const deal = await requireDealInScope(em, parsed.dealId, parsed.tenantId, parsed.organizationId)
90
90
 
@@ -176,7 +176,7 @@ const createCommentCommand: CommandHandler<CommentCreateInput, { commentId: stri
176
176
  appearanceColor: snapshot.appearanceColor,
177
177
  }),
178
178
  beforeRestore: async ({ em, snapshot }) => {
179
- const entity = await requireTimelineParentEntity(em, snapshot.entityId)
179
+ const entity = await requireTimelineParentEntity(em, snapshot.entityId, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId })
180
180
  const deal = await requireDealInScope(em, snapshot.dealId, snapshot.tenantId, snapshot.organizationId)
181
181
  return { entity, deal }
182
182
  },
@@ -201,7 +201,7 @@ const updateCommentCommand: CommandHandler<CommentUpdateInput, { commentId: stri
201
201
  ensureOrganizationScope(ctx, comment.organizationId)
202
202
 
203
203
  if (parsed.entityId !== undefined) {
204
- const entity = await requireTimelineParentEntity(em, parsed.entityId)
204
+ const entity = await requireTimelineParentEntity(em, parsed.entityId, { tenantId: comment.tenantId, organizationId: comment.organizationId })
205
205
  ensureSameScope(entity, comment.organizationId, comment.tenantId)
206
206
  comment.entity = entity
207
207
  }
@@ -275,7 +275,7 @@ const updateCommentCommand: CommandHandler<CommentUpdateInput, { commentId: stri
275
275
  if (!before) return
276
276
  const em = (ctx.container.resolve('em') as EntityManager).fork()
277
277
  let comment = await em.findOne(CustomerComment, { id: before.id })
278
- const entity = await requireTimelineParentEntity(em, before.entityId)
278
+ const entity = await requireTimelineParentEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId })
279
279
  const deal = await requireDealInScope(em, before.dealId, before.tenantId, before.organizationId)
280
280
 
281
281
  if (!comment) {
@@ -380,7 +380,7 @@ const deleteCommentCommand: CommandHandler<{ body?: Record<string, unknown>; que
380
380
  const before = payload?.before
381
381
  if (!before) return
382
382
  const em = (ctx.container.resolve('em') as EntityManager).fork()
383
- const entity = await requireTimelineParentEntity(em, before.entityId)
383
+ const entity = await requireTimelineParentEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId })
384
384
  const deal = await requireDealInScope(em, before.dealId, before.tenantId, before.organizationId)
385
385
  let comment = await em.findOne(CustomerComment, { id: before.id })
386
386
  if (!comment) {
@@ -376,7 +376,7 @@ async function syncDealPeople(
376
376
  if (!personIds || !personIds.length) return
377
377
  const unique = Array.from(new Set(personIds))
378
378
  for (const personId of unique) {
379
- const person = await requireCustomerEntity(em, personId, 'person', 'Person not found')
379
+ const person = await requireCustomerEntity(em, personId, { tenantId: deal.tenantId, organizationId: deal.organizationId }, 'person', 'Person not found')
380
380
  ensureSameScope(person, deal.organizationId, deal.tenantId)
381
381
  const link = em.create(CustomerDealPersonLink, {
382
382
  deal,
@@ -396,7 +396,7 @@ async function syncDealCompanies(
396
396
  if (!companyIds || !companyIds.length) return
397
397
  const unique = Array.from(new Set(companyIds))
398
398
  for (const companyId of unique) {
399
- const company = await requireCustomerEntity(em, companyId, 'company', 'Company not found')
399
+ const company = await requireCustomerEntity(em, companyId, { tenantId: deal.tenantId, organizationId: deal.organizationId }, 'company', 'Company not found')
400
400
  ensureSameScope(company, deal.organizationId, deal.tenantId)
401
401
  const link = em.create(CustomerDealCompanyLink, {
402
402
  deal,
@@ -137,7 +137,7 @@ const createEntityRoleCommand: CommandHandler<EntityRoleCreateInput, { roleId: s
137
137
  ensureOrganizationScope(ctx, parsed.organizationId)
138
138
 
139
139
  const em = (ctx.container.resolve('em') as EntityManager).fork()
140
- const entity = await requireCustomerEntity(em, parsed.entityId, parsed.entityType, 'Customer not found')
140
+ const entity = await requireCustomerEntity(em, parsed.entityId, { tenantId: parsed.tenantId, organizationId: parsed.organizationId }, parsed.entityType, 'Customer not found')
141
141
  ensureSameScope(entity, parsed.organizationId, parsed.tenantId)
142
142
 
143
143
  const activeExisting = await findOneWithDecryption(
@@ -495,6 +495,7 @@ const deleteEntityRoleCommand: CommandHandler<EntityRoleDeleteInput, { roleId: s
495
495
  const entity = await requireCustomerEntity(
496
496
  em,
497
497
  before.role.entityId,
498
+ { tenantId: before.role.tenantId, organizationId: before.role.organizationId },
498
499
  before.role.entityType,
499
500
  'Customer not found',
500
501
  )
@@ -374,7 +374,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
374
374
  const em = (ctx.container.resolve('em') as EntityManager).fork()
375
375
  const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId ?? null, ctx.auth)
376
376
  const { interaction, entityId, nextInteractionId } = await runInTransaction(em, async (trx) => {
377
- const entity = await requireTimelineParentEntity(trx, parsed.entityId)
377
+ const entity = await requireTimelineParentEntity(trx, parsed.entityId, { tenantId: parsed.tenantId, organizationId: parsed.organizationId })
378
378
  ensureTenantScope(ctx, entity.tenantId)
379
379
  ensureOrganizationScope(ctx, entity.organizationId)
380
380
 
@@ -510,7 +510,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
510
510
  }
511
511
  const em = (ctx.container.resolve('em') as EntityManager).fork()
512
512
  const { interaction, nextInteractionId } = await runInTransaction(em, async (trx) => {
513
- const entity = await requireTimelineParentEntity(trx, after.interaction.entityId)
513
+ const entity = await requireTimelineParentEntity(trx, after.interaction.entityId, { tenantId: after.interaction.tenantId, organizationId: after.interaction.organizationId })
514
514
  let interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: after.interaction.id })
515
515
  if (!interaction) {
516
516
  interaction = buildInteractionGraph(trx, {
@@ -781,7 +781,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
781
781
  const em = (ctx.container.resolve('em') as EntityManager).fork()
782
782
  const { interaction, nextInteractionId } = await runInTransaction(em, async (trx) => {
783
783
  let interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id })
784
- const entity = await requireTimelineParentEntity(trx, before.interaction.entityId)
784
+ const entity = await requireTimelineParentEntity(trx, before.interaction.entityId, { tenantId: before.interaction.tenantId, organizationId: before.interaction.organizationId })
785
785
 
786
786
  if (!interaction) {
787
787
  interaction = trx.create(CustomerInteraction, {
@@ -1252,7 +1252,7 @@ const deleteInteractionCommand: CommandHandler<{ body?: Record<string, unknown>;
1252
1252
  if (!before) return
1253
1253
  const em = (ctx.container.resolve('em') as EntityManager).fork()
1254
1254
  const { interaction, nextInteractionId } = await runInTransaction(em, async (trx) => {
1255
- const entity = await requireTimelineParentEntity(trx, before.interaction.entityId)
1255
+ const entity = await requireTimelineParentEntity(trx, before.interaction.entityId, { tenantId: before.interaction.tenantId, organizationId: before.interaction.organizationId })
1256
1256
  let interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id })
1257
1257
  if (!interaction) {
1258
1258
  interaction = trx.create(CustomerInteraction, {
@@ -1372,7 +1372,10 @@ const recomputeNextCommand: CommandHandler<{ entityId: string }, { entityId: str
1372
1372
  const parsed = recomputeNextSchema.parse(rawInput)
1373
1373
  const em = (ctx.container.resolve('em') as EntityManager).fork()
1374
1374
  const projection = await recomputeNextInteraction(em, parsed.entityId)
1375
- const entity = await requireTimelineParentEntity(em, parsed.entityId)
1375
+ const entity = await requireTimelineParentEntity(em, parsed.entityId, {
1376
+ tenantId: ctx.auth?.tenantId ?? '',
1377
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? '',
1378
+ })
1376
1379
  await emitNextInteractionUpdatedEvent(ctx, {
1377
1380
  entityId: parsed.entityId,
1378
1381
  nextInteractionId: projection.nextInteractionId,
@@ -26,13 +26,24 @@ export function normalizeDictionaryIcon(input: unknown): string | null {
26
26
 
27
27
  export { assertFound } from '@open-mercato/shared/lib/crud/errors'
28
28
 
29
+ export type CustomerEntityScope = {
30
+ tenantId: string
31
+ organizationId: string
32
+ }
33
+
29
34
  export async function requireCustomerEntity(
30
35
  em: EntityManager,
31
36
  id: string,
37
+ scope: CustomerEntityScope,
32
38
  kind?: CustomerEntityKind,
33
39
  message = 'Customer entity not found'
34
40
  ): Promise<CustomerEntity> {
35
- const entity = await em.findOne(CustomerEntity, { id, deletedAt: null })
41
+ const entity = await em.findOne(CustomerEntity, {
42
+ id,
43
+ deletedAt: null,
44
+ tenantId: scope.tenantId,
45
+ organizationId: scope.organizationId,
46
+ })
36
47
  if (!entity) throw new CrudHttpError(404, { error: message })
37
48
  if (kind && entity.kind !== kind) {
38
49
  throw new CrudHttpError(400, { error: 'Invalid entity type' })
@@ -43,15 +54,26 @@ export async function requireCustomerEntity(
43
54
  export async function requireTimelineParentEntity(
44
55
  em: EntityManager,
45
56
  id: string,
57
+ scope: CustomerEntityScope,
46
58
  ): Promise<CustomerEntity> {
47
- const entity = await em.findOne(CustomerEntity, { id, deletedAt: null })
59
+ const entity = await em.findOne(CustomerEntity, {
60
+ id,
61
+ deletedAt: null,
62
+ tenantId: scope.tenantId,
63
+ organizationId: scope.organizationId,
64
+ })
48
65
  if (entity) {
49
66
  if (entity.kind !== 'person' && entity.kind !== 'company') {
50
67
  throw new CrudHttpError(422, { error: 'entityId must reference a person or company' })
51
68
  }
52
69
  return entity
53
70
  }
54
- const deal = await em.findOne(CustomerDeal, { id, deletedAt: null })
71
+ const deal = await em.findOne(CustomerDeal, {
72
+ id,
73
+ deletedAt: null,
74
+ tenantId: scope.tenantId,
75
+ organizationId: scope.organizationId,
76
+ })
55
77
  if (deal) {
56
78
  throw new CrudHttpError(422, { error: 'entityId must reference a person or company, not a deal' })
57
79
  }
@@ -111,7 +133,7 @@ export async function requireDealInScope(
111
133
  organizationId: string
112
134
  ): Promise<CustomerDeal | null> {
113
135
  if (!dealId) return null
114
- const deal = await em.findOne(CustomerDeal, { id: dealId, deletedAt: null })
136
+ const deal = await em.findOne(CustomerDeal, { id: dealId, deletedAt: null, tenantId, organizationId })
115
137
  if (!deal) throw new CrudHttpError(400, { error: 'Deal not found' })
116
138
  ensureSameScope(deal, organizationId, tenantId)
117
139
  return deal
@@ -387,7 +387,7 @@ const assignTagCommand: CommandHandler<TagAssignmentInput, { assignmentId: strin
387
387
  const em = (ctx.container.resolve('em') as EntityManager).fork()
388
388
  const tag = await em.findOne(CustomerTag, { id: parsed.tagId, tenantId: parsed.tenantId, organizationId: parsed.organizationId })
389
389
  if (!tag) throw new CrudHttpError(404, { error: 'Tag not found' })
390
- const entity = await requireCustomerEntity(em, parsed.entityId, undefined, 'Customer not found')
390
+ const entity = await requireCustomerEntity(em, parsed.entityId, { tenantId: parsed.tenantId, organizationId: parsed.organizationId }, undefined, 'Customer not found')
391
391
  ensureSameScope(entity, parsed.organizationId, parsed.tenantId)
392
392
  const tagIds = await loadEntityTagIds(em, entity)
393
393
  if (tagIds.includes(parsed.tagId)) {
@@ -478,7 +478,7 @@ const assignTagCommand: CommandHandler<TagAssignmentInput, { assignmentId: strin
478
478
  const em = (ctx.container.resolve('em') as EntityManager).fork()
479
479
  const tag = await em.findOne(CustomerTag, { id: before.tagId })
480
480
  if (!tag) throw new CrudHttpError(404, { error: 'Tag not found' })
481
- const entity = await requireCustomerEntity(em, before.entityId, undefined, 'Customer not found')
481
+ const entity = await requireCustomerEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId }, undefined, 'Customer not found')
482
482
  ensureSameScope(entity, before.organizationId, before.tenantId)
483
483
 
484
484
  let assignment = await em.findOne(CustomerTagAssignment, {
@@ -589,7 +589,7 @@ const unassignTagCommand: CommandHandler<TagAssignmentInput, { assignmentId: str
589
589
  if (!before) return
590
590
  const em = (ctx.container.resolve('em') as EntityManager).fork()
591
591
  const tag = await em.findOne(CustomerTag, { id: before.tagId })
592
- const entity = await requireCustomerEntity(em, before.entityId, undefined, 'Customer not found')
592
+ const entity = await requireCustomerEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId }, undefined, 'Customer not found')
593
593
  ensureSameScope(entity, before.organizationId, before.tenantId)
594
594
  if (!tag) throw new CrudHttpError(404, { error: 'Tag not found' })
595
595
  const existing = await em.findOne(CustomerTagAssignment, {
@@ -23,22 +23,46 @@ export type AssignableStaffMembersPage = {
23
23
  pageSize: number
24
24
  }
25
25
 
26
+ // The assignable-staff roster is owned by the optional, ejectable `staff` module.
27
+ // When that module is disabled, its `/api/staff/team-members/assignable` endpoint is
28
+ // absent and the request resolves to 404. Customers UI (deals / people / companies
29
+ // owner filters, role-assignment dialogs) is core and always enabled, so it must not
30
+ // break in that case — a missing staff module simply means there is no roster to
31
+ // offer. Treat the 404 as an empty page and let any other failure propagate.
32
+ function isAssignableEndpointMissing(error: unknown): boolean {
33
+ return (
34
+ typeof error === 'object' &&
35
+ error !== null &&
36
+ (error as { status?: unknown }).status === 404
37
+ )
38
+ }
39
+
26
40
  export async function fetchAssignableStaffMembersPage(
27
41
  query: string,
28
42
  options?: { page?: number; pageSize?: number; signal?: AbortSignal },
29
43
  ): Promise<AssignableStaffMembersPage> {
44
+ const page = options?.page ?? 1
45
+ const pageSize = options?.pageSize ?? 24
30
46
  const params = new URLSearchParams()
31
- params.set('page', String(options?.page ?? 1))
32
- params.set('pageSize', String(options?.pageSize ?? 24))
47
+ params.set('page', String(page))
48
+ params.set('pageSize', String(pageSize))
33
49
  const normalizedQuery = query.trim()
34
50
  if (normalizedQuery.length > 0) {
35
51
  params.set('search', normalizedQuery)
36
52
  }
37
53
 
38
- const data = await readApiResultOrThrow<AssignableStaffResponse>(
39
- `/api/staff/team-members/assignable?${params.toString()}`,
40
- options?.signal ? { signal: options.signal } : undefined,
41
- )
54
+ let data: AssignableStaffResponse
55
+ try {
56
+ data = await readApiResultOrThrow<AssignableStaffResponse>(
57
+ `/api/staff/team-members/assignable?${params.toString()}`,
58
+ options?.signal ? { signal: options.signal } : undefined,
59
+ )
60
+ } catch (error) {
61
+ if (isAssignableEndpointMissing(error)) {
62
+ return { items: [], total: 0, page, pageSize }
63
+ }
64
+ throw error
65
+ }
42
66
 
43
67
  const rawItems = Array.isArray(data?.items) ? data.items : []
44
68
  const deduped = new Map<string, AssignableStaffMember>()
@@ -108,11 +132,11 @@ export async function fetchAssignableStaffMembersPage(
108
132
  page:
109
133
  typeof data?.page === 'number' && Number.isFinite(data.page)
110
134
  ? data.page
111
- : options?.page ?? 1,
135
+ : page,
112
136
  pageSize:
113
137
  typeof data?.pageSize === 'number' && Number.isFinite(data.pageSize)
114
138
  ? data.pageSize
115
- : options?.pageSize ?? 24,
139
+ : pageSize,
116
140
  }
117
141
  }
118
142
 
@@ -15,7 +15,7 @@ import { Migration } from '@mikro-orm/migrations';
15
15
  * 'pink', '', or NULL. Lane.tsx now reads the value directly as a tone identifier.
16
16
  *
17
17
  * Unknown / unmappable hex values collapse to 'neutral'. Forward-only — reverting tones to
18
- * the original hex is lossy and not supported. See `.ai/specs/2026-05-19-customers-deals-kanban-ux-review-fixes.md`.
18
+ * the original hex is lossy and not supported. See `.ai/specs/implemented/2026-05-19-customers-deals-kanban-ux-review-fixes.md`.
19
19
  */
20
20
  export class Migration20260519120000_pipeline_stage_color_tones extends Migration {
21
21
  override up(): void | Promise<void> {
@@ -107,7 +107,7 @@ export async function POST(req: Request) {
107
107
  const stack = error instanceof Error ? error.stack : undefined
108
108
  console.error('[data_sync.run] unhandled error', { message, stack })
109
109
  return NextResponse.json(
110
- { error: 'Failed to start data sync run.', message, stack },
110
+ { error: 'Failed to start data sync run.' },
111
111
  { status: 500 },
112
112
  )
113
113
  }
@@ -5,16 +5,13 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
5
  import { GatewayTransaction } from '../../data/entities'
6
6
  import { listTransactionsQuerySchema } from '../../data/validators'
7
7
  import { paymentGatewaysTag } from '../openapi'
8
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
8
9
 
9
10
  export const metadata = {
10
11
  path: '/payment_gateways/transactions',
11
12
  GET: { requireAuth: true, requireFeatures: ['payment_gateways.view'] },
12
13
  }
13
14
 
14
- function escapeLikePattern(value: string): string {
15
- return value.replace(/[\\%_]/g, '\\$&')
16
- }
17
-
18
15
  function formatDateValue(value: unknown): string | null {
19
16
  if (!value) return null
20
17
  if (value instanceof Date) return value.toISOString()
@@ -58,7 +55,7 @@ export async function GET(req: Request) {
58
55
  qb.andWhere({ unifiedStatus: status })
59
56
  }
60
57
  if (search) {
61
- const pattern = `%${escapeLikePattern(search)}%`
58
+ const pattern = buildIlikeTerm(search)
62
59
  qb.andWhere(`(
63
60
  cast(gt.id as text) ilike ?
64
61
  or cast(gt.payment_id as text) ilike ?
@@ -26,6 +26,7 @@ export async function GET(req: Request, { params }: { params: { id: string } })
26
26
  const job = await em.findOne(ProgressJob, {
27
27
  id: params.id,
28
28
  tenantId: auth.tenantId,
29
+ ...(auth.orgId ? { organizationId: auth.orgId } : {}),
29
30
  })
30
31
 
31
32
  if (!job) {
@@ -74,7 +75,11 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
74
75
 
75
76
  const container = await createRequestContainer()
76
77
  const em = container.resolve('em') as EntityManager
77
- const existing = await em.findOne(ProgressJob, { id: params.id, tenantId: auth.tenantId })
78
+ const existing = await em.findOne(ProgressJob, {
79
+ id: params.id,
80
+ tenantId: auth.tenantId,
81
+ ...(auth.orgId ? { organizationId: auth.orgId } : {}),
82
+ })
78
83
  if (!existing) {
79
84
  return NextResponse.json({ error: 'Not found' }, { status: 404 })
80
85
  }
@@ -165,7 +165,7 @@ export async function POST(req: Request) {
165
165
  const stack = error instanceof Error ? error.stack : undefined
166
166
  console.error('[progress.jobs.create] unhandled error', { message, stack })
167
167
  return NextResponse.json(
168
- { error: 'Failed to create progress job.', message, stack },
168
+ { error: 'Failed to create progress job.' },
169
169
  { status: 500 },
170
170
  )
171
171
  }
@@ -75,7 +75,11 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
75
75
  },
76
76
 
77
77
  async updateProgress(jobId, input, ctx) {
78
- const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
78
+ const job = await em.findOneOrFail(ProgressJob, {
79
+ id: jobId,
80
+ tenantId: ctx.tenantId,
81
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
82
+ })
79
83
  if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
80
84
  return job
81
85
  }
@@ -197,6 +201,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
197
201
  const job = await em.findOneOrFail(ProgressJob, {
198
202
  id: jobId,
199
203
  tenantId: ctx.tenantId,
204
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
200
205
  cancellable: true,
201
206
  status: { $in: ['pending', 'running'] },
202
207
  })
@@ -279,6 +284,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
279
284
  return em.findOne(ProgressJob, {
280
285
  id: jobId,
281
286
  tenantId: ctx.tenantId,
287
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
282
288
  })
283
289
  },
284
290
 
@@ -2,7 +2,7 @@ import { z } from 'zod'
2
2
  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
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
5
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
6
6
  import type { EntityManager } from '@mikro-orm/postgresql'
7
7
  import { ResourcesResource, ResourcesResourceTagAssignment, ResourcesResourceTag } from '../data/entities'
8
8
  import { resourcesResourceCreateSchema, resourcesResourceUpdateSchema } from '../data/validators'
@@ -107,8 +107,7 @@ const crud = makeCrudRoute({
107
107
  }
108
108
  const term = sanitizeSearchTerm(query.search)
109
109
  if (term) {
110
- const like = `%${escapeLikePattern(term)}%`
111
- filters[F.name] = { $ilike: like }
110
+ filters[F.name] = { $ilike: buildIlikeTerm(term) }
112
111
  }
113
112
  if (query.resourceTypeId) {
114
113
  filters[F.resource_type_id] = query.resourceTypeId
@@ -17,7 +17,7 @@ import {
17
17
  } from '../openapi'
18
18
  import { parseScopedCommandInput, resolveCrudRecordId } from '../utils'
19
19
  import { documentUpdateSchema } from '../../commands/documents'
20
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
20
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
21
21
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
22
22
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
23
23
  import { recalculateOrderTotalsForDisplay } from '../../commands/returns'
@@ -101,7 +101,7 @@ function buildFilters(query: ListQuery, numberColumn: string, kind: DocumentKind
101
101
  const filters: Record<string, unknown> = {}
102
102
  if (query.id) filters.id = { $eq: query.id }
103
103
  if (query.search && query.search.trim().length > 0) {
104
- const term = `%${escapeLikePattern(query.search.trim())}%`
104
+ const term = buildIlikeTerm(query.search.trim())
105
105
  filters[numberColumn] = { $ilike: term }
106
106
  }
107
107
  if (query.customerId) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `staff` module is **optional** and slated for extraction into a standalone `@open-mercato/staff` package published from the [official-modules](https://github.com/open-mercato/official-modules) repository. Core modules MUST NOT take direct dependencies on staff entities, helpers, or services — cross-module contact happens through the public surfaces listed below.
4
4
 
5
- See [`.ai/specs/2026-05-08-staff-decouple-from-core.md`](../../../../../.ai/specs/2026-05-08-staff-decouple-from-core.md) for the decoupling plan, and [`BACKWARD_COMPATIBILITY.md`](../../../../../BACKWARD_COMPATIBILITY.md) for the contract-surface taxonomy referenced below.
5
+ See [`.ai/specs/implemented/2026-05-08-staff-decouple-from-core.md`](../../../../../.ai/specs/implemented/2026-05-08-staff-decouple-from-core.md) for the decoupling plan, and [`BACKWARD_COMPATIBILITY.md`](../../../../../BACKWARD_COMPATIBILITY.md) for the contract-surface taxonomy referenced below.
6
6
 
7
7
  ## MUST Rules
8
8
 
@@ -190,7 +190,7 @@ export async function POST(request: Request) {
190
190
  const stack = error instanceof Error ? error.stack : undefined
191
191
  console.error('[sync_excel.import] unhandled error', { message, stack })
192
192
  return NextResponse.json(
193
- { error: 'Failed to start sync_excel import.', message, stack },
193
+ { error: 'Failed to start sync_excel import.' },
194
194
  { status: 500 },
195
195
  )
196
196
  }
@@ -9,6 +9,7 @@
9
9
  import { NextRequest, NextResponse } from 'next/server'
10
10
  import { z } from 'zod'
11
11
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
12
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
12
13
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
13
14
  import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
14
15
  import { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'
@@ -93,8 +94,8 @@ export async function GET(request: NextRequest) {
93
94
 
94
95
  if (search) {
95
96
  where.$or = [
96
- { workflowId: { $ilike: `%${search}%` } },
97
- { workflowName: { $ilike: `%${search}%` } },
97
+ { workflowId: { $ilike: `%${escapeLikePattern(search)}%` } },
98
+ { workflowName: { $ilike: `%${escapeLikePattern(search)}%` } },
98
99
  ]
99
100
  }
100
101