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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
  3. package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
  4. package/dist/generated/entities/customer_user_invitation/index.js +2 -0
  5. package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
  9. package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
  10. package/dist/modules/business_rules/api/rules/route.js +71 -4
  11. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  12. package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
  13. package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
  14. package/dist/modules/business_rules/components/ActionRow.js +106 -16
  15. package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
  16. package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
  17. package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
  18. package/dist/modules/business_rules/lib/action-executor.js +148 -0
  19. package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
  20. package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
  21. package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
  22. package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
  23. package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
  24. package/dist/modules/business_rules/lib/rule-engine.js +5 -1
  25. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  26. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
  27. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
  28. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
  29. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
  30. package/dist/modules/configs/lib/upgrade-actions.js +12 -0
  31. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  32. package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
  33. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  34. package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
  35. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
  37. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  38. package/dist/modules/customer_accounts/data/entities.js +3 -0
  39. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  40. package/dist/modules/customer_accounts/data/validators.js +1 -0
  41. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  42. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
  43. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
  44. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
  45. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
  46. package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
  47. package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
  48. package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
  49. package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
  50. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
  51. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
  52. package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
  53. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  54. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
  55. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
  56. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  58. package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
  59. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  60. package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
  61. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  62. package/dist/modules/customers/api/deals/[id]/route.js +7 -3
  63. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  64. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
  65. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  66. package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
  67. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  68. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
  69. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  70. package/dist/modules/customers/commands/deals.js +48 -10
  71. package/dist/modules/customers/commands/deals.js.map +2 -2
  72. package/dist/modules/customers/commands/people.js +2 -0
  73. package/dist/modules/customers/commands/people.js.map +2 -2
  74. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
  75. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  76. package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
  77. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  78. package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
  79. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  80. package/dist/modules/customers/data/entities.js +8 -0
  81. package/dist/modules/customers/data/entities.js.map +2 -2
  82. package/dist/modules/customers/data/validators.js +2 -1
  83. package/dist/modules/customers/data/validators.js.map +2 -2
  84. package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
  85. package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
  86. package/dist/modules/entities/api/entity-settings.js +59 -9
  87. package/dist/modules/entities/api/entity-settings.js.map +2 -2
  88. package/dist/modules/entities/backend/config/settings/page.js +10 -0
  89. package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
  90. package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
  91. package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
  92. package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
  93. package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
  94. package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
  95. package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
  96. package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
  97. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  98. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
  99. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  100. package/dist/modules/payment_gateways/data/entities.js +4 -0
  101. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  102. package/dist/modules/payment_gateways/lib/gateway-service.js +12 -2
  103. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  104. package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
  105. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  106. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
  107. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
  108. package/dist/modules/payment_gateways/setup.js +47 -0
  109. package/dist/modules/payment_gateways/setup.js.map +2 -2
  110. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
  111. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
  112. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
  113. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
  114. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
  115. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
  116. package/dist/modules/portal/lib/navigation.js +7 -0
  117. package/dist/modules/portal/lib/navigation.js.map +7 -0
  118. package/dist/modules/query_index/lib/engine.js +43 -0
  119. package/dist/modules/query_index/lib/engine.js.map +2 -2
  120. package/dist/modules/query_index/lib/search-tokens.js +15 -5
  121. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  122. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
  123. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
  124. package/generated/entities/customer_deal_person_link/index.ts +1 -0
  125. package/generated/entities/customer_user_invitation/index.ts +1 -0
  126. package/generated/entity-fields-registry.ts +2 -0
  127. package/package.json +7 -7
  128. package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
  129. package/src/modules/business_rules/api/rules/route.ts +92 -4
  130. package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
  131. package/src/modules/business_rules/components/ActionRow.tsx +156 -18
  132. package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
  133. package/src/modules/business_rules/i18n/de.json +12 -0
  134. package/src/modules/business_rules/i18n/en.json +12 -0
  135. package/src/modules/business_rules/i18n/es.json +12 -0
  136. package/src/modules/business_rules/i18n/pl.json +12 -0
  137. package/src/modules/business_rules/lib/action-executor.ts +198 -0
  138. package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
  139. package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
  140. package/src/modules/business_rules/lib/rule-engine.ts +4 -0
  141. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
  142. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
  143. package/src/modules/configs/lib/upgrade-actions.ts +14 -0
  144. package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
  145. package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
  146. package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
  147. package/src/modules/customer_accounts/data/entities.ts +3 -0
  148. package/src/modules/customer_accounts/data/validators.ts +1 -0
  149. package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
  150. package/src/modules/customer_accounts/i18n/de.json +6 -0
  151. package/src/modules/customer_accounts/i18n/en.json +6 -0
  152. package/src/modules/customer_accounts/i18n/es.json +6 -0
  153. package/src/modules/customer_accounts/i18n/pl.json +6 -0
  154. package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
  155. package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
  156. package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
  157. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
  158. package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
  159. package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
  160. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
  161. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
  162. package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
  163. package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
  164. package/src/modules/customers/api/deals/[id]/route.ts +10 -2
  165. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
  166. package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
  167. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
  168. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
  169. package/src/modules/customers/commands/deals.ts +50 -9
  170. package/src/modules/customers/commands/people.ts +3 -0
  171. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
  172. package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
  173. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
  174. package/src/modules/customers/data/entities.ts +9 -1
  175. package/src/modules/customers/data/validators.ts +1 -0
  176. package/src/modules/customers/i18n/de.json +1 -0
  177. package/src/modules/customers/i18n/en.json +1 -0
  178. package/src/modules/customers/i18n/es.json +1 -0
  179. package/src/modules/customers/i18n/pl.json +1 -0
  180. package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
  181. package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
  182. package/src/modules/entities/api/entity-settings.ts +66 -22
  183. package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
  184. package/src/modules/entities/backend/config/settings/page.tsx +12 -0
  185. package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
  186. package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
  187. package/src/modules/entities/i18n/de.json +9 -0
  188. package/src/modules/entities/i18n/en.json +9 -0
  189. package/src/modules/entities/i18n/es.json +9 -0
  190. package/src/modules/entities/i18n/pl.json +9 -0
  191. package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
  192. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
  193. package/src/modules/payment_gateways/data/entities.ts +5 -0
  194. package/src/modules/payment_gateways/i18n/de.json +5 -1
  195. package/src/modules/payment_gateways/i18n/en.json +5 -1
  196. package/src/modules/payment_gateways/i18n/es.json +5 -1
  197. package/src/modules/payment_gateways/i18n/pl.json +5 -1
  198. package/src/modules/payment_gateways/lib/gateway-service.ts +12 -2
  199. package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
  200. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
  201. package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
  202. package/src/modules/payment_gateways/setup.ts +62 -0
  203. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
  204. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
  205. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
  206. package/src/modules/portal/i18n/de.json +18 -0
  207. package/src/modules/portal/i18n/en.json +18 -0
  208. package/src/modules/portal/i18n/es.json +18 -0
  209. package/src/modules/portal/i18n/pl.json +18 -0
  210. package/src/modules/portal/lib/navigation.ts +3 -0
  211. package/src/modules/query_index/lib/engine.ts +52 -0
  212. package/src/modules/query_index/lib/search-tokens.ts +16 -4
  213. package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
@@ -0,0 +1,63 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { createModuleQueue, type JobContext, type Queue, type QueuedJob, type WorkerMeta } from '@open-mercato/queue'
3
+ import { createLogger } from '@open-mercato/shared/lib/logger'
4
+ import {
5
+ PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,
6
+ PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE,
7
+ pruneCompletedPaymentSessionInitializations,
8
+ } from '../lib/session-idempotency'
9
+
10
+ type PrunePayload = {
11
+ tenantId?: string
12
+ organizationId?: string
13
+ }
14
+
15
+ type HandlerContext = JobContext & {
16
+ resolve: <T = unknown>(name: string) => T
17
+ }
18
+
19
+ const logger = createLogger('payment_gateways')
20
+ let continuationQueue: Queue<PrunePayload> | null = null
21
+
22
+ function getContinuationQueue(): Queue<PrunePayload> {
23
+ continuationQueue ??= createModuleQueue<PrunePayload>(
24
+ PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,
25
+ { concurrency: 1 },
26
+ )
27
+ return continuationQueue
28
+ }
29
+
30
+ export const metadata: WorkerMeta = {
31
+ queue: PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,
32
+ id: 'payment_gateways:session-initialization-prune',
33
+ concurrency: 1,
34
+ }
35
+
36
+ export default async function handle(
37
+ job: QueuedJob<PrunePayload>,
38
+ ctx: HandlerContext,
39
+ ): Promise<void> {
40
+ const tenantId = job.payload?.tenantId
41
+ const organizationId = job.payload?.organizationId
42
+ if (!tenantId || !organizationId) {
43
+ logger.warn('Skipping payment-session initialization prune without tenant scope')
44
+ return
45
+ }
46
+
47
+ const em = ctx.resolve<EntityManager>('em').fork()
48
+ const deleted = await pruneCompletedPaymentSessionInitializations(
49
+ em,
50
+ { tenantId, organizationId },
51
+ { batchSize: PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE },
52
+ )
53
+ if (deleted > 0) {
54
+ logger.info('Pruned completed payment-session initialization claims', {
55
+ deleted,
56
+ tenantId,
57
+ organizationId,
58
+ })
59
+ }
60
+ if (deleted >= PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE) {
61
+ await getContinuationQueue().enqueue({ tenantId, organizationId }, { delayMs: 1_000 })
62
+ }
63
+ }
@@ -0,0 +1,9 @@
1
+ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
2
+
3
+ export const metadata: PageMetadata = {
4
+ titleKey: 'portal.nav.invite',
5
+ title: 'Accept Invitation',
6
+ navHidden: true,
7
+ }
8
+
9
+ export default metadata
@@ -0,0 +1,164 @@
1
+ "use client"
2
+ import { useCallback, useMemo, useState, useEffect } from 'react'
3
+ import { useSearchParams } from 'next/navigation'
4
+ import Link from 'next/link'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
7
+ import { Input } from '@open-mercato/ui/primitives/input'
8
+ import { Label } from '@open-mercato/ui/primitives/label'
9
+ import { Button } from '@open-mercato/ui/primitives/button'
10
+ import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
11
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
12
+ import { SearchX } from 'lucide-react'
13
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
14
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
15
+ import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
16
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
17
+ import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
18
+ import { navigateWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'
19
+
20
+ type Props = { params: { orgSlug: string } }
21
+
22
+ export default function PortalInvitePage({ params }: Props) {
23
+ const t = useT()
24
+ const orgSlug = params.orgSlug
25
+ const { tenant } = usePortalContext()
26
+ const searchParams = useSearchParams()
27
+
28
+ const [displayName, setDisplayName] = useState('')
29
+ const [password, setPassword] = useState('')
30
+ const [confirmPassword, setConfirmPassword] = useState('')
31
+ const [error, setError] = useState<string | null>(null)
32
+ const [submitting, setSubmitting] = useState(false)
33
+ const [token, setToken] = useState<string | null>(null)
34
+
35
+ useEffect(() => {
36
+ const tokenParam = searchParams.get('token')
37
+ if (!tokenParam) {
38
+ setError(t('portal.invite.error.noToken', 'Invalid or missing invitation token.'))
39
+ } else {
40
+ setToken(tokenParam)
41
+ }
42
+ }, [searchParams, t])
43
+
44
+ const handleSubmit = useCallback(
45
+ async (event: React.FormEvent) => {
46
+ event.preventDefault()
47
+ setError(null)
48
+
49
+ if (!token) {
50
+ setError(t('portal.invite.error.noToken', 'Invalid or missing invitation token.'))
51
+ return
52
+ }
53
+
54
+ if (!displayName.trim()) {
55
+ setError(t('portal.invite.error.displayNameRequired', 'Display name is required.'))
56
+ return
57
+ }
58
+
59
+ if (password !== confirmPassword) {
60
+ setError(t('portal.invite.error.passwordMismatch', 'Passwords do not match.'))
61
+ return
62
+ }
63
+
64
+ if (password.length < 8) {
65
+ setError(t('portal.invite.error.passwordTooShort', 'Password must be at least 8 characters long.'))
66
+ return
67
+ }
68
+
69
+ setSubmitting(true)
70
+ try {
71
+ const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/invitations/accept', {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({ token, password, displayName: displayName.trim() }),
75
+ })
76
+
77
+ if (result.ok && result.result?.ok) {
78
+ navigateWithPageReload(`/${orgSlug}/portal/dashboard`)
79
+ return
80
+ }
81
+
82
+ if (result.status === 400) {
83
+ setError(t('portal.invite.error.invalidToken', 'Invalid or expired invitation.'))
84
+ } else {
85
+ setError(result.result?.error || t('portal.invite.error.generic', 'Invitation acceptance failed. Please try again.'))
86
+ }
87
+ } catch {
88
+ setError(t('portal.invite.error.generic', 'Invitation acceptance failed. Please try again.'))
89
+ } finally {
90
+ setSubmitting(false)
91
+ }
92
+ },
93
+ [token, password, confirmPassword, displayName, orgSlug, t],
94
+ )
95
+
96
+ const injectionContext = useMemo(
97
+ () => ({ orgSlug }),
98
+ [orgSlug],
99
+ )
100
+
101
+ if (tenant.loading) {
102
+ return <div className="flex items-center justify-center py-20"><Spinner /></div>
103
+ }
104
+
105
+ if (tenant.error) {
106
+ return (
107
+ <div className="mx-auto w-full max-w-md py-12">
108
+ <EmptyState
109
+ variant="subtle"
110
+ size="lg"
111
+ icon={<SearchX className="h-6 w-6" aria-hidden />}
112
+ title={t('portal.org.invalid', 'Organization not found.')}
113
+ />
114
+ </div>
115
+ )
116
+ }
117
+
118
+ return (
119
+ <div className="mx-auto w-full max-w-sm">
120
+ <div className="mb-8 text-center">
121
+ <h1 className="text-2xl font-bold tracking-tight">{t('portal.invite.title', 'Accept Invitation')}</h1>
122
+ <p className="mt-1.5 text-sm text-muted-foreground">{t('portal.invite.description', 'Create your portal account to accept this invitation.')}</p>
123
+ </div>
124
+
125
+ <InjectionSpot spotId={PortalInjectionSpots.pageBefore('invite')} context={injectionContext} />
126
+
127
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
128
+ {error ? (
129
+ <Alert variant="destructive">
130
+ <AlertDescription>{error}</AlertDescription>
131
+ </Alert>
132
+ ) : null}
133
+
134
+ <div className="flex flex-col gap-1.5">
135
+ <Label htmlFor="invite-display-name" className="text-overline font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.invite.displayName', 'Display Name')}</Label>
136
+ <Input id="invite-display-name" autoComplete="name" required value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting || !token} className="rounded-lg" />
137
+ </div>
138
+
139
+ <div className="flex flex-col gap-1.5">
140
+ <Label htmlFor="invite-password" className="text-overline font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.invite.password', 'Password')}</Label>
141
+ <PasswordInput id="invite-password" autoComplete="new-password" required placeholder={t('portal.invite.password.placeholder', '••••••••')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting || !token} className="rounded-lg" />
142
+ </div>
143
+
144
+ <div className="flex flex-col gap-1.5">
145
+ <Label htmlFor="invite-confirm-password" className="text-overline font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.invite.confirmPassword', 'Confirm Password')}</Label>
146
+ <PasswordInput id="invite-confirm-password" autoComplete="new-password" required placeholder={t('portal.invite.confirmPassword.placeholder', '••••••••')} value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} disabled={submitting || !token} className="rounded-lg" />
147
+ </div>
148
+
149
+ <Button type="submit" disabled={submitting || !token} className="mt-1 w-full rounded-lg">
150
+ {submitting ? t('portal.invite.submitting', 'Accepting invitation...') : t('portal.invite.submit', 'Accept Invitation')}
151
+ </Button>
152
+
153
+ <p className="text-center text-sm text-muted-foreground">
154
+ {t('portal.invite.backToLogin', 'Already have an account?')}{' '}
155
+ <Link href={`/${orgSlug}/portal/login`} className="font-medium text-foreground underline underline-offset-4 hover:opacity-80">
156
+ {t('portal.invite.loginLink', 'Sign in')}
157
+ </Link>
158
+ </p>
159
+ </form>
160
+
161
+ <InjectionSpot spotId={PortalInjectionSpots.pageAfter('invite')} context={injectionContext} />
162
+ </div>
163
+ )
164
+ }
@@ -22,6 +22,23 @@
22
22
  "portal.disabled.title": "Portal nicht verfügbar",
23
23
  "portal.footer.copyright": "© {year} Alle Rechte vorbehalten.",
24
24
  "portal.info.instructions": "Um auf das Portal Ihrer Organisation zuzugreifen, navigieren Sie zur eindeutigen URL Ihrer Organisation. Kontaktieren Sie Ihren Administrator, wenn Sie den Portal-Link nicht kennen.",
25
+ "portal.invite.backToLogin": "Already have an account?",
26
+ "portal.invite.confirmPassword": "Confirm Password",
27
+ "portal.invite.confirmPassword.placeholder": "••••••••",
28
+ "portal.invite.description": "Create your portal account to accept this invitation.",
29
+ "portal.invite.displayName": "Display Name",
30
+ "portal.invite.error.displayNameRequired": "Display name is required.",
31
+ "portal.invite.error.generic": "Invitation acceptance failed. Please try again.",
32
+ "portal.invite.error.invalidToken": "Invalid or expired invitation.",
33
+ "portal.invite.error.noToken": "Invalid or missing invitation token.",
34
+ "portal.invite.error.passwordMismatch": "Passwords do not match.",
35
+ "portal.invite.error.passwordTooShort": "Password must be at least 8 characters long.",
36
+ "portal.invite.loginLink": "Sign in",
37
+ "portal.invite.password": "Password",
38
+ "portal.invite.password.placeholder": "••••••••",
39
+ "portal.invite.submit": "Accept Invitation",
40
+ "portal.invite.submitting": "Accepting invitation…",
41
+ "portal.invite.title": "Accept Invitation",
25
42
  "portal.landing.cta.login": "Anmelden",
26
43
  "portal.landing.cta.signup": "Konto erstellen",
27
44
  "portal.landing.feature.account": "Account Management",
@@ -49,6 +66,7 @@
49
66
  "portal.nav.account": "Konto",
50
67
  "portal.nav.dashboard": "Dashboard",
51
68
  "portal.nav.home": "Portal",
69
+ "portal.nav.invite": "Accept Invitation",
52
70
  "portal.nav.login": "Anmelden",
53
71
  "portal.nav.logout": "Abmelden",
54
72
  "portal.nav.profile": "Profil",
@@ -22,6 +22,23 @@
22
22
  "portal.disabled.title": "Portal Not Available",
23
23
  "portal.footer.copyright": "© {year} All rights reserved.",
24
24
  "portal.info.instructions": "To access your organization's portal, navigate to your organization's unique URL. Contact your administrator if you don't know your portal link.",
25
+ "portal.invite.backToLogin": "Already have an account?",
26
+ "portal.invite.confirmPassword": "Confirm Password",
27
+ "portal.invite.confirmPassword.placeholder": "••••••••",
28
+ "portal.invite.description": "Create your portal account to accept this invitation.",
29
+ "portal.invite.displayName": "Display Name",
30
+ "portal.invite.error.displayNameRequired": "Display name is required.",
31
+ "portal.invite.error.generic": "Invitation acceptance failed. Please try again.",
32
+ "portal.invite.error.invalidToken": "Invalid or expired invitation.",
33
+ "portal.invite.error.noToken": "Invalid or missing invitation token.",
34
+ "portal.invite.error.passwordMismatch": "Passwords do not match.",
35
+ "portal.invite.error.passwordTooShort": "Password must be at least 8 characters long.",
36
+ "portal.invite.loginLink": "Sign in",
37
+ "portal.invite.password": "Password",
38
+ "portal.invite.password.placeholder": "••••••••",
39
+ "portal.invite.submit": "Accept Invitation",
40
+ "portal.invite.submitting": "Accepting invitation…",
41
+ "portal.invite.title": "Accept Invitation",
25
42
  "portal.landing.cta.login": "Log In",
26
43
  "portal.landing.cta.signup": "Create Account",
27
44
  "portal.landing.feature.account": "Account Management",
@@ -49,6 +66,7 @@
49
66
  "portal.nav.account": "Account",
50
67
  "portal.nav.dashboard": "Dashboard",
51
68
  "portal.nav.home": "Portal",
69
+ "portal.nav.invite": "Accept Invitation",
52
70
  "portal.nav.login": "Log In",
53
71
  "portal.nav.logout": "Log Out",
54
72
  "portal.nav.profile": "Profile",
@@ -22,6 +22,23 @@
22
22
  "portal.disabled.title": "Portal no disponible",
23
23
  "portal.footer.copyright": "© {year} Todos los derechos reservados.",
24
24
  "portal.info.instructions": "Para acceder al portal de su organización, navegue a la URL única de su organización. Contacte a su administrador si no conoce el enlace del portal.",
25
+ "portal.invite.backToLogin": "Already have an account?",
26
+ "portal.invite.confirmPassword": "Confirm Password",
27
+ "portal.invite.confirmPassword.placeholder": "••••••••",
28
+ "portal.invite.description": "Create your portal account to accept this invitation.",
29
+ "portal.invite.displayName": "Display Name",
30
+ "portal.invite.error.displayNameRequired": "Display name is required.",
31
+ "portal.invite.error.generic": "Invitation acceptance failed. Please try again.",
32
+ "portal.invite.error.invalidToken": "Invalid or expired invitation.",
33
+ "portal.invite.error.noToken": "Invalid or missing invitation token.",
34
+ "portal.invite.error.passwordMismatch": "Passwords do not match.",
35
+ "portal.invite.error.passwordTooShort": "Password must be at least 8 characters long.",
36
+ "portal.invite.loginLink": "Sign in",
37
+ "portal.invite.password": "Password",
38
+ "portal.invite.password.placeholder": "••••••••",
39
+ "portal.invite.submit": "Accept Invitation",
40
+ "portal.invite.submitting": "Accepting invitation…",
41
+ "portal.invite.title": "Accept Invitation",
25
42
  "portal.landing.cta.login": "Iniciar sesión",
26
43
  "portal.landing.cta.signup": "Crear cuenta",
27
44
  "portal.landing.feature.account": "Account Management",
@@ -49,6 +66,7 @@
49
66
  "portal.nav.account": "Cuenta",
50
67
  "portal.nav.dashboard": "Panel",
51
68
  "portal.nav.home": "Portal",
69
+ "portal.nav.invite": "Accept Invitation",
52
70
  "portal.nav.login": "Iniciar sesión",
53
71
  "portal.nav.logout": "Cerrar sesión",
54
72
  "portal.nav.profile": "Perfil",
@@ -22,6 +22,23 @@
22
22
  "portal.disabled.title": "Portal niedostępny",
23
23
  "portal.footer.copyright": "© {year} Wszelkie prawa zastrzeżone.",
24
24
  "portal.info.instructions": "Aby uzyskać dostęp do portalu organizacji, przejdź do unikalnego adresu URL swojej organizacji. Skontaktuj się z administratorem, jeśli nie znasz linku do portalu.",
25
+ "portal.invite.backToLogin": "Masz już konto?",
26
+ "portal.invite.confirmPassword": "Potwierdź hasło",
27
+ "portal.invite.confirmPassword.placeholder": "••••••••",
28
+ "portal.invite.description": "Utwórz konto w portalu, aby przyjąć to zaproszenie.",
29
+ "portal.invite.displayName": "Nazwa wyświetlana",
30
+ "portal.invite.error.displayNameRequired": "Nazwa wyświetlana jest wymagana.",
31
+ "portal.invite.error.generic": "Nie udało się przyjąć zaproszenia. Spróbuj ponownie.",
32
+ "portal.invite.error.invalidToken": "Nieprawidłowe lub wygasłe zaproszenie.",
33
+ "portal.invite.error.noToken": "Nieprawidłowy lub brakujący token zaproszenia.",
34
+ "portal.invite.error.passwordMismatch": "Hasła nie pasują do siebie.",
35
+ "portal.invite.error.passwordTooShort": "Hasło musi mieć co najmniej 8 znaków.",
36
+ "portal.invite.loginLink": "Zaloguj się",
37
+ "portal.invite.password": "Hasło",
38
+ "portal.invite.password.placeholder": "••••••••",
39
+ "portal.invite.submit": "Przyjmij zaproszenie",
40
+ "portal.invite.submitting": "Przyjmowanie zaproszenia…",
41
+ "portal.invite.title": "Przyjmij zaproszenie",
25
42
  "portal.landing.cta.login": "Zaloguj się",
26
43
  "portal.landing.cta.signup": "Utwórz konto",
27
44
  "portal.landing.feature.account": "Account Management",
@@ -49,6 +66,7 @@
49
66
  "portal.nav.account": "Konto",
50
67
  "portal.nav.dashboard": "Panel",
51
68
  "portal.nav.home": "Portal",
69
+ "portal.nav.invite": "Przyjmij zaproszenie",
52
70
  "portal.nav.login": "Zaloguj się",
53
71
  "portal.nav.logout": "Wyloguj się",
54
72
  "portal.nav.profile": "Profil",
@@ -0,0 +1,3 @@
1
+ export function navigateWithPageReload(path: string): void {
2
+ window.location.assign(path)
3
+ }
@@ -34,10 +34,46 @@ import { warnOnCiphertextLikeFallback } from '@open-mercato/shared/lib/query/cip
34
34
  import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from '@open-mercato/shared/lib/query/encrypted-sort'
35
35
  import { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'
36
36
  import { createLogger } from '@open-mercato/shared/lib/logger'
37
+ import { parseNumberWithDefault } from '@open-mercato/shared/lib/number'
37
38
 
38
39
  const logger = createLogger('query_index').child({ component: 'engine' })
39
40
 
40
41
  const DECRYPT_CONCURRENCY = 8
42
+ const AUTO_REINDEX_DEBOUNCE_DEFAULT_MS = 30_000
43
+ const AUTO_REINDEX_DEBOUNCE_MAX_SCOPES = 10_000
44
+ const AUTO_REINDEX_SCHEDULED_AT_KEY = Symbol.for('@open-mercato/query-index/auto-reindex-scheduled-at')
45
+
46
+ type GlobalWithAutoReindexSchedule = typeof globalThis & {
47
+ [AUTO_REINDEX_SCHEDULED_AT_KEY]?: Map<string, number>
48
+ }
49
+
50
+ function getAutoReindexScheduledAt(): Map<string, number> {
51
+ const globalScope = globalThis as GlobalWithAutoReindexSchedule
52
+ if (!globalScope[AUTO_REINDEX_SCHEDULED_AT_KEY]) {
53
+ globalScope[AUTO_REINDEX_SCHEDULED_AT_KEY] = new Map<string, number>()
54
+ }
55
+ return globalScope[AUTO_REINDEX_SCHEDULED_AT_KEY]
56
+ }
57
+
58
+ function markAutoReindexScheduled(key: string, debounceMs: number, now: number): boolean {
59
+ const autoReindexScheduledAt = getAutoReindexScheduledAt()
60
+ const previous = autoReindexScheduledAt.get(key)
61
+ if (previous !== undefined && now - previous < debounceMs) return false
62
+
63
+ if (autoReindexScheduledAt.size >= AUTO_REINDEX_DEBOUNCE_MAX_SCOPES) {
64
+ for (const [scopeKey, scheduledAt] of autoReindexScheduledAt) {
65
+ if (now - scheduledAt >= debounceMs) autoReindexScheduledAt.delete(scopeKey)
66
+ }
67
+ if (autoReindexScheduledAt.size >= AUTO_REINDEX_DEBOUNCE_MAX_SCOPES) {
68
+ const oldestKey = autoReindexScheduledAt.keys().next().value
69
+ if (oldestKey !== undefined) autoReindexScheduledAt.delete(oldestKey)
70
+ }
71
+ }
72
+
73
+ autoReindexScheduledAt.delete(key)
74
+ autoReindexScheduledAt.set(key, now)
75
+ return true
76
+ }
41
77
 
42
78
  function buildFilterableCustomFieldJoins(
43
79
  sources: QueryCustomFieldSource[] | undefined,
@@ -152,6 +188,7 @@ export class HybridQueryEngine implements QueryEngine {
152
188
  private sqlDebugEnabled: boolean | null = null
153
189
  private forcePartialIndexEnabled: boolean | null = null
154
190
  private autoReindexEnabled: boolean | null = null
191
+ private autoReindexDebounceMs: number | null = null
155
192
  private coverageOptimizationEnabled: boolean | null = null
156
193
  private pendingCoverageRefreshKeys = new Set<string>()
157
194
  private searchAvailabilityInstance: SearchTokenAvailability | null = null
@@ -1998,6 +2035,11 @@ export class HybridQueryEngine implements QueryEngine {
1998
2035
  organizationId: organizationIdOverride ?? opts.organizationId ?? null,
1999
2036
  force: false,
2000
2037
  }
2038
+ const debounceMs = this.getAutoReindexDebounceMs()
2039
+ if (debounceMs > 0) {
2040
+ const key = [payload.entityType, payload.tenantId ?? '__tenant__', payload.organizationId ?? '__org__'].join('|')
2041
+ if (!markAutoReindexScheduled(key, debounceMs, Date.now())) return
2042
+ }
2001
2043
  const context = stats
2002
2044
  ? { entity, tenantId: payload.tenantId, organizationId: payload.organizationId, baseCount: stats.baseCount, indexedCount: stats.indexedCount }
2003
2045
  : { entity, tenantId: payload.tenantId, organizationId: payload.organizationId }
@@ -2067,6 +2109,16 @@ export class HybridQueryEngine implements QueryEngine {
2067
2109
  return this.autoReindexEnabled
2068
2110
  }
2069
2111
 
2112
+ private getAutoReindexDebounceMs(): number {
2113
+ if (this.autoReindexDebounceMs != null) return this.autoReindexDebounceMs
2114
+ this.autoReindexDebounceMs = parseNumberWithDefault(
2115
+ process.env.OM_QUERY_INDEX_AUTO_REINDEX_DEBOUNCE_MS,
2116
+ AUTO_REINDEX_DEBOUNCE_DEFAULT_MS,
2117
+ { integer: true, min: 0 },
2118
+ )
2119
+ return this.autoReindexDebounceMs
2120
+ }
2121
+
2070
2122
  private isCoverageOptimizationEnabled(): boolean {
2071
2123
  if (this.coverageOptimizationEnabled != null) return this.coverageOptimizationEnabled
2072
2124
  const raw = (process.env.OPTIMIZE_INDEX_COVERAGE_STATS ?? '').trim().toLowerCase()
@@ -2,6 +2,7 @@ import { type Kysely, type Transaction, sql } from 'kysely'
2
2
  import {
3
3
  isSearchFieldBlocklisted,
4
4
  resolveSearchConfig,
5
+ resolveSearchTokenLimits,
5
6
  type SearchConfig,
6
7
  } from '@open-mercato/shared/lib/search/config'
7
8
  import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
@@ -79,9 +80,7 @@ function shouldIndexField(
79
80
  if (lower.endsWith('_at')) return false
80
81
  if (['created_at', 'updated_at', 'deleted_at', 'tenant_id', 'organization_id'].includes(lower)) return false
81
82
  if (isSearchFieldBlocklisted(field, entityType, config)) return false
82
- const values = collectTextValues(value)
83
- if (!values.length) return false
84
- return values.some((text) => tokenizeText(text, config).tokens.length > 0)
83
+ return collectTextValues(value).some((text) => text.length > 0)
85
84
  }
86
85
 
87
86
  export function buildSearchTokenRows(params: BuildTokenOptions): SearchTokenRow[] {
@@ -95,19 +94,32 @@ export function buildSearchTokenRows(params: BuildTokenOptions): SearchTokenRow[
95
94
  organizationId: params.organizationId ?? DEFAULT_SCOPE.organizationId,
96
95
  tenantId: params.tenantId ?? DEFAULT_SCOPE.tenantId,
97
96
  }
97
+ const limits = resolveSearchTokenLimits(config)
98
+ const recordLimit = limits.maxTokensPerRecord > 0 ? limits.maxTokensPerRecord : Number.POSITIVE_INFINITY
99
+ const fieldLimit = limits.maxTokensPerField > 0 ? limits.maxTokensPerField : Number.POSITIVE_INFINITY
98
100
 
99
101
  for (const [field, rawValue] of Object.entries(params.doc)) {
102
+ if (tokens.length >= recordLimit) break
100
103
  if (!shouldIndexField(field, rawValue, config, params.entityType)) continue
101
104
  const values = collectTextValues(rawValue)
102
105
  const seen = new Set<string>()
106
+ let fieldTokenCount = 0
103
107
  for (const text of values) {
104
- const { tokens: textTokens, hashes } = tokenizeText(text, config)
108
+ if (tokens.length >= recordLimit || fieldTokenCount >= fieldLimit) break
109
+ const remainingLimit = Math.min(recordLimit - tokens.length, fieldLimit - fieldTokenCount)
110
+ const candidateLimit = fieldTokenCount + remainingLimit
111
+ const tokenConfig = Number.isFinite(candidateLimit)
112
+ ? { ...config, maxTokensPerField: candidateLimit }
113
+ : config
114
+ const { tokens: textTokens, hashes } = tokenizeText(text, tokenConfig)
105
115
  for (let i = 0; i < textTokens.length; i += 1) {
116
+ if (tokens.length >= recordLimit || fieldTokenCount >= fieldLimit) break
106
117
  const token = textTokens[i]
107
118
  const hash = hashes[i]
108
119
  const dedupeKey = `${field}|${hash}`
109
120
  if (seen.has(dedupeKey)) continue
110
121
  seen.add(dedupeKey)
122
+ fieldTokenCount += 1
111
123
  debug('token.generated', { entityType: params.entityType, recordId: params.recordId, field, hash })
112
124
  tokens.push({
113
125
  entity_type: params.entityType,
@@ -11,6 +11,7 @@ import { getShippingAdapter } from '../../../lib/adapter-registry'
11
11
  import { getShippingCarrierQueue } from '../../../lib/queue'
12
12
  import { shippingCarriersTag } from '../../openapi'
13
13
  import { createLogger } from '@open-mercato/shared/lib/logger'
14
+ import { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'
14
15
 
15
16
  const logger = createLogger('shipping_carriers').child({ component: 'webhook' })
16
17
 
@@ -51,7 +52,15 @@ export async function POST(req: Request, { params }: { params: Promise<{ provide
51
52
  const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey)
52
53
  if (rateLimitResponse) return rateLimitResponse
53
54
 
54
- const rawBody = await req.text()
55
+ let rawBody: string
56
+ try {
57
+ rawBody = await readBoundedRequestBody(req)
58
+ } catch (error) {
59
+ if (error instanceof WebhookBodyTooLargeError) {
60
+ return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })
61
+ }
62
+ throw error
63
+ }
55
64
  const headers: Record<string, string> = {}
56
65
  req.headers.forEach((value, key) => {
57
66
  headers[key] = value
@@ -156,6 +165,7 @@ export const openApi = {
156
165
  responses: [
157
166
  { status: 202, description: 'Webhook accepted for async processing' },
158
167
  { status: 401, description: 'Signature verification failed' },
168
+ { status: 413, description: 'Webhook payload too large' },
159
169
  { status: 404, description: 'Unknown provider' },
160
170
  ],
161
171
  },