@open-mercato/core 0.6.7-develop.6606.1.3b1ec9b1ea → 0.6.7-develop.6621.1.cdbe9dee3d

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 (102) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/session/refresh.js +12 -11
  3. package/dist/modules/auth/api/session/refresh.js.map +2 -2
  4. package/dist/modules/auth/lib/rateLimitCheck.js +4 -4
  5. package/dist/modules/auth/lib/rateLimitCheck.js.map +2 -2
  6. package/dist/modules/catalog/api/categories/route.js +8 -14
  7. package/dist/modules/catalog/api/categories/route.js.map +2 -2
  8. package/dist/modules/catalog/api/tags/route.js +3 -10
  9. package/dist/modules/catalog/api/tags/route.js.map +2 -2
  10. package/dist/modules/customers/api/interactions/[id]/visibility/route.js +15 -17
  11. package/dist/modules/customers/api/interactions/[id]/visibility/route.js.map +2 -2
  12. package/dist/modules/customers/api/people/[id]/email-threads/route.js +6 -4
  13. package/dist/modules/customers/api/people/[id]/email-threads/route.js.map +2 -2
  14. package/dist/modules/customers/api/people/[id]/emails/route.js +19 -17
  15. package/dist/modules/customers/api/people/[id]/emails/route.js.map +2 -2
  16. package/dist/modules/customers/components/detail/CustomFieldValuesList.js +22 -5
  17. package/dist/modules/customers/components/detail/CustomFieldValuesList.js.map +2 -2
  18. package/dist/modules/customers/components/detail/create/CreateDealForm.js +5 -2
  19. package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +2 -2
  20. package/dist/modules/customers/components/detail/customFieldUtils.js +10 -3
  21. package/dist/modules/customers/components/detail/customFieldUtils.js.map +2 -2
  22. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.meta.js +4 -2
  23. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.meta.js.map +2 -2
  24. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.meta.js +4 -2
  25. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.meta.js.map +2 -2
  26. package/dist/modules/integrations/api/[id]/credentials/route.js +10 -7
  27. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  28. package/dist/modules/integrations/api/[id]/health/route.js +6 -4
  29. package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
  30. package/dist/modules/integrations/api/[id]/route.js +4 -2
  31. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  32. package/dist/modules/integrations/api/[id]/state/route.js +7 -5
  33. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  34. package/dist/modules/integrations/api/[id]/version/route.js +7 -5
  35. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  36. package/dist/modules/integrations/api/logs/route.js +4 -2
  37. package/dist/modules/integrations/api/logs/route.js.map +2 -2
  38. package/dist/modules/integrations/api/route.js +4 -2
  39. package/dist/modules/integrations/api/route.js.map +2 -2
  40. package/dist/modules/integrations/api/umes-read.js +2 -1
  41. package/dist/modules/integrations/api/umes-read.js.map +2 -2
  42. package/dist/modules/integrations/lib/organization-scope.js +12 -0
  43. package/dist/modules/integrations/lib/organization-scope.js.map +7 -0
  44. package/dist/modules/payment_gateways/lib/gateway-service.js +10 -1
  45. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  46. package/dist/modules/sales/api/document-history/route.js +6 -9
  47. package/dist/modules/sales/api/document-history/route.js.map +2 -2
  48. package/dist/modules/sales/api/quotes/accept/route.js +3 -3
  49. package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
  50. package/dist/modules/sales/api/returns/[id]/route.js +6 -9
  51. package/dist/modules/sales/api/returns/[id]/route.js.map +2 -2
  52. package/dist/modules/sales/backend/sales/documents/[id]/page.js +6 -2
  53. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  54. package/dist/modules/sales/components/documents/AddressesSection.js +6 -4
  55. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  56. package/dist/modules/sales/workflows.js +1 -1
  57. package/dist/modules/sales/workflows.js.map +1 -1
  58. package/dist/modules/workflows/api/definitions/[id]/route.js +10 -1
  59. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  60. package/dist/modules/workflows/lib/event-trigger-service.js +44 -3
  61. package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
  62. package/package.json +7 -7
  63. package/src/modules/auth/api/session/refresh.ts +19 -11
  64. package/src/modules/auth/lib/rateLimitCheck.ts +4 -4
  65. package/src/modules/catalog/api/categories/route.ts +14 -16
  66. package/src/modules/catalog/api/tags/route.ts +6 -10
  67. package/src/modules/catalog/i18n/pl.json +9 -9
  68. package/src/modules/customer_accounts/i18n/pl.json +129 -129
  69. package/src/modules/customers/api/interactions/[id]/visibility/route.ts +24 -20
  70. package/src/modules/customers/api/people/[id]/email-threads/route.ts +10 -6
  71. package/src/modules/customers/api/people/[id]/emails/route.ts +25 -21
  72. package/src/modules/customers/components/detail/CustomFieldValuesList.tsx +28 -4
  73. package/src/modules/customers/components/detail/create/CreateDealForm.tsx +7 -2
  74. package/src/modules/customers/components/detail/customFieldUtils.ts +15 -3
  75. package/src/modules/data_sync/i18n/pl.json +68 -68
  76. package/src/modules/directory/backend/directory/organizations/[id]/edit/page.meta.ts +4 -2
  77. package/src/modules/directory/backend/directory/tenants/[id]/edit/page.meta.ts +4 -2
  78. package/src/modules/integrations/api/[id]/credentials/route.ts +10 -7
  79. package/src/modules/integrations/api/[id]/health/route.ts +6 -4
  80. package/src/modules/integrations/api/[id]/route.ts +4 -2
  81. package/src/modules/integrations/api/[id]/state/route.ts +7 -5
  82. package/src/modules/integrations/api/[id]/version/route.ts +7 -5
  83. package/src/modules/integrations/api/logs/route.ts +4 -2
  84. package/src/modules/integrations/api/route.ts +4 -2
  85. package/src/modules/integrations/api/umes-read.ts +2 -1
  86. package/src/modules/integrations/i18n/de.json +0 -76
  87. package/src/modules/integrations/i18n/en.json +0 -76
  88. package/src/modules/integrations/i18n/es.json +0 -76
  89. package/src/modules/integrations/i18n/pl.json +0 -76
  90. package/src/modules/integrations/lib/organization-scope.ts +27 -0
  91. package/src/modules/payment_gateways/lib/gateway-service.ts +12 -1
  92. package/src/modules/sales/api/document-history/route.ts +10 -9
  93. package/src/modules/sales/api/quotes/accept/route.ts +3 -3
  94. package/src/modules/sales/api/returns/[id]/route.ts +10 -9
  95. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +26 -19
  96. package/src/modules/sales/components/documents/AddressesSection.tsx +8 -4
  97. package/src/modules/sales/i18n/pl.json +24 -24
  98. package/src/modules/sales/workflows.ts +1 -1
  99. package/src/modules/staff/i18n/pl.json +178 -178
  100. package/src/modules/workflows/api/definitions/[id]/route.ts +14 -1
  101. package/src/modules/workflows/i18n/pl.json +128 -128
  102. package/src/modules/workflows/lib/event-trigger-service.ts +87 -8
@@ -52,37 +52,24 @@ export async function PATCH(req: Request, context: RouteContext): Promise<Respon
52
52
  const container = await createRequestContainer()
53
53
  const em = (container.resolve('em') as EntityManager).fork()
54
54
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
55
- const organizationId = scope?.selectedId ?? (auth as { orgId?: string | null }).orgId ?? null
56
- const dscope = { tenantId: auth.tenantId as string, organizationId }
57
55
  const userId = resolveAuthActorId(auth)
58
56
 
59
- const guardResult = await validateCrudMutationGuard(container, {
60
- tenantId: auth.tenantId,
61
- organizationId,
62
- userId,
63
- resourceKind: 'customers.interaction',
64
- resourceId: id,
65
- operation: 'custom',
66
- requestMethod: req.method,
67
- requestHeaders: req.headers,
68
- })
69
- if (guardResult && !guardResult.ok) {
70
- return NextResponse.json(guardResult.body, { status: guardResult.status })
71
- }
72
-
57
+ // Load the interaction by its tenant-unique id (not a hand-rolled selected
58
+ // org) so this keeps working under the "All organizations" scope. The record's
59
+ // own organization then becomes the concrete org the guard, command, and audit
60
+ // event are attributed to.
73
61
  const interaction = (await findOneWithDecryption(
74
62
  em,
75
63
  CustomerInteraction,
76
64
  {
77
65
  id,
78
66
  tenantId: auth.tenantId,
79
- organizationId,
80
67
  deletedAt: null,
81
68
  interactionType: 'email',
82
69
  } as any,
83
70
  undefined,
84
- dscope,
85
- )) as { id: string; authorUserId?: string | null; visibility?: string | null } | null
71
+ { tenantId: auth.tenantId as string, organizationId: scope?.selectedId ?? (auth as { orgId?: string | null }).orgId ?? null },
72
+ )) as { id: string; organizationId?: string | null; authorUserId?: string | null; visibility?: string | null } | null
86
73
 
87
74
  if (!interaction) {
88
75
  return NextResponse.json({ error: 'Email not found' }, { status: 404 })
@@ -91,12 +78,29 @@ export async function PATCH(req: Request, context: RouteContext): Promise<Respon
91
78
  // Personal mailbox privacy (v1: strict owner-only): ONLY the author may flip
92
79
  // their own email's visibility — no admin bypass. Return 404 (not 403) for
93
80
  // everyone else so we don't leak the row's existence — this also covers
94
- // non-authors who cannot see a private email in the first place.
81
+ // non-authors who cannot see a private email in the first place. This owner
82
+ // check is stricter than any org gate, so no separate org read guard is needed.
95
83
  const isAuthor = !!interaction.authorUserId && interaction.authorUserId === auth.sub
96
84
  if (!isAuthor) {
97
85
  return NextResponse.json({ error: 'Email not found' }, { status: 404 })
98
86
  }
99
87
 
88
+ const organizationId = interaction.organizationId ?? null
89
+
90
+ const guardResult = await validateCrudMutationGuard(container, {
91
+ tenantId: auth.tenantId,
92
+ organizationId,
93
+ userId,
94
+ resourceKind: 'customers.interaction',
95
+ resourceId: id,
96
+ operation: 'custom',
97
+ requestMethod: req.method,
98
+ requestHeaders: req.headers,
99
+ })
100
+ if (guardResult && !guardResult.ok) {
101
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
102
+ }
103
+
100
104
  // No-op: visibility is already at the requested value.
101
105
  if (interaction.visibility === body.visibility) {
102
106
  return NextResponse.json({ ok: true, changed: false })
@@ -5,6 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
6
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
7
  import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
8
+ import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
8
9
  import { CustomerEntity } from '../../../../data/entities'
9
10
  import { buildPersonEmailThreads } from '../../../../lib/personEmailThreads'
10
11
 
@@ -33,12 +34,12 @@ export async function GET(req: Request, context: RouteContext): Promise<Response
33
34
 
34
35
  const container = await createRequestContainer()
35
36
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
36
- const organizationId = scope?.selectedId ?? (auth as { orgId?: string | null }).orgId ?? null
37
37
  const em = (container.resolve('em') as EntityManager).fork()
38
- const dscope = { tenantId: auth.tenantId as string, organizationId }
39
38
 
40
- // Verify the Person exists in the caller's tenant/org (ownership check,
41
- // same pattern as the [id]/emails compose route).
39
+ // Verify the Person exists in the caller's tenant, then fail-closed on the
40
+ // record's own organization — same pattern as the [id] detail route. Loading
41
+ // by tenant + id (not a hand-rolled selected org) keeps this working under the
42
+ // "All organizations" scope, where no concrete org is carried.
42
43
  const person = await findOneWithDecryption(
43
44
  em,
44
45
  CustomerEntity,
@@ -46,15 +47,18 @@ export async function GET(req: Request, context: RouteContext): Promise<Response
46
47
  id: personId,
47
48
  kind: 'person',
48
49
  tenantId: auth.tenantId,
49
- organizationId,
50
50
  deletedAt: null,
51
51
  } as never,
52
52
  undefined,
53
- dscope,
53
+ { tenantId: auth.tenantId as string, organizationId: scope?.selectedId ?? (auth as { orgId?: string | null }).orgId ?? null },
54
54
  )
55
55
  if (!person) {
56
56
  return NextResponse.json({ error: 'Person not found' }, { status: 404 })
57
57
  }
58
+ const organizationId = (person as { organizationId?: string | null }).organizationId ?? null
59
+ if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId })) {
60
+ return NextResponse.json({ error: 'Person not found' }, { status: 404 })
61
+ }
58
62
 
59
63
  const viewerUserId = auth.isApiKey ? null : auth.sub ?? null
60
64
 
@@ -10,6 +10,7 @@ import {
10
10
  runCrudMutationGuardAfterSuccess,
11
11
  } from '@open-mercato/shared/lib/crud/mutation-guard'
12
12
  import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
13
+ import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
13
14
  import { resolveAuthActorId } from '../../../../lib/interactionRequestContext'
14
15
  import { CustomerEntity } from '../../../../data/entities'
15
16
  import type { SendAsUserService } from '@open-mercato/core/modules/communication_channels/lib/send-as-user'
@@ -66,28 +67,14 @@ export async function POST(req: Request, context: RouteContext): Promise<Respons
66
67
  const container = await createRequestContainer()
67
68
  const userId = resolveAuthActorId(auth)
68
69
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
69
- const organizationId = scope?.selectedId ?? (auth as { orgId?: string | null }).orgId ?? null
70
-
71
- const guardResult = await validateCrudMutationGuard(container, {
72
- tenantId: auth.tenantId,
73
- organizationId,
74
- userId,
75
- resourceKind: 'customers.person',
76
- resourceId: personId,
77
- operation: 'custom',
78
- requestMethod: req.method,
79
- requestHeaders: req.headers,
80
- })
81
- if (guardResult && !guardResult.ok) {
82
- return NextResponse.json(guardResult.body, { status: guardResult.status })
83
- }
84
70
 
85
71
  const em = (container.resolve('em') as EntityManager).fork()
86
- const dscope = { tenantId: auth.tenantId as string, organizationId }
87
72
 
88
- // 1. Verify the Person exists in the caller's tenant.
89
- // Uses CustomerEntity (kind='person') as the canonical ownership check —
90
- // the same pattern as the existing [id]/route.ts GET handler.
73
+ // 1. Verify the Person exists in the caller's tenant, then fail-closed on the
74
+ // record's own organization — same pattern as the [id]/route.ts GET handler.
75
+ // Loading by tenant + id (not a hand-rolled selected org) keeps this working
76
+ // under the "All organizations" scope; the record's own org then becomes the
77
+ // concrete org the guard and the outbound message are attributed to.
91
78
  const person = await findOneWithDecryption(
92
79
  em,
93
80
  CustomerEntity,
@@ -95,15 +82,32 @@ export async function POST(req: Request, context: RouteContext): Promise<Respons
95
82
  id: personId,
96
83
  kind: 'person',
97
84
  tenantId: auth.tenantId,
98
- organizationId,
99
85
  deletedAt: null,
100
86
  } as never,
101
87
  undefined,
102
- dscope,
88
+ { tenantId: auth.tenantId as string, organizationId: scope?.selectedId ?? (auth as { orgId?: string | null }).orgId ?? null },
103
89
  )
104
90
  if (!person) {
105
91
  return NextResponse.json({ error: 'Person not found' }, { status: 404 })
106
92
  }
93
+ const organizationId = (person as { organizationId?: string | null }).organizationId ?? null
94
+ if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId })) {
95
+ return NextResponse.json({ error: 'Person not found' }, { status: 404 })
96
+ }
97
+
98
+ const guardResult = await validateCrudMutationGuard(container, {
99
+ tenantId: auth.tenantId,
100
+ organizationId,
101
+ userId,
102
+ resourceKind: 'customers.person',
103
+ resourceId: personId,
104
+ operation: 'custom',
105
+ requestMethod: req.method,
106
+ requestHeaders: req.headers,
107
+ })
108
+ if (guardResult && !guardResult.ok) {
109
+ return NextResponse.json(guardResult.body, { status: guardResult.status })
110
+ }
107
111
 
108
112
  // 2. Call the hub's send-as-user facade in-process (resolved via DI) so the
109
113
  // customers module makes no HTTP self-call. `crmVisibility` and `crmPersonId`
@@ -30,6 +30,8 @@ type DisplayEntry = {
30
30
  value: unknown
31
31
  dictionaryMap: DictionaryMap | null
32
32
  multi: boolean
33
+ kind?: string
34
+ options?: CustomFieldDefDto['options']
33
35
  }
34
36
 
35
37
  type CustomFieldValuesListProps = {
@@ -97,10 +99,23 @@ function renderDictionaryValues(
97
99
  )
98
100
  }
99
101
 
100
- function renderPrimitiveValues(value: unknown): React.ReactNode {
102
+ function resolveOptionLabel(value: unknown, options: CustomFieldDefDto['options']): unknown {
103
+ if (!options?.length || typeof value !== 'string') return value
104
+ const trimmed = value.trim()
105
+ const match = options.find((option) => option.value === trimmed)
106
+ return match ? match.label : value
107
+ }
108
+
109
+ function applyOptionLabels(value: unknown, options: CustomFieldDefDto['options']): unknown {
110
+ if (!options?.length) return value
111
+ if (Array.isArray(value)) return value.map((entry) => resolveOptionLabel(entry, options))
112
+ return resolveOptionLabel(value, options)
113
+ }
114
+
115
+ function renderPrimitiveValues(value: unknown, kind?: string): React.ReactNode {
101
116
  if (Array.isArray(value)) {
102
117
  const parts = value
103
- .map((entry) => stringifyCustomValue(entry))
118
+ .map((entry) => stringifyCustomValue(entry, { kind }))
104
119
  .map((entry) => entry.trim())
105
120
  .filter((entry) => entry.length > 0)
106
121
  if (!parts.length) return null
@@ -117,7 +132,7 @@ function renderPrimitiveValues(value: unknown): React.ReactNode {
117
132
  </div>
118
133
  )
119
134
  }
120
- const label = stringifyCustomValue(value).trim()
135
+ const label = stringifyCustomValue(value, { kind }).trim()
121
136
  if (!label.length) return null
122
137
  return <span className="text-sm text-foreground">{label}</span>
123
138
  }
@@ -179,6 +194,12 @@ function buildDisplayEntries(
179
194
  definitions.forEach((def, index) => {
180
195
  const normalizedKey = normalizeCustomFieldKey(def.key)
181
196
  if (!normalizedKey) return
197
+ // Operational fields marked listVisible:false must not render — neither
198
+ // here nor as a definition-less "extra" below, so consume the key anyway.
199
+ if (def.listVisible === false) {
200
+ consumedKeys.add(normalizedKey)
201
+ return
202
+ }
182
203
  const entry = combined.get(normalizedKey)
183
204
  if (!entry || isEmptyCustomValue(entry.value)) return
184
205
  const label = resolveCustomFieldLabel(entry.label ?? def.label, entry.key)
@@ -191,6 +212,8 @@ function buildDisplayEntries(
191
212
  value: entry.value,
192
213
  dictionaryMap,
193
214
  multi: def.multi ?? Array.isArray(entry.value),
215
+ kind: def.kind,
216
+ options: def.options,
194
217
  })
195
218
  consumedKeys.add(normalizedKey)
196
219
  })
@@ -242,7 +265,8 @@ export function CustomFieldValuesList({
242
265
  <div className={cn('grid gap-3 sm:grid-cols-2', className)}>
243
266
  {displayEntries.map((entry, index) => {
244
267
  const dictionaryContent = renderDictionaryValues(entry.value, entry.dictionaryMap, entry.multi)
245
- const primitiveContent = dictionaryContent ?? renderPrimitiveValues(entry.value)
268
+ const primitiveContent =
269
+ dictionaryContent ?? renderPrimitiveValues(applyOptionLabels(entry.value, entry.options), entry.kind)
246
270
  const content =
247
271
  dictionaryContent ??
248
272
  primitiveContent ??
@@ -27,9 +27,11 @@ const CUSTOM_FIELDS_MANAGE_HREF = `/backend/entities/system/${encodeURIComponent
27
27
 
28
28
  export type CreateDealFormProps = {
29
29
  returnTo: string
30
+ /** Seed values merged over EMPTY_VALUES for the initial form state. Entries set to `undefined` are ignored (the EMPTY_VALUES default wins), so a sparse `Partial<BaseValues>` can never unset a required field. Additive: omitting it preserves current behavior. */
31
+ initialValues?: Partial<BaseValues>
30
32
  }
31
33
 
32
- export function CreateDealForm({ returnTo }: CreateDealFormProps) {
34
+ export function CreateDealForm({ returnTo, initialValues }: CreateDealFormProps) {
33
35
  const t = useT()
34
36
  const router = useRouter()
35
37
  const tr = React.useCallback(
@@ -38,7 +40,10 @@ export function CreateDealForm({ returnTo }: CreateDealFormProps) {
38
40
  [t],
39
41
  )
40
42
 
41
- const [values, setValues] = React.useState<BaseValues>(EMPTY_VALUES)
43
+ const [values, setValues] = React.useState<BaseValues>(() => {
44
+ const definedSeedEntries = Object.entries(initialValues ?? {}).filter(([, seedValue]) => seedValue !== undefined)
45
+ return { ...EMPTY_VALUES, ...Object.fromEntries(definedSeedEntries) }
46
+ })
42
47
  const [errors, setErrors] = React.useState<Record<string, string>>({})
43
48
  const [isSubmitting, setIsSubmitting] = React.useState(false)
44
49
 
@@ -53,6 +53,7 @@ export function resolveCustomFieldLabel(label: string | null | undefined, key: s
53
53
 
54
54
  export function isEmptyCustomValue(value: unknown): boolean {
55
55
  if (value === null || value === undefined) return true
56
+ if (value === false) return true
56
57
  if (typeof value === 'string') return value.trim().length === 0
57
58
  if (Array.isArray(value)) return value.length === 0 || value.every((entry) => isEmptyCustomValue(entry))
58
59
  if (typeof value === 'object') {
@@ -65,11 +66,14 @@ export function isEmptyCustomValue(value: unknown): boolean {
65
66
  return false
66
67
  }
67
68
 
68
- export function stringifyCustomValue(value: unknown): string {
69
+ const DATE_FIELD_KINDS = new Set(['date', 'datetime'])
70
+ const ISO_DATE_PREFIX_RE = /^\d{4}-\d{2}-\d{2}([T ]|$)/
71
+
72
+ export function stringifyCustomValue(value: unknown, options?: { kind?: string }): string {
69
73
  if (value === null || value === undefined) return ''
70
74
  if (Array.isArray(value)) {
71
75
  const parts = value
72
- .map((entry) => stringifyCustomValue(entry))
76
+ .map((entry) => stringifyCustomValue(entry, options))
73
77
  .map((entry) => entry.trim())
74
78
  .filter((entry) => entry.length > 0)
75
79
  return parts.join(', ')
@@ -81,7 +85,15 @@ export function stringifyCustomValue(value: unknown): string {
81
85
  if (typeof value === 'string') {
82
86
  const trimmed = value.trim()
83
87
  if (!trimmed.length) return ''
84
- return formatDateTime(trimmed) ?? trimmed
88
+ // Date-format strings only when the definition says the field is a date,
89
+ // or (for definition-less extras) when the value is unambiguously ISO-like.
90
+ // `new Date(...)` parses far too loosely to run on every string — it used
91
+ // to mangle plain text such as "Report 2024" into a date.
92
+ const kind = options?.kind
93
+ if (kind ? DATE_FIELD_KINDS.has(kind) : ISO_DATE_PREFIX_RE.test(trimmed)) {
94
+ return formatDateTime(trimmed) ?? trimmed
95
+ }
96
+ return trimmed
85
97
  }
86
98
  if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') {
87
99
  return String(value)
@@ -16,56 +16,56 @@
16
16
  "data_sync.dashboard.loadError": "Nie udało się załadować przebiegów",
17
17
  "data_sync.dashboard.noRuns": "Brak przebiegów synchronizacji",
18
18
  "data_sync.dashboard.schedule.cron": "Cron",
19
- "data_sync.dashboard.schedule.cronHelp": "Example: `0 * * * *` runs at the start of every hour.",
20
- "data_sync.dashboard.schedule.cronValue": "Cron expression",
21
- "data_sync.dashboard.schedule.delete": "Remove schedule",
22
- "data_sync.dashboard.schedule.deleteError": "Failed to remove recurring schedule",
23
- "data_sync.dashboard.schedule.deleteSuccess": "Recurring schedule removed",
24
- "data_sync.dashboard.schedule.deleting": "Removing...",
25
- "data_sync.dashboard.schedule.description": "Save a repeating schedule for the selected integration, entity, and direction without leaving this dashboard.",
26
- "data_sync.dashboard.schedule.enabled": "Schedule enabled",
27
- "data_sync.dashboard.schedule.enabledHelp": "Pause the recurring job without deleting the schedule definition.",
28
- "data_sync.dashboard.schedule.error": "Failed to save recurring schedule",
29
- "data_sync.dashboard.schedule.fullSync": "Run scheduled jobs as full sync",
30
- "data_sync.dashboard.schedule.fullSyncHelp": "When enabled, every recurring run starts from the beginning instead of the saved cursor.",
31
- "data_sync.dashboard.schedule.interval": "Interval",
32
- "data_sync.dashboard.schedule.intervalHelp": "Example: `1h`, `6h`, or `24h` for repeating intervals.",
33
- "data_sync.dashboard.schedule.intervalValue": "Interval",
34
- "data_sync.dashboard.schedule.invalidValue": "Provide a schedule value before saving.",
35
- "data_sync.dashboard.schedule.lastRun": "Last scheduled run: {value}",
36
- "data_sync.dashboard.schedule.neverRun": "Saved, but no scheduled execution has completed yet.",
37
- "data_sync.dashboard.schedule.none": "No recurring schedule saved for this target yet.",
38
- "data_sync.dashboard.schedule.save": "Save recurring schedule",
39
- "data_sync.dashboard.schedule.saving": "Saving...",
40
- "data_sync.dashboard.schedule.status.disabled": "Recurring schedule paused",
41
- "data_sync.dashboard.schedule.status.enabled": "Recurring schedule active",
42
- "data_sync.dashboard.schedule.status.none": "No recurring schedule",
43
- "data_sync.dashboard.schedule.status.shortDisabled": "Paused",
44
- "data_sync.dashboard.schedule.status.shortEnabled": "Scheduled",
45
- "data_sync.dashboard.schedule.status.shortNone": "One-time only",
46
- "data_sync.dashboard.schedule.success": "Recurring schedule saved",
47
- "data_sync.dashboard.schedule.timezone": "Timezone",
48
- "data_sync.dashboard.schedule.title": "Recurring schedule",
49
- "data_sync.dashboard.schedule.type": "Schedule type",
19
+ "data_sync.dashboard.schedule.cronHelp": "Przykład: `0 * * * *` uruchamia zadanie na początku każdej godziny.",
20
+ "data_sync.dashboard.schedule.cronValue": "Wyrażenie cron",
21
+ "data_sync.dashboard.schedule.delete": "Usuń harmonogram",
22
+ "data_sync.dashboard.schedule.deleteError": "Nie udało się usunąć harmonogramu cyklicznego",
23
+ "data_sync.dashboard.schedule.deleteSuccess": "Usunięto harmonogram cykliczny",
24
+ "data_sync.dashboard.schedule.deleting": "Usuwanie...",
25
+ "data_sync.dashboard.schedule.description": "Zapisz powtarzalny harmonogram dla wybranej integracji, encji i kierunku bez opuszczania tego pulpitu.",
26
+ "data_sync.dashboard.schedule.enabled": "Harmonogram włączony",
27
+ "data_sync.dashboard.schedule.enabledHelp": "Wstrzymaj zadanie cykliczne bez usuwania definicji harmonogramu.",
28
+ "data_sync.dashboard.schedule.error": "Nie udało się zapisać harmonogramu cyklicznego",
29
+ "data_sync.dashboard.schedule.fullSync": "Uruchamiaj zaplanowane zadania jako pełną synchronizację",
30
+ "data_sync.dashboard.schedule.fullSyncHelp": "Gdy opcja jest włączona, każdy cykliczny przebieg zaczyna się od początku zamiast od zapisanego kursora.",
31
+ "data_sync.dashboard.schedule.interval": "Interwał",
32
+ "data_sync.dashboard.schedule.intervalHelp": "Przykład: `1h`, `6h` lub `24h` dla powtarzalnych interwałów.",
33
+ "data_sync.dashboard.schedule.intervalValue": "Interwał",
34
+ "data_sync.dashboard.schedule.invalidValue": "Podaj wartość harmonogramu przed zapisaniem.",
35
+ "data_sync.dashboard.schedule.lastRun": "Ostatni zaplanowany przebieg: {value}",
36
+ "data_sync.dashboard.schedule.neverRun": "Zapisano, ale żadne zaplanowane wykonanie jeszcze się nie zakończyło.",
37
+ "data_sync.dashboard.schedule.none": "Dla tego celu nie zapisano jeszcze harmonogramu cyklicznego.",
38
+ "data_sync.dashboard.schedule.save": "Zapisz harmonogram cykliczny",
39
+ "data_sync.dashboard.schedule.saving": "Zapisywanie...",
40
+ "data_sync.dashboard.schedule.status.disabled": "Harmonogram cykliczny wstrzymany",
41
+ "data_sync.dashboard.schedule.status.enabled": "Harmonogram cykliczny aktywny",
42
+ "data_sync.dashboard.schedule.status.none": "Brak harmonogramu cyklicznego",
43
+ "data_sync.dashboard.schedule.status.shortDisabled": "Wstrzymany",
44
+ "data_sync.dashboard.schedule.status.shortEnabled": "Zaplanowany",
45
+ "data_sync.dashboard.schedule.status.shortNone": "Tylko jednorazowo",
46
+ "data_sync.dashboard.schedule.success": "Zapisano harmonogram cykliczny",
47
+ "data_sync.dashboard.schedule.timezone": "Strefa czasowa",
48
+ "data_sync.dashboard.schedule.title": "Harmonogram cykliczny",
49
+ "data_sync.dashboard.schedule.type": "Typ harmonogramu",
50
50
  "data_sync.dashboard.searchPlaceholder": "Szukaj po integracji, typie encji, statusie lub ID przebiegu",
51
- "data_sync.dashboard.start.batchSize": "Batch Size",
52
- "data_sync.dashboard.start.description": "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.",
53
- "data_sync.dashboard.start.error": "Failed to start sync run",
54
- "data_sync.dashboard.start.eyebrow": "Run once or keep it recurring",
55
- "data_sync.dashboard.start.fullSync": "Run as full sync",
56
- "data_sync.dashboard.start.fullSyncHelp": "Ignore the saved cursor and process the entire source again for this run.",
57
- "data_sync.dashboard.start.invalidBatchSize": "Batch size must be between 1 and 1000.",
51
+ "data_sync.dashboard.start.batchSize": "Rozmiar partii",
52
+ "data_sync.dashboard.start.description": "Wybierz cel synchronizacji, uruchom przebieg doraźny albo zapisz harmonogram cykliczny dla tej samej encji i kierunku z poziomu tej strony.",
53
+ "data_sync.dashboard.start.error": "Nie udało się uruchomić przebiegu synchronizacji",
54
+ "data_sync.dashboard.start.eyebrow": "Uruchom raz lub ustaw cyklicznie",
55
+ "data_sync.dashboard.start.fullSync": "Uruchom jako pełną synchronizację",
56
+ "data_sync.dashboard.start.fullSyncHelp": "Zignoruj zapisany kursor i przetwórz w tym przebiegu całe źródło od nowa.",
57
+ "data_sync.dashboard.start.invalidBatchSize": "Rozmiar partii musi mieścić się w zakresie od 1 do 1000.",
58
58
  "data_sync.dashboard.start.providerManaged": "Ta integracja uruchamia synchronizacje z własnego przepływu konfiguracji. Otwórz ustawienia integracji, aby kontynuować.",
59
- "data_sync.dashboard.start.runNowDescription": "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.",
60
- "data_sync.dashboard.start.runNowFootnote": "Manual runs show progress immediately and land on the run detail page after launch.",
61
- "data_sync.dashboard.start.runNowTitle": "Run once now",
62
- "data_sync.dashboard.start.status.credentialsMissing": "Credentials missing",
63
- "data_sync.dashboard.start.status.credentialsReady": "Credentials ready",
64
- "data_sync.dashboard.start.status.disabled": "Integration disabled",
65
- "data_sync.dashboard.start.status.enabled": "Integration enabled",
66
- "data_sync.dashboard.start.submit": "Start sync",
67
- "data_sync.dashboard.start.success": "Sync run started",
68
- "data_sync.dashboard.start.title": "Start or schedule a sync",
59
+ "data_sync.dashboard.start.runNowDescription": "Użyj tej opcji dla najbliższej natychmiastowej synchronizacji. Rozmiar partii i tryb pełnej synchronizacji dotyczą wyłącznie tego ręcznego przebiegu.",
60
+ "data_sync.dashboard.start.runNowFootnote": "Ręczne przebiegi od razu pokazują postęp, a po uruchomieniu przenoszą na stronę szczegółów przebiegu.",
61
+ "data_sync.dashboard.start.runNowTitle": "Uruchom teraz jednorazowo",
62
+ "data_sync.dashboard.start.status.credentialsMissing": "Brak poświadczeń",
63
+ "data_sync.dashboard.start.status.credentialsReady": "Poświadczenia gotowe",
64
+ "data_sync.dashboard.start.status.disabled": "Integracja wyłączona",
65
+ "data_sync.dashboard.start.status.enabled": "Integracja włączona",
66
+ "data_sync.dashboard.start.submit": "Uruchom synchronizację",
67
+ "data_sync.dashboard.start.success": "Uruchomiono przebieg synchronizacji",
68
+ "data_sync.dashboard.start.title": "Uruchom lub zaplanuj synchronizację",
69
69
  "data_sync.dashboard.status.cancelled": "Anulowany",
70
70
  "data_sync.dashboard.status.completed": "Zakończony",
71
71
  "data_sync.dashboard.status.failed": "Nieudany",
@@ -73,28 +73,28 @@
73
73
  "data_sync.dashboard.status.pending": "Oczekujący",
74
74
  "data_sync.dashboard.status.running": "W toku",
75
75
  "data_sync.dashboard.title": "Przebiegi synchronizacji",
76
- "data_sync.integrationTab.columns.actions": "Actions",
77
- "data_sync.integrationTab.columns.lastRun": "Last run",
78
- "data_sync.integrationTab.columns.value": "Value",
79
- "data_sync.integrationTab.credentialsMissing": "Configure credentials before starting a sync.",
80
- "data_sync.integrationTab.credentialsMissingNotice": "Credentials are still missing. Save schedules first if you want, but manual and scheduled runs will fail until credentials are configured.",
81
- "data_sync.integrationTab.description": "Run one-off syncs or save recurring schedules for every supported entity directly from the integration detail page.",
82
- "data_sync.integrationTab.empty": "This provider does not expose any schedulable sync entities yet.",
83
- "data_sync.integrationTab.fullSyncShort": "Full",
84
- "data_sync.integrationTab.integrationDisabled": "Enable the integration before starting a sync.",
85
- "data_sync.integrationTab.integrationDisabledNotice": "The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.",
86
- "data_sync.integrationTab.loadError": "Failed to load sync schedules.",
87
- "data_sync.integrationTab.notAvailable": "This integration is not registered as a data sync provider.",
76
+ "data_sync.integrationTab.columns.actions": "Akcje",
77
+ "data_sync.integrationTab.columns.lastRun": "Ostatni przebieg",
78
+ "data_sync.integrationTab.columns.value": "Wartość",
79
+ "data_sync.integrationTab.credentialsMissing": "Skonfiguruj poświadczenia przed uruchomieniem synchronizacji.",
80
+ "data_sync.integrationTab.credentialsMissingNotice": "Wciąż brakuje poświadczeń. Możesz najpierw zapisać harmonogramy, ale ręczne i zaplanowane przebiegi będą kończyć się błędem, dopóki poświadczenia nie zostaną skonfigurowane.",
81
+ "data_sync.integrationTab.description": "Uruchamiaj jednorazowe synchronizacje albo zapisuj harmonogramy cykliczne dla każdej obsługiwanej encji bezpośrednio ze strony szczegółów integracji.",
82
+ "data_sync.integrationTab.empty": "Ten dostawca nie udostępnia jeszcze żadnych encji, które można zaplanować do synchronizacji.",
83
+ "data_sync.integrationTab.fullSyncShort": "Pełna",
84
+ "data_sync.integrationTab.integrationDisabled": "Włącz integrację przed uruchomieniem synchronizacji.",
85
+ "data_sync.integrationTab.integrationDisabledNotice": "Integracja jest wyłączona. Możesz teraz zapisać harmonogramy, ale przebiegi pozostaną zablokowane do czasu włączenia integracji.",
86
+ "data_sync.integrationTab.loadError": "Nie udało się załadować harmonogramów synchronizacji.",
87
+ "data_sync.integrationTab.notAvailable": "Ta integracja nie jest zarejestrowana jako dostawca synchronizacji danych.",
88
88
  "data_sync.integrationTab.providerManaged": "Uruchom tę integrację z jej przepływu konfiguracji dostawcy.",
89
89
  "data_sync.integrationTab.providerManagedNotice": "Ten dostawca wymaga własnego przepływu konfiguracji, zanim będzie można uruchomić synchronizację. Użyj karty dostawcy na tej stronie zamiast ogólnych harmonogramów.",
90
- "data_sync.integrationTab.refresh": "Refresh",
91
- "data_sync.integrationTab.runError": "Failed to start sync.",
92
- "data_sync.integrationTab.runNowHelp": "Run now uses the full-sync checkbox from the same row and starts progress tracking immediately in Data Sync.",
93
- "data_sync.integrationTab.runNowTitle": "Manual runs",
94
- "data_sync.integrationTab.runStarted": "Sync run started.",
95
- "data_sync.integrationTab.start": "Run now",
96
- "data_sync.integrationTab.starting": "Starting...",
97
- "data_sync.integrationTab.title": "Sync schedules",
90
+ "data_sync.integrationTab.refresh": "Odśwież",
91
+ "data_sync.integrationTab.runError": "Nie udało się uruchomić synchronizacji.",
92
+ "data_sync.integrationTab.runNowHelp": "Opcja Uruchom teraz korzysta z pola pełnej synchronizacji z tego samego wiersza i od razu rozpoczyna śledzenie postępu w Synchronizacji danych.",
93
+ "data_sync.integrationTab.runNowTitle": "Ręczne przebiegi",
94
+ "data_sync.integrationTab.runStarted": "Uruchomiono przebieg synchronizacji.",
95
+ "data_sync.integrationTab.start": "Uruchom teraz",
96
+ "data_sync.integrationTab.starting": "Uruchamianie...",
97
+ "data_sync.integrationTab.title": "Harmonogramy synchronizacji",
98
98
  "data_sync.nav.title": "Synchronizacja danych",
99
99
  "data_sync.runs.detail.back": "Powrót do przebiegów",
100
100
  "data_sync.runs.detail.cancel": "Anuluj",
@@ -2,11 +2,13 @@ export const metadata = {
2
2
  requireAuth: true,
3
3
  requireFeatures: ['directory.organizations.manage'],
4
4
  pageTitle: 'Edit Organization',
5
+ pageTitleKey: 'directory.organizations.form.title.edit',
5
6
  pageGroup: 'Directory',
7
+ pageGroupKey: 'directory.nav.group',
6
8
  navHidden: true,
7
9
  breadcrumb: [
8
- { label: 'Organizations', href: '/backend/directory/organizations' },
9
- { label: 'Edit' },
10
+ { label: 'Organizations', labelKey: 'directory.nav.organizations', href: '/backend/directory/organizations' },
11
+ { label: 'Edit', labelKey: 'common.edit' },
10
12
  ],
11
13
  }
12
14
 
@@ -2,11 +2,13 @@ export const metadata = {
2
2
  requireAuth: true,
3
3
  requireFeatures: ['directory.tenants.manage'],
4
4
  pageTitle: 'Edit Tenant',
5
+ pageTitleKey: 'directory.tenants.form.title.edit',
5
6
  pageGroup: 'Directory',
7
+ pageGroupKey: 'directory.nav.group',
6
8
  navHidden: true,
7
9
  breadcrumb: [
8
- { label: 'Tenants', href: '/backend/directory/tenants' },
9
- { label: 'Edit' },
10
+ { label: 'Tenants', labelKey: 'directory.nav.tenants', href: '/backend/directory/tenants' },
11
+ { label: 'Edit', labelKey: 'common.edit' },
10
12
  ],
11
13
  }
12
14
 
@@ -21,6 +21,7 @@ import {
21
21
  runIntegrationMutationGuardAfterSuccess,
22
22
  runIntegrationMutationGuards,
23
23
  } from '../../guards'
24
+ import { resolveIntegrationsOrganizationId } from '../../../lib/organization-scope'
24
25
 
25
26
  const idParamsSchema = z.object({ id: z.string().min(1) })
26
27
 
@@ -44,7 +45,8 @@ function resolveParams(ctx: { params?: Promise<{ id?: string }> | { id?: string
44
45
 
45
46
  export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
46
47
  const auth = await getAuthFromRequest(req)
47
- if (!auth?.tenantId || !auth.orgId) {
48
+ const organizationId = resolveIntegrationsOrganizationId(auth)
49
+ if (!auth?.tenantId || !organizationId) {
48
50
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
49
51
  }
50
52
 
@@ -61,7 +63,7 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
61
63
 
62
64
  const container = await createRequestContainer()
63
65
  const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
64
- const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
66
+ const scope = { organizationId: organizationId, tenantId: auth.tenantId }
65
67
 
66
68
  let values: Record<string, unknown> | null
67
69
  let updatedAt: Date | null
@@ -89,7 +91,8 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
89
91
 
90
92
  export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
91
93
  const auth = await getAuthFromRequest(req)
92
- if (!auth?.tenantId || !auth.orgId) {
94
+ const organizationId = resolveIntegrationsOrganizationId(auth)
95
+ if (!auth?.tenantId || !organizationId) {
93
96
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
94
97
  }
95
98
 
@@ -115,7 +118,7 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
115
118
  container,
116
119
  {
117
120
  tenantId: auth.tenantId,
118
- organizationId: auth.orgId,
121
+ organizationId,
119
122
  userId: auth.sub ?? '',
120
123
  resourceKind: 'integrations.integration',
121
124
  resourceId: integration.id,
@@ -141,7 +144,7 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
141
144
  }
142
145
 
143
146
  const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
144
- const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
147
+ const scope = { organizationId: organizationId, tenantId: auth.tenantId }
145
148
  const schema = credentialsService.getSchema(integration.id)
146
149
 
147
150
  try {
@@ -190,13 +193,13 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
190
193
  await emitIntegrationsEvent('integrations.credentials.updated', {
191
194
  integrationId: integration.id,
192
195
  tenantId: auth.tenantId,
193
- organizationId: auth.orgId,
196
+ organizationId,
194
197
  userId: auth.sub,
195
198
  })
196
199
 
197
200
  await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
198
201
  tenantId: auth.tenantId,
199
- organizationId: auth.orgId,
202
+ organizationId,
200
203
  userId: auth.sub ?? '',
201
204
  resourceKind: 'integrations.integration',
202
205
  resourceId: integration.id,