@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -1
- package/dist/modules/attachments/api/library/route.js +2 -2
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentContentPreview.js +9 -5
- package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +3 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
- package/dist/modules/auth/commands/users.js +20 -14
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/data/entities.js +1 -1
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260610120000.js +30 -0
- package/dist/modules/auth/migrations/Migration20260610120000.js.map +7 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js.map +1 -1
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +1 -1
- package/dist/modules/catalog/ai-tools/products-pack.js.map +1 -1
- package/dist/modules/catalog/ai-tools/variants-pack.js.map +1 -1
- package/dist/modules/communication_channels/data/entities.js.map +1 -1
- package/dist/modules/communication_channels/encryption.js.map +1 -1
- package/dist/modules/communication_channels/lib/thread-matcher.js.map +1 -1
- package/dist/modules/communication_channels/lib/thread-token.js.map +1 -1
- package/dist/modules/currencies/api/currencies/route.js +4 -3
- package/dist/modules/currencies/api/currencies/route.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles.js +2 -1
- package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
- package/dist/modules/customer_accounts/events.js +1 -1
- package/dist/modules/customer_accounts/events.js.map +1 -1
- package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +1 -1
- package/dist/modules/customers/acl.js +1 -1
- package/dist/modules/customers/acl.js.map +1 -1
- package/dist/modules/customers/ai-tools/companies-pack.js.map +1 -1
- package/dist/modules/customers/ai-tools/deals-pack.js.map +1 -1
- package/dist/modules/customers/ai-tools/people-pack.js.map +1 -1
- package/dist/modules/customers/api/companies/route.js +4 -4
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +4 -4
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/commands/addresses.js +5 -5
- package/dist/modules/customers/commands/addresses.js.map +2 -2
- package/dist/modules/customers/commands/comments.js +5 -5
- package/dist/modules/customers/commands/comments.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +2 -2
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/entity-roles.js +2 -1
- package/dist/modules/customers/commands/entity-roles.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +8 -5
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/commands/shared.js +21 -6
- package/dist/modules/customers/commands/shared.js.map +2 -2
- package/dist/modules/customers/commands/tags.js +3 -3
- package/dist/modules/customers/commands/tags.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +21 -8
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +1 -1
- package/dist/modules/data_sync/api/run.js +1 -1
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/payment_gateways/api/transactions/route.js +2 -4
- package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
- package/dist/modules/progress/api/jobs/[id]/route.js +7 -2
- package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
- package/dist/modules/progress/api/jobs/route.js +1 -1
- package/dist/modules/progress/api/jobs/route.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +8 -2
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/resources/api/resources.js +2 -3
- package/dist/modules/resources/api/resources.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +2 -2
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sync_excel/api/import/route.js +1 -1
- package/dist/modules/sync_excel/api/import/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/route.js +3 -2
- package/dist/modules/workflows/api/definitions/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/api/library/route.ts +2 -2
- package/src/modules/attachments/components/AttachmentContentPreview.tsx +6 -6
- package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +14 -2
- package/src/modules/auth/commands/users.ts +32 -15
- package/src/modules/auth/data/entities.ts +11 -1
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +0 -10
- package/src/modules/auth/migrations/Migration20260610120000.ts +53 -0
- package/src/modules/catalog/ai-tools/configuration-pack.ts +1 -1
- package/src/modules/catalog/ai-tools/prices-offers-pack.ts +1 -1
- package/src/modules/catalog/ai-tools/products-pack.ts +1 -1
- package/src/modules/catalog/ai-tools/variants-pack.ts +1 -1
- package/src/modules/communication_channels/data/entities.ts +2 -2
- package/src/modules/communication_channels/encryption.ts +1 -1
- package/src/modules/communication_channels/lib/adapter.ts +1 -1
- package/src/modules/communication_channels/lib/thread-matcher.ts +1 -1
- package/src/modules/communication_channels/lib/thread-token.ts +1 -1
- package/src/modules/currencies/api/currencies/route.ts +4 -3
- package/src/modules/customer_accounts/api/admin/roles.ts +2 -1
- package/src/modules/customer_accounts/events.ts +1 -1
- package/src/modules/customer_accounts/lib/resolveTenantContext.ts +2 -2
- package/src/modules/customers/acl.ts +1 -1
- package/src/modules/customers/ai-tools/companies-pack.ts +1 -1
- package/src/modules/customers/ai-tools/deals-pack.ts +1 -1
- package/src/modules/customers/ai-tools/people-pack.ts +1 -1
- package/src/modules/customers/api/companies/route.ts +4 -4
- package/src/modules/customers/api/people/route.ts +4 -4
- package/src/modules/customers/commands/addresses.ts +5 -5
- package/src/modules/customers/commands/comments.ts +5 -5
- package/src/modules/customers/commands/deals.ts +2 -2
- package/src/modules/customers/commands/entity-roles.ts +2 -1
- package/src/modules/customers/commands/interactions.ts +8 -5
- package/src/modules/customers/commands/shared.ts +26 -4
- package/src/modules/customers/commands/tags.ts +3 -3
- package/src/modules/customers/components/detail/assignableStaff.ts +32 -8
- package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +1 -1
- package/src/modules/data_sync/api/run.ts +1 -1
- package/src/modules/payment_gateways/api/transactions/route.ts +2 -5
- package/src/modules/progress/api/jobs/[id]/route.ts +6 -1
- package/src/modules/progress/api/jobs/route.ts +1 -1
- package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
- package/src/modules/resources/api/resources.ts +2 -3
- package/src/modules/sales/api/documents/factory.ts +2 -2
- package/src/modules/staff/AGENTS.md +1 -1
- package/src/modules/sync_excel/api/import/route.ts +1 -1
- 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 {
|
|
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 =
|
|
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:
|
|
279
|
+
filters.primary_email = { $ilike: buildIlikeTerm(emailStartsWith, 'startsWith') }
|
|
280
280
|
} else if (emailContains) {
|
|
281
|
-
filters.primary_email = { $ilike:
|
|
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 {
|
|
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 =
|
|
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:
|
|
192
|
+
filters.primary_email = { $ilike: buildIlikeTerm(emailStartsWith, 'startsWith') }
|
|
193
193
|
} else if (emailContains) {
|
|
194
|
-
filters.primary_email = { $ilike:
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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(
|
|
32
|
-
params.set('pageSize', String(
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
:
|
|
135
|
+
: page,
|
|
112
136
|
pageSize:
|
|
113
137
|
typeof data?.pageSize === 'number' && Number.isFinite(data.pageSize)
|
|
114
138
|
? data.pageSize
|
|
115
|
-
:
|
|
139
|
+
: pageSize,
|
|
116
140
|
}
|
|
117
141
|
}
|
|
118
142
|
|
package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts
CHANGED
|
@@ -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.'
|
|
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 =
|
|
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, {
|
|
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.'
|
|
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, {
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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.'
|
|
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
|
|