@open-mercato/core 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6824.1.21fff90682

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 (228) 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/auth/lib/backendChrome.js +10 -9
  9. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  10. package/dist/modules/auth/lib/tenantAccess.js +7 -0
  11. package/dist/modules/auth/lib/tenantAccess.js.map +2 -2
  12. package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
  13. package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
  14. package/dist/modules/business_rules/api/rules/route.js +71 -4
  15. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  16. package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
  17. package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
  18. package/dist/modules/business_rules/components/ActionRow.js +106 -16
  19. package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
  20. package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
  21. package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
  22. package/dist/modules/business_rules/lib/action-executor.js +148 -0
  23. package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
  24. package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
  25. package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
  26. package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
  27. package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
  28. package/dist/modules/business_rules/lib/rule-engine.js +5 -1
  29. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  30. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
  31. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
  32. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
  33. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
  34. package/dist/modules/configs/lib/upgrade-actions.js +12 -0
  35. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
  37. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
  39. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
  40. package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
  41. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  42. package/dist/modules/customer_accounts/data/entities.js +3 -0
  43. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  44. package/dist/modules/customer_accounts/data/validators.js +1 -0
  45. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  46. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
  47. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
  48. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
  49. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
  50. package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
  51. package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
  52. package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
  53. package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
  54. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
  55. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
  56. package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
  57. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  58. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
  59. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
  60. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
  61. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  62. package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
  63. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  64. package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
  65. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  66. package/dist/modules/customers/api/deals/[id]/route.js +7 -3
  67. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  68. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
  69. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  70. package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
  71. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  72. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
  73. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  74. package/dist/modules/customers/commands/deals.js +48 -10
  75. package/dist/modules/customers/commands/deals.js.map +2 -2
  76. package/dist/modules/customers/commands/people.js +2 -0
  77. package/dist/modules/customers/commands/people.js.map +2 -2
  78. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
  79. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  80. package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
  81. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  82. package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
  83. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  84. package/dist/modules/customers/data/entities.js +8 -0
  85. package/dist/modules/customers/data/entities.js.map +2 -2
  86. package/dist/modules/customers/data/validators.js +2 -1
  87. package/dist/modules/customers/data/validators.js.map +2 -2
  88. package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
  89. package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
  90. package/dist/modules/directory/api/tenants/route.js +8 -2
  91. package/dist/modules/directory/api/tenants/route.js.map +2 -2
  92. package/dist/modules/directory/commands/tenants.js +6 -0
  93. package/dist/modules/directory/commands/tenants.js.map +2 -2
  94. package/dist/modules/entities/api/entity-settings.js +59 -9
  95. package/dist/modules/entities/api/entity-settings.js.map +2 -2
  96. package/dist/modules/entities/backend/config/settings/page.js +10 -0
  97. package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
  98. package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
  99. package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
  100. package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
  101. package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
  102. package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
  103. package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
  104. package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
  105. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  106. package/dist/modules/payment_gateways/api/sessions/route.js +1 -3
  107. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  108. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
  109. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  110. package/dist/modules/payment_gateways/data/entities.js +4 -0
  111. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  112. package/dist/modules/payment_gateways/lib/gateway-service.js +15 -6
  113. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  114. package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
  115. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  116. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
  117. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
  118. package/dist/modules/payment_gateways/setup.js +47 -0
  119. package/dist/modules/payment_gateways/setup.js.map +2 -2
  120. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
  121. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
  122. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
  123. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
  124. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
  125. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
  126. package/dist/modules/portal/lib/navigation.js +7 -0
  127. package/dist/modules/portal/lib/navigation.js.map +7 -0
  128. package/dist/modules/query_index/lib/engine.js +43 -0
  129. package/dist/modules/query_index/lib/engine.js.map +2 -2
  130. package/dist/modules/query_index/lib/search-tokens.js +15 -5
  131. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  132. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
  133. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
  134. package/generated/entities/customer_deal_person_link/index.ts +1 -0
  135. package/generated/entities/customer_user_invitation/index.ts +1 -0
  136. package/generated/entity-fields-registry.ts +2 -0
  137. package/package.json +7 -7
  138. package/src/modules/auth/lib/backendChrome.tsx +15 -9
  139. package/src/modules/auth/lib/tenantAccess.ts +8 -0
  140. package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
  141. package/src/modules/business_rules/api/rules/route.ts +92 -4
  142. package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
  143. package/src/modules/business_rules/components/ActionRow.tsx +156 -18
  144. package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
  145. package/src/modules/business_rules/i18n/de.json +12 -0
  146. package/src/modules/business_rules/i18n/en.json +12 -0
  147. package/src/modules/business_rules/i18n/es.json +12 -0
  148. package/src/modules/business_rules/i18n/pl.json +12 -0
  149. package/src/modules/business_rules/lib/action-executor.ts +198 -0
  150. package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
  151. package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
  152. package/src/modules/business_rules/lib/rule-engine.ts +4 -0
  153. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
  154. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
  155. package/src/modules/configs/lib/upgrade-actions.ts +14 -0
  156. package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
  157. package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
  158. package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
  159. package/src/modules/customer_accounts/data/entities.ts +3 -0
  160. package/src/modules/customer_accounts/data/validators.ts +1 -0
  161. package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
  162. package/src/modules/customer_accounts/i18n/de.json +6 -0
  163. package/src/modules/customer_accounts/i18n/en.json +6 -0
  164. package/src/modules/customer_accounts/i18n/es.json +6 -0
  165. package/src/modules/customer_accounts/i18n/pl.json +6 -0
  166. package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
  167. package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
  168. package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
  169. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
  170. package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
  171. package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
  172. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
  173. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
  174. package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
  175. package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
  176. package/src/modules/customers/api/deals/[id]/route.ts +10 -2
  177. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
  178. package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
  179. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
  180. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
  181. package/src/modules/customers/commands/deals.ts +50 -9
  182. package/src/modules/customers/commands/people.ts +3 -0
  183. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
  184. package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
  185. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
  186. package/src/modules/customers/data/entities.ts +9 -1
  187. package/src/modules/customers/data/validators.ts +1 -0
  188. package/src/modules/customers/i18n/de.json +1 -0
  189. package/src/modules/customers/i18n/en.json +1 -0
  190. package/src/modules/customers/i18n/es.json +1 -0
  191. package/src/modules/customers/i18n/pl.json +1 -0
  192. package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
  193. package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
  194. package/src/modules/directory/api/tenants/route.ts +9 -1
  195. package/src/modules/directory/commands/tenants.ts +6 -0
  196. package/src/modules/entities/api/entity-settings.ts +66 -22
  197. package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
  198. package/src/modules/entities/backend/config/settings/page.tsx +12 -0
  199. package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
  200. package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
  201. package/src/modules/entities/i18n/de.json +9 -0
  202. package/src/modules/entities/i18n/en.json +9 -0
  203. package/src/modules/entities/i18n/es.json +9 -0
  204. package/src/modules/entities/i18n/pl.json +9 -0
  205. package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
  206. package/src/modules/payment_gateways/api/sessions/route.ts +1 -3
  207. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
  208. package/src/modules/payment_gateways/data/entities.ts +5 -0
  209. package/src/modules/payment_gateways/i18n/de.json +5 -1
  210. package/src/modules/payment_gateways/i18n/en.json +5 -1
  211. package/src/modules/payment_gateways/i18n/es.json +5 -1
  212. package/src/modules/payment_gateways/i18n/pl.json +5 -1
  213. package/src/modules/payment_gateways/lib/gateway-service.ts +17 -8
  214. package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
  215. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
  216. package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
  217. package/src/modules/payment_gateways/setup.ts +62 -0
  218. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
  219. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
  220. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
  221. package/src/modules/portal/i18n/de.json +18 -0
  222. package/src/modules/portal/i18n/en.json +18 -0
  223. package/src/modules/portal/i18n/es.json +18 -0
  224. package/src/modules/portal/i18n/pl.json +18 -0
  225. package/src/modules/portal/lib/navigation.ts +3 -0
  226. package/src/modules/query_index/lib/engine.ts +52 -0
  227. package/src/modules/query_index/lib/search-tokens.ts +16 -4
  228. package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
@@ -91,6 +91,20 @@ export const upgradeActions: UpgradeActionDefinition[] = [
91
91
  }
92
92
  },
93
93
  },
94
+ {
95
+ id: 'payment_gateways.register-session-initialization-prune',
96
+ version: '0.6.6',
97
+ messageKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.message',
98
+ ctaKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.cta',
99
+ successKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.success',
100
+ loadingKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.loading',
101
+ run: async ({ container, tenantId, organizationId }) => {
102
+ const { registerSessionInitializationPruneSchedule } = await import(
103
+ '@open-mercato/core/modules/payment_gateways/setup'
104
+ )
105
+ await registerSessionInitializationPruneSchedule(container, { tenantId, organizationId })
106
+ },
107
+ },
94
108
  ]
95
109
 
96
110
  export function actionsUpToVersion(version: string): UpgradeActionDefinition[] {
@@ -11,7 +11,7 @@ import { CustomerRbacService } from '@open-mercato/core/modules/customer_account
11
11
  import { adminUpdateUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
12
12
  import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
13
13
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
14
- import { isOwnedCompanyEntity } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'
14
+ import { isOwnedCompanyEntity, isOwnedPersonEntity } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'
15
15
  import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
16
16
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
17
17
 
@@ -168,6 +168,21 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
168
168
  }
169
169
  }
170
170
 
171
+ // Same guard for the person FK. Without it the invite-side check is trivially
172
+ // bypassable: create the user normally, then PUT an unowned personEntityId.
173
+ // It persists (autoLinkCrm short-circuits on any non-null value) and leaks
174
+ // account status into the other org's people list via the account-status
175
+ // enricher. A null value (unlink) needs no ownership check.
176
+ if (parsed.data.personEntityId) {
177
+ const owned = await isOwnedPersonEntity(em, parsed.data.personEntityId, {
178
+ tenantId: auth.tenantId,
179
+ organizationId: auth.orgId,
180
+ })
181
+ if (!owned) {
182
+ return NextResponse.json({ ok: false, error: 'Person not found' }, { status: 400 })
183
+ }
184
+ }
185
+
171
186
  // Always bump updated_at so the optimistic-lock version advances on every save.
172
187
  // `nativeUpdate` bypasses MikroORM's `onUpdate` hook, so set it explicitly — without
173
188
  // this the version never changes and concurrent edits cannot be detected (#2055).
@@ -7,6 +7,7 @@ import { RbacService } from '@open-mercato/core/modules/auth/services/rbacServic
7
7
  import { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'
8
8
  import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
9
9
  import { inviteUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
10
+ import { isOwnedCompanyEntity, isOwnedPersonEntity, resolveOwnedCompanyForPerson } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'
10
11
  import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'
11
12
  import {
12
13
  checkAuthRateLimit,
@@ -14,9 +15,15 @@ import {
14
15
  customerInviteIpRateLimitConfig,
15
16
  } from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'
16
17
  import { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'
18
+ import { sendCustomerInvitationEmail } from '@open-mercato/core/modules/customer_accounts/lib/invitationEmail'
17
19
 
18
20
  export const metadata = {}
19
21
 
22
+ function resolveInvitedByUserId(auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>): string | null {
23
+ if (auth.isApiKey) return auth.userId ?? null
24
+ return auth.sub
25
+ }
26
+
20
27
  export async function POST(req: Request) {
21
28
  const auth = await getAuthFromRequest(req)
22
29
  if (!auth) {
@@ -51,19 +58,80 @@ export async function POST(req: Request) {
51
58
  return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })
52
59
  }
53
60
 
61
+ // Reject a customerEntityId the caller does not own. customerEntityId is the
62
+ // CRM company FK; without this check a non-company (e.g. a person) entity id
63
+ // or a company from another org poisons the invitation and every later user
64
+ // edit fails with "Company not found" (#4362, #2693).
65
+ if (parsed.data.customerEntityId) {
66
+ const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager
67
+ const owned = await isOwnedCompanyEntity(em, parsed.data.customerEntityId, {
68
+ tenantId: auth.tenantId,
69
+ organizationId: auth.orgId,
70
+ })
71
+ if (!owned) {
72
+ return NextResponse.json({ ok: false, error: 'Company not found' }, { status: 400 })
73
+ }
74
+ }
75
+
76
+ // Same guard for the person FK: it is copied onto the customer user on accept
77
+ // and makes `autoLinkCrm` short-circuit, so an unowned id would permanently
78
+ // cross-link the portal user to another org's CRM person.
79
+ let resolvedCustomerEntityId = parsed.data.customerEntityId || null
80
+ if (parsed.data.personEntityId) {
81
+ const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager
82
+ const owned = await isOwnedPersonEntity(em, parsed.data.personEntityId, {
83
+ tenantId: auth.tenantId,
84
+ organizationId: auth.orgId,
85
+ })
86
+ if (!owned) {
87
+ return NextResponse.json({ ok: false, error: 'Person not found' }, { status: 400 })
88
+ }
89
+
90
+ // An invitation raised from a person card carries only the person FK, and the
91
+ // accepted user then short-circuits `autoLinkCrm`. Resolve the person's company
92
+ // here so the user lands with a company scope key: the portal Users page, portal
93
+ // invitations, and the company detail "Portal users" group all filter on it.
94
+ if (!resolvedCustomerEntityId) {
95
+ resolvedCustomerEntityId = await resolveOwnedCompanyForPerson(em, parsed.data.personEntityId, {
96
+ tenantId: auth.tenantId,
97
+ organizationId: auth.orgId,
98
+ })
99
+ }
100
+ }
101
+
54
102
  const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService
55
103
 
56
- const { invitation } = await customerInvitationService.createInvitation(
104
+ const { invitation, rawToken, rollbackState } = await customerInvitationService.createInvitation(
57
105
  parsed.data.email,
58
106
  { tenantId: auth.tenantId!, organizationId: auth.orgId! },
59
107
  {
60
- customerEntityId: parsed.data.customerEntityId || null,
108
+ customerEntityId: resolvedCustomerEntityId,
109
+ personEntityId: parsed.data.personEntityId || null,
61
110
  roleIds: parsed.data.roleIds,
62
- invitedByUserId: auth.sub,
111
+ invitedByUserId: resolveInvitedByUserId(auth),
63
112
  displayName: parsed.data.displayName || null,
64
113
  },
65
114
  )
66
115
 
116
+ try {
117
+ await sendCustomerInvitationEmail({
118
+ container,
119
+ organizationId: auth.orgId!,
120
+ email: invitation.email,
121
+ rawToken,
122
+ })
123
+ } catch (error) {
124
+ console.error('[customer_accounts.admin.users-invite] invitation email failed', error)
125
+ try {
126
+ await customerInvitationService.rollbackInvitation(invitation, rollbackState)
127
+ } catch (rollbackError) {
128
+ console.error('[customer_accounts.admin.users-invite] invitation rollback failed', rollbackError)
129
+ }
130
+ return NextResponse.json({ ok: false, error: 'Invitation email could not be sent' }, { status: 502 })
131
+ }
132
+
133
+ // Emit only after the email is sent, so a subscriber observing "invited" can
134
+ // assume the recipient was actually notified (no event fires on the 502 path).
67
135
  void emitCustomerAccountsEvent('customer_accounts.user.invited', {
68
136
  invitationId: invitation.id,
69
137
  email: invitation.email,
@@ -78,6 +146,10 @@ export async function POST(req: Request) {
78
146
  invitation: {
79
147
  id: invitation.id,
80
148
  email: invitation.email,
149
+ // Echo the stored CRM links so the caller can see which company the person
150
+ // invite resolved to. The token stays unexposed.
151
+ customerEntityId: invitation.customerEntityId || null,
152
+ personEntityId: invitation.personEntityId || null,
81
153
  expiresAt: invitation.expiresAt,
82
154
  },
83
155
  }, { status: 201 })
@@ -88,6 +160,8 @@ const successSchema = z.object({
88
160
  invitation: z.object({
89
161
  id: z.string().uuid(),
90
162
  email: z.string(),
163
+ customerEntityId: z.string().uuid().nullable(),
164
+ personEntityId: z.string().uuid().nullable(),
91
165
  expiresAt: z.string().datetime(),
92
166
  }),
93
167
  })
@@ -104,6 +178,7 @@ const methodDoc: OpenApiMethodDoc = {
104
178
  { status: 401, description: 'Not authenticated', schema: errorSchema },
105
179
  { status: 403, description: 'Insufficient permissions', schema: errorSchema },
106
180
  { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },
181
+ { status: 502, description: 'Invitation email could not be sent', schema: errorSchema },
107
182
  ],
108
183
  }
109
184
 
@@ -16,6 +16,7 @@ import {
16
16
  customerInviteIpRateLimitConfig,
17
17
  } from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'
18
18
  import { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'
19
+ import { sendCustomerInvitationEmail } from '@open-mercato/core/modules/customer_accounts/lib/invitationEmail'
19
20
 
20
21
  export const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }
21
22
 
@@ -90,7 +91,7 @@ export async function POST(req: Request) {
90
91
 
91
92
  const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService
92
93
 
93
- const { invitation } = await customerInvitationService.createInvitation(
94
+ const { invitation, rawToken, rollbackState } = await customerInvitationService.createInvitation(
94
95
  parsed.data.email,
95
96
  { tenantId: auth.tenantId, organizationId: auth.orgId },
96
97
  {
@@ -101,6 +102,25 @@ export async function POST(req: Request) {
101
102
  },
102
103
  )
103
104
 
105
+ try {
106
+ await sendCustomerInvitationEmail({
107
+ container,
108
+ organizationId: auth.orgId,
109
+ email: invitation.email,
110
+ rawToken,
111
+ })
112
+ } catch (error) {
113
+ console.error('[customer_accounts.portal.users-invite] invitation email failed', error)
114
+ try {
115
+ await customerInvitationService.rollbackInvitation(invitation, rollbackState)
116
+ } catch (rollbackError) {
117
+ console.error('[customer_accounts.portal.users-invite] invitation rollback failed', rollbackError)
118
+ }
119
+ return NextResponse.json({ ok: false, error: 'Invitation email could not be sent' }, { status: 502 })
120
+ }
121
+
122
+ // Emit only after the email is sent, so a subscriber observing "invited" can
123
+ // assume the recipient was actually notified (no event fires on the 502 path).
104
124
  void emitCustomerAccountsEvent('customer_accounts.user.invited', {
105
125
  invitationId: invitation.id,
106
126
  email: invitation.email,
@@ -141,6 +161,7 @@ const methodDoc: OpenApiMethodDoc = {
141
161
  { status: 401, description: 'Not authenticated', schema: errorSchema },
142
162
  { status: 403, description: 'Insufficient permissions or non-assignable role', schema: errorSchema },
143
163
  { status: 429, description: 'Too many invitation requests', schema: rateLimitErrorSchema },
164
+ { status: 502, description: 'Invitation email could not be sent', schema: errorSchema },
144
165
  ],
145
166
  }
146
167
 
@@ -291,6 +291,9 @@ export class CustomerUserInvitation {
291
291
  @Property({ name: 'customer_entity_id', type: 'uuid', nullable: true })
292
292
  customerEntityId?: string | null
293
293
 
294
+ @Property({ name: 'person_entity_id', type: 'uuid', nullable: true })
295
+ personEntityId?: string | null
296
+
294
297
  @Property({ name: 'role_ids_json', type: 'json', nullable: true })
295
298
  roleIdsJson?: string[] | null
296
299
 
@@ -81,6 +81,7 @@ export const updateRoleAclSchema = z.object({
81
81
  export const inviteUserSchema = z.object({
82
82
  email: emailField,
83
83
  customerEntityId: z.string().uuid().optional(),
84
+ personEntityId: z.string().uuid().optional(),
84
85
  roleIds: z.array(z.string().uuid()).min(1),
85
86
  displayName: displayNameField.optional(),
86
87
  })
@@ -0,0 +1,45 @@
1
+ import * as React from 'react'
2
+ import { Html, Head, Preview, Body, Container, Text, Section, Link } from '@react-email/components'
3
+
4
+ export type CustomerInvitationEmailCopy = {
5
+ preview: string
6
+ title: string
7
+ body: string
8
+ cta: string
9
+ hint: string
10
+ }
11
+
12
+ export type CustomerInvitationEmailProps = {
13
+ inviteUrl: string
14
+ copy: CustomerInvitationEmailCopy
15
+ }
16
+
17
+ export function CustomerInvitationEmail({ inviteUrl, copy }: CustomerInvitationEmailProps) {
18
+ return (
19
+ <Html>
20
+ <Head />
21
+ <Preview>{copy.preview}</Preview>
22
+ <Body style={body}>
23
+ <Container style={container}>
24
+ <Section>
25
+ <Text style={title}>{copy.title}</Text>
26
+ <Text style={paragraph}>{copy.body}</Text>
27
+ <Text>
28
+ <Link href={inviteUrl} style={button}>{copy.cta}</Link>
29
+ </Text>
30
+ <Text style={hint}>{copy.hint}</Text>
31
+ </Section>
32
+ </Container>
33
+ </Body>
34
+ </Html>
35
+ )
36
+ }
37
+
38
+ const body: React.CSSProperties = { backgroundColor: '#f9fafb', margin: 0, padding: '24px 0' }
39
+ const container: React.CSSProperties = { backgroundColor: '#ffffff', borderRadius: 12, padding: 24, margin: '0 auto', maxWidth: 520, boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }
40
+ const title: React.CSSProperties = { fontSize: 20, fontWeight: 600, color: '#111827', margin: '0 0 12px' }
41
+ const paragraph: React.CSSProperties = { fontSize: 14, color: '#4b5563', lineHeight: '20px', margin: '0 0 16px' }
42
+ const button: React.CSSProperties = { display: 'inline-block', backgroundColor: '#111827', color: '#ffffff', padding: '10px 14px', borderRadius: 8, textDecoration: 'none', fontSize: 14 }
43
+ const hint: React.CSSProperties = { fontSize: 12, color: '#6b7280', marginTop: 16 }
44
+
45
+ export default CustomerInvitationEmail
@@ -274,6 +274,12 @@
274
274
  "customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
275
275
  "customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
276
276
  "customer_accounts.domainMapping.verifyNow": "Check Now",
277
+ "customer_accounts.invitation.email.body": "Use this secure invitation link to create your password and access the customer portal.",
278
+ "customer_accounts.invitation.email.cta": "Accept invitation",
279
+ "customer_accounts.invitation.email.hint": "This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.",
280
+ "customer_accounts.invitation.email.preview": "Accept your invitation to finish setting up portal access.",
281
+ "customer_accounts.invitation.email.subject": "You have been invited to the customer portal",
282
+ "customer_accounts.invitation.email.title": "You have been invited",
277
283
  "customer_accounts.nav.group": "Customers",
278
284
  "customer_accounts.nav.role_create": "Rolle erstellen",
279
285
  "customer_accounts.nav.role_detail": "Rollendetails",
@@ -274,6 +274,12 @@
274
274
  "customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
275
275
  "customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
276
276
  "customer_accounts.domainMapping.verifyNow": "Check Now",
277
+ "customer_accounts.invitation.email.body": "Use this secure invitation link to create your password and access the customer portal.",
278
+ "customer_accounts.invitation.email.cta": "Accept invitation",
279
+ "customer_accounts.invitation.email.hint": "This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.",
280
+ "customer_accounts.invitation.email.preview": "Accept your invitation to finish setting up portal access.",
281
+ "customer_accounts.invitation.email.subject": "You have been invited to the customer portal",
282
+ "customer_accounts.invitation.email.title": "You have been invited",
277
283
  "customer_accounts.nav.group": "Customers",
278
284
  "customer_accounts.nav.role_create": "Create Role",
279
285
  "customer_accounts.nav.role_detail": "Role Detail",
@@ -274,6 +274,12 @@
274
274
  "customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
275
275
  "customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
276
276
  "customer_accounts.domainMapping.verifyNow": "Check Now",
277
+ "customer_accounts.invitation.email.body": "Use this secure invitation link to create your password and access the customer portal.",
278
+ "customer_accounts.invitation.email.cta": "Accept invitation",
279
+ "customer_accounts.invitation.email.hint": "This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.",
280
+ "customer_accounts.invitation.email.preview": "Accept your invitation to finish setting up portal access.",
281
+ "customer_accounts.invitation.email.subject": "You have been invited to the customer portal",
282
+ "customer_accounts.invitation.email.title": "You have been invited",
277
283
  "customer_accounts.nav.group": "Customers",
278
284
  "customer_accounts.nav.role_create": "Create Role",
279
285
  "customer_accounts.nav.role_detail": "Role Detail",
@@ -274,6 +274,12 @@
274
274
  "customer_accounts.domainMapping.tls.diagnostics.title": "Problem z certyfikatem SSL",
275
275
  "customer_accounts.domainMapping.tls.retryButton": "Ponów SSL",
276
276
  "customer_accounts.domainMapping.verifyNow": "Sprawdź teraz",
277
+ "customer_accounts.invitation.email.body": "Użyj bezpiecznego linku zaproszenia, aby utworzyć hasło i uzyskać dostęp do portalu klienta.",
278
+ "customer_accounts.invitation.email.cta": "Przyjmij zaproszenie",
279
+ "customer_accounts.invitation.email.hint": "Ten link zaproszenia wygasa po 72 godzinach. Jeśli nie spodziewasz się tego zaproszenia, możesz zignorować tę wiadomość.",
280
+ "customer_accounts.invitation.email.preview": "Przyjmij zaproszenie, aby dokończyć konfigurację dostępu do portalu.",
281
+ "customer_accounts.invitation.email.subject": "Masz zaproszenie do portalu klienta",
282
+ "customer_accounts.invitation.email.title": "Masz zaproszenie",
277
283
  "customer_accounts.nav.group": "Klienci",
278
284
  "customer_accounts.nav.role_create": "Utwórz rolę",
279
285
  "customer_accounts.nav.role_detail": "Szczegóły roli",
@@ -20,16 +20,82 @@ export async function isOwnedCompanyEntity(
20
20
  em: EntityManager,
21
21
  customerEntityId: string,
22
22
  scope: CustomerEntityScope,
23
+ ): Promise<boolean> {
24
+ return isOwnedEntityOfKind(em, customerEntityId, 'company', scope)
25
+ }
26
+
27
+ /**
28
+ * Confirms that a caller-supplied `personEntityId` (the CRM person FK linked
29
+ * onto a customer user) exists, is a person, is not soft-deleted, and belongs
30
+ * to the caller's tenant and organization.
31
+ *
32
+ * This is the symmetric guard to {@link isOwnedCompanyEntity}: the person FK is
33
+ * copied onto the customer user on accept and short-circuits CRM auto-linking,
34
+ * so an unvalidated id would permanently cross-link a portal user to another
35
+ * org's person (the #4362/#2693 class of bug, on the person side).
36
+ */
37
+ export async function isOwnedPersonEntity(
38
+ em: EntityManager,
39
+ personEntityId: string,
40
+ scope: CustomerEntityScope,
41
+ ): Promise<boolean> {
42
+ return isOwnedEntityOfKind(em, personEntityId, 'person', scope)
43
+ }
44
+
45
+ /**
46
+ * Resolves the CRM company a person belongs to, but only when both the profile
47
+ * lookup and the company itself stay inside the given tenant/organization scope.
48
+ * Returns null when the person has no company or that company is out of scope.
49
+ *
50
+ * Both the invite route and the CRM auto-link subscriber need this answer, and
51
+ * both write the result into `customerEntityId` — the portal company scope key —
52
+ * so they must agree on what counts as an in-scope company.
53
+ */
54
+ export async function resolveOwnedCompanyForPerson(
55
+ em: EntityManager,
56
+ personEntityId: string,
57
+ scope: CustomerEntityScope,
58
+ ): Promise<string | null> {
59
+ const { CustomerPersonProfile } = await import('@open-mercato/core/modules/customers/data/entities')
60
+ const profile = await em.findOne(CustomerPersonProfile as any, {
61
+ entity: personEntityId,
62
+ tenantId: scope.tenantId,
63
+ organizationId: scope.organizationId,
64
+ } as any)
65
+ const candidate = readCompanyEntityId(profile)
66
+ if (!candidate) return null
67
+ return (await isOwnedCompanyEntity(em, candidate, scope)) ? candidate : null
68
+ }
69
+
70
+ /**
71
+ * `CustomerPersonProfile.company` is a relation on `company_entity_id`, not a
72
+ * scalar — MikroORM hands back either the raw id or an entity reference, never a
73
+ * `companyEntityId` property. Reading the wrong one silently yields undefined and
74
+ * turns every company recovery into a clear.
75
+ */
76
+ function readCompanyEntityId(profile: unknown): string | null {
77
+ const company = (profile as { company?: unknown } | null | undefined)?.company
78
+ if (!company) return null
79
+ if (typeof company === 'string') return company
80
+ const id = (company as { id?: unknown }).id
81
+ return typeof id === 'string' ? id : null
82
+ }
83
+
84
+ async function isOwnedEntityOfKind(
85
+ em: EntityManager,
86
+ entityId: string,
87
+ kind: 'company' | 'person',
88
+ scope: CustomerEntityScope,
23
89
  ): Promise<boolean> {
24
90
  const { CustomerEntity } = await import('@open-mercato/core/modules/customers/data/entities')
25
91
  const entity = await findOneWithDecryption(
26
92
  em,
27
93
  CustomerEntity,
28
94
  {
29
- id: customerEntityId,
95
+ id: entityId,
30
96
  tenantId: scope.tenantId,
31
97
  organizationId: scope.organizationId,
32
- kind: 'company',
98
+ kind,
33
99
  deletedAt: null,
34
100
  } as any,
35
101
  undefined,
@@ -5,6 +5,8 @@
5
5
 
6
6
  import type { AppContainer } from '@open-mercato/shared/lib/di/container'
7
7
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
8
+ import { Organization } from '@open-mercato/core/modules/directory/data/entities'
9
+ import type { EntityManager } from '@mikro-orm/postgresql'
8
10
 
9
11
  type DomainMappingService = {
10
12
  resolveActiveByOrg(orgId: string): Promise<{ hostname: string } | null>
@@ -14,6 +16,10 @@ type OrgService = {
14
16
  findById(orgId: string): Promise<{ id: string; slug: string | null } | null>
15
17
  }
16
18
 
19
+ type EntityManagerResolver = {
20
+ resolve(name: 'em'): EntityManager
21
+ }
22
+
17
23
  function platformBaseUrl(): string {
18
24
  const fromEnv = process.env.PLATFORM_PORTAL_BASE_URL?.trim()
19
25
  if (fromEnv && fromEnv.length > 0) return fromEnv.replace(/\/+$/, '')
@@ -32,6 +38,26 @@ function resolveContainer(container?: AppContainer): Promise<AppContainer> {
32
38
  return createRequestContainer()
33
39
  }
34
40
 
41
+ async function resolveOrgSlug(container: AppContainer, orgId: string): Promise<string | null> {
42
+ try {
43
+ const orgService = container.resolve('orgService') as OrgService | undefined
44
+ if (orgService && typeof orgService.findById === 'function') {
45
+ const org = await orgService.findById(orgId)
46
+ return org?.slug ?? null
47
+ }
48
+ } catch {
49
+ // orgService is optional in standalone/runtime contexts — fall through to EntityManager.
50
+ }
51
+
52
+ try {
53
+ const em = (container as AppContainer & EntityManagerResolver).resolve('em')
54
+ const org = await em.findOne(Organization, { id: orgId, deletedAt: null })
55
+ return org?.slug ?? null
56
+ } catch {
57
+ return null
58
+ }
59
+ }
60
+
35
61
  export async function urlForCustomerOrg(
36
62
  orgId: string,
37
63
  path: string,
@@ -55,17 +81,7 @@ export async function urlForCustomerOrg(
55
81
  return `https://${active.hostname}${safePath}`
56
82
  }
57
83
 
58
- let orgSlug: string | null = null
59
- try {
60
- const orgService = container.resolve('orgService') as OrgService | undefined
61
- if (orgService && typeof orgService.findById === 'function') {
62
- const org = await orgService.findById(orgId)
63
- orgSlug = org?.slug ?? null
64
- }
65
- } catch {
66
- orgSlug = null
67
- }
68
-
84
+ const orgSlug = await resolveOrgSlug(container, orgId)
69
85
  const base = platformBaseUrl()
70
86
  if (orgSlug) return `${base}/${orgSlug}/portal${safePath}`
71
87
  return `${base}${safePath}`
@@ -0,0 +1,42 @@
1
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
2
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
3
+ import { sendEmail } from '@open-mercato/shared/lib/email/send'
4
+ import CustomerInvitationEmail from '@open-mercato/core/modules/customer_accounts/emails/CustomerInvitationEmail'
5
+ import { urlForCustomerOrg } from '@open-mercato/core/modules/customer_accounts/lib/customerUrl'
6
+
7
+ export type CustomerInvitationEmailInput = {
8
+ container: AppContainer
9
+ organizationId: string
10
+ email: string
11
+ rawToken: string
12
+ }
13
+
14
+ export async function sendCustomerInvitationEmail(input: CustomerInvitationEmailInput): Promise<void> {
15
+ const { translate } = await resolveTranslations()
16
+ const inviteUrl = await urlForCustomerOrg(
17
+ input.organizationId,
18
+ `/invite?token=${encodeURIComponent(input.rawToken)}`,
19
+ { container: input.container },
20
+ )
21
+
22
+ const subject = translate('customer_accounts.invitation.email.subject', 'You have been invited to the customer portal')
23
+ const copy = {
24
+ preview: translate('customer_accounts.invitation.email.preview', 'Accept your invitation to finish setting up portal access.'),
25
+ title: translate('customer_accounts.invitation.email.title', 'You have been invited'),
26
+ body: translate(
27
+ 'customer_accounts.invitation.email.body',
28
+ 'Use this secure invitation link to create your password and access the customer portal.',
29
+ ),
30
+ cta: translate('customer_accounts.invitation.email.cta', 'Accept invitation'),
31
+ hint: translate(
32
+ 'customer_accounts.invitation.email.hint',
33
+ 'This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.',
34
+ ),
35
+ }
36
+
37
+ await sendEmail({
38
+ to: input.email,
39
+ subject,
40
+ react: CustomerInvitationEmail({ inviteUrl, copy }),
41
+ })
42
+ }
@@ -923,6 +923,22 @@
923
923
  "enumItems": [],
924
924
  "mappedType": "uuid"
925
925
  },
926
+ "person_entity_id": {
927
+ "name": "person_entity_id",
928
+ "type": "uuid",
929
+ "unsigned": false,
930
+ "autoincrement": false,
931
+ "primary": false,
932
+ "nullable": true,
933
+ "unique": false,
934
+ "length": null,
935
+ "precision": null,
936
+ "scale": null,
937
+ "default": null,
938
+ "comment": null,
939
+ "enumItems": [],
940
+ "mappedType": "uuid"
941
+ },
926
942
  "role_ids_json": {
927
943
  "name": "role_ids_json",
928
944
  "type": "jsonb",
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260723120000_customer_accounts extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "customer_user_invitations" add column "person_entity_id" uuid null;`);
7
+ }
8
+
9
+ override async down(): Promise<void> {
10
+ this.addSql(`alter table "customer_user_invitations" drop column "person_entity_id";`);
11
+ }
12
+
13
+ }