@open-mercato/core 0.6.5-develop.4695.1.42ee0ddf0e → 0.6.5-develop.4718.1.56d834bb34

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 (30) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/currenciesFixtures.js +2 -1
  3. package/dist/helpers/integration/currenciesFixtures.js.map +2 -2
  4. package/dist/helpers/integration/undoHarness.js +2 -1
  5. package/dist/helpers/integration/undoHarness.js.map +2 -2
  6. package/dist/modules/auth/frontend/reset/[token]/page.js +6 -2
  7. package/dist/modules/auth/frontend/reset/[token]/page.js.map +2 -2
  8. package/dist/modules/entities/lib/helpers.js +1 -2
  9. package/dist/modules/entities/lib/helpers.js.map +2 -2
  10. package/package.json +7 -7
  11. package/src/helpers/integration/currenciesFixtures.ts +2 -1
  12. package/src/helpers/integration/undoHarness.ts +2 -1
  13. package/src/modules/auth/frontend/reset/[token]/page.tsx +7 -2
  14. package/src/modules/currencies/i18n/de.json +1 -0
  15. package/src/modules/currencies/i18n/en.json +1 -0
  16. package/src/modules/currencies/i18n/es.json +1 -0
  17. package/src/modules/currencies/i18n/pl.json +1 -0
  18. package/src/modules/data_sync/i18n/de.json +1 -0
  19. package/src/modules/data_sync/i18n/en.json +1 -0
  20. package/src/modules/data_sync/i18n/es.json +1 -0
  21. package/src/modules/data_sync/i18n/pl.json +1 -0
  22. package/src/modules/entities/lib/helpers.ts +7 -11
  23. package/src/modules/inbox_ops/i18n/de.json +2 -0
  24. package/src/modules/inbox_ops/i18n/en.json +2 -0
  25. package/src/modules/inbox_ops/i18n/es.json +2 -0
  26. package/src/modules/inbox_ops/i18n/pl.json +2 -0
  27. package/src/modules/integrations/i18n/de.json +2 -0
  28. package/src/modules/integrations/i18n/en.json +2 -0
  29. package/src/modules/integrations/i18n/es.json +2 -0
  30. package/src/modules/integrations/i18n/pl.json +2 -0
@@ -1,4 +1,4 @@
1
- [build:core] found 3206 entry points
1
+ [build:core] found 3213 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 185 entry points
4
4
  [build:core:generated] built successfully
@@ -1,4 +1,5 @@
1
1
  import { expect } from "@playwright/test";
2
+ import { randomInt } from "node:crypto";
2
3
  import { apiRequest } from "./api.js";
3
4
  import { getTokenContext } from "./generalFixtures.js";
4
5
  async function createCurrencyFixture(request, token, input) {
@@ -26,7 +27,7 @@ const SEEDED_CURRENCY_CODES = /* @__PURE__ */ new Set([
26
27
  ]);
27
28
  const reservedCurrencyCodes = /* @__PURE__ */ new Set();
28
29
  function generateUniqueCurrencyCode() {
29
- const letter = () => String.fromCharCode(65 + Math.floor(Math.random() * 26));
30
+ const letter = () => String.fromCharCode(65 + randomInt(26));
30
31
  for (let attempt = 0; attempt < 200; attempt += 1) {
31
32
  const code = `${letter()}${letter()}${letter()}`;
32
33
  if (!SEEDED_CURRENCY_CODES.has(code) && !reservedCurrencyCodes.has(code)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/helpers/integration/currenciesFixtures.ts"],
4
- "sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test';\nimport { apiRequest } from './api';\nimport { getTokenContext } from './generalFixtures';\n\nexport async function createCurrencyFixture(\n request: APIRequestContext,\n token: string,\n input: { code: string; name: string; symbol?: string },\n): Promise<string> {\n const { organizationId, tenantId } = getTokenContext(token);\n const response = await apiRequest(request, 'POST', '/api/currencies/currencies', {\n token,\n data: { organizationId, tenantId, code: input.code, name: input.name, symbol: input.symbol ?? null },\n });\n expect(response.ok(), `Failed to create currency fixture: ${response.status()}`).toBeTruthy();\n const body = (await response.json()) as { id?: string };\n expect(typeof body.id === 'string' && body.id.length > 0).toBeTruthy();\n return body.id as string;\n}\n\n// Codes seeded by the currencies module (seedExampleCurrencies). Generated test\n// codes avoid these so fixtures never collide with seeded rows.\nconst SEEDED_CURRENCY_CODES = new Set([\n 'USD', 'EUR', 'JPY', 'GBP', 'CHF', 'CAD', 'AUD', 'CNY', 'CNH', 'PLN',\n]);\n// Reserved across the worker so two fixtures never draw the same code in one run.\nconst reservedCurrencyCodes = new Set<string>();\n\n/** Draws an ISO-style three-letter code unused by seeds or earlier fixtures. */\nexport function generateUniqueCurrencyCode(): string {\n const letter = () => String.fromCharCode(65 + Math.floor(Math.random() * 26));\n for (let attempt = 0; attempt < 200; attempt += 1) {\n const code = `${letter()}${letter()}${letter()}`;\n if (!SEEDED_CURRENCY_CODES.has(code) && !reservedCurrencyCodes.has(code)) {\n reservedCurrencyCodes.add(code);\n return code;\n }\n }\n throw new Error('[internal] exhausted unique currency code space');\n}\n\n/**\n * Creates a currency with a generated unique code and returns its id and code.\n *\n * Currency DELETE is a soft delete, but the (organization, tenant, code) unique\n * constraint still counts soft-deleted rows \u2014 re-using a code an earlier test\n * soft-deleted makes the create fail. Drawing from the full three-letter space\n * (minus seeds) and retrying with a fresh code on an accidental collision keeps\n * fixture setup deterministic across runs that share a database.\n */\nexport async function createRandomCurrencyFixture(\n request: APIRequestContext,\n token: string,\n input: { name: string; symbol?: string; isActive?: boolean },\n): Promise<{ id: string; code: string }> {\n const { organizationId, tenantId } = getTokenContext(token);\n let lastStatus = 0;\n for (let attempt = 0; attempt < 8; attempt += 1) {\n const code = generateUniqueCurrencyCode();\n const data: Record<string, unknown> = {\n organizationId,\n tenantId,\n code,\n name: input.name,\n symbol: input.symbol ?? null,\n };\n if (typeof input.isActive === 'boolean') data.isActive = input.isActive;\n const response = await apiRequest(request, 'POST', '/api/currencies/currencies', { token, data });\n if (response.status() === 201) {\n const body = (await response.json()) as { id?: string };\n if (typeof body.id === 'string' && body.id.length > 0) {\n return { id: body.id, code };\n }\n }\n lastStatus = response.status();\n }\n throw new Error(`[internal] failed to create currency fixture after retries (last status ${lastStatus})`);\n}\n\nexport async function createFetchConfigFixture(\n request: APIRequestContext,\n token: string,\n input: { provider: string; isEnabled: boolean },\n): Promise<string> {\n const response = await apiRequest(request, 'POST', '/api/currencies/fetch-configs', {\n token,\n data: input,\n });\n expect(response.ok(), `Failed to create fetch config fixture: ${response.status()}`).toBeTruthy();\n const body = (await response.json()) as { config?: { id?: string } };\n const id = body.config?.id;\n expect(typeof id === 'string' && id.length > 0).toBeTruthy();\n return id as string;\n}\n\nexport async function deleteCurrenciesEntityIfExists(\n request: APIRequestContext,\n token: string | null,\n path: string,\n id: string | null,\n): Promise<void> {\n if (!token || !id) return;\n try {\n await apiRequest(request, 'DELETE', `${path}?id=${encodeURIComponent(id)}`, { token });\n } catch {\n return;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test';\nimport { randomInt } from 'node:crypto';\nimport { apiRequest } from './api';\nimport { getTokenContext } from './generalFixtures';\n\nexport async function createCurrencyFixture(\n request: APIRequestContext,\n token: string,\n input: { code: string; name: string; symbol?: string },\n): Promise<string> {\n const { organizationId, tenantId } = getTokenContext(token);\n const response = await apiRequest(request, 'POST', '/api/currencies/currencies', {\n token,\n data: { organizationId, tenantId, code: input.code, name: input.name, symbol: input.symbol ?? null },\n });\n expect(response.ok(), `Failed to create currency fixture: ${response.status()}`).toBeTruthy();\n const body = (await response.json()) as { id?: string };\n expect(typeof body.id === 'string' && body.id.length > 0).toBeTruthy();\n return body.id as string;\n}\n\n// Codes seeded by the currencies module (seedExampleCurrencies). Generated test\n// codes avoid these so fixtures never collide with seeded rows.\nconst SEEDED_CURRENCY_CODES = new Set([\n 'USD', 'EUR', 'JPY', 'GBP', 'CHF', 'CAD', 'AUD', 'CNY', 'CNH', 'PLN',\n]);\n// Reserved across the worker so two fixtures never draw the same code in one run.\nconst reservedCurrencyCodes = new Set<string>();\n\n/** Draws an ISO-style three-letter code unused by seeds or earlier fixtures. */\nexport function generateUniqueCurrencyCode(): string {\n const letter = () => String.fromCharCode(65 + randomInt(26));\n for (let attempt = 0; attempt < 200; attempt += 1) {\n const code = `${letter()}${letter()}${letter()}`;\n if (!SEEDED_CURRENCY_CODES.has(code) && !reservedCurrencyCodes.has(code)) {\n reservedCurrencyCodes.add(code);\n return code;\n }\n }\n throw new Error('[internal] exhausted unique currency code space');\n}\n\n/**\n * Creates a currency with a generated unique code and returns its id and code.\n *\n * Currency DELETE is a soft delete, but the (organization, tenant, code) unique\n * constraint still counts soft-deleted rows \u2014 re-using a code an earlier test\n * soft-deleted makes the create fail. Drawing from the full three-letter space\n * (minus seeds) and retrying with a fresh code on an accidental collision keeps\n * fixture setup deterministic across runs that share a database.\n */\nexport async function createRandomCurrencyFixture(\n request: APIRequestContext,\n token: string,\n input: { name: string; symbol?: string; isActive?: boolean },\n): Promise<{ id: string; code: string }> {\n const { organizationId, tenantId } = getTokenContext(token);\n let lastStatus = 0;\n for (let attempt = 0; attempt < 8; attempt += 1) {\n const code = generateUniqueCurrencyCode();\n const data: Record<string, unknown> = {\n organizationId,\n tenantId,\n code,\n name: input.name,\n symbol: input.symbol ?? null,\n };\n if (typeof input.isActive === 'boolean') data.isActive = input.isActive;\n const response = await apiRequest(request, 'POST', '/api/currencies/currencies', { token, data });\n if (response.status() === 201) {\n const body = (await response.json()) as { id?: string };\n if (typeof body.id === 'string' && body.id.length > 0) {\n return { id: body.id, code };\n }\n }\n lastStatus = response.status();\n }\n throw new Error(`[internal] failed to create currency fixture after retries (last status ${lastStatus})`);\n}\n\nexport async function createFetchConfigFixture(\n request: APIRequestContext,\n token: string,\n input: { provider: string; isEnabled: boolean },\n): Promise<string> {\n const response = await apiRequest(request, 'POST', '/api/currencies/fetch-configs', {\n token,\n data: input,\n });\n expect(response.ok(), `Failed to create fetch config fixture: ${response.status()}`).toBeTruthy();\n const body = (await response.json()) as { config?: { id?: string } };\n const id = body.config?.id;\n expect(typeof id === 'string' && id.length > 0).toBeTruthy();\n return id as string;\n}\n\nexport async function deleteCurrenciesEntityIfExists(\n request: APIRequestContext,\n token: string | null,\n path: string,\n id: string | null,\n): Promise<void> {\n if (!token || !id) return;\n try {\n await apiRequest(request, 'DELETE', `${path}?id=${encodeURIComponent(id)}`, { token });\n } catch {\n return;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { expect, test } from "@playwright/test";
2
+ import { randomInt } from "node:crypto";
2
3
  import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
3
4
  import { apiRequest } from "./api.js";
4
5
  import { expectId, readJsonSafe } from "./generalFixtures.js";
@@ -102,7 +103,7 @@ async function deleteEntity(request, token, entity, id) {
102
103
  return apiRequest(request, "DELETE", path, { token });
103
104
  }
104
105
  async function runCrudUndoRoundTrip(request, token, entity) {
105
- const stamp = `${Date.now()}${Math.floor(Math.random() * 1e3)}`;
106
+ const stamp = `${Date.now()}${randomInt(1e3)}`;
106
107
  let createUndoId = null;
107
108
  let cycleId = null;
108
109
  try {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/helpers/integration/undoHarness.ts"],
4
- "sourcesContent": ["import { type APIRequestContext, type APIResponse, expect, test } from '@playwright/test'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { apiRequest } from './api'\nimport { expectId, readJsonSafe } from './generalFixtures'\n\n/**\n * Shared harness for verifying Undo/Redo correctness against the real command bus.\n *\n * Every mutating Open Mercato API response carries the operation metadata in the\n * `x-om-operation` header (`omop:<urlencoded JSON>`) containing the `undoToken` and the\n * audit log `id` (used as `logId` for redo). These helpers extract that envelope and drive\n * the real undo/redo endpoints so tests can assert full state restoration per TC-UNDO-001.\n */\n\nconst HEADER_PREFIX = 'omop:'\nconst UNDO_PATH = '/api/audit_logs/audit-logs/actions/undo'\nconst REDO_PATH = '/api/audit_logs/audit-logs/actions/redo'\nconst ACTIONS_PATH = '/api/audit_logs/audit-logs/actions'\nexport const UNDO_TESTS_DISABLED_ENV = 'OM_INTEGRATION_UNDO_TESTS_DISABLED'\n\nexport type Operation = {\n logId: string\n undoToken: string\n commandId: string\n resourceKind: string | null\n resourceId: string | null\n}\n\nexport type CrudUndoEntityConfig = {\n label: string\n collectionPath: string\n field: string\n createPayload: (stamp: string) => Record<string, unknown>\n updatePayload: (id: string, stamp: string) => Record<string, unknown>\n readPath?: (id: string) => string\n deletePath?: (id: string) => string\n createStatus?: number\n updateStatus?: number\n}\n\nexport function undoTestsDisabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBooleanWithDefault(env[UNDO_TESTS_DISABLED_ENV], false)\n}\n\nexport function skipIfUndoTestsDisabled(): void {\n test.skip(undoTestsDisabled(), `${UNDO_TESTS_DISABLED_ENV} is set \u2014 undo/redo integration tests skipped`)\n}\n\n/** Parse the `x-om-operation` header into a structured operation, or null when absent/malformed. */\nexport function extractOperation(response: APIResponse): Operation | null {\n const header = response.headers()['x-om-operation']\n if (!header || typeof header !== 'string') return null\n const trimmed = header.startsWith(HEADER_PREFIX) ? header.slice(HEADER_PREFIX.length) : header\n try {\n const parsed = JSON.parse(decodeURIComponent(trimmed)) as Record<string, unknown>\n if (typeof parsed.id !== 'string' || typeof parsed.commandId !== 'string') return null\n if (typeof parsed.undoToken !== 'string' || !parsed.undoToken) return null\n return {\n logId: parsed.id,\n undoToken: parsed.undoToken,\n commandId: parsed.commandId,\n resourceKind: (parsed.resourceKind as string) ?? null,\n resourceId: (parsed.resourceId as string) ?? null,\n }\n } catch {\n return null\n }\n}\n\n/** Like extractOperation but fails the test if no undo token was issued. */\nexport function expectOperation(response: APIResponse, context: string): Operation {\n const op = extractOperation(response)\n expect(op, `Expected an undo token (x-om-operation header) for ${context}, got none`).toBeTruthy()\n return op as Operation\n}\n\nexport async function undoByToken(request: APIRequestContext, token: string, undoToken: string): Promise<APIResponse> {\n return apiRequest(request, 'POST', UNDO_PATH, { token, data: { undoToken } })\n}\n\nexport async function redoByLogId(request: APIRequestContext, token: string, logId: string): Promise<APIResponse> {\n return apiRequest(request, 'POST', REDO_PATH, { token, data: { logId } })\n}\n\n/** Undo and assert success; returns the resolved logId. */\nexport async function undoOk(request: APIRequestContext, token: string, undoToken: string, context: string): Promise<string> {\n const res = await undoByToken(request, token, undoToken)\n const body = (await res.json().catch(() => null)) as { ok?: boolean; logId?: string } | null\n expect(res.ok(), `Undo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy()\n expect(body?.ok, `Undo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy()\n return body?.logId as string\n}\n\n/** Redo and assert success; returns the new operation (new undoToken + logId). */\nexport async function redoOk(request: APIRequestContext, token: string, logId: string, context: string): Promise<{ logId: string; undoToken: string | null }> {\n const res = await redoByLogId(request, token, logId)\n const body = (await res.json().catch(() => null)) as { ok?: boolean; logId?: string; undoToken?: string } | null\n expect(res.ok(), `Redo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy()\n expect(body?.ok, `Redo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy()\n return { logId: body?.logId as string, undoToken: body?.undoToken ?? null }\n}\n\n/** Assert that undoing an already-consumed token is rejected (token consumption / no double-undo). */\nexport async function expectTokenConsumed(request: APIRequestContext, token: string, undoToken: string, context: string): Promise<void> {\n const res = await undoByToken(request, token, undoToken)\n expect(res.ok(), `Expected double-undo to be rejected for ${context}, but it succeeded`).toBeFalsy()\n}\n\n/** Fetch undoable actions list (for Version History assertions). */\nexport async function listUndoable(request: APIRequestContext, token: string, params: Record<string, string> = {}): Promise<unknown> {\n const qs = new URLSearchParams({ undoableOnly: 'true', ...params }).toString()\n const res = await apiRequest(request, 'GET', `${ACTIONS_PATH}?${qs}`, { token })\n return res.json().catch(() => null)\n}\n\n/**\n * Deep-equality assertion for a selected set of fields between two entity snapshots.\n * Reports the first mismatching field with context for clear bug triage.\n */\nexport function assertFieldsEqual(\n actual: Record<string, unknown> | null | undefined,\n expected: Record<string, unknown> | null | undefined,\n fields: string[],\n context: string,\n): void {\n expect(actual, `${context}: actual entity missing`).toBeTruthy()\n expect(expected, `${context}: expected entity missing`).toBeTruthy()\n for (const field of fields) {\n expect(\n JSON.stringify((actual as Record<string, unknown>)[field]),\n `${context}: field \"${field}\" not restored (expected ${JSON.stringify((expected as Record<string, unknown>)[field])}, got ${JSON.stringify((actual as Record<string, unknown>)[field])})`,\n ).toBe(JSON.stringify((expected as Record<string, unknown>)[field]))\n }\n}\n\nfunction findRecord(body: unknown, id: string): Record<string, unknown> | null {\n if (!body || typeof body !== 'object') return null\n if (!Array.isArray(body) && (body as Record<string, unknown>).id === id) {\n return body as Record<string, unknown>\n }\n for (const value of Array.isArray(body) ? body : Object.values(body)) {\n const found = findRecord(value, id)\n if (found) return found\n }\n return null\n}\n\nasync function readRecord(\n request: APIRequestContext,\n token: string,\n entity: CrudUndoEntityConfig,\n id: string,\n): Promise<Record<string, unknown> | null> {\n const path = entity.readPath?.(id) ?? `${entity.collectionPath}?id=${encodeURIComponent(id)}`\n const response = await apiRequest(request, 'GET', path, { token })\n const body = await readJsonSafe(response)\n if (!response.ok()) return null\n return findRecord(body, id)\n}\n\nfunction fieldValue(record: Record<string, unknown> | null, field: string): unknown {\n return record?.[field]\n}\n\nasync function deleteEntity(\n request: APIRequestContext,\n token: string,\n entity: CrudUndoEntityConfig,\n id: string,\n): Promise<APIResponse> {\n const path = entity.deletePath?.(id) ?? `${entity.collectionPath}?id=${encodeURIComponent(id)}`\n return apiRequest(request, 'DELETE', path, { token })\n}\n\nexport async function runCrudUndoRoundTrip(\n request: APIRequestContext,\n token: string,\n entity: CrudUndoEntityConfig,\n): Promise<void> {\n const stamp = `${Date.now()}${Math.floor(Math.random() * 1000)}`\n let createUndoId: string | null = null\n let cycleId: string | null = null\n\n try {\n const createUndoRes = await apiRequest(request, 'POST', entity.collectionPath, {\n token,\n data: entity.createPayload(`${stamp}a`),\n })\n expect(createUndoRes.status(), `${entity.label} create-for-undo status`).toBe(entity.createStatus ?? 201)\n const createUndoOp = expectOperation(createUndoRes, `${entity.label}.create`)\n createUndoId = createUndoOp.resourceId || expectId((await readJsonSafe<Record<string, unknown>>(createUndoRes))?.id, `${entity.label} create id`)\n expect(fieldValue(await readRecord(request, token, entity, createUndoId), entity.field), `${entity.label} field readable after create`).toBeDefined()\n\n await undoOk(request, token, createUndoOp.undoToken, `${entity.label} undo create`)\n expect(await readRecord(request, token, entity, createUndoId), `${entity.label} create\u2192undo soft-deletes/removes the record (I3)`).toBeNull()\n await expectTokenConsumed(request, token, createUndoOp.undoToken, `${entity.label} create token consumed (I5)`)\n\n const createRes = await apiRequest(request, 'POST', entity.collectionPath, {\n token,\n data: entity.createPayload(`${stamp}b`),\n })\n expect(createRes.status(), `${entity.label} create status`).toBe(entity.createStatus ?? 201)\n const createOp = expectOperation(createRes, `${entity.label}.create`)\n cycleId = createOp.resourceId || expectId((await readJsonSafe<Record<string, unknown>>(createRes))?.id, `${entity.label} cycle id`)\n\n const beforeUpdate = await readRecord(request, token, entity, cycleId)\n const beforeValue = fieldValue(beforeUpdate, entity.field)\n expect(beforeValue, `${entity.label} field readable before update`).toBeDefined()\n\n const updateRes = await apiRequest(request, 'PUT', entity.collectionPath, {\n token,\n data: entity.updatePayload(cycleId, stamp),\n })\n expect(updateRes.status(), `${entity.label} update status`).toBe(entity.updateStatus ?? 200)\n const updateOp = expectOperation(updateRes, `${entity.label}.update`)\n const afterUpdate = await readRecord(request, token, entity, cycleId)\n const afterUpdateValue = fieldValue(afterUpdate, entity.field)\n expect(JSON.stringify(afterUpdateValue), `${entity.label} field changed by update`).not.toBe(JSON.stringify(beforeValue))\n\n await new Promise((resolve) => setTimeout(resolve, 10))\n await undoOk(request, token, updateOp.undoToken, `${entity.label} undo update`)\n const afterUndo = await readRecord(request, token, entity, cycleId)\n expect(JSON.stringify(fieldValue(afterUndo, entity.field)), `${entity.label} update\u2192undo restores ${entity.field} (I1)`).toBe(JSON.stringify(beforeValue))\n if (typeof beforeUpdate?.updatedAt === 'string' && typeof afterUndo?.updatedAt === 'string') {\n expect(afterUndo.updatedAt, `${entity.label} undo bumps updatedAt`).not.toBe(beforeUpdate.updatedAt)\n }\n\n await redoOk(request, token, updateOp.logId, `${entity.label} redo update`)\n expect(JSON.stringify(fieldValue(await readRecord(request, token, entity, cycleId), entity.field)), `${entity.label} redo re-applies update (I6)`).toBe(JSON.stringify(afterUpdateValue))\n\n const deleteRes = await deleteEntity(request, token, entity, cycleId)\n expect(deleteRes.ok(), `${entity.label} delete status ${deleteRes.status()}`).toBeTruthy()\n const deleteOp = expectOperation(deleteRes, `${entity.label}.delete`)\n expect(await readRecord(request, token, entity, cycleId), `${entity.label} deleted record should not read`).toBeNull()\n\n await undoOk(request, token, deleteOp.undoToken, `${entity.label} undo delete`)\n expect(fieldValue(await readRecord(request, token, entity, cycleId), entity.field), `${entity.label} delete\u2192undo re-materializes (I2)`).toBeDefined()\n } finally {\n if (createUndoId) await deleteEntity(request, token, entity, createUndoId).catch(() => {})\n if (cycleId) await deleteEntity(request, token, entity, cycleId).catch(() => {})\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { type APIRequestContext, type APIResponse, expect, test } from '@playwright/test'\nimport { randomInt } from 'node:crypto'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { apiRequest } from './api'\nimport { expectId, readJsonSafe } from './generalFixtures'\n\n/**\n * Shared harness for verifying Undo/Redo correctness against the real command bus.\n *\n * Every mutating Open Mercato API response carries the operation metadata in the\n * `x-om-operation` header (`omop:<urlencoded JSON>`) containing the `undoToken` and the\n * audit log `id` (used as `logId` for redo). These helpers extract that envelope and drive\n * the real undo/redo endpoints so tests can assert full state restoration per TC-UNDO-001.\n */\n\nconst HEADER_PREFIX = 'omop:'\nconst UNDO_PATH = '/api/audit_logs/audit-logs/actions/undo'\nconst REDO_PATH = '/api/audit_logs/audit-logs/actions/redo'\nconst ACTIONS_PATH = '/api/audit_logs/audit-logs/actions'\nexport const UNDO_TESTS_DISABLED_ENV = 'OM_INTEGRATION_UNDO_TESTS_DISABLED'\n\nexport type Operation = {\n logId: string\n undoToken: string\n commandId: string\n resourceKind: string | null\n resourceId: string | null\n}\n\nexport type CrudUndoEntityConfig = {\n label: string\n collectionPath: string\n field: string\n createPayload: (stamp: string) => Record<string, unknown>\n updatePayload: (id: string, stamp: string) => Record<string, unknown>\n readPath?: (id: string) => string\n deletePath?: (id: string) => string\n createStatus?: number\n updateStatus?: number\n}\n\nexport function undoTestsDisabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return parseBooleanWithDefault(env[UNDO_TESTS_DISABLED_ENV], false)\n}\n\nexport function skipIfUndoTestsDisabled(): void {\n test.skip(undoTestsDisabled(), `${UNDO_TESTS_DISABLED_ENV} is set \u2014 undo/redo integration tests skipped`)\n}\n\n/** Parse the `x-om-operation` header into a structured operation, or null when absent/malformed. */\nexport function extractOperation(response: APIResponse): Operation | null {\n const header = response.headers()['x-om-operation']\n if (!header || typeof header !== 'string') return null\n const trimmed = header.startsWith(HEADER_PREFIX) ? header.slice(HEADER_PREFIX.length) : header\n try {\n const parsed = JSON.parse(decodeURIComponent(trimmed)) as Record<string, unknown>\n if (typeof parsed.id !== 'string' || typeof parsed.commandId !== 'string') return null\n if (typeof parsed.undoToken !== 'string' || !parsed.undoToken) return null\n return {\n logId: parsed.id,\n undoToken: parsed.undoToken,\n commandId: parsed.commandId,\n resourceKind: (parsed.resourceKind as string) ?? null,\n resourceId: (parsed.resourceId as string) ?? null,\n }\n } catch {\n return null\n }\n}\n\n/** Like extractOperation but fails the test if no undo token was issued. */\nexport function expectOperation(response: APIResponse, context: string): Operation {\n const op = extractOperation(response)\n expect(op, `Expected an undo token (x-om-operation header) for ${context}, got none`).toBeTruthy()\n return op as Operation\n}\n\nexport async function undoByToken(request: APIRequestContext, token: string, undoToken: string): Promise<APIResponse> {\n return apiRequest(request, 'POST', UNDO_PATH, { token, data: { undoToken } })\n}\n\nexport async function redoByLogId(request: APIRequestContext, token: string, logId: string): Promise<APIResponse> {\n return apiRequest(request, 'POST', REDO_PATH, { token, data: { logId } })\n}\n\n/** Undo and assert success; returns the resolved logId. */\nexport async function undoOk(request: APIRequestContext, token: string, undoToken: string, context: string): Promise<string> {\n const res = await undoByToken(request, token, undoToken)\n const body = (await res.json().catch(() => null)) as { ok?: boolean; logId?: string } | null\n expect(res.ok(), `Undo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy()\n expect(body?.ok, `Undo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy()\n return body?.logId as string\n}\n\n/** Redo and assert success; returns the new operation (new undoToken + logId). */\nexport async function redoOk(request: APIRequestContext, token: string, logId: string, context: string): Promise<{ logId: string; undoToken: string | null }> {\n const res = await redoByLogId(request, token, logId)\n const body = (await res.json().catch(() => null)) as { ok?: boolean; logId?: string; undoToken?: string } | null\n expect(res.ok(), `Redo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy()\n expect(body?.ok, `Redo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy()\n return { logId: body?.logId as string, undoToken: body?.undoToken ?? null }\n}\n\n/** Assert that undoing an already-consumed token is rejected (token consumption / no double-undo). */\nexport async function expectTokenConsumed(request: APIRequestContext, token: string, undoToken: string, context: string): Promise<void> {\n const res = await undoByToken(request, token, undoToken)\n expect(res.ok(), `Expected double-undo to be rejected for ${context}, but it succeeded`).toBeFalsy()\n}\n\n/** Fetch undoable actions list (for Version History assertions). */\nexport async function listUndoable(request: APIRequestContext, token: string, params: Record<string, string> = {}): Promise<unknown> {\n const qs = new URLSearchParams({ undoableOnly: 'true', ...params }).toString()\n const res = await apiRequest(request, 'GET', `${ACTIONS_PATH}?${qs}`, { token })\n return res.json().catch(() => null)\n}\n\n/**\n * Deep-equality assertion for a selected set of fields between two entity snapshots.\n * Reports the first mismatching field with context for clear bug triage.\n */\nexport function assertFieldsEqual(\n actual: Record<string, unknown> | null | undefined,\n expected: Record<string, unknown> | null | undefined,\n fields: string[],\n context: string,\n): void {\n expect(actual, `${context}: actual entity missing`).toBeTruthy()\n expect(expected, `${context}: expected entity missing`).toBeTruthy()\n for (const field of fields) {\n expect(\n JSON.stringify((actual as Record<string, unknown>)[field]),\n `${context}: field \"${field}\" not restored (expected ${JSON.stringify((expected as Record<string, unknown>)[field])}, got ${JSON.stringify((actual as Record<string, unknown>)[field])})`,\n ).toBe(JSON.stringify((expected as Record<string, unknown>)[field]))\n }\n}\n\nfunction findRecord(body: unknown, id: string): Record<string, unknown> | null {\n if (!body || typeof body !== 'object') return null\n if (!Array.isArray(body) && (body as Record<string, unknown>).id === id) {\n return body as Record<string, unknown>\n }\n for (const value of Array.isArray(body) ? body : Object.values(body)) {\n const found = findRecord(value, id)\n if (found) return found\n }\n return null\n}\n\nasync function readRecord(\n request: APIRequestContext,\n token: string,\n entity: CrudUndoEntityConfig,\n id: string,\n): Promise<Record<string, unknown> | null> {\n const path = entity.readPath?.(id) ?? `${entity.collectionPath}?id=${encodeURIComponent(id)}`\n const response = await apiRequest(request, 'GET', path, { token })\n const body = await readJsonSafe(response)\n if (!response.ok()) return null\n return findRecord(body, id)\n}\n\nfunction fieldValue(record: Record<string, unknown> | null, field: string): unknown {\n return record?.[field]\n}\n\nasync function deleteEntity(\n request: APIRequestContext,\n token: string,\n entity: CrudUndoEntityConfig,\n id: string,\n): Promise<APIResponse> {\n const path = entity.deletePath?.(id) ?? `${entity.collectionPath}?id=${encodeURIComponent(id)}`\n return apiRequest(request, 'DELETE', path, { token })\n}\n\nexport async function runCrudUndoRoundTrip(\n request: APIRequestContext,\n token: string,\n entity: CrudUndoEntityConfig,\n): Promise<void> {\n const stamp = `${Date.now()}${randomInt(1000)}`\n let createUndoId: string | null = null\n let cycleId: string | null = null\n\n try {\n const createUndoRes = await apiRequest(request, 'POST', entity.collectionPath, {\n token,\n data: entity.createPayload(`${stamp}a`),\n })\n expect(createUndoRes.status(), `${entity.label} create-for-undo status`).toBe(entity.createStatus ?? 201)\n const createUndoOp = expectOperation(createUndoRes, `${entity.label}.create`)\n createUndoId = createUndoOp.resourceId || expectId((await readJsonSafe<Record<string, unknown>>(createUndoRes))?.id, `${entity.label} create id`)\n expect(fieldValue(await readRecord(request, token, entity, createUndoId), entity.field), `${entity.label} field readable after create`).toBeDefined()\n\n await undoOk(request, token, createUndoOp.undoToken, `${entity.label} undo create`)\n expect(await readRecord(request, token, entity, createUndoId), `${entity.label} create\u2192undo soft-deletes/removes the record (I3)`).toBeNull()\n await expectTokenConsumed(request, token, createUndoOp.undoToken, `${entity.label} create token consumed (I5)`)\n\n const createRes = await apiRequest(request, 'POST', entity.collectionPath, {\n token,\n data: entity.createPayload(`${stamp}b`),\n })\n expect(createRes.status(), `${entity.label} create status`).toBe(entity.createStatus ?? 201)\n const createOp = expectOperation(createRes, `${entity.label}.create`)\n cycleId = createOp.resourceId || expectId((await readJsonSafe<Record<string, unknown>>(createRes))?.id, `${entity.label} cycle id`)\n\n const beforeUpdate = await readRecord(request, token, entity, cycleId)\n const beforeValue = fieldValue(beforeUpdate, entity.field)\n expect(beforeValue, `${entity.label} field readable before update`).toBeDefined()\n\n const updateRes = await apiRequest(request, 'PUT', entity.collectionPath, {\n token,\n data: entity.updatePayload(cycleId, stamp),\n })\n expect(updateRes.status(), `${entity.label} update status`).toBe(entity.updateStatus ?? 200)\n const updateOp = expectOperation(updateRes, `${entity.label}.update`)\n const afterUpdate = await readRecord(request, token, entity, cycleId)\n const afterUpdateValue = fieldValue(afterUpdate, entity.field)\n expect(JSON.stringify(afterUpdateValue), `${entity.label} field changed by update`).not.toBe(JSON.stringify(beforeValue))\n\n await new Promise((resolve) => setTimeout(resolve, 10))\n await undoOk(request, token, updateOp.undoToken, `${entity.label} undo update`)\n const afterUndo = await readRecord(request, token, entity, cycleId)\n expect(JSON.stringify(fieldValue(afterUndo, entity.field)), `${entity.label} update\u2192undo restores ${entity.field} (I1)`).toBe(JSON.stringify(beforeValue))\n if (typeof beforeUpdate?.updatedAt === 'string' && typeof afterUndo?.updatedAt === 'string') {\n expect(afterUndo.updatedAt, `${entity.label} undo bumps updatedAt`).not.toBe(beforeUpdate.updatedAt)\n }\n\n await redoOk(request, token, updateOp.logId, `${entity.label} redo update`)\n expect(JSON.stringify(fieldValue(await readRecord(request, token, entity, cycleId), entity.field)), `${entity.label} redo re-applies update (I6)`).toBe(JSON.stringify(afterUpdateValue))\n\n const deleteRes = await deleteEntity(request, token, entity, cycleId)\n expect(deleteRes.ok(), `${entity.label} delete status ${deleteRes.status()}`).toBeTruthy()\n const deleteOp = expectOperation(deleteRes, `${entity.label}.delete`)\n expect(await readRecord(request, token, entity, cycleId), `${entity.label} deleted record should not read`).toBeNull()\n\n await undoOk(request, token, deleteOp.undoToken, `${entity.label} undo delete`)\n expect(fieldValue(await readRecord(request, token, entity, cycleId), entity.field), `${entity.label} delete\u2192undo re-materializes (I2)`).toBeDefined()\n } finally {\n if (createUndoId) await deleteEntity(request, token, entity, createUndoId).catch(() => {})\n if (cycleId) await deleteEntity(request, token, entity, cycleId).catch(() => {})\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@open
4
4
  import { Button } from "@open-mercato/ui/primitives/button";
5
5
  import { PasswordInput } from "@open-mercato/ui/primitives/password-input";
6
6
  import { Label } from "@open-mercato/ui/primitives/label";
7
- import { useState } from "react";
7
+ import { useEffect, useState } from "react";
8
8
  import { useRouter } from "next/navigation";
9
9
  import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
10
10
  import { useT } from "@open-mercato/shared/lib/i18n/context";
@@ -14,9 +14,13 @@ function ResetWithTokenPage({ params }) {
14
14
  const t = useT();
15
15
  const [error, setError] = useState(null);
16
16
  const [submitting, setSubmitting] = useState(false);
17
+ const [clientReady, setClientReady] = useState(false);
17
18
  const passwordPolicy = getPasswordPolicy();
18
19
  const passwordRequirements = formatPasswordRequirements(passwordPolicy, t);
19
20
  const passwordDescription = passwordRequirements ? t("auth.password.requirements.help", "Password requirements: {requirements}", { requirements: passwordRequirements }) : "";
21
+ useEffect(() => {
22
+ setClientReady(true);
23
+ }, []);
20
24
  async function onSubmit(e) {
21
25
  e.preventDefault();
22
26
  setError(null);
@@ -56,7 +60,7 @@ function ResetWithTokenPage({ params }) {
56
60
  /* @__PURE__ */ jsx(CardTitle, { children: t("auth.reset.title", "Set a new password") }),
57
61
  /* @__PURE__ */ jsx(CardDescription, { children: t("auth.reset.subtitle", "Choose a strong password for your account.") })
58
62
  ] }),
59
- /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("form", { className: "grid gap-3", onSubmit, children: [
63
+ /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("form", { className: "grid gap-3", onSubmit, "data-auth-ready": clientReady ? "1" : "0", children: [
60
64
  error && /* @__PURE__ */ jsx("div", { className: "text-sm text-status-error-text", children: error }),
61
65
  /* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
62
66
  /* @__PURE__ */ jsx(Label, { htmlFor: "password", children: t("auth.reset.form.password", "New password") }),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/frontend/reset/%5Btoken%5D/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@open-mercato/ui/primitives/card'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nexport default function ResetWithTokenPage({ params }: { params: { token: string } }) {\n const router = useRouter()\n const t = useT()\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const passwordPolicy = getPasswordPolicy()\n const passwordRequirements = formatPasswordRequirements(passwordPolicy, t)\n const passwordDescription = passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : ''\n\n async function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n setError(null)\n const form = new FormData(e.currentTarget)\n const password = String(form.get('password') ?? '')\n const confirmPassword = String(form.get('confirmPassword') ?? '')\n\n if (!password) {\n setError(t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'))\n return\n }\n if (!confirmPassword) {\n setError(t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'))\n return\n }\n if (password !== confirmPassword) {\n setError(t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'))\n return\n }\n\n setSubmitting(true)\n try {\n form.set('token', params.token)\n const { ok, result } = await apiCall<{ ok?: boolean; error?: string; redirect?: string }>(\n '/api/auth/reset/confirm',\n { method: 'POST', body: form },\n )\n if (!ok || result?.ok === false) {\n setError(result?.error || t('auth.reset.errors.failed', 'Unable to reset password'))\n return\n }\n router.replace(result?.redirect || '/login')\n } finally {\n setSubmitting(false)\n }\n }\n\n return (\n <div className=\"min-h-svh flex items-center justify-center p-4\">\n <Card className=\"w-full max-w-sm\">\n <CardHeader>\n <CardTitle>{t('auth.reset.title', 'Set a new password')}</CardTitle>\n <CardDescription>{t('auth.reset.subtitle', 'Choose a strong password for your account.')}</CardDescription>\n </CardHeader>\n <CardContent>\n <form className=\"grid gap-3\" onSubmit={onSubmit}>\n {error && <div className=\"text-sm text-status-error-text\">{error}</div>}\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{t('auth.reset.form.password', 'New password')}</Label>\n <PasswordInput id=\"password\" name=\"password\" required minLength={passwordPolicy.minLength} autoComplete=\"new-password\" />\n {passwordDescription ? (\n <p className=\"text-xs text-muted-foreground\">{passwordDescription}</p>\n ) : null}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"confirmPassword\">{t('auth.profile.form.confirmPassword', 'Confirm new password')}</Label>\n <PasswordInput\n id=\"confirmPassword\"\n name=\"confirmPassword\"\n required\n minLength={passwordPolicy.minLength}\n autoComplete=\"new-password\"\n />\n </div>\n <Button type=\"submit\" className=\"mt-2 w-full\" disabled={submitting}>\n {submitting ? t('auth.reset.form.loading', '...') : t('auth.reset.form.submit', 'Update password')}\n </Button>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@open-mercato/ui/primitives/card'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useEffect, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nexport default function ResetWithTokenPage({ params }: { params: { token: string } }) {\n const router = useRouter()\n const t = useT()\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [clientReady, setClientReady] = useState(false)\n const passwordPolicy = getPasswordPolicy()\n const passwordRequirements = formatPasswordRequirements(passwordPolicy, t)\n const passwordDescription = passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : ''\n\n useEffect(() => {\n setClientReady(true)\n }, [])\n\n async function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n setError(null)\n const form = new FormData(e.currentTarget)\n const password = String(form.get('password') ?? '')\n const confirmPassword = String(form.get('confirmPassword') ?? '')\n\n if (!password) {\n setError(t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'))\n return\n }\n if (!confirmPassword) {\n setError(t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'))\n return\n }\n if (password !== confirmPassword) {\n setError(t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'))\n return\n }\n\n setSubmitting(true)\n try {\n form.set('token', params.token)\n const { ok, result } = await apiCall<{ ok?: boolean; error?: string; redirect?: string }>(\n '/api/auth/reset/confirm',\n { method: 'POST', body: form },\n )\n if (!ok || result?.ok === false) {\n setError(result?.error || t('auth.reset.errors.failed', 'Unable to reset password'))\n return\n }\n router.replace(result?.redirect || '/login')\n } finally {\n setSubmitting(false)\n }\n }\n\n return (\n <div className=\"min-h-svh flex items-center justify-center p-4\">\n <Card className=\"w-full max-w-sm\">\n <CardHeader>\n <CardTitle>{t('auth.reset.title', 'Set a new password')}</CardTitle>\n <CardDescription>{t('auth.reset.subtitle', 'Choose a strong password for your account.')}</CardDescription>\n </CardHeader>\n <CardContent>\n <form className=\"grid gap-3\" onSubmit={onSubmit} data-auth-ready={clientReady ? '1' : '0'}>\n {error && <div className=\"text-sm text-status-error-text\">{error}</div>}\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{t('auth.reset.form.password', 'New password')}</Label>\n <PasswordInput id=\"password\" name=\"password\" required minLength={passwordPolicy.minLength} autoComplete=\"new-password\" />\n {passwordDescription ? (\n <p className=\"text-xs text-muted-foreground\">{passwordDescription}</p>\n ) : null}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"confirmPassword\">{t('auth.profile.form.confirmPassword', 'Confirm new password')}</Label>\n <PasswordInput\n id=\"confirmPassword\"\n name=\"confirmPassword\"\n required\n minLength={passwordPolicy.minLength}\n autoComplete=\"new-password\"\n />\n </div>\n <Button type=\"submit\" className=\"mt-2 w-full\" disabled={submitting}>\n {submitting ? t('auth.reset.form.loading', '...') : t('auth.reset.form.submit', 'Update password')}\n </Button>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -98,8 +98,7 @@ async function setRecordCustomFields(em, opts) {
98
98
  const isArray = Array.isArray(raw);
99
99
  if (isArray) {
100
100
  const arr = raw;
101
- const existing = await em.find(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey });
102
- for (const stale of existing) em.remove(stale);
101
+ await em.nativeDelete(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey });
103
102
  for (const val of arr) {
104
103
  const col = encrypted ? "valueText" : def ? columnFromKind(def.kind) : columnFromJsValue(val);
105
104
  const cf2 = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: /* @__PURE__ */ new Date() });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/lib/helpers.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { encryptCustomFieldValue, resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport {\n MAX_CUSTOM_FIELD_KEYS_PER_RECORD,\n TOO_MANY_CUSTOM_FIELDS_ERROR,\n} from '@open-mercato/shared/modules/entities/validation'\nimport { CustomFieldDef, CustomFieldValue } from '../data/entities'\n\ntype Primitive = string | number | boolean | null | undefined\ntype PrimitiveOrArray = Primitive | Primitive[]\n\nexport type SetRecordCustomFieldsOptions = {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, PrimitiveOrArray>\n // When true (default), try to use field definitions to decide storage column\n preferDefs?: boolean\n // Optional: notify external systems (e.g., indexing) when values changed\n onChanged?: (payload: { entityId: string; recordId: string; organizationId: string | null; tenantId: string | null }) => Promise<void> | void\n // Optional: re-use an existing tenant encryption service instance\n encryptionService?: TenantDataEncryptionService | null\n}\n\nfunction columnFromKind(kind: string): keyof CustomFieldValue {\n switch (kind) {\n case 'text':\n case 'select':\n case 'currency':\n case 'dictionary':\n return 'valueText'\n case 'multiline':\n return 'valueMultiline'\n case 'integer':\n return 'valueInt'\n case 'float':\n return 'valueFloat'\n case 'boolean':\n return 'valueBool'\n default:\n return 'valueText'\n }\n}\n\nfunction columnFromJsValue(v: Primitive): keyof CustomFieldValue {\n if (v === null || v === undefined) return 'valueText'\n if (typeof v === 'boolean') return 'valueBool'\n if (typeof v === 'number') return Number.isInteger(v) ? 'valueInt' : 'valueFloat'\n return 'valueText'\n}\n\n// Clears all value columns to avoid leftovers on update\nfunction clearValueColumns(cf: CustomFieldValue) {\n cf.valueText = null\n cf.valueMultiline = null\n cf.valueInt = null\n cf.valueFloat = null\n cf.valueBool = null\n}\n\nexport async function setRecordCustomFields(\n em: EntityManager,\n opts: SetRecordCustomFieldsOptions,\n): Promise<void> {\n const { entityId, recordId, values } = opts\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const preferDefs = opts.preferDefs !== false\n\n let defsByKey: Record<string, CustomFieldDef> | undefined\n if (preferDefs) {\n const defs = await em.find(CustomFieldDef, {\n entityId,\n isActive: true,\n deletedAt: null,\n organizationId: { $in: [organizationId, null] as any },\n tenantId: { $in: [tenantId, null] as any },\n })\n const scopeScore = (def: CustomFieldDef) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0)\n defsByKey = {}\n for (const d of defs) {\n const existing = defsByKey[d.key]\n if (!existing) {\n defsByKey[d.key] = d\n continue\n }\n const nextScore = scopeScore(d)\n const existingScore = scopeScore(existing)\n if (nextScore > existingScore) {\n defsByKey[d.key] = d\n continue\n }\n if (nextScore < existingScore) continue\n\n const nextUpdatedAt = d.updatedAt instanceof Date ? d.updatedAt.getTime() : new Date(d.updatedAt).getTime()\n const existingUpdatedAt = existing.updatedAt instanceof Date\n ? existing.updatedAt.getTime()\n : new Date(existing.updatedAt).getTime()\n if (nextUpdatedAt >= existingUpdatedAt) {\n defsByKey[d.key] = d\n }\n }\n }\n\n const toPersist: CustomFieldValue[] = []\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em as any, opts.encryptionService)\n return encryptionService\n }\n const keys = Object.keys(values)\n const presentKeyCount = keys.filter((key) => values[key] !== undefined).length\n if (preferDefs && presentKeyCount > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {\n throw new Error(TOO_MANY_CUSTOM_FIELDS_ERROR)\n }\n\n // Run the per-key delete+insert work inside ONE database transaction so a\n // multi-value replacement is atomic and isolated. The array branch deletes the\n // existing rows for a key and inserts the replacements; without an enclosing\n // transaction those can land in separate commit boundaries under MikroORM's\n // FlushMode.AUTO (a query elsewhere in the unit auto-flushes part of the work),\n // which intermittently left the field with the delete applied but the inserts\n // missing \u2014 the multi-select EDIT reverted to []. The single commit below makes\n // it all-or-nothing. We only open our own transaction when the caller has not\n // already started one (commands fork the request em and may run setCustomFields\n // outside their own withAtomicFlush tx); join an ambient transaction otherwise.\n const txEm = em as {\n begin?: () => Promise<void>\n commit?: () => Promise<void>\n rollback?: () => Promise<void>\n isInTransaction?: () => boolean\n }\n const txCapable =\n typeof txEm.begin === 'function' &&\n typeof txEm.commit === 'function' &&\n typeof txEm.rollback === 'function' &&\n typeof txEm.isInTransaction === 'function'\n const ownCustomFieldTransaction = txCapable && !txEm.isInTransaction!()\n if (ownCustomFieldTransaction) await txEm.begin!()\n try {\n for (const fieldKey of keys) {\n const raw = values[fieldKey]\n if (raw === undefined) continue\n\n const def = defsByKey?.[fieldKey]\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const isArray = Array.isArray(raw)\n // When array (multi-value): replace all existing rows for the key. The old\n // rows are removed via em.remove (a DEFERRED delete keyed by primary id),\n // not nativeDelete, so the DELETE and the replacement INSERTs are applied in\n // the SAME em.flush() \u2014 MikroORM wraps a flush in one transaction, making the\n // replacement atomic (the field can never be left empty by a partial\n // failure between delete and insert). It also removes the FlushMode.AUTO\n // footgun the old code had: a nativeDelete issued after em.create()\n // auto-flushed the new rows and then deleted them by fieldKey, wiping the\n // value on EDIT. Regression: TC-CAT-CF-MULTI-EDIT-001 / TC-CRM-CF-MULTI-EDIT-001.\n if (isArray) {\n const arr = raw as Primitive[]\n const existing = await em.find(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n for (const stale of existing) em.remove(stale)\n for (const val of arr) {\n const col: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(val)\n const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n clearValueColumns(cf)\n const stored = encrypted\n ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache)\n : val\n switch (col) {\n case 'valueText': cf.valueText = stored == null ? null : String(stored); break\n case 'valueMultiline': cf.valueMultiline = stored == null ? null : String(stored); break\n case 'valueInt': cf.valueInt = stored == null ? null : Number(stored); break\n case 'valueFloat': cf.valueFloat = stored == null ? null : Number(stored); break\n case 'valueBool': cf.valueBool = stored == null ? null : Boolean(stored); break\n default: cf.valueText = stored == null ? null : String(stored); break\n }\n toPersist.push(cf)\n }\n continue\n }\n\n const column: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(raw as Primitive)\n const storedValue = encrypted\n ? await encryptCustomFieldValue(raw as Primitive, tenantId, getEncryptionService(), encryptionCache)\n : raw\n\n let cf = await em.findOne(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n if (!cf) {\n cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n toPersist.push(cf)\n }\n clearValueColumns(cf)\n switch (column) {\n case 'valueText':\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueMultiline':\n cf.valueMultiline = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueInt':\n cf.valueInt = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueFloat':\n cf.valueFloat = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueBool':\n cf.valueBool = (storedValue as Primitive) == null ? null : Boolean(storedValue as Primitive)\n break\n default:\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n }\n }\n\n if (toPersist.length) em.persist(toPersist)\n await em.flush()\n if (ownCustomFieldTransaction) await txEm.commit!()\n } catch (err) {\n if (ownCustomFieldTransaction) {\n try { await txEm.rollback!() } catch { /* surface the original error, not a rollback failure */ }\n }\n throw err\n }\n // Emit hook for indexing if requested (outside CRUD flows). Runs AFTER the\n // transaction commits so consumers observe the persisted rows.\n try {\n if (typeof opts.onChanged === 'function') {\n await opts.onChanged({ entityId, recordId, organizationId, tenantId })\n }\n } catch {\n // Non-blocking\n }\n}\n"],
5
- "mappings": "AAEA,SAAS,yBAAyB,sCAAsC;AACxE;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB,wBAAwB;AAmBjD,SAAS,eAAe,MAAsC;AAC5D,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,kBAAkB,GAAsC;AAC/D,MAAI,MAAM,QAAQ,MAAM,OAAW,QAAO;AAC1C,MAAI,OAAO,MAAM,UAAW,QAAO;AACnC,MAAI,OAAO,MAAM,SAAU,QAAO,OAAO,UAAU,CAAC,IAAI,aAAa;AACrE,SAAO;AACT;AAGA,SAAS,kBAAkB,IAAsB;AAC/C,KAAG,YAAY;AACf,KAAG,iBAAiB;AACpB,KAAG,WAAW;AACd,KAAG,aAAa;AAChB,KAAG,YAAY;AACjB;AAEA,eAAsB,sBACpB,IACA,MACe;AACf,QAAM,EAAE,UAAU,UAAU,OAAO,IAAI;AACvC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,aAAa,KAAK,eAAe;AAEvC,MAAI;AACJ,MAAI,YAAY;AACd,UAAM,OAAO,MAAM,GAAG,KAAK,gBAAgB;AAAA,MACzC;AAAA,MACA,UAAU;AAAA,MACV,WAAW;AAAA,MACX,gBAAgB,EAAE,KAAK,CAAC,gBAAgB,IAAI,EAAS;AAAA,MACrD,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,EAAS;AAAA,IAC3C,CAAC;AACD,UAAM,aAAa,CAAC,SAAyB,IAAI,WAAW,IAAI,MAAM,IAAI,iBAAiB,IAAI;AAC/F,gBAAY,CAAC;AACb,eAAW,KAAK,MAAM;AACpB,YAAM,WAAW,UAAU,EAAE,GAAG;AAChC,UAAI,CAAC,UAAU;AACb,kBAAU,EAAE,GAAG,IAAI;AACnB;AAAA,MACF;AACA,YAAM,YAAY,WAAW,CAAC;AAC9B,YAAM,gBAAgB,WAAW,QAAQ;AACzC,UAAI,YAAY,eAAe;AAC7B,kBAAU,EAAE,GAAG,IAAI;AACnB;AAAA,MACF;AACA,UAAI,YAAY,cAAe;AAE/B,YAAM,gBAAgB,EAAE,qBAAqB,OAAO,EAAE,UAAU,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AAC1G,YAAM,oBAAoB,SAAS,qBAAqB,OACpD,SAAS,UAAU,QAAQ,IAC3B,IAAI,KAAK,SAAS,SAAS,EAAE,QAAQ;AACzC,UAAI,iBAAiB,mBAAmB;AACtC,kBAAU,EAAE,GAAG,IAAI;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAgC,CAAC;AACvC,MAAI;AACJ,QAAM,kBAAkB,oBAAI,IAAkC;AAC9D,QAAM,uBAAuB,MAAM;AACjC,QAAI,sBAAsB,OAAW,QAAO;AAC5C,wBAAoB,+BAA+B,IAAW,KAAK,iBAAiB;AACpF,WAAO;AAAA,EACT;AACA,QAAM,OAAO,OAAO,KAAK,MAAM;AAC/B,QAAM,kBAAkB,KAAK,OAAO,CAAC,QAAQ,OAAO,GAAG,MAAM,MAAS,EAAE;AACxE,MAAI,cAAc,kBAAkB,kCAAkC;AACpE,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAYA,QAAM,OAAO;AAMb,QAAM,YACJ,OAAO,KAAK,UAAU,cACtB,OAAO,KAAK,WAAW,cACvB,OAAO,KAAK,aAAa,cACzB,OAAO,KAAK,oBAAoB;AAClC,QAAM,4BAA4B,aAAa,CAAC,KAAK,gBAAiB;AACtE,MAAI,0BAA2B,OAAM,KAAK,MAAO;AACjD,MAAI;AACJ,eAAW,YAAY,MAAM;AAC3B,YAAM,MAAM,OAAO,QAAQ;AAC3B,UAAI,QAAQ,OAAW;AAEvB,YAAM,MAAM,YAAY,QAAQ;AAChC,YAAM,YAAY,QAAQ,KAAK,cAAe,IAAY,YAAY,SAAS;AAC/E,YAAM,UAAU,MAAM,QAAQ,GAAG;AAUjC,UAAI,SAAS;AACX,cAAM,MAAM;AACZ,cAAM,WAAW,MAAM,GAAG,KAAK,kBAAkB,EAAE,UAAU,UAAU,gBAAgB,UAAU,SAAS,CAAC;AAC3G,mBAAW,SAAS,SAAU,IAAG,OAAO,KAAK;AAC7C,mBAAW,OAAO,KAAK;AACrB,gBAAM,MAA8B,YAAY,cAAc,MAAM,eAAe,IAAI,IAAI,IAAI,kBAAkB,GAAG;AACpH,gBAAMA,MAAK,GAAG,OAAO,kBAAkB,EAAE,UAAU,UAAU,gBAAgB,UAAU,UAAU,WAAW,oBAAI,KAAK,EAAE,CAAC;AACxH,4BAAkBA,GAAE;AACpB,gBAAM,SAAS,YACX,MAAM,wBAAwB,KAAK,UAAU,qBAAqB,GAAG,eAAe,IACpF;AACJ,kBAAQ,KAAK;AAAA,YACX,KAAK;AAAa,cAAAA,IAAG,YAAY,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,YACzE,KAAK;AAAkB,cAAAA,IAAG,iBAAiB,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,YACnF,KAAK;AAAY,cAAAA,IAAG,WAAW,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,YACvE,KAAK;AAAc,cAAAA,IAAG,aAAa,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,YAC3E,KAAK;AAAa,cAAAA,IAAG,YAAY,UAAU,OAAO,OAAO,QAAQ,MAAM;AAAG;AAAA,YAC1E;AAAS,cAAAA,IAAG,YAAY,UAAU,OAAO,OAAO,OAAO,MAAM;AAAG;AAAA,UAClE;AACA,oBAAU,KAAKA,GAAE;AAAA,QACnB;AACA;AAAA,MACF;AAEA,YAAM,SAAiC,YAAY,cAAc,MAAM,eAAe,IAAI,IAAI,IAAI,kBAAkB,GAAgB;AACpI,YAAM,cAAc,YAChB,MAAM,wBAAwB,KAAkB,UAAU,qBAAqB,GAAG,eAAe,IACjG;AAEJ,UAAI,KAAK,MAAM,GAAG,QAAQ,kBAAkB,EAAE,UAAU,UAAU,gBAAgB,UAAU,SAAS,CAAC;AACtG,UAAI,CAAC,IAAI;AACP,aAAK,GAAG,OAAO,kBAAkB,EAAE,UAAU,UAAU,gBAAgB,UAAU,UAAU,WAAW,oBAAI,KAAK,EAAE,CAAC;AAClH,kBAAU,KAAK,EAAE;AAAA,MACnB;AACA,wBAAkB,EAAE;AACpB,cAAQ,QAAQ;AAAA,QACd,KAAK;AACH,aAAG,YAAa,eAA6B,OAAO,OAAO,OAAO,WAAwB;AAC1F;AAAA,QACF,KAAK;AACH,aAAG,iBAAkB,eAA6B,OAAO,OAAO,OAAO,WAAwB;AAC/F;AAAA,QACF,KAAK;AACH,aAAG,WAAY,eAA6B,OAAO,OAAO,OAAO,WAAwB;AACzF;AAAA,QACF,KAAK;AACH,aAAG,aAAc,eAA6B,OAAO,OAAO,OAAO,WAAwB;AAC3F;AAAA,QACF,KAAK;AACH,aAAG,YAAa,eAA6B,OAAO,OAAO,QAAQ,WAAwB;AAC3F;AAAA,QACF;AACE,aAAG,YAAa,eAA6B,OAAO,OAAO,OAAO,WAAwB;AAC1F;AAAA,MACJ;AAAA,IACF;AAEA,QAAI,UAAU,OAAQ,IAAG,QAAQ,SAAS;AAC1C,UAAM,GAAG,MAAM;AACb,QAAI,0BAA2B,OAAM,KAAK,OAAQ;AAAA,EACpD,SAAS,KAAK;AACZ,QAAI,2BAA2B;AAC7B,UAAI;AAAE,cAAM,KAAK,SAAU;AAAA,MAAE,QAAQ;AAAA,MAA2D;AAAA,IAClG;AACA,UAAM;AAAA,EACR;AAGA,MAAI;AACF,QAAI,OAAO,KAAK,cAAc,YAAY;AACxC,YAAM,KAAK,UAAU,EAAE,UAAU,UAAU,gBAAgB,SAAS,CAAC;AAAA,IACvE;AAAA,EACF,QAAQ;AAAA,EAER;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { encryptCustomFieldValue, resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport {\n MAX_CUSTOM_FIELD_KEYS_PER_RECORD,\n TOO_MANY_CUSTOM_FIELDS_ERROR,\n} from '@open-mercato/shared/modules/entities/validation'\nimport { CustomFieldDef, CustomFieldValue } from '../data/entities'\n\ntype Primitive = string | number | boolean | null | undefined\ntype PrimitiveOrArray = Primitive | Primitive[]\n\nexport type SetRecordCustomFieldsOptions = {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, PrimitiveOrArray>\n // When true (default), try to use field definitions to decide storage column\n preferDefs?: boolean\n // Optional: notify external systems (e.g., indexing) when values changed\n onChanged?: (payload: { entityId: string; recordId: string; organizationId: string | null; tenantId: string | null }) => Promise<void> | void\n // Optional: re-use an existing tenant encryption service instance\n encryptionService?: TenantDataEncryptionService | null\n}\n\nfunction columnFromKind(kind: string): keyof CustomFieldValue {\n switch (kind) {\n case 'text':\n case 'select':\n case 'currency':\n case 'dictionary':\n return 'valueText'\n case 'multiline':\n return 'valueMultiline'\n case 'integer':\n return 'valueInt'\n case 'float':\n return 'valueFloat'\n case 'boolean':\n return 'valueBool'\n default:\n return 'valueText'\n }\n}\n\nfunction columnFromJsValue(v: Primitive): keyof CustomFieldValue {\n if (v === null || v === undefined) return 'valueText'\n if (typeof v === 'boolean') return 'valueBool'\n if (typeof v === 'number') return Number.isInteger(v) ? 'valueInt' : 'valueFloat'\n return 'valueText'\n}\n\n// Clears all value columns to avoid leftovers on update\nfunction clearValueColumns(cf: CustomFieldValue) {\n cf.valueText = null\n cf.valueMultiline = null\n cf.valueInt = null\n cf.valueFloat = null\n cf.valueBool = null\n}\n\nexport async function setRecordCustomFields(\n em: EntityManager,\n opts: SetRecordCustomFieldsOptions,\n): Promise<void> {\n const { entityId, recordId, values } = opts\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const preferDefs = opts.preferDefs !== false\n\n let defsByKey: Record<string, CustomFieldDef> | undefined\n if (preferDefs) {\n const defs = await em.find(CustomFieldDef, {\n entityId,\n isActive: true,\n deletedAt: null,\n organizationId: { $in: [organizationId, null] as any },\n tenantId: { $in: [tenantId, null] as any },\n })\n const scopeScore = (def: CustomFieldDef) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0)\n defsByKey = {}\n for (const d of defs) {\n const existing = defsByKey[d.key]\n if (!existing) {\n defsByKey[d.key] = d\n continue\n }\n const nextScore = scopeScore(d)\n const existingScore = scopeScore(existing)\n if (nextScore > existingScore) {\n defsByKey[d.key] = d\n continue\n }\n if (nextScore < existingScore) continue\n\n const nextUpdatedAt = d.updatedAt instanceof Date ? d.updatedAt.getTime() : new Date(d.updatedAt).getTime()\n const existingUpdatedAt = existing.updatedAt instanceof Date\n ? existing.updatedAt.getTime()\n : new Date(existing.updatedAt).getTime()\n if (nextUpdatedAt >= existingUpdatedAt) {\n defsByKey[d.key] = d\n }\n }\n }\n\n const toPersist: CustomFieldValue[] = []\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em as any, opts.encryptionService)\n return encryptionService\n }\n const keys = Object.keys(values)\n const presentKeyCount = keys.filter((key) => values[key] !== undefined).length\n if (preferDefs && presentKeyCount > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {\n throw new Error(TOO_MANY_CUSTOM_FIELDS_ERROR)\n }\n\n // Run the per-key delete+insert work inside ONE database transaction so a\n // multi-value replacement is atomic and isolated. The array branch deletes the\n // existing rows for a key and inserts the replacements; without an enclosing\n // transaction those can land in separate commit boundaries under MikroORM's\n // FlushMode.AUTO (a query elsewhere in the unit auto-flushes part of the work),\n // which intermittently left the field with the delete applied but the inserts\n // missing \u2014 the multi-select EDIT reverted to []. The single commit below makes\n // it all-or-nothing. We only open our own transaction when the caller has not\n // already started one (commands fork the request em and may run setCustomFields\n // outside their own withAtomicFlush tx); join an ambient transaction otherwise.\n const txEm = em as {\n begin?: () => Promise<void>\n commit?: () => Promise<void>\n rollback?: () => Promise<void>\n isInTransaction?: () => boolean\n }\n const txCapable =\n typeof txEm.begin === 'function' &&\n typeof txEm.commit === 'function' &&\n typeof txEm.rollback === 'function' &&\n typeof txEm.isInTransaction === 'function'\n const ownCustomFieldTransaction = txCapable && !txEm.isInTransaction!()\n if (ownCustomFieldTransaction) await txEm.begin!()\n try {\n for (const fieldKey of keys) {\n const raw = values[fieldKey]\n if (raw === undefined) continue\n\n const def = defsByKey?.[fieldKey]\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const isArray = Array.isArray(raw)\n // When array (multi-value): replace all existing rows for the key. Delete\n // first, then create replacements, all inside the transaction opened above.\n // Creating rows before a native delete can auto-flush and delete the new\n // values; mixing em.remove(stale) with new rows for the same EAV scope was\n // observed to commit an empty set under MikroORM v7. The explicit order keeps\n // the replacement atomic without letting old-row cleanup target new rows.\n if (isArray) {\n const arr = raw as Primitive[]\n await em.nativeDelete(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n for (const val of arr) {\n const col: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(val)\n const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n clearValueColumns(cf)\n const stored = encrypted\n ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache)\n : val\n switch (col) {\n case 'valueText': cf.valueText = stored == null ? null : String(stored); break\n case 'valueMultiline': cf.valueMultiline = stored == null ? null : String(stored); break\n case 'valueInt': cf.valueInt = stored == null ? null : Number(stored); break\n case 'valueFloat': cf.valueFloat = stored == null ? null : Number(stored); break\n case 'valueBool': cf.valueBool = stored == null ? null : Boolean(stored); break\n default: cf.valueText = stored == null ? null : String(stored); break\n }\n toPersist.push(cf)\n }\n continue\n }\n\n const column: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(raw as Primitive)\n const storedValue = encrypted\n ? await encryptCustomFieldValue(raw as Primitive, tenantId, getEncryptionService(), encryptionCache)\n : raw\n\n let cf = await em.findOne(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n if (!cf) {\n cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n toPersist.push(cf)\n }\n clearValueColumns(cf)\n switch (column) {\n case 'valueText':\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueMultiline':\n cf.valueMultiline = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueInt':\n cf.valueInt = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueFloat':\n cf.valueFloat = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueBool':\n cf.valueBool = (storedValue as Primitive) == null ? null : Boolean(storedValue as Primitive)\n break\n default:\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n }\n }\n\n if (toPersist.length) em.persist(toPersist)\n await em.flush()\n if (ownCustomFieldTransaction) await txEm.commit!()\n } catch (err) {\n if (ownCustomFieldTransaction) {\n try { await txEm.rollback!() } catch { /* surface the original error, not a rollback failure */ }\n }\n throw err\n }\n // Emit hook for indexing if requested (outside CRUD flows). Runs AFTER the\n // transaction commits so consumers observe the persisted rows.\n try {\n if (typeof opts.onChanged === 'function') {\n await opts.onChanged({ entityId, recordId, organizationId, tenantId })\n }\n } catch {\n // Non-blocking\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["cf"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.5-develop.4695.1.42ee0ddf0e",
3
+ "version": "0.6.5-develop.4718.1.56d834bb34",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -245,16 +245,16 @@
245
245
  "zod": "^4.4.3"
246
246
  },
247
247
  "peerDependencies": {
248
- "@open-mercato/ai-assistant": "0.6.5-develop.4695.1.42ee0ddf0e",
249
- "@open-mercato/shared": "0.6.5-develop.4695.1.42ee0ddf0e",
250
- "@open-mercato/ui": "0.6.5-develop.4695.1.42ee0ddf0e",
248
+ "@open-mercato/ai-assistant": "0.6.5-develop.4718.1.56d834bb34",
249
+ "@open-mercato/shared": "0.6.5-develop.4718.1.56d834bb34",
250
+ "@open-mercato/ui": "0.6.5-develop.4718.1.56d834bb34",
251
251
  "react": "^19.0.0",
252
252
  "react-dom": "^19.0.0"
253
253
  },
254
254
  "devDependencies": {
255
- "@open-mercato/ai-assistant": "0.6.5-develop.4695.1.42ee0ddf0e",
256
- "@open-mercato/shared": "0.6.5-develop.4695.1.42ee0ddf0e",
257
- "@open-mercato/ui": "0.6.5-develop.4695.1.42ee0ddf0e",
255
+ "@open-mercato/ai-assistant": "0.6.5-develop.4718.1.56d834bb34",
256
+ "@open-mercato/shared": "0.6.5-develop.4718.1.56d834bb34",
257
+ "@open-mercato/ui": "0.6.5-develop.4718.1.56d834bb34",
258
258
  "@testing-library/dom": "^10.4.1",
259
259
  "@testing-library/jest-dom": "^6.9.1",
260
260
  "@testing-library/react": "^16.3.1",
@@ -1,4 +1,5 @@
1
1
  import { expect, type APIRequestContext } from '@playwright/test';
2
+ import { randomInt } from 'node:crypto';
2
3
  import { apiRequest } from './api';
3
4
  import { getTokenContext } from './generalFixtures';
4
5
 
@@ -28,7 +29,7 @@ const reservedCurrencyCodes = new Set<string>();
28
29
 
29
30
  /** Draws an ISO-style three-letter code unused by seeds or earlier fixtures. */
30
31
  export function generateUniqueCurrencyCode(): string {
31
- const letter = () => String.fromCharCode(65 + Math.floor(Math.random() * 26));
32
+ const letter = () => String.fromCharCode(65 + randomInt(26));
32
33
  for (let attempt = 0; attempt < 200; attempt += 1) {
33
34
  const code = `${letter()}${letter()}${letter()}`;
34
35
  if (!SEEDED_CURRENCY_CODES.has(code) && !reservedCurrencyCodes.has(code)) {
@@ -1,4 +1,5 @@
1
1
  import { type APIRequestContext, type APIResponse, expect, test } from '@playwright/test'
2
+ import { randomInt } from 'node:crypto'
2
3
  import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
3
4
  import { apiRequest } from './api'
4
5
  import { expectId, readJsonSafe } from './generalFixtures'
@@ -177,7 +178,7 @@ export async function runCrudUndoRoundTrip(
177
178
  token: string,
178
179
  entity: CrudUndoEntityConfig,
179
180
  ): Promise<void> {
180
- const stamp = `${Date.now()}${Math.floor(Math.random() * 1000)}`
181
+ const stamp = `${Date.now()}${randomInt(1000)}`
181
182
  let createUndoId: string | null = null
182
183
  let cycleId: string | null = null
183
184
 
@@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@open
3
3
  import { Button } from '@open-mercato/ui/primitives/button'
4
4
  import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
5
5
  import { Label } from '@open-mercato/ui/primitives/label'
6
- import { useState } from 'react'
6
+ import { useEffect, useState } from 'react'
7
7
  import { useRouter } from 'next/navigation'
8
8
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
9
9
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -14,12 +14,17 @@ export default function ResetWithTokenPage({ params }: { params: { token: string
14
14
  const t = useT()
15
15
  const [error, setError] = useState<string | null>(null)
16
16
  const [submitting, setSubmitting] = useState(false)
17
+ const [clientReady, setClientReady] = useState(false)
17
18
  const passwordPolicy = getPasswordPolicy()
18
19
  const passwordRequirements = formatPasswordRequirements(passwordPolicy, t)
19
20
  const passwordDescription = passwordRequirements
20
21
  ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })
21
22
  : ''
22
23
 
24
+ useEffect(() => {
25
+ setClientReady(true)
26
+ }, [])
27
+
23
28
  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
24
29
  e.preventDefault()
25
30
  setError(null)
@@ -65,7 +70,7 @@ export default function ResetWithTokenPage({ params }: { params: { token: string
65
70
  <CardDescription>{t('auth.reset.subtitle', 'Choose a strong password for your account.')}</CardDescription>
66
71
  </CardHeader>
67
72
  <CardContent>
68
- <form className="grid gap-3" onSubmit={onSubmit}>
73
+ <form className="grid gap-3" onSubmit={onSubmit} data-auth-ready={clientReady ? '1' : '0'}>
69
74
  {error && <div className="text-sm text-status-error-text">{error}</div>}
70
75
  <div className="grid gap-1">
71
76
  <Label htmlFor="password">{t('auth.reset.form.password', 'New password')}</Label>
@@ -95,6 +95,7 @@
95
95
  "exchangeRates.flash.updated": "Wechselkurs erfolgreich aktualisiert",
96
96
  "exchangeRates.form.action.create": "Wechselkurs erstellen",
97
97
  "exchangeRates.form.action.save": "Änderungen speichern",
98
+ "exchangeRates.form.actions.backToList": "Zurück zu Wechselkursen",
98
99
  "exchangeRates.form.errors.currencyCodeFormat": "Muss genau 3 Großbuchstaben sein",
99
100
  "exchangeRates.form.errors.fromCurrencyFormat": "Ungültiger Von-Währungscode",
100
101
  "exchangeRates.form.errors.invalidDate": "Datum und Uhrzeit sind erforderlich",
@@ -95,6 +95,7 @@
95
95
  "exchangeRates.flash.updated": "Exchange rate updated successfully",
96
96
  "exchangeRates.form.action.create": "Create Exchange Rate",
97
97
  "exchangeRates.form.action.save": "Save Changes",
98
+ "exchangeRates.form.actions.backToList": "Back to exchange rates",
98
99
  "exchangeRates.form.errors.currencyCodeFormat": "Must be exactly 3 uppercase letters",
99
100
  "exchangeRates.form.errors.fromCurrencyFormat": "Invalid from currency code",
100
101
  "exchangeRates.form.errors.invalidDate": "Date and time is required",
@@ -95,6 +95,7 @@
95
95
  "exchangeRates.flash.updated": "Tipo de cambio actualizado correctamente",
96
96
  "exchangeRates.form.action.create": "Crear Tipo de Cambio",
97
97
  "exchangeRates.form.action.save": "Guardar Cambios",
98
+ "exchangeRates.form.actions.backToList": "Volver a tipos de cambio",
98
99
  "exchangeRates.form.errors.currencyCodeFormat": "Debe ser exactamente 3 letras mayúsculas",
99
100
  "exchangeRates.form.errors.fromCurrencyFormat": "Código de moneda de origen inválido",
100
101
  "exchangeRates.form.errors.invalidDate": "La fecha y hora son requeridas",
@@ -95,6 +95,7 @@
95
95
  "exchangeRates.flash.updated": "Kurs wymiany zaktualizowany pomyślnie",
96
96
  "exchangeRates.form.action.create": "Utwórz Kurs Wymiany",
97
97
  "exchangeRates.form.action.save": "Zapisz Zmiany",
98
+ "exchangeRates.form.actions.backToList": "Wróć do kursów walut",
98
99
  "exchangeRates.form.errors.currencyCodeFormat": "Musi składać się z dokładnie 3 wielkich liter",
99
100
  "exchangeRates.form.errors.fromCurrencyFormat": "Nieprawidłowy kod waluty źródłowej",
100
101
  "exchangeRates.form.errors.invalidDate": "Data i czas są wymagane",
@@ -110,6 +110,7 @@
110
110
  "data_sync.runs.detail.logs.message": "Nachricht",
111
111
  "data_sync.runs.detail.logs.time": "Zeit",
112
112
  "data_sync.runs.detail.noLogs": "Keine Protokolleinträge",
113
+ "data_sync.runs.detail.notFound": "Synchronisierungslauf nicht gefunden.",
113
114
  "data_sync.runs.detail.progress": "Fortschritt",
114
115
  "data_sync.runs.detail.progress.batches": "{count} Stapel",
115
116
  "data_sync.runs.detail.progress.eta": "{eta} verbleibend",
@@ -110,6 +110,7 @@
110
110
  "data_sync.runs.detail.logs.message": "Message",
111
111
  "data_sync.runs.detail.logs.time": "Time",
112
112
  "data_sync.runs.detail.noLogs": "No log entries",
113
+ "data_sync.runs.detail.notFound": "Sync run not found.",
113
114
  "data_sync.runs.detail.progress": "Progress",
114
115
  "data_sync.runs.detail.progress.batches": "{count} batches",
115
116
  "data_sync.runs.detail.progress.eta": "{eta} remaining",
@@ -110,6 +110,7 @@
110
110
  "data_sync.runs.detail.logs.message": "Mensaje",
111
111
  "data_sync.runs.detail.logs.time": "Hora",
112
112
  "data_sync.runs.detail.noLogs": "Sin entradas de registro",
113
+ "data_sync.runs.detail.notFound": "Ejecución de sincronización no encontrada.",
113
114
  "data_sync.runs.detail.progress": "Progreso",
114
115
  "data_sync.runs.detail.progress.batches": "{count} lotes",
115
116
  "data_sync.runs.detail.progress.eta": "Quedan {eta}",
@@ -110,6 +110,7 @@
110
110
  "data_sync.runs.detail.logs.message": "Wiadomość",
111
111
  "data_sync.runs.detail.logs.time": "Czas",
112
112
  "data_sync.runs.detail.noLogs": "Brak wpisów w logach",
113
+ "data_sync.runs.detail.notFound": "Nie znaleziono przebiegu synchronizacji.",
113
114
  "data_sync.runs.detail.progress": "Postęp",
114
115
  "data_sync.runs.detail.progress.batches": "{count} partii",
115
116
  "data_sync.runs.detail.progress.eta": "Pozostało {eta}",
@@ -149,19 +149,15 @@ export async function setRecordCustomFields(
149
149
  const def = defsByKey?.[fieldKey]
150
150
  const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)
151
151
  const isArray = Array.isArray(raw)
152
- // When array (multi-value): replace all existing rows for the key. The old
153
- // rows are removed via em.remove (a DEFERRED delete keyed by primary id),
154
- // not nativeDelete, so the DELETE and the replacement INSERTs are applied in
155
- // the SAME em.flush() MikroORM wraps a flush in one transaction, making the
156
- // replacement atomic (the field can never be left empty by a partial
157
- // failure between delete and insert). It also removes the FlushMode.AUTO
158
- // footgun the old code had: a nativeDelete issued after em.create()
159
- // auto-flushed the new rows and then deleted them by fieldKey, wiping the
160
- // value on EDIT. Regression: TC-CAT-CF-MULTI-EDIT-001 / TC-CRM-CF-MULTI-EDIT-001.
152
+ // When array (multi-value): replace all existing rows for the key. Delete
153
+ // first, then create replacements, all inside the transaction opened above.
154
+ // Creating rows before a native delete can auto-flush and delete the new
155
+ // values; mixing em.remove(stale) with new rows for the same EAV scope was
156
+ // observed to commit an empty set under MikroORM v7. The explicit order keeps
157
+ // the replacement atomic without letting old-row cleanup target new rows.
161
158
  if (isArray) {
162
159
  const arr = raw as Primitive[]
163
- const existing = await em.find(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })
164
- for (const stale of existing) em.remove(stale)
160
+ await em.nativeDelete(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })
165
161
  for (const val of arr) {
166
162
  const col: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(val)
167
163
  const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })
@@ -155,6 +155,8 @@
155
155
  "inbox_ops.preview.sku": "SKU",
156
156
  "inbox_ops.processing_log": "Verarbeitungsprotokoll",
157
157
  "inbox_ops.proposal": "Vorschlag",
158
+ "inbox_ops.proposal.backToList": "Zurück zum Posteingang",
159
+ "inbox_ops.proposal.notFound": "Vorschlag nicht gefunden.",
158
160
  "inbox_ops.recategorize": "Kategorie \u00e4ndern",
159
161
  "inbox_ops.received_at": "Empfangen",
160
162
  "inbox_ops.reply.send": "Antwort senden",
@@ -155,6 +155,8 @@
155
155
  "inbox_ops.preview.sku": "SKU",
156
156
  "inbox_ops.processing_log": "Processing Log",
157
157
  "inbox_ops.proposal": "Proposal",
158
+ "inbox_ops.proposal.backToList": "Back to inbox",
159
+ "inbox_ops.proposal.notFound": "Proposal not found.",
158
160
  "inbox_ops.recategorize": "Change Category",
159
161
  "inbox_ops.received_at": "Received",
160
162
  "inbox_ops.reply.send": "Send Reply",
@@ -155,6 +155,8 @@
155
155
  "inbox_ops.preview.sku": "SKU",
156
156
  "inbox_ops.processing_log": "Registro de procesamiento",
157
157
  "inbox_ops.proposal": "Propuesta",
158
+ "inbox_ops.proposal.backToList": "Volver a la bandeja de entrada",
159
+ "inbox_ops.proposal.notFound": "Propuesta no encontrada.",
158
160
  "inbox_ops.recategorize": "Cambiar categor\u00eda",
159
161
  "inbox_ops.received_at": "Recibido",
160
162
  "inbox_ops.reply.send": "Enviar respuesta",
@@ -155,6 +155,8 @@
155
155
  "inbox_ops.preview.sku": "SKU",
156
156
  "inbox_ops.processing_log": "Dziennik przetwarzania",
157
157
  "inbox_ops.proposal": "Propozycja",
158
+ "inbox_ops.proposal.backToList": "Wróć do skrzynki",
159
+ "inbox_ops.proposal.notFound": "Nie znaleziono propozycji.",
158
160
  "inbox_ops.recategorize": "Zmie\u0144 kategori\u0119",
159
161
  "inbox_ops.received_at": "Odebrano",
160
162
  "inbox_ops.reply.send": "Wy\u015blij odpowied\u017a",
@@ -85,6 +85,7 @@
85
85
  "integrations.detail.analytics.title": "Nutzung (30 Tage)",
86
86
  "integrations.detail.analytics.totalEvents": "{count} Log-Ereignisse",
87
87
  "integrations.detail.back": "Zurück zu Integrationen",
88
+ "integrations.detail.backToList": "Zurück zu Integrationen",
88
89
  "integrations.detail.credentials.bundleShared": "Gemeinsame Zugangsdaten aus Paket: {bundle}",
89
90
  "integrations.detail.credentials.notConfigured": "Noch keine Zugangsdaten konfiguriert",
90
91
  "integrations.detail.credentials.save": "Zugangsdaten speichern",
@@ -126,6 +127,7 @@
126
127
  "integrations.detail.logs.level.info": "Info",
127
128
  "integrations.detail.logs.level.warn": "Warnung",
128
129
  "integrations.detail.logs.title": "Betriebsprotokolle",
130
+ "integrations.detail.notFound": "Integration nicht gefunden.",
129
131
  "integrations.detail.runActivity.lastRefreshed": "Zuletzt aktualisiert",
130
132
  "integrations.detail.runActivity.processed": "Verarbeitet",
131
133
  "integrations.detail.runActivity.progress": "Fortschritt",
@@ -85,6 +85,7 @@
85
85
  "integrations.detail.analytics.title": "Usage (30 days)",
86
86
  "integrations.detail.analytics.totalEvents": "{count} log events",
87
87
  "integrations.detail.back": "Back to Integrations",
88
+ "integrations.detail.backToList": "Back to integrations",
88
89
  "integrations.detail.credentials.bundleShared": "Shared credentials from bundle: {bundle}",
89
90
  "integrations.detail.credentials.notConfigured": "No credentials configured yet",
90
91
  "integrations.detail.credentials.save": "Save Credentials",
@@ -126,6 +127,7 @@
126
127
  "integrations.detail.logs.level.info": "Info",
127
128
  "integrations.detail.logs.level.warn": "Warning",
128
129
  "integrations.detail.logs.title": "Operation Logs",
130
+ "integrations.detail.notFound": "Integration not found.",
129
131
  "integrations.detail.runActivity.lastRefreshed": "Last refreshed",
130
132
  "integrations.detail.runActivity.processed": "Processed",
131
133
  "integrations.detail.runActivity.progress": "Progress",
@@ -85,6 +85,7 @@
85
85
  "integrations.detail.analytics.title": "Uso (30 días)",
86
86
  "integrations.detail.analytics.totalEvents": "{count} eventos de registro",
87
87
  "integrations.detail.back": "Volver a integraciones",
88
+ "integrations.detail.backToList": "Volver a integraciones",
88
89
  "integrations.detail.credentials.bundleShared": "Credenciales compartidas del paquete: {bundle}",
89
90
  "integrations.detail.credentials.notConfigured": "No hay credenciales configuradas",
90
91
  "integrations.detail.credentials.save": "Guardar credenciales",
@@ -126,6 +127,7 @@
126
127
  "integrations.detail.logs.level.info": "Info",
127
128
  "integrations.detail.logs.level.warn": "Advertencia",
128
129
  "integrations.detail.logs.title": "Registros de operación",
130
+ "integrations.detail.notFound": "Integración no encontrada.",
129
131
  "integrations.detail.runActivity.lastRefreshed": "Última actualización",
130
132
  "integrations.detail.runActivity.processed": "Procesados",
131
133
  "integrations.detail.runActivity.progress": "Progreso",
@@ -85,6 +85,7 @@
85
85
  "integrations.detail.analytics.title": "Użycie (30 dni)",
86
86
  "integrations.detail.analytics.totalEvents": "{count} wpisów dziennika",
87
87
  "integrations.detail.back": "Powrót do integracji",
88
+ "integrations.detail.backToList": "Wróć do integracji",
88
89
  "integrations.detail.credentials.bundleShared": "Współdzielone dane z pakietu: {bundle}",
89
90
  "integrations.detail.credentials.notConfigured": "Brak skonfigurowanych danych",
90
91
  "integrations.detail.credentials.save": "Zapisz dane",
@@ -126,6 +127,7 @@
126
127
  "integrations.detail.logs.level.info": "Info",
127
128
  "integrations.detail.logs.level.warn": "Ostrzeżenie",
128
129
  "integrations.detail.logs.title": "Logi operacji",
130
+ "integrations.detail.notFound": "Nie znaleziono integracji.",
129
131
  "integrations.detail.runActivity.lastRefreshed": "Ostatnio odświeżono",
130
132
  "integrations.detail.runActivity.processed": "Przetworzone",
131
133
  "integrations.detail.runActivity.progress": "Postęp",