@open-mercato/core 0.6.6-develop.6299.1.29224e2d86 → 0.6.6-develop.6304.1.4cf2b975cb

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 (30) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  3. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -22
  4. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  5. package/dist/modules/customers/components/detail/CompanyKpiBar.js +8 -7
  6. package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
  7. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +6 -5
  8. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  9. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +32 -10
  10. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  11. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +77 -32
  12. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  13. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +6 -0
  14. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  15. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +6 -0
  16. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  17. package/dist/modules/messages/api/route.js.map +2 -2
  18. package/package.json +7 -7
  19. package/src/modules/customers/api/people/[id]/route.ts +7 -0
  20. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +29 -22
  21. package/src/modules/customers/components/detail/CompanyKpiBar.tsx +8 -7
  22. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +6 -5
  23. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +32 -10
  24. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +77 -33
  25. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +7 -0
  26. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +7 -0
  27. package/src/modules/messages/api/route.ts +8 -0
  28. package/dist/modules/customers/components/detail/CompanyDashboardTab.js +0 -133
  29. package/dist/modules/customers/components/detail/CompanyDashboardTab.js.map +0 -7
  30. package/src/modules/customers/components/detail/CompanyDashboardTab.tsx +0 -161
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/dictionaries/api/%5BdictionaryId%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n resolveDictionaryEntrySortMode,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport {\n dictionariesErrorSchema,\n dictionariesOkSchema,\n dictionariesTag,\n dictionaryDetailSchema,\n dictionaryIdParamsSchema,\n dictionaryUpdateSchema,\n upsertDictionarySchema,\n} from '../openapi'\nimport { dictionaryKeySchema } from '@open-mercato/core/modules/dictionaries/data/validators'\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n// System dictionaries use namespaced keys (e.g. `sales.deal_loss_reason`,\n// `resources.activity-types`) that the strict create-key regex rejects. The\n// manager edit dialog disables the key field but still resubmits the existing\n// key, so the update parse must accept any stored key verbatim. The strict\n// user-key regex is only enforced below when the key actually changes.\nconst updateKeySchema = z.string().trim().min(1).max(100)\nconst updateSchema = upsertDictionarySchema\n .partial()\n .extend({ key: updateKeySchema.optional() })\n .refine((data) => Object.keys(data).length > 0, {\n message: 'Provide at least one field to update.',\n })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dictionaries.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nfunction isProtectedCurrencyDictionary(dictionary: Dictionary) {\n const key = dictionary.key?.trim().toLowerCase() ?? ''\n return key === 'currency' || key === 'currencies'\n}\n\nasync function loadDictionary(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n id: string,\n options: { allowInherited?: boolean } = {},\n) {\n const { allowInherited = false } = options\n if (!allowInherited && !context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const baseFilter = {\n id,\n tenantId: context.tenantId,\n deletedAt: null,\n }\n const filter = allowInherited\n ? {\n ...baseFilter,\n ...(context.readableOrganizationIds.length\n ? { organizationId: { $in: context.readableOrganizationIds } }\n : {}),\n }\n : {\n ...baseFilter,\n organizationId: context.organizationId,\n }\n const dictionary = await context.em.findOne(Dictionary, filter)\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nexport async function GET(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })\n return NextResponse.json({\n id: dictionary.id,\n key: dictionary.key,\n name: dictionary.name,\n description: dictionary.description,\n isSystem: dictionary.isSystem,\n isActive: dictionary.isActive,\n managerVisibility: dictionary.managerVisibility,\n entrySortMode: resolveDictionaryEntrySortMode(dictionary.entrySortMode),\n organizationId: dictionary.organizationId,\n isInherited: context.organizationId ? dictionary.organizationId !== context.organizationId : false,\n createdAt: dictionary.createdAt,\n updatedAt: dictionary.updatedAt,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('[dictionaries/:id.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to load dictionary' }, { status: 500 })\n }\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const payload = updateSchema.parse(await req.json().catch(() => ({})))\n const dictionary = await loadDictionary(context, dictionaryId)\n\n enforceCommandOptimisticLock({\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n current: dictionary.updatedAt ?? null,\n request: req,\n })\n\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n if (isProtectedCurrencyDictionary(dictionary)) {\n if (payload.key && payload.key.trim().toLowerCase() !== dictionary.key) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.currency_protected', 'The currency dictionary cannot be modified or deleted.') })\n }\n if (payload.isActive === false) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.currency_protected', 'The currency dictionary cannot be modified or deleted.') })\n }\n }\n\n if (payload.key) {\n const key = payload.key.trim().toLowerCase()\n if (key !== dictionary.key) {\n const strictKey = dictionaryKeySchema.safeParse(key)\n if (!strictKey.success) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.invalid_key', 'Use lowercase letters, numbers, hyphen, or underscore.') })\n }\n const organizationId = context.organizationId\n if (!organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const existing = await context.em.findOne(Dictionary, {\n key,\n organizationId,\n tenantId: context.tenantId,\n deletedAt: null,\n })\n if (existing) {\n throw new CrudHttpError(409, { error: context.translate('dictionaries.errors.duplicate', 'A dictionary with this key already exists') })\n }\n dictionary.key = key\n }\n }\n\n if (payload.name) {\n dictionary.name = payload.name.trim()\n }\n if (payload.description !== undefined) {\n dictionary.description = payload.description ? payload.description.trim() : null\n }\n if (payload.isActive !== undefined) {\n dictionary.isActive = Boolean(payload.isActive)\n if (!dictionary.isActive) {\n dictionary.deletedAt = dictionary.deletedAt ?? new Date()\n } else {\n dictionary.deletedAt = null\n }\n }\n if (payload.entrySortMode !== undefined) {\n dictionary.entrySortMode = payload.entrySortMode\n }\n\n dictionary.updatedAt = new Date()\n await context.em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n id: dictionary.id,\n key: dictionary.key,\n name: dictionary.name,\n description: dictionary.description,\n isSystem: dictionary.isSystem,\n isActive: dictionary.isActive,\n managerVisibility: dictionary.managerVisibility,\n entrySortMode: resolveDictionaryEntrySortMode(dictionary.entrySortMode),\n createdAt: dictionary.createdAt,\n updatedAt: dictionary.updatedAt,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: err.issues[0]?.message ?? 'Validation failed' }, { status: 400 })\n }\n console.error('[dictionaries/:id.PATCH] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to update dictionary' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const dictionary = await loadDictionary(context, dictionaryId)\n\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n if (isProtectedCurrencyDictionary(dictionary)) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.currency_protected', 'The currency dictionary cannot be modified or deleted.') })\n }\n\n dictionary.isActive = false\n dictionary.deletedAt = dictionary.deletedAt ?? new Date()\n await context.em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('[dictionaries/:id.DELETE] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to delete dictionary' }, { status: 500 })\n }\n}\n\nconst dictionaryGetDoc: OpenApiMethodDoc = {\n summary: 'Get dictionary',\n description: 'Returns details for the specified dictionary, including inheritance flags.',\n tags: [dictionariesTag],\n responses: [\n { status: 200, description: 'Dictionary details.', schema: dictionaryDetailSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to load dictionary', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryPatchDoc: OpenApiMethodDoc = {\n summary: 'Update dictionary',\n description: 'Updates mutable attributes of the dictionary. Currency dictionaries are protected from modification.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: dictionaryUpdateSchema,\n description: 'Fields to update on the dictionary.',\n },\n responses: [\n { status: 200, description: 'Dictionary updated.', schema: dictionaryDetailSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed or protected dictionary', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 409, description: 'Dictionary key already exists', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to update dictionary', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete dictionary',\n description: 'Soft deletes the dictionary unless it is the protected currency dictionary.',\n tags: [dictionariesTag],\n responses: [\n { status: 200, description: 'Dictionary archived.', schema: dictionariesOkSchema },\n ],\n errors: [\n { status: 400, description: 'Protected dictionary cannot be deleted', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to delete dictionary', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary resource',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n GET: dictionaryGetDoc,\n PATCH: dictionaryPatchDoc,\n DELETE: dictionaryDeleteDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n resolveDictionaryEntrySortMode,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport {\n dictionariesErrorSchema,\n dictionariesOkSchema,\n dictionariesTag,\n dictionaryDetailSchema,\n dictionaryIdParamsSchema,\n dictionaryUpdateSchema,\n upsertDictionarySchema,\n} from '../openapi'\nimport { dictionaryKeySchema } from '@open-mercato/core/modules/dictionaries/data/validators'\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n// System dictionaries use namespaced keys (e.g. `sales.deal_loss_reason`,\n// `resources.activity-types`) that the strict create-key regex rejects. The\n// manager edit dialog disables the key field but still resubmits the existing\n// key, so the update parse must accept any stored key verbatim. The strict\n// user-key regex is only enforced below when the key actually changes.\nconst updateKeySchema = z.string().trim().min(1).max(100)\nconst updateSchema = upsertDictionarySchema\n .partial()\n .extend({ key: updateKeySchema.optional() })\n .refine((data) => Object.keys(data).length > 0, {\n message: 'Provide at least one field to update.',\n })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dictionaries.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nfunction isProtectedCurrencyDictionary(dictionary: Dictionary) {\n const key = dictionary.key?.trim().toLowerCase() ?? ''\n return key === 'currency' || key === 'currencies'\n}\n\nasync function loadDictionary(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n id: string,\n options: { allowInherited?: boolean } = {},\n) {\n const { allowInherited = false } = options\n if (!allowInherited && !context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const baseFilter = {\n id,\n tenantId: context.tenantId,\n deletedAt: null,\n }\n const filter = allowInherited\n ? {\n ...baseFilter,\n ...(context.readableOrganizationIds.length\n ? { organizationId: { $in: context.readableOrganizationIds } }\n : {}),\n }\n : {\n ...baseFilter,\n organizationId: context.organizationId,\n }\n const dictionary = await context.em.findOne(Dictionary, filter)\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nexport async function GET(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })\n return NextResponse.json({\n id: dictionary.id,\n key: dictionary.key,\n name: dictionary.name,\n description: dictionary.description,\n isSystem: dictionary.isSystem,\n isActive: dictionary.isActive,\n managerVisibility: dictionary.managerVisibility,\n entrySortMode: resolveDictionaryEntrySortMode(dictionary.entrySortMode),\n organizationId: dictionary.organizationId,\n isInherited: context.organizationId ? dictionary.organizationId !== context.organizationId : false,\n createdAt: dictionary.createdAt,\n updatedAt: dictionary.updatedAt,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('[dictionaries/:id.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to load dictionary' }, { status: 500 })\n }\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const payload = updateSchema.parse(await req.json().catch(() => ({})))\n const dictionary = await loadDictionary(context, dictionaryId)\n\n enforceCommandOptimisticLock({\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n current: dictionary.updatedAt ?? null,\n request: req,\n })\n\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n if (isProtectedCurrencyDictionary(dictionary)) {\n if (payload.key && payload.key.trim().toLowerCase() !== dictionary.key) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.currency_protected', 'The currency dictionary cannot be modified or deleted.') })\n }\n if (payload.isActive === false) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.currency_protected', 'The currency dictionary cannot be modified or deleted.') })\n }\n }\n\n if (payload.key) {\n const key = payload.key.trim().toLowerCase()\n if (key !== dictionary.key) {\n const strictKey = dictionaryKeySchema.safeParse(key)\n if (!strictKey.success) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.invalid_key', 'Use lowercase letters, numbers, hyphen, or underscore.') })\n }\n const organizationId = context.organizationId\n if (!organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const existing = await context.em.findOne(Dictionary, {\n key,\n organizationId,\n tenantId: context.tenantId,\n deletedAt: null,\n })\n if (existing) {\n throw new CrudHttpError(409, { error: context.translate('dictionaries.errors.duplicate', 'A dictionary with this key already exists') })\n }\n dictionary.key = key\n }\n }\n\n if (payload.name) {\n dictionary.name = payload.name.trim()\n }\n if (payload.description !== undefined) {\n dictionary.description = payload.description ? payload.description.trim() : null\n }\n if (payload.isActive !== undefined) {\n dictionary.isActive = Boolean(payload.isActive)\n if (!dictionary.isActive) {\n dictionary.deletedAt = dictionary.deletedAt ?? new Date()\n } else {\n dictionary.deletedAt = null\n }\n }\n if (payload.entrySortMode !== undefined) {\n dictionary.entrySortMode = payload.entrySortMode\n }\n\n dictionary.updatedAt = new Date()\n await context.em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n id: dictionary.id,\n key: dictionary.key,\n name: dictionary.name,\n description: dictionary.description,\n isSystem: dictionary.isSystem,\n isActive: dictionary.isActive,\n managerVisibility: dictionary.managerVisibility,\n entrySortMode: resolveDictionaryEntrySortMode(dictionary.entrySortMode),\n createdAt: dictionary.createdAt,\n updatedAt: dictionary.updatedAt,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: err.issues[0]?.message ?? 'Validation failed' }, { status: 400 })\n }\n console.error('[dictionaries/:id.PATCH] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to update dictionary' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const dictionary = await loadDictionary(context, dictionaryId)\n\n enforceCommandOptimisticLock({\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n current: dictionary.updatedAt ?? null,\n request: req,\n })\n\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n if (isProtectedCurrencyDictionary(dictionary)) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.currency_protected', 'The currency dictionary cannot be modified or deleted.') })\n }\n\n dictionary.isActive = false\n dictionary.deletedAt = dictionary.deletedAt ?? new Date()\n await context.em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'dictionaries.dictionary',\n resourceId: dictionary.id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('[dictionaries/:id.DELETE] Unexpected error', err)\n return NextResponse.json({ error: 'Failed to delete dictionary' }, { status: 500 })\n }\n}\n\nconst dictionaryGetDoc: OpenApiMethodDoc = {\n summary: 'Get dictionary',\n description: 'Returns details for the specified dictionary, including inheritance flags.',\n tags: [dictionariesTag],\n responses: [\n { status: 200, description: 'Dictionary details.', schema: dictionaryDetailSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to load dictionary', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryPatchDoc: OpenApiMethodDoc = {\n summary: 'Update dictionary',\n description: 'Updates mutable attributes of the dictionary. Currency dictionaries are protected from modification.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: dictionaryUpdateSchema,\n description: 'Fields to update on the dictionary.',\n },\n responses: [\n { status: 200, description: 'Dictionary updated.', schema: dictionaryDetailSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed or protected dictionary', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 409, description: 'Dictionary key already exists', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to update dictionary', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete dictionary',\n description: 'Soft deletes the dictionary unless it is the protected currency dictionary.',\n tags: [dictionariesTag],\n responses: [\n { status: 200, description: 'Dictionary archived.', schema: dictionariesOkSchema },\n ],\n errors: [\n { status: 400, description: 'Protected dictionary cannot be deleted', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to delete dictionary', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary resource',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n GET: dictionaryGetDoc,\n PATCH: dictionaryPatchDoc,\n DELETE: dictionaryDeleteDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/api/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageObject } from '../data/entities'\nimport { composeMessageSchema, listMessagesSchema } from '../data/validators'\nimport { MESSAGE_ATTACHMENT_ENTITY_ID } from '../lib/constants'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { validateMessageObjectsForType } from '../lib/object-validation'\nimport { attachOperationMetadataHeader } from '../lib/operationMetadata'\nimport { canUseMessageEmailFeature, resolveMessageContext } from '../lib/routeHelpers'\nimport { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from './guards'\nimport { findMessageIdsBySearchTokens } from '../lib/searchLookup'\nimport { MessageCommandExecuteResult } from '../commands/shared'\nimport {\n composeMessageSchema as composeSchema,\n composeResponseSchema,\n listMessagesSchema as listSchema,\n messageListItemSchema,\n} from './openapi'\n\ntype MessageCommandExecuteResultWithThreadId = MessageCommandExecuteResult & {\n threadId: string\n}\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\ntype MessageListScopeRow = {\n id: string\n sender_user_id: string\n is_draft: boolean\n recipient_status: string | null\n read_at: string | null\n}\n\ntype AttachmentCountRow = {\n record_id: string\n count: string | number\n}\n\ntype RecipientCountRow = {\n message_id: string\n count: string | number\n}\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['messages.compose'] },\n}\n\nexport async function GET(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n const url = new URL(req.url)\n const params = Object.fromEntries(url.searchParams)\n const input = listMessagesSchema.parse(params)\n const db = getDb(em) as any\n\n const searchIds = input.search\n ? await findMessageIdsBySearchTokens({\n em,\n query: input.search,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId,\n })\n : undefined\n\n const buildBaseQuery = () => {\n let q: any = db\n .selectFrom('messages as m')\n .where('m.tenant_id', '=', scope.tenantId)\n .where('m.deleted_at', 'is', null)\n\n if (scope.organizationId) {\n q = q.where('m.organization_id', '=', scope.organizationId)\n } else {\n q = q.where('m.organization_id', 'is', null)\n }\n\n const joinRecipient = () => {\n q = q.leftJoin('message_recipients as r', (jb: any) => jb\n .onRef('m.id', '=', 'r.message_id')\n .on('r.recipient_user_id', '=', scope.userId))\n }\n\n switch (input.folder) {\n case 'inbox':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is', null)\n .where('m.is_draft', '=', false)\n break\n case 'archived':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is not', null)\n break\n case 'sent':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', false)\n joinRecipient()\n break\n case 'drafts':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', true)\n joinRecipient()\n break\n case 'all':\n joinRecipient()\n q = q.where((eb: any) => eb.or([\n eb('m.sender_user_id', '=', scope.userId),\n eb('r.message_id', 'is not', null),\n ]))\n break\n default: {\n const unsupportedFolder: never = input.folder\n throw new Error(`Unsupported folder: ${String(unsupportedFolder)}`)\n }\n }\n\n if (input.status) q = q.where('r.status', '=', input.status)\n if (input.type) q = q.where('m.type', '=', input.type)\n if (input.visibility) q = q.where('m.visibility', '=', input.visibility)\n if (input.sourceEntityType) q = q.where('m.source_entity_type', '=', input.sourceEntityType)\n if (input.sourceEntityId) q = q.where('m.source_entity_id', '=', input.sourceEntityId)\n if (input.externalEmail) q = q.where('m.external_email_hash', 'in', lookupHashCandidates(input.externalEmail))\n if (input.senderId) q = q.where('m.sender_user_id', '=', input.senderId)\n\n if (input.search) {\n if (!searchIds || searchIds.length === 0) {\n q = q.where('m.id', '=', NO_MATCH_ID)\n } else {\n q = q.where('m.id', 'in', searchIds)\n }\n }\n\n if (input.since) q = q.where('m.sent_at', '>', new Date(input.since))\n\n if (input.hasObjects !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n ))\n q = input.hasObjects ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasAttachments !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n ))\n q = input.hasAttachments ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasActions !== undefined) {\n q = input.hasActions\n ? q.where('m.action_data', 'is not', null)\n : q.where('m.action_data', 'is', null)\n }\n\n return q\n }\n\n const countResult = await buildBaseQuery()\n .select(sql<number>`count(*)`.as('count'))\n .executeTakeFirst() as { count: string | number } | undefined\n const total = Number(countResult?.count ?? 0)\n\n const offset = (input.page - 1) * input.pageSize\n const scopeRows = await buildBaseQuery()\n .select([\n 'm.id',\n 'm.sender_user_id',\n 'm.is_draft',\n 'r.status as recipient_status',\n 'r.read_at',\n ])\n .orderBy('m.sent_at', 'desc')\n .offset(offset)\n .limit(input.pageSize)\n .execute()\n\n const typedRows = scopeRows as MessageListScopeRow[]\n const messageIds = typedRows.map((row) => row.id)\n\n const messageEntities = messageIds.length > 0\n ? await findWithDecryption(\n em,\n Message,\n { id: { $in: messageIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const messagesById = new Map<string, Message>()\n for (const message of messageEntities) {\n messagesById.set(message.id, message)\n }\n\n const objects = messageIds.length > 0\n ? await em.find(MessageObject, { messageId: { $in: messageIds } })\n : []\n\n const objectsByMessage = objects.reduce((acc, obj) => {\n if (!acc[obj.messageId]) acc[obj.messageId] = []\n acc[obj.messageId].push(obj)\n return acc\n }, {} as Record<string, MessageObject[]>)\n\n const attachmentCounts: AttachmentCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('attachments')\n .select(['record_id', sql<string>`count(*)`.as('count')])\n .where('entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where('record_id', 'in', messageIds)\n .groupBy('record_id')\n .execute()\n : []\n\n const attachmentCountByMessage = attachmentCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.record_id] = Number(row.count)\n return acc\n }, {})\n\n const recipientCounts: RecipientCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('message_recipients')\n .select(['message_id', sql<string>`count(*)`.as('count')])\n .where('message_id', 'in', messageIds)\n .where('deleted_at', 'is', null)\n .groupBy('message_id')\n .execute()\n : []\n\n const recipientCountByMessage = recipientCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.message_id] = Number(row.count)\n return acc\n }, {})\n\n const senderUserIds = Array.from(new Set(typedRows.map((row) => row.sender_user_id).filter(Boolean)))\n const senderUsers = senderUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: senderUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const senderMetaById = new Map<string, { name: string | null; email: string | null }>()\n senderUsers.forEach((user) => {\n const name = typeof user.name === 'string' && user.name.trim().length ? user.name.trim() : null\n senderMetaById.set(user.id, { name, email: user.email ?? null })\n })\n\n return Response.json({\n items: typedRows\n .map((row) => {\n const message = messagesById.get(row.id)\n if (!message) return null\n const body = typeof message.body === 'string' ? message.body : ''\n const bodyPreview = body.substring(0, 150) + (body.length > 150 ? '...' : '')\n const actionData = message.actionData ?? null\n return {\n ...(senderMetaById.get(row.sender_user_id)\n ? {\n senderName: senderMetaById.get(row.sender_user_id)?.name ?? null,\n senderEmail: senderMetaById.get(row.sender_user_id)?.email ?? null,\n }\n : { senderName: null, senderEmail: null }),\n id: message.id,\n type: message.type,\n visibility: message.visibility ?? null,\n sourceEntityType: message.sourceEntityType ?? null,\n sourceEntityId: message.sourceEntityId ?? null,\n externalEmail: message.externalEmail ?? null,\n externalName: message.externalName ?? null,\n subject: message.subject,\n bodyPreview,\n senderUserId: message.senderUserId,\n priority: message.priority,\n status: row.recipient_status ?? (row.is_draft ? 'draft' : 'sent'),\n hasObjects: (objectsByMessage[message.id] || []).length > 0,\n objectCount: (objectsByMessage[message.id] || []).length,\n hasAttachments: (attachmentCountByMessage[message.id] || 0) > 0,\n attachmentCount: attachmentCountByMessage[message.id] || 0,\n recipientCount: recipientCountByMessage[message.id] || 0,\n hasActions:\n Boolean(actionData?.actions?.length)\n || Boolean(getMessageType(message.type)?.defaultActions?.length)\n || (objectsByMessage[message.id] || []).some((item) => item.actionRequired && Boolean(item.actionType)),\n actionTaken: message.actionTaken ?? null,\n sentAt: message.sentAt ? message.sentAt.toISOString() : null,\n readAt: row.read_at,\n threadId: message.threadId ?? null,\n }\n })\n .filter((item): item is NonNullable<typeof item> => item !== null),\n page: input.page,\n pageSize: input.pageSize,\n total,\n totalPages: Math.ceil(total / input.pageSize),\n })\n}\n\nexport async function POST(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const body = await req.json().catch(() => ({}))\n const input = composeMessageSchema.parse(body)\n\n const isPublicVisibility = input.visibility === 'public'\n const sendViaEmail = isPublicVisibility ? true : input.sendViaEmail\n if (sendViaEmail && !(await canUseMessageEmailFeature(ctx, scope))) {\n return Response.json({ error: 'Missing feature: messages.email' }, { status: 403 })\n }\n\n if (input.objects?.length) {\n const objectValidationError = validateMessageObjectsForType(input.type, input.objects)\n if (objectValidationError) {\n return Response.json({ error: objectValidationError }, { status: 400 })\n }\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input as Record<string, unknown>,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const { result, logEntry } = await commandBus.execute('messages.messages.compose', {\n input: {\n ...input,\n sendViaEmail,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n const { id: messageId, threadId: responseThreadId } = result as unknown as MessageCommandExecuteResultWithThreadId\n\n const response = Response.json({ id: messageId, threadId: responseThreadId }, { status: 201 })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: messageId,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: messageId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Messages',\n methods: {\n GET: {\n summary: 'List messages',\n query: listSchema,\n responses: [\n {\n status: 200,\n description: 'Message list',\n schema: z.object({\n items: z.array(messageListItemSchema),\n page: z.number(),\n pageSize: z.number(),\n total: z.number(),\n totalPages: z.number(),\n }),\n },\n ],\n },\n POST: {\n summary: 'Compose a message',\n requestBody: {\n schema: composeSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Message created',\n schema: composeResponseSchema,\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageObject } from '../data/entities'\nimport { composeMessageSchema, listMessagesSchema } from '../data/validators'\nimport { MESSAGE_ATTACHMENT_ENTITY_ID } from '../lib/constants'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { validateMessageObjectsForType } from '../lib/object-validation'\nimport { attachOperationMetadataHeader } from '../lib/operationMetadata'\nimport { canUseMessageEmailFeature, resolveMessageContext } from '../lib/routeHelpers'\nimport { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from './guards'\nimport { findMessageIdsBySearchTokens } from '../lib/searchLookup'\nimport { MessageCommandExecuteResult } from '../commands/shared'\nimport {\n composeMessageSchema as composeSchema,\n composeResponseSchema,\n listMessagesSchema as listSchema,\n messageListItemSchema,\n} from './openapi'\n\ntype MessageCommandExecuteResultWithThreadId = MessageCommandExecuteResult & {\n threadId: string\n}\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\ntype MessageListScopeRow = {\n id: string\n sender_user_id: string\n is_draft: boolean\n recipient_status: string | null\n read_at: string | null\n}\n\ntype AttachmentCountRow = {\n record_id: string\n count: string | number\n}\n\ntype RecipientCountRow = {\n message_id: string\n count: string | number\n}\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['messages.compose'] },\n}\n\nexport async function GET(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n const url = new URL(req.url)\n const params = Object.fromEntries(url.searchParams)\n const input = listMessagesSchema.parse(params)\n const db = getDb(em) as any\n\n const searchIds = input.search\n ? await findMessageIdsBySearchTokens({\n em,\n query: input.search,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId,\n })\n : undefined\n\n const buildBaseQuery = () => {\n let q: any = db\n .selectFrom('messages as m')\n .where('m.tenant_id', '=', scope.tenantId)\n .where('m.deleted_at', 'is', null)\n\n if (scope.organizationId) {\n q = q.where('m.organization_id', '=', scope.organizationId)\n } else {\n q = q.where('m.organization_id', 'is', null)\n }\n\n const joinRecipient = () => {\n q = q.leftJoin('message_recipients as r', (jb: any) => jb\n .onRef('m.id', '=', 'r.message_id')\n .on('r.recipient_user_id', '=', scope.userId))\n }\n\n switch (input.folder) {\n case 'inbox':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is', null)\n .where('m.is_draft', '=', false)\n break\n case 'archived':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is not', null)\n break\n case 'sent':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', false)\n joinRecipient()\n break\n case 'drafts':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', true)\n joinRecipient()\n break\n case 'all':\n joinRecipient()\n q = q.where((eb: any) => eb.or([\n eb('m.sender_user_id', '=', scope.userId),\n eb('r.message_id', 'is not', null),\n ]))\n break\n default: {\n const unsupportedFolder: never = input.folder\n throw new Error(`Unsupported folder: ${String(unsupportedFolder)}`)\n }\n }\n\n if (input.status) q = q.where('r.status', '=', input.status)\n if (input.type) q = q.where('m.type', '=', input.type)\n if (input.visibility) q = q.where('m.visibility', '=', input.visibility)\n if (input.sourceEntityType) q = q.where('m.source_entity_type', '=', input.sourceEntityType)\n if (input.sourceEntityId) q = q.where('m.source_entity_id', '=', input.sourceEntityId)\n if (input.externalEmail) q = q.where('m.external_email_hash', 'in', lookupHashCandidates(input.externalEmail))\n if (input.senderId) q = q.where('m.sender_user_id', '=', input.senderId)\n\n if (input.search) {\n if (!searchIds || searchIds.length === 0) {\n q = q.where('m.id', '=', NO_MATCH_ID)\n } else {\n q = q.where('m.id', 'in', searchIds)\n }\n }\n\n if (input.since) q = q.where('m.sent_at', '>', new Date(input.since))\n\n if (input.hasObjects !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n ))\n q = input.hasObjects ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasAttachments !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n ))\n q = input.hasAttachments ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasActions !== undefined) {\n q = input.hasActions\n ? q.where('m.action_data', 'is not', null)\n : q.where('m.action_data', 'is', null)\n }\n\n return q\n }\n\n // Audited for #3386 rollout (P3): sort is on m.sent_at (a plain timestamp \u2014\n // not in the messages:message encryption map whose encrypted fields are:\n // subject, body, external_email, external_name, action_data, action_result).\n // The handler already uses the correct two-phase shape: Kysely SQL\n // ORDER BY + LIMIT/OFFSET produces a bounded page of IDs, then\n // findWithDecryption is called only for those IDs \u2014 never for the full\n // result set. The #3278 unbounded-decrypt hazard does not apply here.\n // Covered by __tests__/list.test.ts.\n const countResult = await buildBaseQuery()\n .select(sql<number>`count(*)`.as('count'))\n .executeTakeFirst() as { count: string | number } | undefined\n const total = Number(countResult?.count ?? 0)\n\n const offset = (input.page - 1) * input.pageSize\n const scopeRows = await buildBaseQuery()\n .select([\n 'm.id',\n 'm.sender_user_id',\n 'm.is_draft',\n 'r.status as recipient_status',\n 'r.read_at',\n ])\n .orderBy('m.sent_at', 'desc')\n .offset(offset)\n .limit(input.pageSize)\n .execute()\n\n const typedRows = scopeRows as MessageListScopeRow[]\n const messageIds = typedRows.map((row) => row.id)\n\n const messageEntities = messageIds.length > 0\n ? await findWithDecryption(\n em,\n Message,\n { id: { $in: messageIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const messagesById = new Map<string, Message>()\n for (const message of messageEntities) {\n messagesById.set(message.id, message)\n }\n\n const objects = messageIds.length > 0\n ? await em.find(MessageObject, { messageId: { $in: messageIds } })\n : []\n\n const objectsByMessage = objects.reduce((acc, obj) => {\n if (!acc[obj.messageId]) acc[obj.messageId] = []\n acc[obj.messageId].push(obj)\n return acc\n }, {} as Record<string, MessageObject[]>)\n\n const attachmentCounts: AttachmentCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('attachments')\n .select(['record_id', sql<string>`count(*)`.as('count')])\n .where('entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where('record_id', 'in', messageIds)\n .groupBy('record_id')\n .execute()\n : []\n\n const attachmentCountByMessage = attachmentCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.record_id] = Number(row.count)\n return acc\n }, {})\n\n const recipientCounts: RecipientCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('message_recipients')\n .select(['message_id', sql<string>`count(*)`.as('count')])\n .where('message_id', 'in', messageIds)\n .where('deleted_at', 'is', null)\n .groupBy('message_id')\n .execute()\n : []\n\n const recipientCountByMessage = recipientCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.message_id] = Number(row.count)\n return acc\n }, {})\n\n const senderUserIds = Array.from(new Set(typedRows.map((row) => row.sender_user_id).filter(Boolean)))\n const senderUsers = senderUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: senderUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const senderMetaById = new Map<string, { name: string | null; email: string | null }>()\n senderUsers.forEach((user) => {\n const name = typeof user.name === 'string' && user.name.trim().length ? user.name.trim() : null\n senderMetaById.set(user.id, { name, email: user.email ?? null })\n })\n\n return Response.json({\n items: typedRows\n .map((row) => {\n const message = messagesById.get(row.id)\n if (!message) return null\n const body = typeof message.body === 'string' ? message.body : ''\n const bodyPreview = body.substring(0, 150) + (body.length > 150 ? '...' : '')\n const actionData = message.actionData ?? null\n return {\n ...(senderMetaById.get(row.sender_user_id)\n ? {\n senderName: senderMetaById.get(row.sender_user_id)?.name ?? null,\n senderEmail: senderMetaById.get(row.sender_user_id)?.email ?? null,\n }\n : { senderName: null, senderEmail: null }),\n id: message.id,\n type: message.type,\n visibility: message.visibility ?? null,\n sourceEntityType: message.sourceEntityType ?? null,\n sourceEntityId: message.sourceEntityId ?? null,\n externalEmail: message.externalEmail ?? null,\n externalName: message.externalName ?? null,\n subject: message.subject,\n bodyPreview,\n senderUserId: message.senderUserId,\n priority: message.priority,\n status: row.recipient_status ?? (row.is_draft ? 'draft' : 'sent'),\n hasObjects: (objectsByMessage[message.id] || []).length > 0,\n objectCount: (objectsByMessage[message.id] || []).length,\n hasAttachments: (attachmentCountByMessage[message.id] || 0) > 0,\n attachmentCount: attachmentCountByMessage[message.id] || 0,\n recipientCount: recipientCountByMessage[message.id] || 0,\n hasActions:\n Boolean(actionData?.actions?.length)\n || Boolean(getMessageType(message.type)?.defaultActions?.length)\n || (objectsByMessage[message.id] || []).some((item) => item.actionRequired && Boolean(item.actionType)),\n actionTaken: message.actionTaken ?? null,\n sentAt: message.sentAt ? message.sentAt.toISOString() : null,\n readAt: row.read_at,\n threadId: message.threadId ?? null,\n }\n })\n .filter((item): item is NonNullable<typeof item> => item !== null),\n page: input.page,\n pageSize: input.pageSize,\n total,\n totalPages: Math.ceil(total / input.pageSize),\n })\n}\n\nexport async function POST(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const body = await req.json().catch(() => ({}))\n const input = composeMessageSchema.parse(body)\n\n const isPublicVisibility = input.visibility === 'public'\n const sendViaEmail = isPublicVisibility ? true : input.sendViaEmail\n if (sendViaEmail && !(await canUseMessageEmailFeature(ctx, scope))) {\n return Response.json({ error: 'Missing feature: messages.email' }, { status: 403 })\n }\n\n if (input.objects?.length) {\n const objectValidationError = validateMessageObjectsForType(input.type, input.objects)\n if (objectValidationError) {\n return Response.json({ error: objectValidationError }, { status: 400 })\n }\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input as Record<string, unknown>,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const { result, logEntry } = await commandBus.execute('messages.messages.compose', {\n input: {\n ...input,\n sendViaEmail,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n const { id: messageId, threadId: responseThreadId } = result as unknown as MessageCommandExecuteResultWithThreadId\n\n const response = Response.json({ id: messageId, threadId: responseThreadId }, { status: 201 })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: messageId,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: messageId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Messages',\n methods: {\n GET: {\n summary: 'List messages',\n query: listSchema,\n responses: [\n {\n status: 200,\n description: 'Message list',\n schema: z.object({\n items: z.array(messageListItemSchema),\n page: z.number(),\n pageSize: z.number(),\n total: z.number(),\n totalPages: z.number(),\n }),\n },\n ],\n },\n POST: {\n summary: 'Compose a message',\n requestBody: {\n schema: composeSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Message created',\n schema: composeResponseSchema,\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6299.1.29224e2d86",
3
+ "version": "0.6.6-develop.6304.1.4cf2b975cb",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -246,16 +246,16 @@
246
246
  "zod": "^4.4.3"
247
247
  },
248
248
  "peerDependencies": {
249
- "@open-mercato/ai-assistant": "0.6.6-develop.6299.1.29224e2d86",
250
- "@open-mercato/shared": "0.6.6-develop.6299.1.29224e2d86",
251
- "@open-mercato/ui": "0.6.6-develop.6299.1.29224e2d86",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6304.1.4cf2b975cb",
250
+ "@open-mercato/shared": "0.6.6-develop.6304.1.4cf2b975cb",
251
+ "@open-mercato/ui": "0.6.6-develop.6304.1.4cf2b975cb",
252
252
  "react": "^19.0.0",
253
253
  "react-dom": "^19.0.0"
254
254
  },
255
255
  "devDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6299.1.29224e2d86",
257
- "@open-mercato/shared": "0.6.6-develop.6299.1.29224e2d86",
258
- "@open-mercato/ui": "0.6.6-develop.6299.1.29224e2d86",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6304.1.4cf2b975cb",
257
+ "@open-mercato/shared": "0.6.6-develop.6304.1.4cf2b975cb",
258
+ "@open-mercato/ui": "0.6.6-develop.6304.1.4cf2b975cb",
259
259
  "@testing-library/dom": "^10.4.1",
260
260
  "@testing-library/jest-dom": "^6.9.1",
261
261
  "@testing-library/react": "^16.3.1",
@@ -507,6 +507,13 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
507
507
  const personScope = { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null }
508
508
  const shouldLoadCanonicalInteractions = includeInteractions || includeActivities || includeTodos
509
509
 
510
+ // Audited for #3386 rollout (P3): every findWithDecryption call in this
511
+ // block and in the activities/todoLinks fetches below sorts only on
512
+ // non-encrypted system columns (isPrimary, createdAt, occurredAt,
513
+ // scheduledAt). Each fetch is bounded by entity scope (one person) plus
514
+ // an explicit limit, so neither the paginated-list hazard (#3278) nor the
515
+ // two-phase encrypted-sort migration apply here.
516
+ //
510
517
  // These reads only depend on the resolved person + scope + email visibility
511
518
  // filter, so dispatch them together to avoid a server-side request waterfall
512
519
  // before the detail surface can render (issue #3203).
@@ -83,6 +83,11 @@ import { BulkActionsBar } from './components/BulkActionsBar'
83
83
  import { ChangeStageDialog } from './components/ChangeStageDialog'
84
84
  import { ChangeOwnerDialog } from './components/ChangeOwnerDialog'
85
85
  import { buildCrudExportUrl, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
86
+ import {
87
+ readVersionedPreference,
88
+ writeVersionedPreference,
89
+ clearVersionedPreference,
90
+ } from '@open-mercato/shared/lib/browser/versionedPreference'
86
91
  import { runBulkDelete, groupBulkDeleteFailures } from '@open-mercato/ui/backend/utils/bulkDelete'
87
92
  import {
88
93
  fetchAssignableStaffMembers,
@@ -170,35 +175,37 @@ const MIN_LANE_WIDTH = 240
170
175
  const MAX_LANE_WIDTH = 576
171
176
  const LANE_GAP = 14
172
177
  const LANE_WIDTHS_STORAGE_KEY_PREFIX = 'kanban-lane-widths-v2'
178
+ const LANE_WIDTHS_STORAGE_VERSION = 1
179
+
180
+ function isNumberRecord(value: unknown): value is Record<string, number> {
181
+ return !!value && typeof value === 'object' && !Array.isArray(value)
182
+ && Object.values(value as Record<string, unknown>).every((v) => typeof v === 'number')
183
+ }
173
184
 
174
185
  function loadLaneWidths(scopeKey: string): Record<string, number> {
175
- if (typeof window === 'undefined') return {}
176
- try {
177
- const raw = window.localStorage.getItem(`${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`)
178
- if (!raw) return {}
179
- const parsed = JSON.parse(raw)
180
- if (!parsed || typeof parsed !== 'object') return {}
181
- const out: Record<string, number> = {}
182
- for (const [k, v] of Object.entries(parsed)) {
183
- if (typeof v === 'number' && Number.isFinite(v) && v >= MIN_LANE_WIDTH && v <= MAX_LANE_WIDTH) {
184
- out[k] = v
185
- }
186
+ const raw = readVersionedPreference<Record<string, number>>(
187
+ `${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`,
188
+ LANE_WIDTHS_STORAGE_VERSION,
189
+ isNumberRecord,
190
+ {},
191
+ { legacyIsValid: isNumberRecord },
192
+ )
193
+ const out: Record<string, number> = {}
194
+ for (const [k, v] of Object.entries(raw)) {
195
+ if (Number.isFinite(v) && v >= MIN_LANE_WIDTH && v <= MAX_LANE_WIDTH) {
196
+ out[k] = v
186
197
  }
187
- return out
188
- } catch {
189
- return {}
190
198
  }
199
+ return out
191
200
  }
192
201
 
193
202
  function saveLaneWidths(scopeKey: string, widths: Record<string, number>) {
194
- if (typeof window === 'undefined') return
195
- try {
196
- if (Object.keys(widths).length === 0) {
197
- window.localStorage.removeItem(`${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`)
198
- } else {
199
- window.localStorage.setItem(`${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`, JSON.stringify(widths))
200
- }
201
- } catch {}
203
+ const key = `${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`
204
+ if (Object.keys(widths).length === 0) {
205
+ clearVersionedPreference(key)
206
+ } else {
207
+ writeVersionedPreference(key, LANE_WIDTHS_STORAGE_VERSION, widths)
208
+ }
202
209
  }
203
210
 
204
211
  function normalizeAmount(value: unknown): number | null {
@@ -7,14 +7,15 @@ import { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts/KpiCard'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
8
8
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
9
9
  import {
10
- readJsonFromLocalStorage,
11
- writeJsonToLocalStorage,
12
- removeLocalStorageKey,
13
- } from '@open-mercato/shared/lib/browser/safeLocalStorage'
10
+ readVersionedIdSet,
11
+ writeVersionedIdSet,
12
+ clearVersionedPreference,
13
+ } from '@open-mercato/shared/lib/browser/versionedPreference'
14
14
  import type { CompanyOverview, DealSummary, InteractionSummary } from '../formConfig'
15
15
  import { formatCurrency } from './utils'
16
16
 
17
17
  const STORAGE_KEY = 'om:company-detail-kpi-hidden'
18
+ const STORAGE_VERSION = 1
18
19
 
19
20
  function sumActiveDeals(deals: DealSummary[]): number {
20
21
  return deals
@@ -100,7 +101,7 @@ export function CompanyKpiBar({ data }: CompanyKpiBarProps) {
100
101
  }, [data.interactions, data.kpis?.clientTenureYears])
101
102
 
102
103
  const [hiddenTiles, setHiddenTiles] = React.useState<Set<string>>(
103
- () => new Set(readJsonFromLocalStorage<string[]>(STORAGE_KEY, [])),
104
+ () => readVersionedIdSet(STORAGE_KEY, STORAGE_VERSION),
104
105
  )
105
106
 
106
107
  const toggleTile = React.useCallback((tileId: string) => {
@@ -108,14 +109,14 @@ export function CompanyKpiBar({ data }: CompanyKpiBarProps) {
108
109
  const next = new Set(prev)
109
110
  if (next.has(tileId)) next.delete(tileId)
110
111
  else next.add(tileId)
111
- writeJsonToLocalStorage(STORAGE_KEY, [...next])
112
+ writeVersionedIdSet(STORAGE_KEY, STORAGE_VERSION, next)
112
113
  return next
113
114
  })
114
115
  }, [])
115
116
 
116
117
  const showAllTiles = React.useCallback(() => {
117
118
  setHiddenTiles(new Set())
118
- removeLocalStorageKey(STORAGE_KEY)
119
+ clearVersionedPreference(STORAGE_KEY)
119
120
  }, [])
120
121
 
121
122
  const kpiTiles = React.useMemo(() => [
@@ -8,9 +8,9 @@ import { Badge } from '@open-mercato/ui/primitives/badge'
8
8
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
9
  import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
10
10
  import {
11
- readJsonFromLocalStorage,
12
- writeJsonToLocalStorage,
13
- } from '@open-mercato/shared/lib/browser/safeLocalStorage'
11
+ readVersionedIdSet,
12
+ writeVersionedIdSet,
13
+ } from '@open-mercato/shared/lib/browser/versionedPreference'
14
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
15
  import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
16
16
  import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
@@ -60,6 +60,7 @@ export type CompanyPeopleSectionProps = {
60
60
  }
61
61
 
62
62
  const COMPANY_PEOPLE_PAGE_SIZE = 20
63
+ const STARRED_PEOPLE_STORAGE_VERSION = 1
63
64
 
64
65
  function normalizeCompanyPerson(record: Record<string, unknown>): CompanyPersonSummary | null {
65
66
  const id = typeof record.id === 'string' ? record.id : null
@@ -206,7 +207,7 @@ export function CompanyPeopleSection({
206
207
  const [listTotalCount, setListTotalCount] = React.useState(initialPeople.length)
207
208
  const [listLoading, setListLoading] = React.useState(true)
208
209
  const [starredIds, setStarredIds] = React.useState<Set<string>>(
209
- () => new Set(readJsonFromLocalStorage<string[]>(`om:starred-people:${companyId}`, [])),
210
+ () => readVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION),
210
211
  )
211
212
  const pendingPeopleChangeRef = React.useRef(false)
212
213
 
@@ -229,7 +230,7 @@ export function CompanyPeopleSection({
229
230
  const next = new Set(prev)
230
231
  if (next.has(personId)) next.delete(personId)
231
232
  else next.add(personId)
232
- writeJsonToLocalStorage(`om:starred-people:${companyId}`, [...next])
233
+ writeVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION, next)
233
234
  return next
234
235
  })
235
236
  },
@@ -11,6 +11,7 @@ import { LogList, type LogListEntry } from '@open-mercato/ui/backend/LogList'
11
11
  import { Progress } from '@open-mercato/ui/primitives/progress'
12
12
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
13
13
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
14
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
14
15
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
16
  import { useT } from '@open-mercato/shared/lib/i18n/context'
16
17
  import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
@@ -94,6 +95,9 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
94
95
  const router = useRouter()
95
96
  const runId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)
96
97
  const t = useT()
98
+ const { runMutation } = useGuardedMutation<Record<string, unknown>>({
99
+ contextId: 'data_sync.runDetail',
100
+ })
97
101
 
98
102
  const [run, setRun] = React.useState<SyncRunDetail | null>(null)
99
103
  const [isLoading, setIsLoading] = React.useState(true)
@@ -213,32 +217,50 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
213
217
  const handleCancel = React.useCallback(async () => {
214
218
  const currentRunId = resolveCurrentRunId()
215
219
  if (!currentRunId) return
216
- const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/cancel`, {
217
- method: 'POST',
218
- }, { fallback: null })
220
+ const call = await runMutation({
221
+ // optimistic-lock-exempt: run lifecycle action endpoint (cancel), not a concurrent record edit
222
+ operation: () => apiCall(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/cancel`, {
223
+ method: 'POST',
224
+ }, { fallback: null }),
225
+ mutationPayload: { runId: currentRunId },
226
+ context: {
227
+ operation: 'update',
228
+ actionId: 'cancel-sync-run',
229
+ runId: currentRunId,
230
+ },
231
+ })
219
232
  if (call.ok) {
220
233
  flash(t('data_sync.runs.detail.cancelSuccess'), 'success')
221
234
  void loadRun()
222
235
  } else {
223
236
  flash(t('data_sync.runs.detail.cancelError'), 'error')
224
237
  }
225
- }, [resolveCurrentRunId, t, loadRun])
238
+ }, [resolveCurrentRunId, runMutation, t, loadRun])
226
239
 
227
240
  const handleRetry = React.useCallback(async () => {
228
241
  const currentRunId = resolveCurrentRunId()
229
242
  if (!currentRunId) return
230
- const call = await apiCall<{ id: string }>(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/retry`, {
231
- method: 'POST',
232
- headers: { 'Content-Type': 'application/json' },
233
- body: JSON.stringify({ fromBeginning: false }),
234
- }, { fallback: null })
243
+ const call = await runMutation({
244
+ // optimistic-lock-exempt: starts a new retry run (create), not a concurrent record edit
245
+ operation: () => apiCall<{ id: string }>(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}/retry`, {
246
+ method: 'POST',
247
+ headers: { 'Content-Type': 'application/json' },
248
+ body: JSON.stringify({ fromBeginning: false }),
249
+ }, { fallback: null }),
250
+ mutationPayload: { runId: currentRunId, fromBeginning: false },
251
+ context: {
252
+ operation: 'create',
253
+ actionId: 'retry-sync-run',
254
+ runId: currentRunId,
255
+ },
256
+ })
235
257
  if (call.ok && call.result) {
236
258
  flash(t('data_sync.runs.detail.retrySuccess'), 'success')
237
259
  router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`)
238
260
  } else {
239
261
  flash(t('data_sync.runs.detail.retryError'), 'error')
240
262
  }
241
- }, [resolveCurrentRunId, router, t])
263
+ }, [resolveCurrentRunId, router, runMutation, t])
242
264
 
243
265
  if (isLoading) return <Page><PageBody><LoadingMessage label={t('data_sync.runs.detail.title')} /></PageBody></Page>
244
266
  if (isNotFound) {