@open-mercato/core 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6814.1.0627c7e9f1

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 (213) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
  3. package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
  4. package/dist/generated/entities/customer_user_invitation/index.js +2 -0
  5. package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
  9. package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
  10. package/dist/modules/business_rules/api/rules/route.js +71 -4
  11. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  12. package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
  13. package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
  14. package/dist/modules/business_rules/components/ActionRow.js +106 -16
  15. package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
  16. package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
  17. package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
  18. package/dist/modules/business_rules/lib/action-executor.js +148 -0
  19. package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
  20. package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
  21. package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
  22. package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
  23. package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
  24. package/dist/modules/business_rules/lib/rule-engine.js +5 -1
  25. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  26. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
  27. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
  28. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
  29. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
  30. package/dist/modules/configs/lib/upgrade-actions.js +12 -0
  31. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  32. package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
  33. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  34. package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
  35. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
  37. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  38. package/dist/modules/customer_accounts/data/entities.js +3 -0
  39. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  40. package/dist/modules/customer_accounts/data/validators.js +1 -0
  41. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  42. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
  43. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
  44. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
  45. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
  46. package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
  47. package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
  48. package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
  49. package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
  50. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
  51. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
  52. package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
  53. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  54. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
  55. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
  56. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  58. package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
  59. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  60. package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
  61. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  62. package/dist/modules/customers/api/deals/[id]/route.js +7 -3
  63. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  64. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
  65. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  66. package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
  67. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  68. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
  69. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  70. package/dist/modules/customers/commands/deals.js +48 -10
  71. package/dist/modules/customers/commands/deals.js.map +2 -2
  72. package/dist/modules/customers/commands/people.js +2 -0
  73. package/dist/modules/customers/commands/people.js.map +2 -2
  74. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
  75. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  76. package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
  77. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  78. package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
  79. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  80. package/dist/modules/customers/data/entities.js +8 -0
  81. package/dist/modules/customers/data/entities.js.map +2 -2
  82. package/dist/modules/customers/data/validators.js +2 -1
  83. package/dist/modules/customers/data/validators.js.map +2 -2
  84. package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
  85. package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
  86. package/dist/modules/entities/api/entity-settings.js +59 -9
  87. package/dist/modules/entities/api/entity-settings.js.map +2 -2
  88. package/dist/modules/entities/backend/config/settings/page.js +10 -0
  89. package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
  90. package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
  91. package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
  92. package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
  93. package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
  94. package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
  95. package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
  96. package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
  97. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  98. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
  99. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  100. package/dist/modules/payment_gateways/data/entities.js +4 -0
  101. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  102. package/dist/modules/payment_gateways/lib/gateway-service.js +12 -2
  103. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  104. package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
  105. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  106. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
  107. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
  108. package/dist/modules/payment_gateways/setup.js +47 -0
  109. package/dist/modules/payment_gateways/setup.js.map +2 -2
  110. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
  111. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
  112. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
  113. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
  114. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
  115. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
  116. package/dist/modules/portal/lib/navigation.js +7 -0
  117. package/dist/modules/portal/lib/navigation.js.map +7 -0
  118. package/dist/modules/query_index/lib/engine.js +43 -0
  119. package/dist/modules/query_index/lib/engine.js.map +2 -2
  120. package/dist/modules/query_index/lib/search-tokens.js +15 -5
  121. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  122. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
  123. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
  124. package/generated/entities/customer_deal_person_link/index.ts +1 -0
  125. package/generated/entities/customer_user_invitation/index.ts +1 -0
  126. package/generated/entity-fields-registry.ts +2 -0
  127. package/package.json +7 -7
  128. package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
  129. package/src/modules/business_rules/api/rules/route.ts +92 -4
  130. package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
  131. package/src/modules/business_rules/components/ActionRow.tsx +156 -18
  132. package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
  133. package/src/modules/business_rules/i18n/de.json +12 -0
  134. package/src/modules/business_rules/i18n/en.json +12 -0
  135. package/src/modules/business_rules/i18n/es.json +12 -0
  136. package/src/modules/business_rules/i18n/pl.json +12 -0
  137. package/src/modules/business_rules/lib/action-executor.ts +198 -0
  138. package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
  139. package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
  140. package/src/modules/business_rules/lib/rule-engine.ts +4 -0
  141. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
  142. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
  143. package/src/modules/configs/lib/upgrade-actions.ts +14 -0
  144. package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
  145. package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
  146. package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
  147. package/src/modules/customer_accounts/data/entities.ts +3 -0
  148. package/src/modules/customer_accounts/data/validators.ts +1 -0
  149. package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
  150. package/src/modules/customer_accounts/i18n/de.json +6 -0
  151. package/src/modules/customer_accounts/i18n/en.json +6 -0
  152. package/src/modules/customer_accounts/i18n/es.json +6 -0
  153. package/src/modules/customer_accounts/i18n/pl.json +6 -0
  154. package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
  155. package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
  156. package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
  157. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
  158. package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
  159. package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
  160. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
  161. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
  162. package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
  163. package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
  164. package/src/modules/customers/api/deals/[id]/route.ts +10 -2
  165. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
  166. package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
  167. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
  168. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
  169. package/src/modules/customers/commands/deals.ts +50 -9
  170. package/src/modules/customers/commands/people.ts +3 -0
  171. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
  172. package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
  173. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
  174. package/src/modules/customers/data/entities.ts +9 -1
  175. package/src/modules/customers/data/validators.ts +1 -0
  176. package/src/modules/customers/i18n/de.json +1 -0
  177. package/src/modules/customers/i18n/en.json +1 -0
  178. package/src/modules/customers/i18n/es.json +1 -0
  179. package/src/modules/customers/i18n/pl.json +1 -0
  180. package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
  181. package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
  182. package/src/modules/entities/api/entity-settings.ts +66 -22
  183. package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
  184. package/src/modules/entities/backend/config/settings/page.tsx +12 -0
  185. package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
  186. package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
  187. package/src/modules/entities/i18n/de.json +9 -0
  188. package/src/modules/entities/i18n/en.json +9 -0
  189. package/src/modules/entities/i18n/es.json +9 -0
  190. package/src/modules/entities/i18n/pl.json +9 -0
  191. package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
  192. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
  193. package/src/modules/payment_gateways/data/entities.ts +5 -0
  194. package/src/modules/payment_gateways/i18n/de.json +5 -1
  195. package/src/modules/payment_gateways/i18n/en.json +5 -1
  196. package/src/modules/payment_gateways/i18n/es.json +5 -1
  197. package/src/modules/payment_gateways/i18n/pl.json +5 -1
  198. package/src/modules/payment_gateways/lib/gateway-service.ts +12 -2
  199. package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
  200. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
  201. package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
  202. package/src/modules/payment_gateways/setup.ts +62 -0
  203. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
  204. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
  205. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
  206. package/src/modules/portal/i18n/de.json +18 -0
  207. package/src/modules/portal/i18n/en.json +18 -0
  208. package/src/modules/portal/i18n/es.json +18 -0
  209. package/src/modules/portal/i18n/pl.json +18 -0
  210. package/src/modules/portal/lib/navigation.ts +3 -0
  211. package/src/modules/query_index/lib/engine.ts +52 -0
  212. package/src/modules/query_index/lib/search-tokens.ts +16 -4
  213. package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
@@ -13,6 +13,18 @@ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/
13
13
  const BCRYPT_COST = 10
14
14
  const INVITATION_TTL_MS = 72 * 60 * 60 * 1000 // 72 hours
15
15
 
16
+ export type CustomerInvitationRollbackState = {
17
+ email: string
18
+ token: string
19
+ customerEntityId: string | null
20
+ personEntityId: string | null
21
+ roleIdsJson: string[]
22
+ invitedByUserId: string | null
23
+ invitedByCustomerUserId: string | null
24
+ displayName: string | null
25
+ expiresAt: Date
26
+ }
27
+
16
28
  export class CustomerInvitationService {
17
29
  constructor(private em: EntityManager) {}
18
30
 
@@ -21,12 +33,18 @@ export class CustomerInvitationService {
21
33
  scope: { tenantId: string; organizationId: string },
22
34
  options: {
23
35
  customerEntityId?: string | null
36
+ personEntityId?: string | null
24
37
  roleIds: string[]
25
38
  invitedByUserId?: string | null
26
39
  invitedByCustomerUserId?: string | null
27
40
  displayName?: string | null
28
41
  },
29
- ): Promise<{ invitation: CustomerUserInvitation; rawToken: string }> {
42
+ ): Promise<{
43
+ invitation: CustomerUserInvitation
44
+ rawToken: string
45
+ reused: boolean
46
+ rollbackState: CustomerInvitationRollbackState | null
47
+ }> {
30
48
  const token = generateSecureToken()
31
49
  const emailHash = hashForLookup(email)
32
50
  const normalizedEmail = email.toLowerCase().trim()
@@ -53,16 +71,28 @@ export class CustomerInvitationService {
53
71
  )
54
72
 
55
73
  if (existing) {
74
+ const rollbackState: CustomerInvitationRollbackState = {
75
+ email: existing.email,
76
+ token: existing.token,
77
+ customerEntityId: existing.customerEntityId ?? null,
78
+ personEntityId: existing.personEntityId ?? null,
79
+ roleIdsJson: [...(existing.roleIdsJson ?? [])],
80
+ invitedByUserId: existing.invitedByUserId ?? null,
81
+ invitedByCustomerUserId: existing.invitedByCustomerUserId ?? null,
82
+ displayName: existing.displayName ?? null,
83
+ expiresAt: new Date(existing.expiresAt),
84
+ }
56
85
  existing.email = normalizedEmail
57
86
  existing.token = tokenHashed
58
87
  existing.customerEntityId = options.customerEntityId || null
88
+ existing.personEntityId = options.personEntityId || null
59
89
  existing.roleIdsJson = options.roleIds
60
90
  existing.invitedByUserId = options.invitedByUserId || null
61
91
  existing.invitedByCustomerUserId = options.invitedByCustomerUserId || null
62
92
  existing.displayName = options.displayName || null
63
93
  existing.expiresAt = expiresAt
64
94
  await this.em.flush()
65
- return { invitation: existing, rawToken: token }
95
+ return { invitation: existing, rawToken: token, reused: true, rollbackState }
66
96
  }
67
97
 
68
98
  const invitation = this.em.create(CustomerUserInvitation, {
@@ -72,6 +102,7 @@ export class CustomerInvitationService {
72
102
  emailHash,
73
103
  token: tokenHashed,
74
104
  customerEntityId: options.customerEntityId || null,
105
+ personEntityId: options.personEntityId || null,
75
106
  roleIdsJson: options.roleIds,
76
107
  invitedByUserId: options.invitedByUserId || null,
77
108
  invitedByCustomerUserId: options.invitedByCustomerUserId || null,
@@ -80,7 +111,28 @@ export class CustomerInvitationService {
80
111
  createdAt: new Date(),
81
112
  } as any) as CustomerUserInvitation
82
113
  await this.em.persist(invitation).flush()
83
- return { invitation, rawToken: token }
114
+ return { invitation, rawToken: token, reused: false, rollbackState: null }
115
+ }
116
+
117
+ async rollbackInvitation(
118
+ invitation: CustomerUserInvitation,
119
+ rollbackState: CustomerInvitationRollbackState | null,
120
+ ): Promise<void> {
121
+ if (!rollbackState) {
122
+ await this.em.remove(invitation).flush()
123
+ return
124
+ }
125
+
126
+ invitation.email = rollbackState.email
127
+ invitation.token = rollbackState.token
128
+ invitation.customerEntityId = rollbackState.customerEntityId
129
+ invitation.personEntityId = rollbackState.personEntityId
130
+ invitation.roleIdsJson = [...rollbackState.roleIdsJson]
131
+ invitation.invitedByUserId = rollbackState.invitedByUserId
132
+ invitation.invitedByCustomerUserId = rollbackState.invitedByCustomerUserId
133
+ invitation.displayName = rollbackState.displayName
134
+ invitation.expiresAt = new Date(rollbackState.expiresAt)
135
+ await this.em.flush()
84
136
  }
85
137
 
86
138
  async findByToken(token: string): Promise<CustomerUserInvitation | null> {
@@ -117,6 +169,7 @@ export class CustomerInvitationService {
117
169
  tenantId: invitation.tenantId,
118
170
  organizationId: invitation.organizationId,
119
171
  customerEntityId: invitation.customerEntityId || null,
172
+ personEntityId: invitation.personEntityId || null,
120
173
  isActive: true,
121
174
  emailVerifiedAt: new Date(), // Invitation implicitly verifies email
122
175
  failedLoginAttempts: 0,
@@ -1,6 +1,7 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'
3
3
  import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
+ import { resolveOwnedCompanyForPerson } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'
4
5
  import { createLogger } from '@open-mercato/shared/lib/logger'
5
6
 
6
7
  const logger = createLogger('customer_accounts').child({ component: 'auto-link-crm' })
@@ -26,17 +27,81 @@ export default async function handle(
26
27
  try {
27
28
  const user = await findOneWithDecryption(em, CustomerUser, { id: userId, tenantId, deletedAt: null }, undefined, { tenantId, organizationId })
28
29
  if (!user) return
29
- if (user.personEntityId) return
30
+
31
+ const { CustomerEntity } = await import('@open-mercato/core/modules/customers/data/entities')
32
+
33
+ // Every lookup and write below normalizes against the user's OWN org, not just
34
+ // the tenant: customerEntityId is the portal's company scope key (portal user
35
+ // and invite routes filter on it), so adopting an entity from another org would
36
+ // widen the user's portal access instead of repairing it.
37
+ const ownScope = { tenantId, organizationId: user.organizationId }
38
+
39
+ // Defensive normalization for NEW users only — this subscriber runs on
40
+ // `customer_accounts.user.created`, which is never re-emitted for an existing
41
+ // row. customerEntityId is the CRM company FK, and earlier invite flows (#4362)
42
+ // could poison it with a person entity id, which then breaks every later user
43
+ // edit ("Company not found"). If the linked entity is a person (not a company)
44
+ // or sits outside the user's org, drop it — or recover the person's real company
45
+ // from their profile. Correct company links are left untouched. Rows created
46
+ // before this change keep their bad FK; see #4473 for the backfill.
47
+ if (user.customerEntityId) {
48
+ const linked = await findOneWithDecryption(
49
+ em,
50
+ CustomerEntity,
51
+ { id: user.customerEntityId, tenantId, organizationId: user.organizationId, deletedAt: null } as any,
52
+ undefined,
53
+ { tenantId, organizationId },
54
+ ) as any
55
+ if (!linked) {
56
+ // Not resolvable inside the user's own org — a cross-org (or deleted)
57
+ // entity. Clear it rather than leave it: this FK is the portal scope key.
58
+ await em.nativeUpdate(
59
+ CustomerUser,
60
+ { id: userId, tenantId, organizationId: user.organizationId },
61
+ { customerEntityId: null },
62
+ )
63
+ user.customerEntityId = null
64
+ } else if (linked.kind === 'person') {
65
+ // Only adopt a recovered company that is itself in the user's org.
66
+ const replacement = await resolveOwnedCompanyForPerson(em, user.customerEntityId, ownScope)
67
+ await em.nativeUpdate(
68
+ CustomerUser,
69
+ { id: userId, tenantId, organizationId: user.organizationId },
70
+ { customerEntityId: replacement },
71
+ )
72
+ user.customerEntityId = replacement
73
+ }
74
+ }
75
+
76
+ // A user invited from a CRM person card arrives with personEntityId already
77
+ // set, so the email-matching path below never runs for it. Derive the company
78
+ // FK from that person's own profile instead of leaving it null forever — the
79
+ // portal Users page, portal invitations, and the company detail "Portal users"
80
+ // group all key off customerEntityId (#4362).
81
+ if (user.personEntityId) {
82
+ if (user.customerEntityId) return
83
+ const recovered = await resolveOwnedCompanyForPerson(em, user.personEntityId, ownScope)
84
+ if (recovered) {
85
+ await em.nativeUpdate(
86
+ CustomerUser,
87
+ { id: userId, tenantId, organizationId: user.organizationId },
88
+ { customerEntityId: recovered },
89
+ )
90
+ }
91
+ return
92
+ }
30
93
 
31
94
  const email = (data?.email ? (data.email as string) : user.email)?.toLowerCase().trim()
32
95
  if (!email) return
33
96
 
34
- const { CustomerEntity } = await import('@open-mercato/core/modules/customers/data/entities')
35
-
97
+ // Scope the candidate lookup to the user's own org. The fifth argument is a
98
+ // decryption scope, not a filter, so without organizationId in the `where`
99
+ // this matches people across every org in the tenant and writes a cross-org
100
+ // link that the normalization block above just cleared.
36
101
  const personEntities = await findWithDecryption(
37
102
  em,
38
103
  CustomerEntity,
39
- { tenantId, kind: 'person', deletedAt: null } as any,
104
+ { tenantId, organizationId: user.organizationId, kind: 'person', deletedAt: null } as any,
40
105
  { limit: 500 } as any,
41
106
  { tenantId, organizationId },
42
107
  )
@@ -47,16 +112,18 @@ export default async function handle(
47
112
 
48
113
  if (!matchingEntity) return
49
114
 
50
- const { CustomerPersonProfile } = await import('@open-mercato/core/modules/customers/data/entities')
51
- const profile = await em.findOne(CustomerPersonProfile as any, { entity: matchingEntity.id } as any) as any
52
- const companyEntityId = profile?.companyEntityId as string | undefined
115
+ const companyEntityId = await resolveOwnedCompanyForPerson(em, matchingEntity.id, ownScope)
53
116
 
54
117
  const updates: Record<string, unknown> = { personEntityId: matchingEntity.id }
55
118
  if (companyEntityId) {
56
119
  updates.customerEntityId = companyEntityId
57
120
  }
58
121
 
59
- await em.nativeUpdate(CustomerUser, { id: userId }, updates)
122
+ await em.nativeUpdate(
123
+ CustomerUser,
124
+ { id: userId, tenantId, organizationId: user.organizationId },
125
+ updates,
126
+ )
60
127
  } catch (err) {
61
128
  logger.error('Failed to link customer user to CRM person', { err })
62
129
  }
@@ -130,7 +130,7 @@ function InviteForm({ personEntityId, onSuccess }: { personEntityId: string; onS
130
130
  try {
131
131
  await runMutation({
132
132
  context: { entityType: 'customer_accounts:user' },
133
- mutationPayload: { customerEntityId: personEntityId, roleIds: selectedRoleIds },
133
+ mutationPayload: { personEntityId, roleIds: selectedRoleIds },
134
134
  operation: async () => {
135
135
  // optimistic-lock-exempt: creates a new portal invitation, not a concurrent record edit
136
136
  const call = await apiCall<{ ok: boolean; error?: string }>(
@@ -142,7 +142,7 @@ function InviteForm({ personEntityId, onSuccess }: { personEntityId: string; onS
142
142
  email: trimmedEmail,
143
143
  roleIds: selectedRoleIds,
144
144
  displayName: displayName.trim() || undefined,
145
- customerEntityId: personEntityId,
145
+ personEntityId,
146
146
  }),
147
147
  },
148
148
  )
@@ -8,10 +8,13 @@ const accountStatusWidget = {
8
8
  priority: 200,
9
9
  } as const
10
10
 
11
+ // Column 1 on purpose (#4400): any column-2 group makes CrudForm switch the
12
+ // company detail page to the narrow 7fr/3fr two-column layout, compressing the
13
+ // left-side groups. Portal users belongs in the left stack as a full-width row.
11
14
  const companyUsersWidget = {
12
15
  widgetId: 'customer_accounts.injection.company-users',
13
16
  kind: 'group',
14
- column: 2,
17
+ column: 1,
15
18
  groupLabel: 'customer_accounts.widgets.portalUsers',
16
19
  priority: 200,
17
20
  } as const
@@ -39,6 +39,7 @@ type DealPersonItem = {
39
39
  subtitle: string | null
40
40
  kind: 'person'
41
41
  linkedAt: string
42
+ isPrimary: boolean
42
43
  }
43
44
 
44
45
  function matchesSearch(item: DealPersonItem, query: string): boolean {
@@ -124,6 +125,7 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
124
125
  subtitle: person.primaryEmail ?? person.primaryPhone ?? null,
125
126
  kind: 'person',
126
127
  linkedAt: link.createdAt.toISOString(),
128
+ isPrimary: link.isPrimary === true,
127
129
  } satisfies DealPersonItem
128
130
  })
129
131
  .filter((item): item is DealPersonItem => item !== null)
@@ -169,6 +171,7 @@ export const openApi: OpenApiRouteDoc = {
169
171
  subtitle: z.string().nullable(),
170
172
  kind: z.literal('person'),
171
173
  linkedAt: z.string(),
174
+ isPrimary: z.boolean(),
172
175
  }),
173
176
  ),
174
177
  total: z.number().int().nonnegative(),
@@ -56,6 +56,7 @@ type DealAssociation = {
56
56
  label: string
57
57
  subtitle: string | null
58
58
  kind: 'person' | 'company'
59
+ isPrimary?: boolean
59
60
  }
60
61
 
61
62
  function normalizePersonAssociation(entity: CustomerEntity): { label: string; subtitle: string | null } {
@@ -602,13 +603,19 @@ export async function GET(request: Request, context: { params?: Record<string, u
602
603
  )
603
604
  : [],
604
605
  ])
606
+ const primaryPersonIds = new Set(
607
+ personLinkRows
608
+ .filter((link) => link.isPrimary === true)
609
+ .map((link) => (typeof link.person === 'string' ? link.person : link.person?.id))
610
+ .filter((value): value is string => typeof value === 'string'),
611
+ )
605
612
  const previewPeopleMap = new Map(previewPeople.map((entity) => [entity.id, entity]))
606
613
  const previewCompaniesMap = new Map(previewCompanies.map((entity) => [entity.id, entity]))
607
614
  const people = linkedPersonIds.slice(0, 3).reduce<DealAssociation[]>((acc, personId) => {
608
615
  const entity = previewPeopleMap.get(personId) ?? null
609
616
  if (!entity || entity.deletedAt) return acc
610
617
  const { label, subtitle } = normalizePersonAssociation(entity)
611
- acc.push({ id: entity.id, label, subtitle, kind: 'person' })
618
+ acc.push({ id: entity.id, label, subtitle, kind: 'person', isPrimary: primaryPersonIds.has(entity.id) })
612
619
  return acc
613
620
  }, [])
614
621
  const companies = linkedCompanyIds.slice(0, 3).reduce<DealAssociation[]>((acc, companyId) => {
@@ -654,7 +661,7 @@ export async function GET(request: Request, context: { params?: Record<string, u
654
661
  const entity = link.person as CustomerEntity | null
655
662
  if (!entity || entity.deletedAt) return acc
656
663
  const { label, subtitle } = normalizePersonAssociation(entity)
657
- acc.push({ id: entity.id, label, subtitle, kind: 'person' })
664
+ acc.push({ id: entity.id, label, subtitle, kind: 'person', isPrimary: link.isPrimary === true })
658
665
  return acc
659
666
  }, [])
660
667
 
@@ -927,6 +934,7 @@ const dealDetailResponseSchema = z.object({
927
934
  label: z.string(),
928
935
  subtitle: z.string().nullable().optional(),
929
936
  kind: z.literal('person'),
937
+ isPrimary: z.boolean().optional(),
930
938
  }),
931
939
  ),
932
940
  companies: z.array(
@@ -70,6 +70,17 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
70
70
  return resolveLegacyTab(searchParams?.get('tab'))
71
71
  }, [searchParams])
72
72
  const [activeTab, setActiveTab] = React.useState<CompanyTabId>(initialTab)
73
+
74
+ const handleTabChange = React.useCallback(
75
+ (tab: CompanyTabId) => {
76
+ setActiveTab(tab)
77
+ if (!pathname) return
78
+ const nextParams = new URLSearchParams(searchParams?.toString() ?? '')
79
+ nextParams.set('tab', tab)
80
+ router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false })
81
+ },
82
+ [pathname, router, searchParams],
83
+ )
73
84
  const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
74
85
  const { canViewDeals, isReady: isDealsAccessReady } = useDealsAccess()
75
86
 
@@ -520,7 +531,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
520
531
  zone2={
521
532
  <CompanyDetailTabs
522
533
  activeTab={activeTab}
523
- onTabChange={setActiveTab}
534
+ onTabChange={handleTabChange}
524
535
  injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
525
536
  peopleCount={data.counts?.people ?? 0}
526
537
  dealsCount={dealCount}
@@ -3,6 +3,7 @@ export type DealAssociation = {
3
3
  label: string
4
4
  subtitle: string | null
5
5
  kind: 'person' | 'company'
6
+ isPrimary?: boolean
6
7
  }
7
8
 
8
9
  export type PersonAssociationApiRecord = {
@@ -69,13 +69,6 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
69
69
  } = useScheduleDialog()
70
70
  const formWrapperRef = React.useRef<HTMLDivElement>(null)
71
71
 
72
- const initialTab = React.useMemo(() => resolveLegacyTab(searchParams?.get('tab')), [searchParams])
73
- const [activeTab, setActiveTab] = React.useState<DealTabId>(initialTab)
74
-
75
- React.useEffect(() => {
76
- setActiveTab(initialTab)
77
- }, [initialTab])
78
-
79
72
  const currentDealId = data?.deal.id ?? id
80
73
  const { injectionContext, runMutationWithContext } = useDealMutationContext({
81
74
  currentDealId,
@@ -108,6 +101,17 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
108
101
  setData,
109
102
  })
110
103
 
104
+ const injectedTabIds = React.useMemo(() => injectedTabs.map((tab) => tab.id), [injectedTabs])
105
+ const initialTab = React.useMemo(
106
+ () => resolveLegacyTab(searchParams?.get('tab'), injectedTabIds),
107
+ [injectedTabIds, searchParams],
108
+ )
109
+ const [activeTab, setActiveTab] = React.useState<DealTabId>(initialTab)
110
+
111
+ React.useEffect(() => {
112
+ setActiveTab(initialTab)
113
+ }, [initialTab])
114
+
111
115
  const { searchPeoplePage, fetchPeopleByIds, searchCompaniesPage, fetchCompaniesByIds } = useDealAssociationLookups({
112
116
  excludeLinkedDealId: data?.deal.id ?? null,
113
117
  })
@@ -131,6 +135,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
131
135
  id: entry.id,
132
136
  label: entry.subtitle ? `${entry.label} · ${entry.subtitle}` : entry.label,
133
137
  kind: entry.kind,
138
+ isPrimary: entry.isPrimary === true,
134
139
  }))
135
140
  : []),
136
141
  [data],
@@ -141,7 +146,8 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
141
146
  setSelectedActivityEntityId((current) => {
142
147
  if (activityEntities.length === 1) return activityEntities[0].id
143
148
  if (current && activityEntities.some((entry) => entry.id === current)) return current
144
- return null
149
+ const primary = activityEntities.find((entry) => entry.isPrimary)
150
+ return (primary ?? activityEntities[0])?.id ?? null
145
151
  })
146
152
  }, [activityEntities])
147
153
 
@@ -629,7 +635,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
629
635
  isSaving={isSaving}
630
636
  />
631
637
 
632
- <InjectionSpot spotId={extensionPoints.hosts.dealStatusBadges.spotId} context={injectionContext} data={data} />
638
+ <InjectionSpot spotId={extensionPoints.hosts.dealStatusBadges.spotId} context={injectionContext} data={data} onDataChange={setData} />
633
639
 
634
640
  <PipelineStepper
635
641
  stages={data.pipelineStages}
@@ -92,10 +92,6 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
92
92
  const [isSaving, setIsSaving] = React.useState(false)
93
93
  const formWrapperRef = React.useRef<HTMLDivElement>(null)
94
94
 
95
- const initialTab = React.useMemo(() => {
96
- return resolveLegacyTab(searchParams?.get('tab'))
97
- }, [searchParams])
98
- const [activeTab, setActiveTab] = React.useState<PersonTabId>(initialTab)
99
95
  const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
100
96
  const [scheduleDialogOpen, setScheduleDialogOpen] = React.useState(false)
101
97
  const [scheduleEditData, setScheduleEditData] = React.useState<ScheduleActivityEditData | null>(null)
@@ -329,6 +325,28 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
329
325
 
330
326
  const injectedTabMap = React.useMemo(() => new Map(injectedTabs.map((tab) => [tab.id, tab.render])), [injectedTabs])
331
327
 
328
+ const injectedTabIds = React.useMemo(() => injectedTabs.map((tab) => tab.id), [injectedTabs])
329
+ const initialTab = React.useMemo(
330
+ () => resolveLegacyTab(searchParams?.get('tab'), injectedTabIds),
331
+ [injectedTabIds, searchParams],
332
+ )
333
+ const [activeTab, setActiveTab] = React.useState<PersonTabId>(initialTab)
334
+
335
+ React.useEffect(() => {
336
+ setActiveTab(initialTab)
337
+ }, [initialTab])
338
+
339
+ const handleTabChange = React.useCallback(
340
+ (tab: PersonTabId) => {
341
+ setActiveTab(tab)
342
+ if (!pathname) return
343
+ const nextParams = new URLSearchParams(searchParams?.toString() ?? '')
344
+ nextParams.set('tab', tab)
345
+ router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false })
346
+ },
347
+ [pathname, router, searchParams],
348
+ )
349
+
332
350
  // Tags
333
351
  const handleTagsChange = React.useCallback((nextTags: TagSummary[]) => {
334
352
  setData((prev) => (prev ? { ...prev, tags: nextTags } : prev))
@@ -515,7 +533,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
515
533
  onDelete={handleFormDelete}
516
534
  isDirty={isDirty}
517
535
  isSaving={isSaving}
518
- onOpenCompaniesTab={() => setActiveTab('companies')}
536
+ onOpenCompaniesTab={() => handleTabChange('companies')}
519
537
  onDataReload={() => { loadData().catch((err) => logger.warn('onDataReload failed', { component: 'people-v2', err })) }}
520
538
  onFocusField={(fieldName) => {
521
539
  const selectorMap: Record<string, string> = {
@@ -557,7 +575,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
557
575
  const zone2Content = (
558
576
  <PersonDetailTabs
559
577
  activeTab={activeTab}
560
- onTabChange={setActiveTab}
578
+ onTabChange={handleTabChange}
561
579
  injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
562
580
  activitiesCount={interactionCount}
563
581
  dealsCount={dealCount}
@@ -273,6 +273,7 @@ type DealSnapshot = {
273
273
  lossNotes: string | null
274
274
  }
275
275
  people: string[]
276
+ primaryPersonEntityId?: string | null
276
277
  companies: string[]
277
278
  transitions: DealStageTransitionSnapshot[]
278
279
  custom?: Record<string, unknown>
@@ -324,6 +325,7 @@ async function loadDealSnapshot(em: EntityManager, id: string): Promise<DealSnap
324
325
  tenantId: deal.tenantId,
325
326
  organizationId: deal.organizationId,
326
327
  })
328
+ const primaryPerson = peopleLinks.find((link) => link.isPrimary)?.person
327
329
  return {
328
330
  deal: {
329
331
  id: deal.id,
@@ -348,6 +350,8 @@ async function loadDealSnapshot(em: EntityManager, id: string): Promise<DealSnap
348
350
  people: peopleLinks.map((link) =>
349
351
  typeof link.person === 'string' ? link.person : link.person.id
350
352
  ),
353
+ primaryPersonEntityId:
354
+ typeof primaryPerson === 'string' ? primaryPerson : primaryPerson?.id ?? null,
351
355
  companies: companyLinks.map((link) =>
352
356
  typeof link.company === 'string' ? link.company : link.company.id
353
357
  ),
@@ -372,18 +376,55 @@ function toNumericString(value: number | null | undefined): string | null {
372
376
  async function syncDealPeople(
373
377
  em: EntityManager,
374
378
  deal: CustomerDeal,
375
- personIds: string[] | undefined | null
379
+ personIds: string[] | undefined | null,
380
+ primaryPersonEntityId?: string | null
376
381
  ): Promise<void> {
377
- if (personIds === undefined) return
382
+ if (personIds === undefined) {
383
+ if (primaryPersonEntityId === undefined) return
384
+ const links = await em.find(CustomerDealPersonLink, { deal })
385
+ if (primaryPersonEntityId !== null && !links.some((link) => link.person.id === primaryPersonEntityId)) {
386
+ const { translate } = await resolveTranslations()
387
+ throw new CrudHttpError(400, {
388
+ error: translate(
389
+ 'customers.errors.primaryPersonMustBeLinked',
390
+ 'Primary person must be linked to the deal',
391
+ ),
392
+ })
393
+ }
394
+ for (const link of links) {
395
+ link.isPrimary = false
396
+ }
397
+ await em.flush()
398
+ if (primaryPersonEntityId !== null) {
399
+ const primaryLink = links.find((link) => link.person.id === primaryPersonEntityId)
400
+ if (primaryLink) primaryLink.isPrimary = true
401
+ }
402
+ return
403
+ }
404
+ const unique = Array.from(new Set(personIds ?? []))
405
+ if (primaryPersonEntityId !== undefined && primaryPersonEntityId !== null && !unique.includes(primaryPersonEntityId)) {
406
+ const { translate } = await resolveTranslations()
407
+ throw new CrudHttpError(400, {
408
+ error: translate(
409
+ 'customers.errors.primaryPersonMustBeLinked',
410
+ 'Primary person must be linked to the deal',
411
+ ),
412
+ })
413
+ }
414
+ let effectivePrimaryId = primaryPersonEntityId
415
+ if (effectivePrimaryId === undefined) {
416
+ const existingPrimary = await em.findOne(CustomerDealPersonLink, { deal, isPrimary: true })
417
+ effectivePrimaryId = existingPrimary?.person?.id ?? null
418
+ }
378
419
  await em.nativeDelete(CustomerDealPersonLink, { deal })
379
- if (!personIds || !personIds.length) return
380
- const unique = Array.from(new Set(personIds))
420
+ if (!unique.length) return
381
421
  for (const personId of unique) {
382
422
  const person = await requireCustomerEntity(em, personId, { tenantId: deal.tenantId, organizationId: deal.organizationId }, 'person', 'Person not found')
383
423
  ensureSameScope(person, deal.organizationId, deal.tenantId)
384
424
  const link = em.create(CustomerDealPersonLink, {
385
425
  deal,
386
426
  person,
427
+ isPrimary: personId === effectivePrimaryId,
387
428
  })
388
429
  em.persist(link)
389
430
  }
@@ -479,7 +520,7 @@ const createDealCommand: CommandHandler<DealCreateInput, { dealId: string }> = {
479
520
  transitionedByUserId: normalizedTransitionAuthorUserId,
480
521
  })
481
522
  },
482
- () => syncDealPeople(em, deal, parsed.personIds ?? []),
523
+ () => syncDealPeople(em, deal, parsed.personIds ?? [], parsed.primaryPersonEntityId),
483
524
  () => syncDealCompanies(em, deal, parsed.companyIds ?? []),
484
525
  ], { transaction: true })
485
526
 
@@ -575,7 +616,7 @@ const createDealCommand: CommandHandler<DealCreateInput, { dealId: string }> = {
575
616
  }
576
617
  const restoredDeal = deal
577
618
  await withAtomicFlush(em, [
578
- () => syncDealPeople(em, restoredDeal, after.people),
619
+ () => syncDealPeople(em, restoredDeal, after.people, after.primaryPersonEntityId),
579
620
  () => syncDealCompanies(em, restoredDeal, after.companies),
580
621
  () => deleteDealStageTransitions(em, restoredDeal),
581
622
  () => restoreDealStageTransitions(em, restoredDeal, after.transitions),
@@ -746,7 +787,7 @@ const updateDealCommand: CommandHandler<DealUpdateInput, { dealId: string }> = {
746
787
  transitionedByUserId: normalizedTransitionAuthorUserId,
747
788
  })
748
789
  },
749
- () => syncDealPeople(em, record, parsed.personIds),
790
+ () => syncDealPeople(em, record, parsed.personIds, parsed.primaryPersonEntityId),
750
791
  () => syncDealCompanies(em, record, parsed.companyIds),
751
792
  ],
752
793
  })
@@ -889,7 +930,7 @@ const updateDealCommand: CommandHandler<DealUpdateInput, { dealId: string }> = {
889
930
  transitionedByUserId: normalizedTransitionAuthorUserId,
890
931
  })
891
932
  },
892
- () => syncDealPeople(em, deal, before.people),
933
+ () => syncDealPeople(em, deal, before.people, before.primaryPersonEntityId),
893
934
  () => syncDealCompanies(em, deal, before.companies),
894
935
  ], { transaction: true })
895
936
 
@@ -1008,7 +1049,7 @@ const deleteDealCommand: CommandHandler<{ body?: Record<string, unknown>; query?
1008
1049
  em.persist(deal)
1009
1050
  }
1010
1051
  await withAtomicFlush(em, [
1011
- () => syncDealPeople(em, deal, before.people),
1052
+ () => syncDealPeople(em, deal, before.people, before.primaryPersonEntityId),
1012
1053
  () => syncDealCompanies(em, deal, before.companies),
1013
1054
  () => deleteDealStageTransitions(em, deal),
1014
1055
  () => restoreDealStageTransitions(em, deal, before.transitions),
@@ -181,6 +181,7 @@ type PersonSnapshot = {
181
181
  id: string
182
182
  dealId: string
183
183
  participantRole: string | null
184
+ isPrimary?: boolean
184
185
  createdAt: Date
185
186
  }>
186
187
  activities: PersonActivitySnapshot[]
@@ -345,6 +346,7 @@ function serializePersonSnapshot(
345
346
  id: link.id,
346
347
  dealId: link.deal.id,
347
348
  participantRole: link.participantRole ?? null,
349
+ isPrimary: link.isPrimary === true,
348
350
  createdAt: link.createdAt,
349
351
  })),
350
352
  activities: activities.map((activity) => ({
@@ -1522,6 +1524,7 @@ const deletePersonCommand: CommandHandler<{ body?: Record<string, unknown>; quer
1522
1524
  deal,
1523
1525
  person: entity,
1524
1526
  participantRole: link.participantRole,
1527
+ isPrimary: link.isPrimary === true,
1525
1528
  createdAt: link.createdAt,
1526
1529
  })
1527
1530
  em.persist(restoredLink)