@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
- package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
- package/dist/generated/entities/customer_user_invitation/index.js +2 -0
- package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
- package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
- package/dist/modules/business_rules/api/rules/route.js +71 -4
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
- package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
- package/dist/modules/business_rules/components/ActionRow.js +106 -16
- package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
- package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
- package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
- package/dist/modules/business_rules/lib/action-executor.js +148 -0
- package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
- package/dist/modules/business_rules/lib/rule-engine.js +5 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +12 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +3 -0
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +1 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
- package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
- package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +7 -3
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +48 -10
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/people.js +2 -0
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/data/entities.js +8 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +2 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
- package/dist/modules/entities/api/entity-settings.js +59 -9
- package/dist/modules/entities/api/entity-settings.js.map +2 -2
- package/dist/modules/entities/backend/config/settings/page.js +10 -0
- package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
- package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
- package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
- package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +4 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +12 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
- package/dist/modules/payment_gateways/setup.js +47 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
- package/dist/modules/portal/lib/navigation.js +7 -0
- package/dist/modules/portal/lib/navigation.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +43 -0
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +15 -5
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/generated/entities/customer_deal_person_link/index.ts +1 -0
- package/generated/entities/customer_user_invitation/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
- package/src/modules/business_rules/api/rules/route.ts +92 -4
- package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
- package/src/modules/business_rules/components/ActionRow.tsx +156 -18
- package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
- package/src/modules/business_rules/i18n/de.json +12 -0
- package/src/modules/business_rules/i18n/en.json +12 -0
- package/src/modules/business_rules/i18n/es.json +12 -0
- package/src/modules/business_rules/i18n/pl.json +12 -0
- package/src/modules/business_rules/lib/action-executor.ts +198 -0
- package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
- package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
- package/src/modules/business_rules/lib/rule-engine.ts +4 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
- package/src/modules/configs/lib/upgrade-actions.ts +14 -0
- package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
- package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
- package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
- package/src/modules/customer_accounts/data/entities.ts +3 -0
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
- package/src/modules/customer_accounts/i18n/de.json +6 -0
- package/src/modules/customer_accounts/i18n/en.json +6 -0
- package/src/modules/customer_accounts/i18n/es.json +6 -0
- package/src/modules/customer_accounts/i18n/pl.json +6 -0
- package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
- package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
- package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
- package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
- package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
- package/src/modules/customers/api/deals/[id]/route.ts +10 -2
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
- package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
- package/src/modules/customers/commands/deals.ts +50 -9
- package/src/modules/customers/commands/people.ts +3 -0
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
- package/src/modules/customers/data/entities.ts +9 -1
- package/src/modules/customers/data/validators.ts +1 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
- package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
- package/src/modules/entities/api/entity-settings.ts +66 -22
- package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
- package/src/modules/entities/backend/config/settings/page.tsx +12 -0
- package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
- package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
- package/src/modules/entities/i18n/de.json +9 -0
- package/src/modules/entities/i18n/en.json +9 -0
- package/src/modules/entities/i18n/es.json +9 -0
- package/src/modules/entities/i18n/pl.json +9 -0
- package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
- package/src/modules/payment_gateways/data/entities.ts +5 -0
- package/src/modules/payment_gateways/i18n/de.json +5 -1
- package/src/modules/payment_gateways/i18n/en.json +5 -1
- package/src/modules/payment_gateways/i18n/es.json +5 -1
- package/src/modules/payment_gateways/i18n/pl.json +5 -1
- package/src/modules/payment_gateways/lib/gateway-service.ts +12 -2
- package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
- package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
- package/src/modules/payment_gateways/setup.ts +62 -0
- package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
- package/src/modules/portal/i18n/de.json +18 -0
- package/src/modules/portal/i18n/en.json +18 -0
- package/src/modules/portal/i18n/es.json +18 -0
- package/src/modules/portal/i18n/pl.json +18 -0
- package/src/modules/portal/lib/navigation.ts +3 -0
- package/src/modules/query_index/lib/engine.ts +52 -0
- package/src/modules/query_index/lib/search-tokens.ts +16 -4
- 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<{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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: {
|
|
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
|
-
|
|
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:
|
|
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={
|
|
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}
|
|
@@ -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
|
-
|
|
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={() =>
|
|
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={
|
|
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)
|
|
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 (!
|
|
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)
|