@open-mercato/core 0.6.7-develop.6553.1.2ec7246e04 → 0.6.7-develop.6565.1.1026d85fff

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 (138) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/lib/reconcileOrganization.js +9 -7
  3. package/dist/modules/attachments/lib/reconcileOrganization.js.map +2 -2
  4. package/dist/modules/customers/api/companies/[id]/people/route.js +2 -2
  5. package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
  6. package/dist/modules/customers/api/deals/[id]/companies/route.js +2 -2
  7. package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
  8. package/dist/modules/customers/api/deals/[id]/people/route.js +2 -2
  9. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  10. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +4 -4
  11. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  12. package/dist/modules/customers/api/entity-roles-factory.js +3 -3
  13. package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
  14. package/dist/modules/customers/api/labels/assign/route.js +2 -2
  15. package/dist/modules/customers/api/labels/assign/route.js.map +2 -2
  16. package/dist/modules/customers/api/labels/unassign/route.js +2 -2
  17. package/dist/modules/customers/api/labels/unassign/route.js.map +2 -2
  18. package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js +3 -7
  19. package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js.map +2 -2
  20. package/dist/modules/customers/api/people/[id]/companies/context.js +3 -3
  21. package/dist/modules/customers/api/people/[id]/companies/context.js.map +2 -2
  22. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +2 -2
  23. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
  24. package/dist/modules/customers/api/todos/route.js +2 -2
  25. package/dist/modules/customers/api/todos/route.js.map +2 -2
  26. package/dist/modules/customers/commands/addresses.js +3 -3
  27. package/dist/modules/customers/commands/addresses.js.map +2 -2
  28. package/dist/modules/customers/commands/comments.js +3 -3
  29. package/dist/modules/customers/commands/comments.js.map +2 -2
  30. package/dist/modules/customers/commands/companies.js +2 -2
  31. package/dist/modules/customers/commands/companies.js.map +2 -2
  32. package/dist/modules/customers/commands/deals.js +3 -3
  33. package/dist/modules/customers/commands/deals.js.map +2 -2
  34. package/dist/modules/customers/commands/dictionaries.js +3 -3
  35. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  36. package/dist/modules/customers/commands/entity-roles.js +3 -3
  37. package/dist/modules/customers/commands/entity-roles.js.map +2 -2
  38. package/dist/modules/customers/commands/interactions.js +6 -6
  39. package/dist/modules/customers/commands/interactions.js.map +2 -2
  40. package/dist/modules/customers/commands/labels.js +7 -7
  41. package/dist/modules/customers/commands/labels.js.map +2 -2
  42. package/dist/modules/customers/commands/people.js +3 -3
  43. package/dist/modules/customers/commands/people.js.map +2 -2
  44. package/dist/modules/customers/commands/personCompanyLinks.js +6 -6
  45. package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
  46. package/dist/modules/customers/commands/pipeline-stages.js +3 -3
  47. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  48. package/dist/modules/customers/commands/pipelines.js +3 -3
  49. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  50. package/dist/modules/customers/commands/shared.js +3 -3
  51. package/dist/modules/customers/commands/shared.js.map +2 -2
  52. package/dist/modules/customers/commands/tags.js +7 -7
  53. package/dist/modules/customers/commands/tags.js.map +2 -2
  54. package/dist/modules/customers/commands/todos.js +3 -3
  55. package/dist/modules/customers/commands/todos.js.map +2 -2
  56. package/dist/modules/customers/lib/personCompanies.js +4 -4
  57. package/dist/modules/customers/lib/personCompanies.js.map +2 -2
  58. package/dist/modules/perspectives/data/validators.js +4 -0
  59. package/dist/modules/perspectives/data/validators.js.map +2 -2
  60. package/dist/modules/sales/api/notes/route.js +2 -4
  61. package/dist/modules/sales/api/notes/route.js.map +2 -2
  62. package/dist/modules/sales/api/quotes/accept/route.js +2 -2
  63. package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
  64. package/dist/modules/sales/api/quotes/public/[token]/route.js +3 -7
  65. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  66. package/dist/modules/sales/api/quotes/send/route.js +2 -2
  67. package/dist/modules/sales/api/quotes/send/route.js.map +2 -2
  68. package/dist/modules/sales/api/returns/[id]/route.js +2 -2
  69. package/dist/modules/sales/api/returns/[id]/route.js.map +2 -2
  70. package/dist/modules/sales/commands/configuration.js +11 -11
  71. package/dist/modules/sales/commands/configuration.js.map +2 -2
  72. package/dist/modules/sales/commands/documentAddresses.js +3 -3
  73. package/dist/modules/sales/commands/documentAddresses.js.map +2 -2
  74. package/dist/modules/sales/commands/documents.js +31 -39
  75. package/dist/modules/sales/commands/documents.js.map +2 -2
  76. package/dist/modules/sales/commands/notes.js +7 -7
  77. package/dist/modules/sales/commands/notes.js.map +2 -2
  78. package/dist/modules/sales/commands/payments.js +2 -2
  79. package/dist/modules/sales/commands/payments.js.map +2 -2
  80. package/dist/modules/sales/commands/returns.js +8 -8
  81. package/dist/modules/sales/commands/returns.js.map +2 -2
  82. package/dist/modules/sales/commands/shared.js +2 -2
  83. package/dist/modules/sales/commands/shared.js.map +2 -2
  84. package/dist/modules/sales/commands/shipments.js +5 -5
  85. package/dist/modules/sales/commands/shipments.js.map +2 -2
  86. package/dist/modules/sales/commands/statuses.js +2 -2
  87. package/dist/modules/sales/commands/statuses.js.map +2 -2
  88. package/dist/modules/sales/commands/tags.js +3 -3
  89. package/dist/modules/sales/commands/tags.js.map +2 -2
  90. package/package.json +7 -7
  91. package/src/modules/attachments/lib/reconcileOrganization.ts +16 -7
  92. package/src/modules/customer_accounts/i18n/de.json +12 -0
  93. package/src/modules/customer_accounts/i18n/en.json +12 -0
  94. package/src/modules/customer_accounts/i18n/es.json +34 -22
  95. package/src/modules/customer_accounts/i18n/pl.json +32 -20
  96. package/src/modules/customers/api/companies/[id]/people/route.ts +2 -2
  97. package/src/modules/customers/api/deals/[id]/companies/route.ts +2 -2
  98. package/src/modules/customers/api/deals/[id]/people/route.ts +2 -2
  99. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +4 -4
  100. package/src/modules/customers/api/entity-roles-factory.ts +3 -3
  101. package/src/modules/customers/api/labels/assign/route.ts +2 -2
  102. package/src/modules/customers/api/labels/unassign/route.ts +2 -2
  103. package/src/modules/customers/api/people/[id]/companies/[linkId]/route.ts +3 -7
  104. package/src/modules/customers/api/people/[id]/companies/context.ts +3 -3
  105. package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +2 -2
  106. package/src/modules/customers/api/todos/route.ts +2 -2
  107. package/src/modules/customers/commands/addresses.ts +3 -3
  108. package/src/modules/customers/commands/comments.ts +3 -3
  109. package/src/modules/customers/commands/companies.ts +2 -2
  110. package/src/modules/customers/commands/deals.ts +3 -3
  111. package/src/modules/customers/commands/dictionaries.ts +3 -3
  112. package/src/modules/customers/commands/entity-roles.ts +3 -3
  113. package/src/modules/customers/commands/interactions.ts +6 -6
  114. package/src/modules/customers/commands/labels.ts +7 -7
  115. package/src/modules/customers/commands/people.ts +3 -3
  116. package/src/modules/customers/commands/personCompanyLinks.ts +6 -6
  117. package/src/modules/customers/commands/pipeline-stages.ts +3 -3
  118. package/src/modules/customers/commands/pipelines.ts +3 -3
  119. package/src/modules/customers/commands/shared.ts +3 -3
  120. package/src/modules/customers/commands/tags.ts +7 -7
  121. package/src/modules/customers/commands/todos.ts +3 -3
  122. package/src/modules/customers/lib/personCompanies.ts +4 -4
  123. package/src/modules/perspectives/data/validators.ts +4 -0
  124. package/src/modules/sales/api/notes/route.ts +2 -4
  125. package/src/modules/sales/api/quotes/accept/route.ts +2 -2
  126. package/src/modules/sales/api/quotes/public/[token]/route.ts +3 -7
  127. package/src/modules/sales/api/quotes/send/route.ts +2 -2
  128. package/src/modules/sales/api/returns/[id]/route.ts +2 -2
  129. package/src/modules/sales/commands/configuration.ts +11 -11
  130. package/src/modules/sales/commands/documentAddresses.ts +3 -3
  131. package/src/modules/sales/commands/documents.ts +23 -31
  132. package/src/modules/sales/commands/notes.ts +7 -7
  133. package/src/modules/sales/commands/payments.ts +2 -2
  134. package/src/modules/sales/commands/returns.ts +8 -8
  135. package/src/modules/sales/commands/shared.ts +2 -2
  136. package/src/modules/sales/commands/shipments.ts +5 -5
  137. package/src/modules/sales/commands/statuses.ts +2 -2
  138. package/src/modules/sales/commands/tags.ts +3 -3
@@ -1,4 +1,4 @@
1
- [build:core] found 3489 entry points
1
+ [build:core] found 3490 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 187 entry points
4
4
  [build:core:generated] built successfully
@@ -54,13 +54,15 @@ async function reconcileAttachmentOrganizations(opts) {
54
54
  for (let index = 0; index < recordIds.length; index += batchSize) {
55
55
  const chunk = recordIds.slice(index, index + batchSize);
56
56
  try {
57
- const result = await queryEngine.query(entityId, {
58
- fields: ["id", "organization_id"],
59
- filters: { id: chunk.length === 1 ? { $eq: chunk[0] } : { $in: chunk } },
60
- tenantId,
61
- withDeleted: true,
62
- page: { pageSize: Math.max(chunk.length, 1) }
63
- });
57
+ const result = await em.transactional(
58
+ async () => queryEngine.query(entityId, {
59
+ fields: ["id", "organization_id"],
60
+ filters: { id: chunk.length === 1 ? { $eq: chunk[0] } : { $in: chunk } },
61
+ tenantId,
62
+ withDeleted: true,
63
+ page: { pageSize: Math.max(chunk.length, 1) }
64
+ })
65
+ );
64
66
  for (const item of result.items ?? []) {
65
67
  const record = item;
66
68
  const recordId = record.id != null ? String(record.id) : null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/lib/reconcileOrganization.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { Attachment } from '../data/entities'\n\nconst logger = createLogger('attachments').child({ component: 'reconcileOrganization' })\n\n/**\n * Virtual entity id for library attachments \u2014 these are not attached to a\n * parent record that owns an organization, so there is nothing to reconcile\n * against. They are counted as skipped rather than unresolved.\n */\nconst LIBRARY_ENTITY_ID = 'attachments:library'\n\nconst DEFAULT_BATCH_SIZE = 500\n\nexport type AttachmentOrgReconcileEntityStat = {\n scanned: number\n updated: number\n unresolved: number\n}\n\nexport type AttachmentOrgReconcileReport = {\n scanned: number\n updated: number\n unresolved: number\n skippedVirtual: number\n byEntity: Record<string, AttachmentOrgReconcileEntityStat>\n}\n\ntype AttachmentScanRow = {\n id: string\n entity_id: string | null\n record_id: string | null\n organization_id: string | null\n}\n\nfunction readParentOrganizationId(record: Record<string, unknown>): string | null {\n const raw = record['organization_id'] ?? record['organizationId']\n if (typeof raw === 'string' && raw.trim().length) return raw.trim()\n return null\n}\n\nfunction ensureBucket(\n report: AttachmentOrgReconcileReport,\n entityId: string,\n): AttachmentOrgReconcileEntityStat {\n const existing = report.byEntity[entityId]\n if (existing) return existing\n const bucket: AttachmentOrgReconcileEntityStat = { scanned: 0, updated: 0, unresolved: 0 }\n report.byEntity[entityId] = bucket\n return bucket\n}\n\n/**\n * Reconcile the `organization_id` of existing attachments to the organization\n * of the record they are attached to.\n *\n * Background (#3765): before the upload route became selected-organization\n * aware, a multi-org admin who switched the header organization uploaded files\n * that were silently stored under their *home* organization instead of the\n * selected one (the organization of the parent record). Those rows are now\n * invisible to org-scoped reads and stay orphaned. This heals them forward.\n *\n * The parent record's organization is the ground truth: an attachment is not\n * distinguishable from a legitimately home-org one by looking at the attachment\n * alone. Parent organizations are resolved generically through the Query Engine\n * by `entityId` (works for both base and custom entities, tenant-scoped and\n * organization-agnostic so the parent's real org is returned regardless of\n * which org the caller sits in) \u2014 the same mechanism the attachments module\n * already uses to enrich assignment details.\n *\n * The reconciliation is:\n * - idempotent \u2014 rows already matching their parent are left untouched;\n * - conservative \u2014 when the parent org cannot be resolved (unregistered\n * entity id, hard-deleted parent, parent without an org column) the row is\n * counted as `unresolved` and left as-is rather than guessed at;\n * - tenant-scoped \u2014 it reconciles every attachment in the tenant across all\n * organizations (the whole point is moving rows between orgs).\n *\n * Writes go through the passed `EntityManager` (which the caller runs inside a\n * transaction) so the whole reconciliation commits atomically.\n */\nexport async function reconcileAttachmentOrganizations(opts: {\n em: EntityManager\n queryEngine: QueryEngine\n tenantId: string\n batchSize?: number\n}): Promise<AttachmentOrgReconcileReport> {\n const { em, queryEngine, tenantId } = opts\n const batchSize = opts.batchSize && opts.batchSize > 0 ? opts.batchSize : DEFAULT_BATCH_SIZE\n const report: AttachmentOrgReconcileReport = {\n scanned: 0,\n updated: 0,\n unresolved: 0,\n skippedVirtual: 0,\n byEntity: {},\n }\n\n const rows = (await em.getConnection().execute(\n 'select id, entity_id, record_id, organization_id from attachments where tenant_id = ?',\n [tenantId],\n )) as AttachmentScanRow[]\n report.scanned = rows.length\n if (!rows.length) return report\n\n const groups = new Map<string, AttachmentScanRow[]>()\n for (const row of rows) {\n if (!row.id || !row.entity_id || !row.record_id) continue\n const list = groups.get(row.entity_id) ?? []\n list.push(row)\n groups.set(row.entity_id, list)\n }\n\n const pendingUpdates: Array<{ id: string; organizationId: string }> = []\n\n for (const [entityId, group] of groups) {\n const bucket = ensureBucket(report, entityId)\n bucket.scanned += group.length\n\n if (entityId === LIBRARY_ENTITY_ID) {\n report.skippedVirtual += group.length\n continue\n }\n\n const recordIds = Array.from(\n new Set(group.map((row) => row.record_id).filter((value): value is string => !!value)),\n )\n const parentOrgByRecordId = new Map<string, string | null>()\n let resolvable = true\n\n for (let index = 0; index < recordIds.length; index += batchSize) {\n const chunk = recordIds.slice(index, index + batchSize)\n try {\n const result = await queryEngine.query(entityId as any, {\n fields: ['id', 'organization_id'],\n filters: { id: chunk.length === 1 ? { $eq: chunk[0] } : { $in: chunk } },\n tenantId,\n withDeleted: true,\n page: { pageSize: Math.max(chunk.length, 1) },\n })\n for (const item of result.items ?? []) {\n const record = item as Record<string, unknown>\n const recordId = record.id != null ? String(record.id) : null\n if (!recordId) continue\n parentOrgByRecordId.set(recordId, readParentOrganizationId(record))\n }\n } catch (error) {\n resolvable = false\n logger.warn('org reconcile: cannot resolve parent organization for entity', {\n entityId,\n error,\n })\n break\n }\n }\n\n if (!resolvable) {\n bucket.unresolved += group.length\n report.unresolved += group.length\n continue\n }\n\n for (const row of group) {\n const target = row.record_id ? parentOrgByRecordId.get(row.record_id) : null\n if (!target) {\n bucket.unresolved += 1\n report.unresolved += 1\n continue\n }\n if ((row.organization_id ?? null) === target) continue\n pendingUpdates.push({ id: row.id, organizationId: target })\n bucket.updated += 1\n report.updated += 1\n }\n }\n\n for (let index = 0; index < pendingUpdates.length; index += batchSize) {\n const chunk = pendingUpdates.slice(index, index + batchSize)\n for (const update of chunk) {\n const reference = em.getReference(Attachment, update.id)\n reference.organizationId = update.organizationId\n }\n await em.flush()\n }\n\n return report\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { Attachment } from '../data/entities'\n\nconst logger = createLogger('attachments').child({ component: 'reconcileOrganization' })\n\n/**\n * Virtual entity id for library attachments \u2014 these are not attached to a\n * parent record that owns an organization, so there is nothing to reconcile\n * against. They are counted as skipped rather than unresolved.\n */\nconst LIBRARY_ENTITY_ID = 'attachments:library'\n\nconst DEFAULT_BATCH_SIZE = 500\n\nexport type AttachmentOrgReconcileEntityStat = {\n scanned: number\n updated: number\n unresolved: number\n}\n\nexport type AttachmentOrgReconcileReport = {\n scanned: number\n updated: number\n unresolved: number\n skippedVirtual: number\n byEntity: Record<string, AttachmentOrgReconcileEntityStat>\n}\n\ntype AttachmentScanRow = {\n id: string\n entity_id: string | null\n record_id: string | null\n organization_id: string | null\n}\n\nfunction readParentOrganizationId(record: Record<string, unknown>): string | null {\n const raw = record['organization_id'] ?? record['organizationId']\n if (typeof raw === 'string' && raw.trim().length) return raw.trim()\n return null\n}\n\nfunction ensureBucket(\n report: AttachmentOrgReconcileReport,\n entityId: string,\n): AttachmentOrgReconcileEntityStat {\n const existing = report.byEntity[entityId]\n if (existing) return existing\n const bucket: AttachmentOrgReconcileEntityStat = { scanned: 0, updated: 0, unresolved: 0 }\n report.byEntity[entityId] = bucket\n return bucket\n}\n\n/**\n * Reconcile the `organization_id` of existing attachments to the organization\n * of the record they are attached to.\n *\n * Background (#3765): before the upload route became selected-organization\n * aware, a multi-org admin who switched the header organization uploaded files\n * that were silently stored under their *home* organization instead of the\n * selected one (the organization of the parent record). Those rows are now\n * invisible to org-scoped reads and stay orphaned. This heals them forward.\n *\n * The parent record's organization is the ground truth: an attachment is not\n * distinguishable from a legitimately home-org one by looking at the attachment\n * alone. Parent organizations are resolved generically through the Query Engine\n * by `entityId` (works for both base and custom entities, tenant-scoped and\n * organization-agnostic so the parent's real org is returned regardless of\n * which org the caller sits in) \u2014 the same mechanism the attachments module\n * already uses to enrich assignment details.\n *\n * The reconciliation is:\n * - idempotent \u2014 rows already matching their parent are left untouched;\n * - conservative \u2014 when the parent org cannot be resolved (unregistered\n * entity id, hard-deleted parent, parent without an org column) the row is\n * counted as `unresolved` and left as-is rather than guessed at;\n * - tenant-scoped \u2014 it reconciles every attachment in the tenant across all\n * organizations (the whole point is moving rows between orgs).\n *\n * Writes go through the passed `EntityManager` (which the caller runs inside a\n * transaction) so the whole reconciliation commits atomically.\n */\nexport async function reconcileAttachmentOrganizations(opts: {\n em: EntityManager\n queryEngine: QueryEngine\n tenantId: string\n batchSize?: number\n}): Promise<AttachmentOrgReconcileReport> {\n const { em, queryEngine, tenantId } = opts\n const batchSize = opts.batchSize && opts.batchSize > 0 ? opts.batchSize : DEFAULT_BATCH_SIZE\n const report: AttachmentOrgReconcileReport = {\n scanned: 0,\n updated: 0,\n unresolved: 0,\n skippedVirtual: 0,\n byEntity: {},\n }\n\n const rows = (await em.getConnection().execute(\n 'select id, entity_id, record_id, organization_id from attachments where tenant_id = ?',\n [tenantId],\n )) as AttachmentScanRow[]\n report.scanned = rows.length\n if (!rows.length) return report\n\n const groups = new Map<string, AttachmentScanRow[]>()\n for (const row of rows) {\n if (!row.id || !row.entity_id || !row.record_id) continue\n const list = groups.get(row.entity_id) ?? []\n list.push(row)\n groups.set(row.entity_id, list)\n }\n\n const pendingUpdates: Array<{ id: string; organizationId: string }> = []\n\n for (const [entityId, group] of groups) {\n const bucket = ensureBucket(report, entityId)\n bucket.scanned += group.length\n\n if (entityId === LIBRARY_ENTITY_ID) {\n report.skippedVirtual += group.length\n continue\n }\n\n const recordIds = Array.from(\n new Set(group.map((row) => row.record_id).filter((value): value is string => !!value)),\n )\n const parentOrgByRecordId = new Map<string, string | null>()\n let resolvable = true\n\n for (let index = 0; index < recordIds.length; index += batchSize) {\n const chunk = recordIds.slice(index, index + batchSize)\n try {\n // An unregistrable entity id makes the Query Engine emit SQL against a\n // non-existent relation, and a failed statement aborts the surrounding\n // Postgres transaction the caller runs this reconcile in \u2014 poisoning\n // the whole backfill (#4145). Running each resolution query inside a\n // nested transaction scopes the failure to a savepoint, so the catch\n // below can mark just this group unresolved while every other group\n // and the final flush keep working.\n const result = await em.transactional(async () =>\n queryEngine.query(entityId as any, {\n fields: ['id', 'organization_id'],\n filters: { id: chunk.length === 1 ? { $eq: chunk[0] } : { $in: chunk } },\n tenantId,\n withDeleted: true,\n page: { pageSize: Math.max(chunk.length, 1) },\n }),\n )\n for (const item of result.items ?? []) {\n const record = item as Record<string, unknown>\n const recordId = record.id != null ? String(record.id) : null\n if (!recordId) continue\n parentOrgByRecordId.set(recordId, readParentOrganizationId(record))\n }\n } catch (error) {\n resolvable = false\n logger.warn('org reconcile: cannot resolve parent organization for entity', {\n entityId,\n error,\n })\n break\n }\n }\n\n if (!resolvable) {\n bucket.unresolved += group.length\n report.unresolved += group.length\n continue\n }\n\n for (const row of group) {\n const target = row.record_id ? parentOrgByRecordId.get(row.record_id) : null\n if (!target) {\n bucket.unresolved += 1\n report.unresolved += 1\n continue\n }\n if ((row.organization_id ?? null) === target) continue\n pendingUpdates.push({ id: row.id, organizationId: target })\n bucket.updated += 1\n report.updated += 1\n }\n }\n\n for (let index = 0; index < pendingUpdates.length; index += batchSize) {\n const chunk = pendingUpdates.slice(index, index + batchSize)\n for (const update of chunk) {\n const reference = em.getReference(Attachment, update.id)\n reference.organizationId = update.organizationId\n }\n await em.flush()\n }\n\n return report\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
- import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
+ import { CrudHttpError, isCrudHttpError, notFound } from "@open-mercato/shared/lib/crud/errors";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
6
6
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
@@ -85,7 +85,7 @@ async function GET(req, ctx) {
85
85
  decryptionScope
86
86
  );
87
87
  if (!company) {
88
- throw new CrudHttpError(404, { error: translate("customers.errors.company_not_found", "Company not found") });
88
+ throw notFound(translate("customers.errors.company_not_found", "Company not found"));
89
89
  }
90
90
  if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: company.organizationId })) {
91
91
  throw new CrudHttpError(403, { error: translate("customers.errors.access_denied", "Access denied") });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/customers/api/companies/%5Bid%5D/people/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport {\n CustomerEntity,\n CustomerPersonCompanyLink,\n CustomerPersonProfile,\n} from '../../../../data/entities'\nimport {\n filterActivePersonCompanyLinks,\n withActiveCustomerPersonCompanyLinkFilter,\n} from '../../../../lib/personCompanyLinkTable'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(20),\n search: z.string().optional(),\n sort: z.enum(['name-asc', 'name-desc', 'recent']).default('name-asc'),\n})\n\ntype CompanyPersonItem = {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n status: string | null\n lifecycleStage: string | null\n jobTitle: string | null\n department: string | null\n createdAt: string\n organizationId: string\n temperature: string | null\n source: string | null\n linkedAt: string | null\n}\n\nfunction matchesSearch(item: CompanyPersonItem, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized.length) return true\n return [\n item.displayName,\n item.primaryEmail,\n item.primaryPhone,\n item.jobTitle,\n item.department,\n item.status,\n item.lifecycleStage,\n item.source,\n ]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .some((value) => value.toLowerCase().includes(normalized))\n}\n\nfunction sortItems(items: CompanyPersonItem[], sort: 'name-asc' | 'name-desc' | 'recent'): CompanyPersonItem[] {\n if (sort === 'recent') {\n return [...items].sort((left, right) => {\n const leftTimestamp = left.linkedAt ? new Date(left.linkedAt).getTime() : new Date(left.createdAt).getTime()\n const rightTimestamp = right.linkedAt ? new Date(right.linkedAt).getTime() : new Date(right.createdAt).getTime()\n if (leftTimestamp === rightTimestamp) return left.displayName.localeCompare(right.displayName)\n return rightTimestamp - leftTimestamp\n })\n }\n\n return [...items].sort((left, right) => {\n const compare = left.displayName.localeCompare(right.displayName, undefined, { sensitivity: 'base' })\n return sort === 'name-asc' ? compare : -compare\n })\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.companies.view'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { id?: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) throw new CrudHttpError(401, { error: 'Unauthorized' })\n\n const query = querySchema.parse({\n page: new URL(req.url).searchParams.get('page') ?? undefined,\n pageSize: new URL(req.url).searchParams.get('pageSize') ?? undefined,\n search: new URL(req.url).searchParams.get('search') ?? undefined,\n sort: new URL(req.url).searchParams.get('sort') ?? undefined,\n })\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = (container.resolve('em') as EntityManager).fork()\n const decryptionScope = {\n tenantId: auth.tenantId,\n organizationId: scope?.selectedId ?? auth.orgId ?? null,\n }\n\n const company = await findOneWithDecryption(\n em,\n CustomerEntity,\n { id, kind: 'company', tenantId: auth.tenantId, deletedAt: null },\n {},\n decryptionScope,\n )\n if (!company) {\n throw new CrudHttpError(404, { error: translate('customers.errors.company_not_found', 'Company not found') })\n }\n\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: company.organizationId })) {\n throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })\n }\n\n const entityScope = { tenantId: auth.tenantId, organizationId: company.organizationId }\n const linkWhere = await withActiveCustomerPersonCompanyLinkFilter(\n em,\n {\n company: company.id,\n tenantId: company.tenantId,\n organizationId: company.organizationId,\n },\n 'customers.companies.people.GET',\n )\n const links = filterActivePersonCompanyLinks(\n await findWithDecryption(\n em,\n CustomerPersonCompanyLink,\n linkWhere,\n { populate: ['person'] },\n entityScope,\n ),\n )\n\n const personIds = links\n .map((link) => link.person?.id)\n .filter((personId): personId is string => typeof personId === 'string' && personId.length > 0)\n\n const profiles = personIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerPersonProfile,\n {\n entity: { $in: personIds },\n tenantId: company.tenantId,\n organizationId: company.organizationId,\n },\n {},\n entityScope,\n )\n : []\n const profileByPersonId = new Map(\n profiles.map((profile) => [(profile.entity as { id: string }).id, profile]),\n )\n\n const items = links\n .map((link) => {\n const person = link.person\n if (!person?.id) return null\n const profile = profileByPersonId.get(person.id) ?? null\n return {\n id: person.id,\n displayName: person.displayName ?? person.primaryEmail ?? person.id,\n primaryEmail: person.primaryEmail ?? null,\n primaryPhone: person.primaryPhone ?? null,\n status: person.status ?? null,\n lifecycleStage: person.lifecycleStage ?? null,\n jobTitle: profile?.jobTitle ?? null,\n department: profile?.department ?? null,\n createdAt: person.createdAt.toISOString(),\n organizationId: person.organizationId,\n temperature: person.temperature ?? null,\n source: person.source ?? null,\n linkedAt: link.createdAt ? link.createdAt.toISOString() : null,\n } satisfies CompanyPersonItem\n })\n .filter((item): item is CompanyPersonItem => item !== null)\n\n const filtered = query.search?.trim().length ? items.filter((item) => matchesSearch(item, query.search ?? '')) : items\n const sorted = sortItems(filtered, query.sort)\n const total = sorted.length\n const totalPages = Math.max(1, Math.ceil(total / query.pageSize))\n const page = Math.min(query.page, totalPages)\n const start = (page - 1) * query.pageSize\n\n return NextResponse.json({\n items: sorted.slice(start, start + query.pageSize),\n total,\n page,\n pageSize: query.pageSize,\n totalPages,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n logger.error('customers.companies.people.GET', { err: error })\n return NextResponse.json({ error: translate('customers.errors.company_people_load_failed', 'Failed to load linked people') }, { status: 500 })\n }\n}\n\nconst companyPeopleItemSchema = z.object({\n id: z.string().uuid(),\n displayName: z.string(),\n primaryEmail: z.string().nullable(),\n primaryPhone: z.string().nullable(),\n status: z.string().nullable(),\n lifecycleStage: z.string().nullable(),\n jobTitle: z.string().nullable(),\n department: z.string().nullable(),\n createdAt: z.string(),\n organizationId: z.string().uuid().nullable(),\n temperature: z.string().nullable(),\n source: z.string().nullable(),\n linkedAt: z.string().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'List linked people for a company',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated linked people',\n schema: z.object({\n items: z.array(companyPeopleItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n }),\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,eAAe,uBAAuB;AAC/C,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,2BAA2B;AAEpC,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,uCAAuC;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW;AAEvC,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAED,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,MAAM,EAAE,KAAK,CAAC,YAAY,aAAa,QAAQ,CAAC,EAAE,QAAQ,UAAU;AACtE,CAAC;AAkBD,SAAS,cAAc,MAAyB,OAAwB;AACtE,QAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,MAAI,CAAC,WAAW,OAAQ,QAAO;AAC/B,SAAO;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP,EACG,OAAO,CAAC,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,EAChF,KAAK,CAAC,UAAU,MAAM,YAAY,EAAE,SAAS,UAAU,CAAC;AAC7D;AAEA,SAAS,UAAU,OAA4B,MAAgE;AAC7G,MAAI,SAAS,UAAU;AACrB,WAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AACtC,YAAM,gBAAgB,KAAK,WAAW,IAAI,KAAK,KAAK,QAAQ,EAAE,QAAQ,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,QAAQ;AAC3G,YAAM,iBAAiB,MAAM,WAAW,IAAI,KAAK,MAAM,QAAQ,EAAE,QAAQ,IAAI,IAAI,KAAK,MAAM,SAAS,EAAE,QAAQ;AAC/G,UAAI,kBAAkB,eAAgB,QAAO,KAAK,YAAY,cAAc,MAAM,WAAW;AAC7F,aAAO,iBAAiB;AAAA,IAC1B,CAAC;AAAA,EACH;AAEA,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AACtC,UAAM,UAAU,KAAK,YAAY,cAAc,MAAM,aAAa,QAAW,EAAE,aAAa,OAAO,CAAC;AACpG,WAAO,SAAS,aAAa,UAAU,CAAC;AAAA,EAC1C,CAAC;AACH;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC1E;AAEA,eAAsB,IAAI,KAAc,KAAmC;AACzE,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI;AACF,UAAM,EAAE,GAAG,IAAI,aAAa,MAAM,EAAE,IAAI,IAAI,QAAQ,GAAG,CAAC;AACxD,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,MAAM,SAAU,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAE3E,UAAM,QAAQ,YAAY,MAAM;AAAA,MAC9B,MAAM,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,MAAM,KAAK;AAAA,MACnD,UAAU,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,UAAU,KAAK;AAAA,MAC3D,QAAQ,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,QAAQ,KAAK;AAAA,MACvD,MAAM,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,MAAM,KAAK;AAAA,IACrD,CAAC;AAED,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,UAAM,kBAAkB;AAAA,MACtB,UAAU,KAAK;AAAA,MACf,gBAAgB,OAAO,cAAc,KAAK,SAAS;AAAA,IACrD;AAEA,UAAM,UAAU,MAAM;AAAA,MACpB;AAAA,MACA;AAAA,MACA,EAAE,IAAI,MAAM,WAAW,UAAU,KAAK,UAAU,WAAW,KAAK;AAAA,MAChE,CAAC;AAAA,MACD;AAAA,IACF;AACA,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,sCAAsC,mBAAmB,EAAE,CAAC;AAAA,IAC9G;AAEA,QAAI,CAAC,gCAAgC,EAAE,OAAO,MAAM,gBAAgB,QAAQ,eAAe,CAAC,GAAG;AAC7F,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,kCAAkC,eAAe,EAAE,CAAC;AAAA,IACtG;AAEA,UAAM,cAAc,EAAE,UAAU,KAAK,UAAU,gBAAgB,QAAQ,eAAe;AACtF,UAAM,YAAY,MAAM;AAAA,MACtB;AAAA,MACA;AAAA,QACE,SAAS,QAAQ;AAAA,QACjB,UAAU,QAAQ;AAAA,QAClB,gBAAgB,QAAQ;AAAA,MAC1B;AAAA,MACA;AAAA,IACF;AACA,UAAM,QAAQ;AAAA,MACZ,MAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,QACA,EAAE,UAAU,CAAC,QAAQ,EAAE;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAEA,UAAM,YAAY,MACf,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,EAC7B,OAAO,CAAC,aAAiC,OAAO,aAAa,YAAY,SAAS,SAAS,CAAC;AAE/F,UAAM,WAAW,UAAU,SAAS,IAChC,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,QACE,QAAQ,EAAE,KAAK,UAAU;AAAA,QACzB,UAAU,QAAQ;AAAA,QAClB,gBAAgB,QAAQ;AAAA,MAC1B;AAAA,MACA,CAAC;AAAA,MACD;AAAA,IACF,IACA,CAAC;AACL,UAAM,oBAAoB,IAAI;AAAA,MAC5B,SAAS,IAAI,CAAC,YAAY,CAAE,QAAQ,OAA0B,IAAI,OAAO,CAAC;AAAA,IAC5E;AAEA,UAAM,QAAQ,MACX,IAAI,CAAC,SAAS;AACb,YAAM,SAAS,KAAK;AACpB,UAAI,CAAC,QAAQ,GAAI,QAAO;AACxB,YAAM,UAAU,kBAAkB,IAAI,OAAO,EAAE,KAAK;AACpD,aAAO;AAAA,QACL,IAAI,OAAO;AAAA,QACX,aAAa,OAAO,eAAe,OAAO,gBAAgB,OAAO;AAAA,QACjE,cAAc,OAAO,gBAAgB;AAAA,QACrC,cAAc,OAAO,gBAAgB;AAAA,QACrC,QAAQ,OAAO,UAAU;AAAA,QACzB,gBAAgB,OAAO,kBAAkB;AAAA,QACzC,UAAU,SAAS,YAAY;AAAA,QAC/B,YAAY,SAAS,cAAc;AAAA,QACnC,WAAW,OAAO,UAAU,YAAY;AAAA,QACxC,gBAAgB,OAAO;AAAA,QACvB,aAAa,OAAO,eAAe;AAAA,QACnC,QAAQ,OAAO,UAAU;AAAA,QACzB,UAAU,KAAK,YAAY,KAAK,UAAU,YAAY,IAAI;AAAA,MAC5D;AAAA,IACF,CAAC,EACA,OAAO,CAAC,SAAoC,SAAS,IAAI;AAE5D,UAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,SAAS,MAAM,OAAO,CAAC,SAAS,cAAc,MAAM,MAAM,UAAU,EAAE,CAAC,IAAI;AACjH,UAAM,SAAS,UAAU,UAAU,MAAM,IAAI;AAC7C,UAAM,QAAQ,OAAO;AACrB,UAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAChE,UAAM,OAAO,KAAK,IAAI,MAAM,MAAM,UAAU;AAC5C,UAAM,SAAS,OAAO,KAAK,MAAM;AAEjC,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO,OAAO,MAAM,OAAO,QAAQ,MAAM,QAAQ;AAAA,MACjD;AAAA,MACA;AAAA,MACA,UAAU,MAAM;AAAA,MAChB;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,QAAI,gBAAgB,KAAK,GAAG;AAC1B,aAAO,aAAa,KAAK,MAAM,MAAM,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,IAC/D;AACA,WAAO,MAAM,kCAAkC,EAAE,KAAK,MAAM,CAAC;AAC7D,WAAO,aAAa,KAAK,EAAE,OAAO,UAAU,+CAA+C,8BAA8B,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/I;AACF;AAEA,MAAM,0BAA0B,EAAE,OAAO;AAAA,EACvC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,aAAa,EAAE,OAAO;AAAA,EACtB,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,gBAAgB,EAAE,OAAO,EAAE,SAAS;AAAA,EACpC,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,WAAW,EAAE,OAAO;AAAA,EACpB,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,OAAO;AAAA,MACP,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO;AAAA,YACf,OAAO,EAAE,MAAM,uBAAuB;AAAA,YACtC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,YACpC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,YAC5B,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,YAChC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,UACpC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport {\n CustomerEntity,\n CustomerPersonCompanyLink,\n CustomerPersonProfile,\n} from '../../../../data/entities'\nimport {\n filterActivePersonCompanyLinks,\n withActiveCustomerPersonCompanyLinkFilter,\n} from '../../../../lib/personCompanyLinkTable'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(20),\n search: z.string().optional(),\n sort: z.enum(['name-asc', 'name-desc', 'recent']).default('name-asc'),\n})\n\ntype CompanyPersonItem = {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n status: string | null\n lifecycleStage: string | null\n jobTitle: string | null\n department: string | null\n createdAt: string\n organizationId: string\n temperature: string | null\n source: string | null\n linkedAt: string | null\n}\n\nfunction matchesSearch(item: CompanyPersonItem, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized.length) return true\n return [\n item.displayName,\n item.primaryEmail,\n item.primaryPhone,\n item.jobTitle,\n item.department,\n item.status,\n item.lifecycleStage,\n item.source,\n ]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .some((value) => value.toLowerCase().includes(normalized))\n}\n\nfunction sortItems(items: CompanyPersonItem[], sort: 'name-asc' | 'name-desc' | 'recent'): CompanyPersonItem[] {\n if (sort === 'recent') {\n return [...items].sort((left, right) => {\n const leftTimestamp = left.linkedAt ? new Date(left.linkedAt).getTime() : new Date(left.createdAt).getTime()\n const rightTimestamp = right.linkedAt ? new Date(right.linkedAt).getTime() : new Date(right.createdAt).getTime()\n if (leftTimestamp === rightTimestamp) return left.displayName.localeCompare(right.displayName)\n return rightTimestamp - leftTimestamp\n })\n }\n\n return [...items].sort((left, right) => {\n const compare = left.displayName.localeCompare(right.displayName, undefined, { sensitivity: 'base' })\n return sort === 'name-asc' ? compare : -compare\n })\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.companies.view'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { id?: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) throw new CrudHttpError(401, { error: 'Unauthorized' })\n\n const query = querySchema.parse({\n page: new URL(req.url).searchParams.get('page') ?? undefined,\n pageSize: new URL(req.url).searchParams.get('pageSize') ?? undefined,\n search: new URL(req.url).searchParams.get('search') ?? undefined,\n sort: new URL(req.url).searchParams.get('sort') ?? undefined,\n })\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = (container.resolve('em') as EntityManager).fork()\n const decryptionScope = {\n tenantId: auth.tenantId,\n organizationId: scope?.selectedId ?? auth.orgId ?? null,\n }\n\n const company = await findOneWithDecryption(\n em,\n CustomerEntity,\n { id, kind: 'company', tenantId: auth.tenantId, deletedAt: null },\n {},\n decryptionScope,\n )\n if (!company) {\n throw notFound(translate('customers.errors.company_not_found', 'Company not found'))\n }\n\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: company.organizationId })) {\n throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })\n }\n\n const entityScope = { tenantId: auth.tenantId, organizationId: company.organizationId }\n const linkWhere = await withActiveCustomerPersonCompanyLinkFilter(\n em,\n {\n company: company.id,\n tenantId: company.tenantId,\n organizationId: company.organizationId,\n },\n 'customers.companies.people.GET',\n )\n const links = filterActivePersonCompanyLinks(\n await findWithDecryption(\n em,\n CustomerPersonCompanyLink,\n linkWhere,\n { populate: ['person'] },\n entityScope,\n ),\n )\n\n const personIds = links\n .map((link) => link.person?.id)\n .filter((personId): personId is string => typeof personId === 'string' && personId.length > 0)\n\n const profiles = personIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerPersonProfile,\n {\n entity: { $in: personIds },\n tenantId: company.tenantId,\n organizationId: company.organizationId,\n },\n {},\n entityScope,\n )\n : []\n const profileByPersonId = new Map(\n profiles.map((profile) => [(profile.entity as { id: string }).id, profile]),\n )\n\n const items = links\n .map((link) => {\n const person = link.person\n if (!person?.id) return null\n const profile = profileByPersonId.get(person.id) ?? null\n return {\n id: person.id,\n displayName: person.displayName ?? person.primaryEmail ?? person.id,\n primaryEmail: person.primaryEmail ?? null,\n primaryPhone: person.primaryPhone ?? null,\n status: person.status ?? null,\n lifecycleStage: person.lifecycleStage ?? null,\n jobTitle: profile?.jobTitle ?? null,\n department: profile?.department ?? null,\n createdAt: person.createdAt.toISOString(),\n organizationId: person.organizationId,\n temperature: person.temperature ?? null,\n source: person.source ?? null,\n linkedAt: link.createdAt ? link.createdAt.toISOString() : null,\n } satisfies CompanyPersonItem\n })\n .filter((item): item is CompanyPersonItem => item !== null)\n\n const filtered = query.search?.trim().length ? items.filter((item) => matchesSearch(item, query.search ?? '')) : items\n const sorted = sortItems(filtered, query.sort)\n const total = sorted.length\n const totalPages = Math.max(1, Math.ceil(total / query.pageSize))\n const page = Math.min(query.page, totalPages)\n const start = (page - 1) * query.pageSize\n\n return NextResponse.json({\n items: sorted.slice(start, start + query.pageSize),\n total,\n page,\n pageSize: query.pageSize,\n totalPages,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n logger.error('customers.companies.people.GET', { err: error })\n return NextResponse.json({ error: translate('customers.errors.company_people_load_failed', 'Failed to load linked people') }, { status: 500 })\n }\n}\n\nconst companyPeopleItemSchema = z.object({\n id: z.string().uuid(),\n displayName: z.string(),\n primaryEmail: z.string().nullable(),\n primaryPhone: z.string().nullable(),\n status: z.string().nullable(),\n lifecycleStage: z.string().nullable(),\n jobTitle: z.string().nullable(),\n department: z.string().nullable(),\n createdAt: z.string(),\n organizationId: z.string().uuid().nullable(),\n temperature: z.string().nullable(),\n source: z.string().nullable(),\n linkedAt: z.string().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'List linked people for a company',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated linked people',\n schema: z.object({\n items: z.array(companyPeopleItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n }),\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
- import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
+ import { CrudHttpError, isCrudHttpError, notFound } from "@open-mercato/shared/lib/crud/errors";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
6
6
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
@@ -73,7 +73,7 @@ async function GET(req, ctx) {
73
73
  decryptionScope
74
74
  );
75
75
  if (!deal) {
76
- throw new CrudHttpError(404, { error: translate("customers.errors.deal_not_found", "Deal not found") });
76
+ throw notFound(translate("customers.errors.deal_not_found", "Deal not found"));
77
77
  }
78
78
  if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
79
79
  throw new CrudHttpError(403, { error: translate("customers.errors.access_denied", "Access denied") });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/customers/api/deals/%5Bid%5D/companies/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { CustomerCompanyProfile, CustomerDeal, CustomerDealCompanyLink, CustomerEntity } from '../../../../data/entities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(20),\n search: z.string().optional(),\n sort: z.enum(['label-asc', 'label-desc', 'name-asc', 'name-desc', 'recent']).default('label-asc'),\n})\n\ntype DealLinkedEntitySort = 'label-asc' | 'label-desc' | 'recent'\n\nfunction normalizeSort(sort: z.infer<typeof querySchema>['sort']): DealLinkedEntitySort {\n if (sort === 'name-asc') return 'label-asc'\n if (sort === 'name-desc') return 'label-desc'\n return sort\n}\n\ntype DealCompanyItem = {\n id: string\n label: string\n subtitle: string | null\n kind: 'company'\n linkedAt: string\n}\n\nfunction matchesSearch(item: DealCompanyItem, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized.length) return true\n return [item.label, item.subtitle]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .some((value) => value.toLowerCase().includes(normalized))\n}\n\nfunction sortItems(items: DealCompanyItem[], sort: 'label-asc' | 'label-desc' | 'recent'): DealCompanyItem[] {\n if (sort === 'recent') {\n return [...items].sort((left, right) => {\n const compare = new Date(right.linkedAt).getTime() - new Date(left.linkedAt).getTime()\n if (compare !== 0) return compare\n return left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n })\n }\n return [...items].sort((left, right) => {\n const compare = left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n return sort === 'label-asc' ? compare : -compare\n })\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { id?: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) throw new CrudHttpError(401, { error: 'Unauthorized' })\n\n const url = new URL(req.url)\n const query = querySchema.parse({\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sort: url.searchParams.get('sort') ?? undefined,\n })\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = (container.resolve('em') as EntityManager).fork()\n const decryptionScope = {\n tenantId: auth.tenantId,\n organizationId: scope?.selectedId ?? auth.orgId ?? null,\n }\n\n const deal = await findOneWithDecryption(\n em,\n CustomerDeal,\n { id, tenantId: auth.tenantId, deletedAt: null },\n {},\n decryptionScope,\n )\n if (!deal) {\n throw new CrudHttpError(404, { error: translate('customers.errors.deal_not_found', 'Deal not found') })\n }\n\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {\n throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })\n }\n\n const entityScope = { tenantId: deal.tenantId, organizationId: deal.organizationId }\n const links = await findWithDecryption(\n em,\n CustomerDealCompanyLink,\n { deal: deal.id },\n { populate: ['company'] },\n entityScope,\n )\n\n const companyIds = links\n .map((link) => link.company?.id)\n .filter((companyId): companyId is string => typeof companyId === 'string' && companyId.length > 0)\n const profiles = companyIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerCompanyProfile,\n {\n entity: { $in: companyIds },\n tenantId: deal.tenantId,\n organizationId: deal.organizationId,\n },\n {},\n entityScope,\n )\n : []\n const profileByCompanyId = new Map(\n profiles.map((profile) => [(profile.entity as { id: string }).id, profile]),\n )\n\n const items = links\n .map((link) => {\n const company = link.company as CustomerEntity | null\n if (!company?.id) return null\n const profile = profileByCompanyId.get(company.id) ?? null\n return {\n id: company.id,\n label: company.displayName ?? profile?.domain ?? company.id,\n subtitle: profile?.domain ?? company.primaryEmail ?? company.primaryPhone ?? null,\n kind: 'company',\n linkedAt: link.createdAt.toISOString(),\n } satisfies DealCompanyItem\n })\n .filter((item): item is DealCompanyItem => item !== null)\n\n const filtered = query.search?.trim().length ? items.filter((item) => matchesSearch(item, query.search ?? '')) : items\n const sorted = sortItems(filtered, normalizeSort(query.sort))\n const total = sorted.length\n const totalPages = Math.max(1, Math.ceil(total / query.pageSize))\n const page = Math.min(query.page, totalPages)\n const start = (page - 1) * query.pageSize\n\n return NextResponse.json({\n items: sorted.slice(start, start + query.pageSize),\n total,\n page,\n pageSize: query.pageSize,\n totalPages,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n logger.error('customers.deals.companies.GET', { err: error })\n return NextResponse.json({ error: translate('customers.errors.deal_companies_load_failed', 'Failed to load linked companies') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'List linked companies for a deal',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated linked companies',\n schema: z.object({\n items: z.array(\n z.object({\n id: z.string().uuid(),\n label: z.string(),\n subtitle: z.string().nullable(),\n kind: z.literal('company'),\n linkedAt: z.string(),\n }),\n ),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n }),\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { CustomerCompanyProfile, CustomerDeal, CustomerDealCompanyLink, CustomerEntity } from '../../../../data/entities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(20),\n search: z.string().optional(),\n sort: z.enum(['label-asc', 'label-desc', 'name-asc', 'name-desc', 'recent']).default('label-asc'),\n})\n\ntype DealLinkedEntitySort = 'label-asc' | 'label-desc' | 'recent'\n\nfunction normalizeSort(sort: z.infer<typeof querySchema>['sort']): DealLinkedEntitySort {\n if (sort === 'name-asc') return 'label-asc'\n if (sort === 'name-desc') return 'label-desc'\n return sort\n}\n\ntype DealCompanyItem = {\n id: string\n label: string\n subtitle: string | null\n kind: 'company'\n linkedAt: string\n}\n\nfunction matchesSearch(item: DealCompanyItem, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized.length) return true\n return [item.label, item.subtitle]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .some((value) => value.toLowerCase().includes(normalized))\n}\n\nfunction sortItems(items: DealCompanyItem[], sort: 'label-asc' | 'label-desc' | 'recent'): DealCompanyItem[] {\n if (sort === 'recent') {\n return [...items].sort((left, right) => {\n const compare = new Date(right.linkedAt).getTime() - new Date(left.linkedAt).getTime()\n if (compare !== 0) return compare\n return left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n })\n }\n return [...items].sort((left, right) => {\n const compare = left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n return sort === 'label-asc' ? compare : -compare\n })\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { id?: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) throw new CrudHttpError(401, { error: 'Unauthorized' })\n\n const url = new URL(req.url)\n const query = querySchema.parse({\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sort: url.searchParams.get('sort') ?? undefined,\n })\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = (container.resolve('em') as EntityManager).fork()\n const decryptionScope = {\n tenantId: auth.tenantId,\n organizationId: scope?.selectedId ?? auth.orgId ?? null,\n }\n\n const deal = await findOneWithDecryption(\n em,\n CustomerDeal,\n { id, tenantId: auth.tenantId, deletedAt: null },\n {},\n decryptionScope,\n )\n if (!deal) {\n throw notFound(translate('customers.errors.deal_not_found', 'Deal not found'))\n }\n\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {\n throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })\n }\n\n const entityScope = { tenantId: deal.tenantId, organizationId: deal.organizationId }\n const links = await findWithDecryption(\n em,\n CustomerDealCompanyLink,\n { deal: deal.id },\n { populate: ['company'] },\n entityScope,\n )\n\n const companyIds = links\n .map((link) => link.company?.id)\n .filter((companyId): companyId is string => typeof companyId === 'string' && companyId.length > 0)\n const profiles = companyIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerCompanyProfile,\n {\n entity: { $in: companyIds },\n tenantId: deal.tenantId,\n organizationId: deal.organizationId,\n },\n {},\n entityScope,\n )\n : []\n const profileByCompanyId = new Map(\n profiles.map((profile) => [(profile.entity as { id: string }).id, profile]),\n )\n\n const items = links\n .map((link) => {\n const company = link.company as CustomerEntity | null\n if (!company?.id) return null\n const profile = profileByCompanyId.get(company.id) ?? null\n return {\n id: company.id,\n label: company.displayName ?? profile?.domain ?? company.id,\n subtitle: profile?.domain ?? company.primaryEmail ?? company.primaryPhone ?? null,\n kind: 'company',\n linkedAt: link.createdAt.toISOString(),\n } satisfies DealCompanyItem\n })\n .filter((item): item is DealCompanyItem => item !== null)\n\n const filtered = query.search?.trim().length ? items.filter((item) => matchesSearch(item, query.search ?? '')) : items\n const sorted = sortItems(filtered, normalizeSort(query.sort))\n const total = sorted.length\n const totalPages = Math.max(1, Math.ceil(total / query.pageSize))\n const page = Math.min(query.page, totalPages)\n const start = (page - 1) * query.pageSize\n\n return NextResponse.json({\n items: sorted.slice(start, start + query.pageSize),\n total,\n page,\n pageSize: query.pageSize,\n totalPages,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n logger.error('customers.deals.companies.GET', { err: error })\n return NextResponse.json({ error: translate('customers.errors.deal_companies_load_failed', 'Failed to load linked companies') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'List linked companies for a deal',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated linked companies',\n schema: z.object({\n items: z.array(\n z.object({\n id: z.string().uuid(),\n label: z.string(),\n subtitle: z.string().nullable(),\n kind: z.literal('company'),\n linkedAt: z.string(),\n }),\n ),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n }),\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
- import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
+ import { CrudHttpError, isCrudHttpError, notFound } from "@open-mercato/shared/lib/crud/errors";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
6
6
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
@@ -73,7 +73,7 @@ async function GET(req, ctx) {
73
73
  decryptionScope
74
74
  );
75
75
  if (!deal) {
76
- throw new CrudHttpError(404, { error: translate("customers.errors.deal_not_found", "Deal not found") });
76
+ throw notFound(translate("customers.errors.deal_not_found", "Deal not found"));
77
77
  }
78
78
  if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
79
79
  throw new CrudHttpError(403, { error: translate("customers.errors.access_denied", "Access denied") });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/customers/api/deals/%5Bid%5D/people/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { CustomerDeal, CustomerDealPersonLink, CustomerEntity } from '../../../../data/entities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(20),\n search: z.string().optional(),\n sort: z.enum(['label-asc', 'label-desc', 'name-asc', 'name-desc', 'recent']).default('label-asc'),\n})\n\ntype DealLinkedEntitySort = 'label-asc' | 'label-desc' | 'recent'\n\nfunction normalizeSort(sort: z.infer<typeof querySchema>['sort']): DealLinkedEntitySort {\n if (sort === 'name-asc') return 'label-asc'\n if (sort === 'name-desc') return 'label-desc'\n return sort\n}\n\ntype DealPersonItem = {\n id: string\n label: string\n subtitle: string | null\n kind: 'person'\n linkedAt: string\n}\n\nfunction matchesSearch(item: DealPersonItem, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized.length) return true\n return [item.label, item.subtitle]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .some((value) => value.toLowerCase().includes(normalized))\n}\n\nfunction sortItems(items: DealPersonItem[], sort: 'label-asc' | 'label-desc' | 'recent'): DealPersonItem[] {\n if (sort === 'recent') {\n return [...items].sort((left, right) => {\n const compare = new Date(right.linkedAt).getTime() - new Date(left.linkedAt).getTime()\n if (compare !== 0) return compare\n return left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n })\n }\n return [...items].sort((left, right) => {\n const compare = left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n return sort === 'label-asc' ? compare : -compare\n })\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { id?: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) throw new CrudHttpError(401, { error: 'Unauthorized' })\n\n const url = new URL(req.url)\n const query = querySchema.parse({\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sort: url.searchParams.get('sort') ?? undefined,\n })\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = (container.resolve('em') as EntityManager).fork()\n const decryptionScope = {\n tenantId: auth.tenantId,\n organizationId: scope?.selectedId ?? auth.orgId ?? null,\n }\n\n const deal = await findOneWithDecryption(\n em,\n CustomerDeal,\n { id, tenantId: auth.tenantId, deletedAt: null },\n {},\n decryptionScope,\n )\n if (!deal) {\n throw new CrudHttpError(404, { error: translate('customers.errors.deal_not_found', 'Deal not found') })\n }\n\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {\n throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })\n }\n\n const entityScope = { tenantId: deal.tenantId, organizationId: deal.organizationId }\n const links = await findWithDecryption(\n em,\n CustomerDealPersonLink,\n { deal: deal.id },\n { populate: ['person'] },\n entityScope,\n )\n\n const items = links\n .map((link) => {\n const person = link.person as CustomerEntity | null\n if (!person?.id) return null\n return {\n id: person.id,\n label: person.displayName ?? person.primaryEmail ?? person.id,\n subtitle: person.primaryEmail ?? person.primaryPhone ?? null,\n kind: 'person',\n linkedAt: link.createdAt.toISOString(),\n } satisfies DealPersonItem\n })\n .filter((item): item is DealPersonItem => item !== null)\n\n const filtered = query.search?.trim().length ? items.filter((item) => matchesSearch(item, query.search ?? '')) : items\n const sorted = sortItems(filtered, normalizeSort(query.sort))\n const total = sorted.length\n const totalPages = Math.max(1, Math.ceil(total / query.pageSize))\n const page = Math.min(query.page, totalPages)\n const start = (page - 1) * query.pageSize\n\n return NextResponse.json({\n items: sorted.slice(start, start + query.pageSize),\n total,\n page,\n pageSize: query.pageSize,\n totalPages,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n logger.error('customers.deals.people.GET', { err: error })\n return NextResponse.json({ error: translate('customers.errors.deal_people_load_failed', 'Failed to load linked people') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'List linked people for a deal',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated linked people',\n schema: z.object({\n items: z.array(\n z.object({\n id: z.string().uuid(),\n label: z.string(),\n subtitle: z.string().nullable(),\n kind: z.literal('person'),\n linkedAt: z.string(),\n }),\n ),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n }),\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { CustomerDeal, CustomerDealPersonLink, CustomerEntity } from '../../../../data/entities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(20),\n search: z.string().optional(),\n sort: z.enum(['label-asc', 'label-desc', 'name-asc', 'name-desc', 'recent']).default('label-asc'),\n})\n\ntype DealLinkedEntitySort = 'label-asc' | 'label-desc' | 'recent'\n\nfunction normalizeSort(sort: z.infer<typeof querySchema>['sort']): DealLinkedEntitySort {\n if (sort === 'name-asc') return 'label-asc'\n if (sort === 'name-desc') return 'label-desc'\n return sort\n}\n\ntype DealPersonItem = {\n id: string\n label: string\n subtitle: string | null\n kind: 'person'\n linkedAt: string\n}\n\nfunction matchesSearch(item: DealPersonItem, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized.length) return true\n return [item.label, item.subtitle]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .some((value) => value.toLowerCase().includes(normalized))\n}\n\nfunction sortItems(items: DealPersonItem[], sort: 'label-asc' | 'label-desc' | 'recent'): DealPersonItem[] {\n if (sort === 'recent') {\n return [...items].sort((left, right) => {\n const compare = new Date(right.linkedAt).getTime() - new Date(left.linkedAt).getTime()\n if (compare !== 0) return compare\n return left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n })\n }\n return [...items].sort((left, right) => {\n const compare = left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n return sort === 'label-asc' ? compare : -compare\n })\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { id?: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) throw new CrudHttpError(401, { error: 'Unauthorized' })\n\n const url = new URL(req.url)\n const query = querySchema.parse({\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sort: url.searchParams.get('sort') ?? undefined,\n })\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = (container.resolve('em') as EntityManager).fork()\n const decryptionScope = {\n tenantId: auth.tenantId,\n organizationId: scope?.selectedId ?? auth.orgId ?? null,\n }\n\n const deal = await findOneWithDecryption(\n em,\n CustomerDeal,\n { id, tenantId: auth.tenantId, deletedAt: null },\n {},\n decryptionScope,\n )\n if (!deal) {\n throw notFound(translate('customers.errors.deal_not_found', 'Deal not found'))\n }\n\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {\n throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })\n }\n\n const entityScope = { tenantId: deal.tenantId, organizationId: deal.organizationId }\n const links = await findWithDecryption(\n em,\n CustomerDealPersonLink,\n { deal: deal.id },\n { populate: ['person'] },\n entityScope,\n )\n\n const items = links\n .map((link) => {\n const person = link.person as CustomerEntity | null\n if (!person?.id) return null\n return {\n id: person.id,\n label: person.displayName ?? person.primaryEmail ?? person.id,\n subtitle: person.primaryEmail ?? person.primaryPhone ?? null,\n kind: 'person',\n linkedAt: link.createdAt.toISOString(),\n } satisfies DealPersonItem\n })\n .filter((item): item is DealPersonItem => item !== null)\n\n const filtered = query.search?.trim().length ? items.filter((item) => matchesSearch(item, query.search ?? '')) : items\n const sorted = sortItems(filtered, normalizeSort(query.sort))\n const total = sorted.length\n const totalPages = Math.max(1, Math.ceil(total / query.pageSize))\n const page = Math.min(query.page, totalPages)\n const start = (page - 1) * query.pageSize\n\n return NextResponse.json({\n items: sorted.slice(start, start + query.pageSize),\n total,\n page,\n pageSize: query.pageSize,\n totalPages,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n logger.error('customers.deals.people.GET', { err: error })\n return NextResponse.json({ error: translate('customers.errors.deal_people_load_failed', 'Failed to load linked people') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'List linked people for a deal',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated linked people',\n schema: z.object({\n items: z.array(\n z.object({\n id: z.string().uuid(),\n label: z.string(),\n subtitle: z.string().nullable(),\n kind: z.literal('person'),\n linkedAt: z.string(),\n }),\n ),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n }),\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
4
- import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
+ import { CrudHttpError, isCrudHttpError, notFound } from "@open-mercato/shared/lib/crud/errors";
5
5
  import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
6
6
  import { CustomerDictionaryEntry } from "../../../../data/entities.js";
7
7
  import { mapDictionaryKind, resolveDictionaryActorId, resolveDictionaryRouteContext } from "../../context.js";
@@ -75,7 +75,7 @@ async function PATCH(req, ctx) {
75
75
  } catch (err) {
76
76
  if (isCrudHttpError(err)) {
77
77
  if (err.status === 404) {
78
- throw new CrudHttpError(404, { error: routeContext.translate("customers.errors.lookup_failed", "Dictionary entry not found") });
78
+ throw notFound(routeContext.translate("customers.errors.lookup_failed", "Dictionary entry not found"));
79
79
  }
80
80
  if (err.status === 409) {
81
81
  if (err.body?.code === "role_type_in_use") {
@@ -111,7 +111,7 @@ async function PATCH(req, ctx) {
111
111
  }
112
112
  );
113
113
  if (!entry) {
114
- throw new CrudHttpError(404, { error: routeContext.translate("customers.errors.lookup_failed", "Dictionary entry not found") });
114
+ throw notFound(routeContext.translate("customers.errors.lookup_failed", "Dictionary entry not found"));
115
115
  }
116
116
  if (result.changed) {
117
117
  await invalidateDictionaryCache(routeContext.cache, {
@@ -203,7 +203,7 @@ async function DELETE(req, ctx) {
203
203
  });
204
204
  } catch (err) {
205
205
  if (isCrudHttpError(err) && err.status === 404) {
206
- throw new CrudHttpError(404, { error: routeContext.translate("customers.errors.lookup_failed", "Dictionary entry not found") });
206
+ throw notFound(routeContext.translate("customers.errors.lookup_failed", "Dictionary entry not found"));
207
207
  }
208
208
  if (isCrudHttpError(err) && err.status === 409 && err.body?.code === "role_type_in_use") {
209
209
  const usageCount = Number(err.body?.usageCount ?? 0);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/customers/api/dictionaries/%5Bkind%5D/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandExecuteResult } from '@open-mercato/shared/lib/commands/types'\nimport { CustomerDictionaryEntry } from '../../../../data/entities'\nimport { mapDictionaryKind, resolveDictionaryActorId, resolveDictionaryRouteContext } from '../../context'\nimport { invalidateDictionaryCache } from '../../cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst patchSchema = z\n .object({\n value: z.string().trim().min(1).max(150).optional(),\n label: z.string().trim().max(150).optional(),\n color: z.union([z.string().trim(), z.null()]).optional(),\n icon: z.union([z.string().trim(), z.null()]).optional(),\n })\n .refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined, {\n message: 'No changes provided',\n })\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\nfunction formatCountMessage(template: string, count: number) {\n return template.replace('{{count}}', String(count))\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { kind?: string; id?: string } }) {\n try {\n const routeContext = await resolveDictionaryRouteContext(req)\n if (!routeContext.organizationId) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const payload = patchSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(routeContext.auth)\n const guardResult = await validateCrudMutationGuard(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: 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 const commandBus = (routeContext.container.resolve('commandBus') as CommandBus)\n let commandResult: CommandExecuteResult<{ entryId: string; changed: boolean }>\n try {\n commandResult = (await commandBus.execute('customers.dictionaryEntries.update', {\n input: {\n id,\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n kind: mappedKind,\n value: payload.value,\n label: payload.label,\n color: payload.color,\n icon: payload.icon,\n },\n ctx: routeContext.ctx,\n })) as CommandExecuteResult<{ entryId: string; changed: boolean }>\n } catch (err) {\n if (isCrudHttpError(err)) {\n if (err.status === 404) {\n throw new CrudHttpError(404, { error: routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found') })\n }\n if (err.status === 409) {\n if ((err.body as Record<string, unknown> | undefined)?.code === 'role_type_in_use') {\n const usageCount = Number((err.body as Record<string, unknown> | undefined)?.usageCount ?? 0)\n throw new CrudHttpError(409, {\n ...(err.body as Record<string, unknown> | undefined),\n error: formatCountMessage(\n routeContext.translate(\n 'customers.config.dictionaries.errors.roleTypeValueInUse',\n 'This role type is assigned to {{count}} records. Remove or replace those assignments before changing its value.',\n ),\n usageCount,\n ),\n })\n }\n throw new CrudHttpError(409, { error: routeContext.translate('customers.config.dictionaries.errors.duplicate', 'An entry with this value already exists') })\n }\n if (err.status === 400) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') })\n }\n }\n throw err\n }\n const { result, logEntry } = commandResult\n\n const entry = await findOneWithDecryption(\n routeContext.em.fork(),\n CustomerDictionaryEntry,\n { id },\n {},\n {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n },\n )\n if (!entry) {\n throw new CrudHttpError(404, { error: routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found') })\n }\n\n if (result.changed) {\n await invalidateDictionaryCache(routeContext.cache, {\n tenantId: routeContext.tenantId,\n mappedKind,\n organizationIds: [routeContext.organizationId],\n })\n }\n\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n organizationId: entry.organizationId,\n isInherited: false,\n })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: entry.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.update failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') }, { status: 400 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { kind?: string; id?: string } }) {\n try {\n const routeContext = await resolveDictionaryRouteContext(req)\n if (!routeContext.organizationId) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const guardUserId = resolveDictionaryActorId(routeContext.auth)\n const guardResult = await validateCrudMutationGuard(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: 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 const commandBus = (routeContext.container.resolve('commandBus') as CommandBus)\n let deleteResult: CommandExecuteResult<{ entryId: string }>\n try {\n deleteResult = (await commandBus.execute('customers.dictionaryEntries.delete', {\n input: {\n id,\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n kind: mappedKind,\n },\n ctx: routeContext.ctx,\n })) as CommandExecuteResult<{ entryId: string }>\n } catch (err) {\n if (isCrudHttpError(err) && err.status === 404) {\n throw new CrudHttpError(404, { error: routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found') })\n }\n if (isCrudHttpError(err) && err.status === 409 && (err.body as Record<string, unknown> | undefined)?.code === 'role_type_in_use') {\n const usageCount = Number((err.body as Record<string, unknown> | undefined)?.usageCount ?? 0)\n throw new CrudHttpError(409, {\n ...(err.body as Record<string, unknown> | undefined),\n error: formatCountMessage(\n routeContext.translate(\n 'customers.config.dictionaries.errors.roleTypeDeleteInUse',\n 'This role type is assigned to {{count}} records. Remove or replace those assignments before deleting it.',\n ),\n usageCount,\n ),\n })\n }\n throw err\n }\n const { logEntry } = deleteResult\n\n await invalidateDictionaryCache(routeContext.cache, {\n tenantId: routeContext.tenantId,\n mappedKind,\n organizationIds: [routeContext.organizationId],\n })\n\n const response = NextResponse.json({ success: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.delete failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to delete dictionary entry') }, { status: 400 })\n }\n}\n\nconst dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n icon: z.string().nullable().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n isInherited: z.boolean().optional(),\n})\n\nconst dictionaryDeleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst dictionaryErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer dictionary entry',\n methods: {\n PATCH: {\n summary: 'Update dictionary entry',\n description: 'Updates value, label, color, or icon for an existing customer dictionary entry.',\n requestBody: {\n contentType: 'application/json',\n schema: patchSchema,\n },\n responses: [\n { status: 200, description: 'Updated dictionary entry', schema: dictionaryEntrySchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: dictionaryErrorSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 404, description: 'Entry not found', schema: dictionaryErrorSchema },\n { status: 409, description: 'Duplicate value conflict', schema: dictionaryErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete dictionary entry',\n description: 'Removes a customer dictionary entry by identifier.',\n responses: [\n { status: 200, description: 'Entry deleted', schema: dictionaryDeleteResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 404, description: 'Entry not found', schema: dictionaryErrorSchema },\n { status: 409, description: 'Entry is in use and cannot be deleted', schema: dictionaryErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandExecuteResult } from '@open-mercato/shared/lib/commands/types'\nimport { CustomerDictionaryEntry } from '../../../../data/entities'\nimport { mapDictionaryKind, resolveDictionaryActorId, resolveDictionaryRouteContext } from '../../context'\nimport { invalidateDictionaryCache } from '../../cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst patchSchema = z\n .object({\n value: z.string().trim().min(1).max(150).optional(),\n label: z.string().trim().max(150).optional(),\n color: z.union([z.string().trim(), z.null()]).optional(),\n icon: z.union([z.string().trim(), z.null()]).optional(),\n })\n .refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined, {\n message: 'No changes provided',\n })\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\nfunction formatCountMessage(template: string, count: number) {\n return template.replace('{{count}}', String(count))\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { kind?: string; id?: string } }) {\n try {\n const routeContext = await resolveDictionaryRouteContext(req)\n if (!routeContext.organizationId) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const payload = patchSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(routeContext.auth)\n const guardResult = await validateCrudMutationGuard(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: 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 const commandBus = (routeContext.container.resolve('commandBus') as CommandBus)\n let commandResult: CommandExecuteResult<{ entryId: string; changed: boolean }>\n try {\n commandResult = (await commandBus.execute('customers.dictionaryEntries.update', {\n input: {\n id,\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n kind: mappedKind,\n value: payload.value,\n label: payload.label,\n color: payload.color,\n icon: payload.icon,\n },\n ctx: routeContext.ctx,\n })) as CommandExecuteResult<{ entryId: string; changed: boolean }>\n } catch (err) {\n if (isCrudHttpError(err)) {\n if (err.status === 404) {\n throw notFound(routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found'))\n }\n if (err.status === 409) {\n if ((err.body as Record<string, unknown> | undefined)?.code === 'role_type_in_use') {\n const usageCount = Number((err.body as Record<string, unknown> | undefined)?.usageCount ?? 0)\n throw new CrudHttpError(409, {\n ...(err.body as Record<string, unknown> | undefined),\n error: formatCountMessage(\n routeContext.translate(\n 'customers.config.dictionaries.errors.roleTypeValueInUse',\n 'This role type is assigned to {{count}} records. Remove or replace those assignments before changing its value.',\n ),\n usageCount,\n ),\n })\n }\n throw new CrudHttpError(409, { error: routeContext.translate('customers.config.dictionaries.errors.duplicate', 'An entry with this value already exists') })\n }\n if (err.status === 400) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') })\n }\n }\n throw err\n }\n const { result, logEntry } = commandResult\n\n const entry = await findOneWithDecryption(\n routeContext.em.fork(),\n CustomerDictionaryEntry,\n { id },\n {},\n {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n },\n )\n if (!entry) {\n throw notFound(routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found'))\n }\n\n if (result.changed) {\n await invalidateDictionaryCache(routeContext.cache, {\n tenantId: routeContext.tenantId,\n mappedKind,\n organizationIds: [routeContext.organizationId],\n })\n }\n\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n organizationId: entry.organizationId,\n isInherited: false,\n })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: entry.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.update failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') }, { status: 400 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { kind?: string; id?: string } }) {\n try {\n const routeContext = await resolveDictionaryRouteContext(req)\n if (!routeContext.organizationId) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const guardUserId = resolveDictionaryActorId(routeContext.auth)\n const guardResult = await validateCrudMutationGuard(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: 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 const commandBus = (routeContext.container.resolve('commandBus') as CommandBus)\n let deleteResult: CommandExecuteResult<{ entryId: string }>\n try {\n deleteResult = (await commandBus.execute('customers.dictionaryEntries.delete', {\n input: {\n id,\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n kind: mappedKind,\n },\n ctx: routeContext.ctx,\n })) as CommandExecuteResult<{ entryId: string }>\n } catch (err) {\n if (isCrudHttpError(err) && err.status === 404) {\n throw notFound(routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found'))\n }\n if (isCrudHttpError(err) && err.status === 409 && (err.body as Record<string, unknown> | undefined)?.code === 'role_type_in_use') {\n const usageCount = Number((err.body as Record<string, unknown> | undefined)?.usageCount ?? 0)\n throw new CrudHttpError(409, {\n ...(err.body as Record<string, unknown> | undefined),\n error: formatCountMessage(\n routeContext.translate(\n 'customers.config.dictionaries.errors.roleTypeDeleteInUse',\n 'This role type is assigned to {{count}} records. Remove or replace those assignments before deleting it.',\n ),\n usageCount,\n ),\n })\n }\n throw err\n }\n const { logEntry } = deleteResult\n\n await invalidateDictionaryCache(routeContext.cache, {\n tenantId: routeContext.tenantId,\n mappedKind,\n organizationIds: [routeContext.organizationId],\n })\n\n const response = NextResponse.json({ success: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.delete failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to delete dictionary entry') }, { status: 400 })\n }\n}\n\nconst dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n icon: z.string().nullable().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n isInherited: z.boolean().optional(),\n})\n\nconst dictionaryDeleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst dictionaryErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer dictionary entry',\n methods: {\n PATCH: {\n summary: 'Update dictionary entry',\n description: 'Updates value, label, color, or icon for an existing customer dictionary entry.',\n requestBody: {\n contentType: 'application/json',\n schema: patchSchema,\n },\n responses: [\n { status: 200, description: 'Updated dictionary entry', schema: dictionaryEntrySchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: dictionaryErrorSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 404, description: 'Entry not found', schema: dictionaryErrorSchema },\n { status: 409, description: 'Duplicate value conflict', schema: dictionaryErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete dictionary entry',\n description: 'Removes a customer dictionary entry by identifier.',\n responses: [\n { status: 200, description: 'Entry deleted', schema: dictionaryDeleteResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 404, description: 'Entry not found', schema: dictionaryErrorSchema },\n { status: 409, description: 'Entry is in use and cannot be deleted', schema: dictionaryErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
4
4
  import { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from "@open-mercato/shared/lib/crud/mutation-guard";
5
- import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
5
+ import { CrudHttpError, isCrudHttpError, notFound } from "@open-mercato/shared/lib/crud/errors";
6
6
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
7
7
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
8
8
  import { isOrganizationReadAccessAllowed } from "@open-mercato/core/modules/directory/utils/organizationScopeGuard";
@@ -94,7 +94,7 @@ async function resolveEntityRouteScope(em, auth, scope, entityType, entityId, tr
94
94
  { tenantId: auth.tenantId, organizationId: scope?.selectedId ?? auth.orgId ?? null }
95
95
  );
96
96
  if (!entity || entity.tenantId !== auth.tenantId) {
97
- throw new CrudHttpError(404, { error: translate("customers.errors.customer_not_found", "Customer not found") });
97
+ throw notFound(translate("customers.errors.customer_not_found", "Customer not found"));
98
98
  }
99
99
  ensureRouteOrganizationAccess(entity.organizationId, scope, auth, translate);
100
100
  return {
@@ -112,7 +112,7 @@ async function resolveRoleRouteScope(em, auth, scope, entityType, entityId, role
112
112
  { tenantId: auth.tenantId, organizationId: scope?.selectedId ?? auth.orgId ?? null }
113
113
  );
114
114
  if (!role || role.tenantId !== auth.tenantId || role.entityType !== entityType || role.entityId !== entityId) {
115
- throw new CrudHttpError(404, { error: translate("customers.errors.role_not_found", "Role not found") });
115
+ throw notFound(translate("customers.errors.role_not_found", "Role not found"));
116
116
  }
117
117
  ensureRouteOrganizationAccess(role.organizationId, scope, auth, translate);
118
118
  return {