@open-mercato/core 0.9.1-develop.7519.1.38f51f8dbb → 0.9.1-develop.7536.1.f9a5776a07

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 (105) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/logout.js +14 -6
  3. package/dist/modules/auth/api/logout.js.map +2 -2
  4. package/dist/modules/auth/api/session/refresh.js +6 -1
  5. package/dist/modules/auth/api/session/refresh.js.map +2 -2
  6. package/dist/modules/catalog/ai-tools/mutation-pack.js +10 -5
  7. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +2 -2
  8. package/dist/modules/catalog/ai-tools/types.js.map +2 -2
  9. package/dist/modules/currencies/backend/currencies/[id]/page.js +5 -2
  10. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  11. package/dist/modules/currencies/commands/currencies.js +399 -137
  12. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  13. package/dist/modules/currencies/lib/seeds.js +2 -0
  14. package/dist/modules/currencies/lib/seeds.js.map +2 -2
  15. package/dist/modules/customer_accounts/api/login.js +14 -7
  16. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  17. package/dist/modules/customer_accounts/services/customerUserService.js +10 -10
  18. package/dist/modules/customer_accounts/services/customerUserService.js.map +2 -2
  19. package/dist/modules/customers/commands/companies.js +10 -3
  20. package/dist/modules/customers/commands/companies.js.map +2 -2
  21. package/dist/modules/customers/commands/interactionRestoreDetails.js +75 -0
  22. package/dist/modules/customers/commands/interactionRestoreDetails.js.map +7 -0
  23. package/dist/modules/customers/commands/people.js +10 -3
  24. package/dist/modules/customers/commands/people.js.map +2 -2
  25. package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js +1 -1
  26. package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js.map +1 -1
  27. package/dist/modules/forms/api/form-submissions/[id]/submit/route.js +12 -2
  28. package/dist/modules/forms/api/form-submissions/[id]/submit/route.js.map +2 -2
  29. package/dist/modules/forms/api/form-submissions/route.js +16 -1
  30. package/dist/modules/forms/api/form-submissions/route.js.map +2 -2
  31. package/dist/modules/forms/api/public/submissions/[id]/submit/route.js +6 -1
  32. package/dist/modules/forms/api/public/submissions/[id]/submit/route.js.map +2 -2
  33. package/dist/modules/forms/api/runtime-helpers.js +16 -0
  34. package/dist/modules/forms/api/runtime-helpers.js.map +2 -2
  35. package/dist/modules/forms/data/validators.js.map +2 -2
  36. package/dist/modules/forms/lib/portal-subject-policy.js +17 -0
  37. package/dist/modules/forms/lib/portal-subject-policy.js.map +7 -0
  38. package/dist/modules/forms/lib/runtime-principal.js +27 -10
  39. package/dist/modules/forms/lib/runtime-principal.js.map +2 -2
  40. package/dist/modules/forms/services/distribution-service.js +35 -13
  41. package/dist/modules/forms/services/distribution-service.js.map +2 -2
  42. package/dist/modules/forms/services/submission-service.js +4 -0
  43. package/dist/modules/forms/services/submission-service.js.map +2 -2
  44. package/dist/modules/staff/commands/leave-requests.js +54 -24
  45. package/dist/modules/staff/commands/leave-requests.js.map +2 -2
  46. package/dist/modules/warranty_claims/api/portal/attachments/route.js +19 -7
  47. package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
  48. package/dist/modules/warranty_claims/commands/claims.js +1 -1
  49. package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
  50. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +6 -6
  51. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +2 -2
  52. package/dist/modules/wms/commands/inventory-actions.js +19 -3
  53. package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
  54. package/dist/modules/workflows/lib/async-activity-wait.js +43 -0
  55. package/dist/modules/workflows/lib/async-activity-wait.js.map +7 -0
  56. package/dist/modules/workflows/lib/parallel-handler.js +20 -14
  57. package/dist/modules/workflows/lib/parallel-handler.js.map +2 -2
  58. package/dist/modules/workflows/lib/workflow-executor.js +21 -23
  59. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  60. package/package.json +7 -7
  61. package/src/modules/auth/api/logout.ts +15 -6
  62. package/src/modules/auth/api/session/refresh.ts +6 -1
  63. package/src/modules/catalog/ai-tools/mutation-pack.ts +14 -5
  64. package/src/modules/catalog/ai-tools/types.ts +1 -0
  65. package/src/modules/currencies/backend/currencies/[id]/page.tsx +5 -1
  66. package/src/modules/currencies/commands/currencies.ts +475 -168
  67. package/src/modules/currencies/i18n/de.json +4 -0
  68. package/src/modules/currencies/i18n/en.json +4 -0
  69. package/src/modules/currencies/i18n/es.json +4 -0
  70. package/src/modules/currencies/i18n/ko.json +4 -0
  71. package/src/modules/currencies/i18n/pl.json +4 -0
  72. package/src/modules/currencies/lib/seeds.ts +2 -0
  73. package/src/modules/customer_accounts/api/login.ts +15 -7
  74. package/src/modules/customer_accounts/services/customerUserService.ts +34 -10
  75. package/src/modules/customers/commands/companies.ts +16 -4
  76. package/src/modules/customers/commands/interactionRestoreDetails.ts +110 -0
  77. package/src/modules/customers/commands/people.ts +16 -4
  78. package/src/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.ts +1 -1
  79. package/src/modules/forms/api/form-submissions/[id]/submit/route.ts +16 -3
  80. package/src/modules/forms/api/form-submissions/route.ts +21 -2
  81. package/src/modules/forms/api/public/submissions/[id]/submit/route.ts +7 -0
  82. package/src/modules/forms/api/runtime-helpers.ts +16 -0
  83. package/src/modules/forms/data/validators.ts +2 -1
  84. package/src/modules/forms/i18n/de.json +1 -0
  85. package/src/modules/forms/i18n/en.json +1 -0
  86. package/src/modules/forms/i18n/es.json +1 -0
  87. package/src/modules/forms/i18n/ko.json +1 -0
  88. package/src/modules/forms/i18n/pl.json +1 -0
  89. package/src/modules/forms/lib/portal-subject-policy.ts +42 -0
  90. package/src/modules/forms/lib/runtime-principal.ts +35 -11
  91. package/src/modules/forms/services/distribution-service.ts +35 -13
  92. package/src/modules/forms/services/submission-service.ts +12 -0
  93. package/src/modules/staff/commands/leave-requests.ts +54 -23
  94. package/src/modules/staff/i18n/de.json +2 -0
  95. package/src/modules/staff/i18n/en.json +2 -0
  96. package/src/modules/staff/i18n/es.json +2 -0
  97. package/src/modules/staff/i18n/ko.json +2 -0
  98. package/src/modules/staff/i18n/pl.json +2 -0
  99. package/src/modules/warranty_claims/api/portal/attachments/route.ts +20 -8
  100. package/src/modules/warranty_claims/commands/claims.ts +1 -1
  101. package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +5 -5
  102. package/src/modules/wms/commands/inventory-actions.ts +30 -10
  103. package/src/modules/workflows/lib/async-activity-wait.ts +71 -0
  104. package/src/modules/workflows/lib/parallel-handler.ts +24 -15
  105. package/src/modules/workflows/lib/workflow-executor.ts +24 -27
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/currencies/commands/currencies.ts"],
4
- "sourcesContent": ["import { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { buildChanges, requireId, emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, conflict, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { Currency, ExchangeRate } from '../data/entities'\nimport {\n currencyCreateSchema,\n currencyUpdateSchema,\n currencyDeleteSchema,\n type CurrencyCreateInput,\n type CurrencyUpdateInput,\n type CurrencyDeleteInput,\n} from '../data/validators'\nimport type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { buildCurrencyCommandWhere, ensureCurrencyCommandScope } from './scope'\n\nconst currencyCrudEvents: CrudEventsConfig = {\n module: 'currencies',\n entity: 'currency',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\ntype CurrencySnapshot = {\n id: string\n organizationId: string\n tenantId: string\n code: string\n name: string\n symbol: string | null\n decimalPlaces: number\n thousandsSeparator: string | null\n decimalSeparator: string | null\n isBase: boolean\n isActive: boolean\n createdAt: string\n updatedAt: string\n}\n\ntype CurrencyUndoPayload = UndoPayload<CurrencySnapshot>\n\nasync function loadCurrencySnapshot(\n em: EntityManager,\n id: string,\n ctx: Parameters<typeof buildCurrencyCommandWhere>[0],\n): Promise<CurrencySnapshot | null> {\n const record = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id }),\n )\n if (!record) return null\n ensureCurrencyCommandScope(ctx, record)\n return {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n code: record.code,\n name: record.name,\n symbol: record.symbol ?? null,\n decimalPlaces: record.decimalPlaces,\n thousandsSeparator: record.thousandsSeparator ?? null,\n decimalSeparator: record.decimalSeparator ?? null,\n isBase: !!record.isBase,\n isActive: !!record.isActive,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt.toISOString(),\n }\n}\n\nasync function enforceBaseCurrency(\n em: EntityManager,\n currencyId: string,\n organizationId: string,\n tenantId: string\n): Promise<void> {\n await em.nativeUpdate(\n Currency,\n {\n organizationId,\n tenantId,\n id: { $ne: currencyId },\n isBase: true,\n deletedAt: null,\n },\n { isBase: false, updatedAt: new Date() }\n )\n}\n\nconst createCurrencyCommand: CommandHandler<CurrencyCreateInput, { currencyId: string }> = {\n id: 'currencies.currencies.create',\n async execute(input, ctx) {\n const parsed = currencyCreateSchema.parse(input)\n ensureCurrencyCommandScope(ctx, parsed)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n\n // Check for duplicate code\n const existing = await em.findOne(Currency, {\n code: parsed.code,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n deletedAt: null,\n })\n if (existing) {\n throw conflict('Currency code already exists for this organization.')\n }\n\n const now = new Date()\n const record = em.create(Currency, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n code: parsed.code,\n name: parsed.name,\n symbol: parsed.symbol ?? null,\n decimalPlaces: parsed.decimalPlaces ?? 2,\n thousandsSeparator: parsed.thousandsSeparator ?? null,\n decimalSeparator: parsed.decimalSeparator ?? null,\n isBase: parsed.isBase ?? false,\n isActive: parsed.isActive !== false,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n\n // Demote any existing base currency and insert the new record in one\n // transaction; a partial commit would leave zero or two base currencies.\n try {\n await withAtomicFlush(\n em,\n [\n () =>\n record.isBase\n ? enforceBaseCurrency(em, record.id, record.organizationId, record.tenantId)\n : undefined,\n ],\n { transaction: true },\n )\n } catch (err) {\n if (isUniqueViolation(err, 'currencies_code_scope_unique')) {\n throw conflict('Currency code already exists for this organization.')\n }\n throw err\n }\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return { currencyId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadCurrencySnapshot(em, result.currencyId, ctx)\n },\n buildLog: async ({ snapshots }) => {\n const after = snapshots.after as CurrencySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.create', 'Create currency'),\n resourceKind: 'currencies.currency',\n resourceId: after.id,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotAfter: after,\n payload: { undo: { after } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const after = payload?.after ?? null\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(Currency, { id: after.id })\n if (!record) return\n record.deletedAt = new Date()\n record.isActive = false\n await em.flush()\n },\n redo: makeCreateRedo<Currency, CurrencySnapshot, CurrencyCreateInput, { currencyId: string }>({\n entityClass: Currency,\n buildResult: (entity) => ({ currencyId: entity.id }),\n events: currencyCrudEvents,\n afterRestore: async ({ em, entity }) => {\n if (entity.isBase) {\n await enforceBaseCurrency(em, entity.id, entity.organizationId, entity.tenantId)\n await em.flush()\n }\n },\n }),\n}\n\nconst updateCurrencyCommand: CommandHandler<CurrencyUpdateInput, { currencyId: string }> = {\n id: 'currencies.currencies.update',\n async prepare(input, ctx) {\n requireId(input.id, 'Currency ID is required')\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadCurrencySnapshot(em, input.id, ctx)\n return { before }\n },\n async execute(input, ctx) {\n const parsed = currencyUpdateSchema.parse(input)\n requireId(parsed.id, 'Currency ID is required')\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!record) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, record)\n\n // Check code uniqueness if changing code\n if (parsed.code && parsed.code !== record.code) {\n const existing = await em.findOne(Currency, {\n code: parsed.code,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n id: { $ne: record.id },\n deletedAt: null,\n })\n if (existing) {\n throw conflict('Currency code already exists for this organization.')\n }\n }\n\n const allChanges = buildChanges(record as unknown as Record<string, unknown>, parsed, [\n 'code',\n 'name',\n 'symbol',\n 'decimalPlaces',\n 'thousandsSeparator',\n 'decimalSeparator',\n 'isBase',\n 'isActive',\n ])\n const changes = Object.fromEntries(\n Object.entries(allChanges).filter(([, c]) => c.to !== undefined),\n ) as Record<string, { from: unknown; to: unknown }>\n\n if (Object.keys(changes).length === 0) {\n return { currencyId: record.id }\n }\n\n // Demote any existing base currency and persist the scalar changes in one\n // transaction; a partial commit would leave zero or two base currencies.\n // The scalar mutations live in the first phase so the per-phase flush issues\n // the pending changeset on the managed `record` before `enforceBaseCurrency`\n // runs its interleaved `nativeUpdate` (which would otherwise drop the pending\n // UPDATE under v7).\n await withAtomicFlush(\n em,\n [\n () => {\n for (const [key, change] of Object.entries(changes)) {\n ;(record as any)[key] = change.to\n }\n record.updatedAt = new Date()\n },\n () =>\n parsed.isBase === true && record.isBase\n ? enforceBaseCurrency(em, record.id, record.organizationId, record.tenantId)\n : undefined,\n ],\n { transaction: true },\n )\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return { currencyId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadCurrencySnapshot(em, result.currencyId, ctx)\n },\n buildLog: async ({ snapshots, result }) => {\n const before = snapshots.before as CurrencySnapshot | undefined\n const after = snapshots.after as CurrencySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.update', 'Update currency'),\n resourceKind: 'currencies.currency',\n resourceId: after.id,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotBefore: before ?? undefined,\n snapshotAfter: after,\n payload: { undo: { before, after } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const before = payload?.before ?? null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(Currency, { id: before.id })\n if (!record) return\n Object.assign(record, {\n code: before.code,\n name: before.name,\n symbol: before.symbol,\n decimalPlaces: before.decimalPlaces,\n thousandsSeparator: before.thousandsSeparator,\n decimalSeparator: before.decimalSeparator,\n isBase: before.isBase,\n isActive: before.isActive,\n updatedAt: new Date(),\n })\n await em.flush()\n },\n}\n\nconst deleteCurrencyCommand: CommandHandler<CurrencyDeleteInput, { currencyId: string }> = {\n id: 'currencies.currencies.delete',\n async prepare(input, ctx) {\n requireId(input.id, 'Currency ID is required')\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadCurrencySnapshot(em, input.id, ctx)\n return { before }\n },\n async execute(input, ctx) {\n const parsed = currencyDeleteSchema.parse(input)\n requireId(parsed.id, 'Currency ID is required')\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!record) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, record)\n\n // Prevent deleting base currency\n if (record.isBase) {\n throw new CrudHttpError(400, { error: 'Cannot delete the base currency' })\n }\n\n // Prevent deleting currency with active exchange rates\n const activeRatesCount = await em.count(ExchangeRate, {\n $or: [\n { fromCurrencyCode: record.code },\n { toCurrencyCode: record.code },\n ],\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n deletedAt: null,\n isActive: true,\n })\n \n if (activeRatesCount > 0) {\n throw new CrudHttpError(400, { \n error: `Cannot delete currency ${record.code} because it has ${activeRatesCount} active exchange rate(s). Please delete or deactivate the exchange rates first.` \n })\n }\n\n record.deletedAt = new Date()\n record.isActive = false\n await em.flush()\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return { currencyId: record.id }\n },\n buildLog: async ({ snapshots, result }) => {\n const before = snapshots.before as CurrencySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.delete', 'Delete currency'),\n resourceKind: 'currencies.currency',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: { undo: { before } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const before = payload?.before ?? null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(Currency, { id: before.id })\n if (!record) return\n record.deletedAt = null\n record.isActive = before.isActive\n record.updatedAt = new Date()\n await em.flush()\n },\n}\n\nregisterCommand(createCurrencyCommand)\nregisterCommand(updateCurrencyCommand)\nregisterCommand(deleteCurrencyCommand)\n\nexport { createCurrencyCommand, updateCurrencyCommand, deleteCurrencyCommand }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { buildChanges, requireId, emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { resolveRedoSnapshot, restoreCreatedRow, reviveSnapshotSeed } from '@open-mercato/shared/lib/commands/redo'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, conflict, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { Currency, ExchangeRate } from '../data/entities'\nimport {\n currencyCreateSchema,\n currencyUpdateSchema,\n currencyDeleteSchema,\n type CurrencyCreateInput,\n type CurrencyUpdateInput,\n type CurrencyDeleteInput,\n} from '../data/validators'\nimport type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { buildCurrencyCommandWhere, ensureCurrencyCommandScope } from './scope'\n\nconst currencyCrudEvents: CrudEventsConfig = {\n module: 'currencies',\n entity: 'currency',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\ntype CurrencySnapshot = {\n id: string\n organizationId: string\n tenantId: string\n code: string\n name: string\n symbol: string | null\n decimalPlaces: number\n thousandsSeparator: string | null\n decimalSeparator: string | null\n isBase: boolean\n isActive: boolean\n createdAt: string\n updatedAt: string\n}\n\nasync function loadCurrencySnapshot(\n em: EntityManager,\n id: string,\n ctx: Parameters<typeof buildCurrencyCommandWhere>[0],\n): Promise<CurrencySnapshot | null> {\n const record = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id }),\n )\n if (!record) return null\n ensureCurrencyCommandScope(ctx, record)\n return {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n code: record.code,\n name: record.name,\n symbol: record.symbol ?? null,\n decimalPlaces: record.decimalPlaces,\n thousandsSeparator: record.thousandsSeparator ?? null,\n decimalSeparator: record.decimalSeparator ?? null,\n isBase: !!record.isBase,\n isActive: !!record.isActive,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt.toISOString(),\n }\n}\n\ntype CurrencyScope = {\n tenantId: string\n organizationId: string\n}\n\ntype BaseCurrencyTransition = {\n promoted: boolean\n demotedBaseCurrencyIds: string[]\n}\n\ntype CurrencyCommandResult = {\n currencyId: string\n base?: BaseCurrencyTransition\n}\n\ntype CurrencyUndoPayload = UndoPayload<CurrencySnapshot> & {\n base?: BaseCurrencyTransition\n}\n\nconst NO_BASE_TRANSITION: BaseCurrencyTransition = { promoted: false, demotedBaseCurrencyIds: [] }\n\nconst BASE_CURRENCY_LOCK_SQL = 'select pg_advisory_xact_lock(hashtextextended(?, 0))'\n\nconst UPDATABLE_CURRENCY_FIELDS = [\n 'code',\n 'name',\n 'symbol',\n 'decimalPlaces',\n 'thousandsSeparator',\n 'decimalSeparator',\n 'isBase',\n 'isActive',\n] as const\n\nfunction baseCurrencyLockKey(scope: CurrencyScope): string {\n return `currencies:base:${scope.tenantId}:${scope.organizationId}`\n}\n\n/**\n * Serialize every currency write inside one tenant and organization so the\n * \"exactly one live base currency\" invariant is decided on fresh rows. The\n * transaction owns the lock, so PostgreSQL releases it on commit/rollback.\n */\nasync function lockBaseCurrencyScope(em: EntityManager, scope: CurrencyScope): Promise<void> {\n if (!em.isInTransaction()) {\n throw new Error('[internal] Currency base lock requires an active transaction')\n }\n await em.execute(BASE_CURRENCY_LOCK_SQL, [baseCurrencyLockKey(scope)])\n}\n\nasync function findLiveBaseCurrencyIds(\n em: EntityManager,\n scope: CurrencyScope,\n excludeId?: string,\n): Promise<string[]> {\n const rows = await em.find(\n Currency,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isBase: true,\n deletedAt: null,\n ...(excludeId ? { id: { $ne: excludeId } } : {}),\n },\n { orderBy: { createdAt: 'asc', id: 'asc' } },\n )\n return rows.map((row) => row.id)\n}\n\nasync function setBaseFlag(\n em: EntityManager,\n scope: CurrencyScope,\n ids: string[],\n isBase: boolean,\n): Promise<void> {\n if (ids.length === 0) return\n await em.nativeUpdate(\n Currency,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n id: { $in: ids },\n },\n { isBase, updatedAt: new Date() },\n )\n}\n\nasync function findFirstLivePredecessorId(\n em: EntityManager,\n scope: CurrencyScope,\n candidateIds: string[],\n): Promise<string | null> {\n if (candidateIds.length === 0) return null\n const rows = await em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n id: { $in: candidateIds },\n deletedAt: null,\n })\n const liveIds = new Set(rows.map((row) => row.id))\n return candidateIds.find((id) => liveIds.has(id)) ?? null\n}\n\nasync function translatedHttpError(status: number, key: string, fallback: string): Promise<CrudHttpError> {\n const { translate } = await resolveTranslations()\n return new CrudHttpError(status, { error: translate(key, fallback) })\n}\n\nasync function resolvePredecessorOrRefuse(\n em: EntityManager,\n scope: CurrencyScope,\n candidateIds: string[],\n): Promise<string> {\n const predecessorId = await findFirstLivePredecessorId(em, scope, candidateIds)\n if (!predecessorId) {\n throw await translatedHttpError(\n 409,\n 'currencies.errors.undoWouldClearBase',\n \"This change can't be undone because it would leave the organization without a base currency.\",\n )\n }\n return predecessorId\n}\n\nfunction countActiveRatesForCode(em: EntityManager, scope: CurrencyScope, code: string): Promise<number> {\n return em.count(ExchangeRate, {\n $or: [\n { fromCurrencyCode: code },\n { toCurrencyCode: code },\n ],\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n isActive: true,\n })\n}\n\nfunction readScopedSnapshotRecord(em: EntityManager, snapshot: CurrencySnapshot): Promise<Currency | null> {\n return em.findOne(Currency, {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n })\n}\n\nfunction scopeOf(record: CurrencyScope): CurrencyScope {\n return { tenantId: record.tenantId, organizationId: record.organizationId }\n}\n\nfunction omitIsBase(input: CurrencyUpdateInput): Partial<CurrencyUpdateInput> {\n const rest: Partial<CurrencyUpdateInput> = { ...input }\n delete rest.isBase\n return rest\n}\n\nfunction isPromotionByLegacyLog(payload: CurrencyUndoPayload | null): boolean {\n return payload?.before?.isBase === false && payload?.after?.isBase === true\n}\n\nfunction isDemotionByLegacyLog(payload: CurrencyUndoPayload | null): boolean {\n return !payload?.base && payload?.before?.isBase === true && payload?.after?.isBase === false\n}\n\nconst createCurrencyCommand: CommandHandler<CurrencyCreateInput, CurrencyCommandResult> = {\n id: 'currencies.currencies.create',\n async execute(input, ctx) {\n const parsed = currencyCreateSchema.parse(input)\n ensureCurrencyCommandScope(ctx, parsed)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = scopeOf(parsed)\n let demotedBaseCurrencyIds: string[] = []\n let record!: Currency\n\n try {\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const existing = await em.findOne(Currency, {\n code: parsed.code,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n deletedAt: null,\n })\n if (existing) {\n throw conflict('Currency code already exists for this organization.')\n }\n if (parsed.isBase) {\n demotedBaseCurrencyIds = await findLiveBaseCurrencyIds(em, scope)\n await setBaseFlag(em, scope, demotedBaseCurrencyIds, false)\n }\n },\n () => {\n const now = new Date()\n record = em.create(Currency, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n code: parsed.code,\n name: parsed.name,\n symbol: parsed.symbol ?? null,\n decimalPlaces: parsed.decimalPlaces ?? 2,\n thousandsSeparator: parsed.thousandsSeparator ?? null,\n decimalSeparator: parsed.decimalSeparator ?? null,\n isBase: parsed.isBase ?? false,\n isActive: parsed.isActive !== false,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n },\n ],\n { transaction: true },\n )\n } catch (err) {\n if (isUniqueViolation(err, 'currencies_code_scope_unique')) {\n throw conflict('Currency code already exists for this organization.')\n }\n throw err\n }\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return record.isBase\n ? { currencyId: record.id, base: { promoted: true, demotedBaseCurrencyIds } }\n : { currencyId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadCurrencySnapshot(em, result.currencyId, ctx)\n },\n buildLog: async ({ snapshots, result }) => {\n const after = snapshots.after as CurrencySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.create', 'Create currency'),\n resourceKind: 'currencies.currency',\n resourceId: after.id,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotAfter: after,\n payload: { undo: { after, base: result?.base ?? NO_BASE_TRANSITION } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const after = payload?.after ?? null\n if (!after) return\n const scope = scopeOf(after)\n const promoted = payload?.base ? payload.base.promoted : after.isBase\n const predecessorIds = payload?.base?.demotedBaseCurrencyIds ?? []\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let record: Currency | null = null\n let restoredBaseId: string | null = null\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await readScopedSnapshotRecord(em, after)\n if (!current || current.deletedAt) return\n const activeRatesCount = await countActiveRatesForCode(em, scope, current.code)\n if (activeRatesCount > 0) {\n throw await translatedHttpError(\n 409,\n 'currencies.errors.undoHasRates',\n \"This change can't be undone because exchange rates now use this currency. Delete or deactivate those exchange rates first.\",\n )\n }\n if (current.isBase) {\n restoredBaseId = await resolvePredecessorOrRefuse(em, scope, promoted ? predecessorIds : [])\n }\n record = current\n },\n () => {\n if (!record) return\n record.deletedAt = new Date()\n record.isActive = false\n record.isBase = false\n },\n () => (restoredBaseId ? setBaseFlag(em, scope, [restoredBaseId], true) : undefined),\n ],\n { transaction: true },\n )\n },\n redo: async ({ ctx, logEntry }) => {\n const snapshot = resolveRedoSnapshot<CurrencySnapshot>(logEntry)\n if (!snapshot || !snapshot.id) {\n throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for create command' })\n }\n const scope = scopeOf(snapshot)\n const isBase = snapshot.isBase === true\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let demotedBaseCurrencyIds: string[] = []\n let record!: Currency\n\n try {\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n if (isBase) {\n demotedBaseCurrencyIds = await findLiveBaseCurrencyIds(em, scope, snapshot.id)\n await setBaseFlag(em, scope, demotedBaseCurrencyIds, false)\n }\n },\n async () => {\n record = await restoreCreatedRow(\n em,\n Currency,\n snapshot.id,\n () => reviveSnapshotSeed(snapshot as unknown as Record<string, unknown>),\n (lookupEm) => readScopedSnapshotRecord(lookupEm, snapshot),\n )\n record.isBase = isBase\n },\n ],\n { transaction: true },\n )\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw conflict('[internal] Cannot redo: a record with the same unique key already exists.')\n }\n throw err\n }\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return isBase\n ? { currencyId: record.id, base: { promoted: true, demotedBaseCurrencyIds } }\n : { currencyId: record.id }\n },\n}\n\nconst updateCurrencyCommand: CommandHandler<CurrencyUpdateInput, CurrencyCommandResult> = {\n id: 'currencies.currencies.update',\n async prepare(input, ctx) {\n requireId(input.id, 'Currency ID is required')\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadCurrencySnapshot(em, input.id, ctx)\n return { before }\n },\n async execute(input, ctx) {\n const parsed = currencyUpdateSchema.parse(input)\n requireId(parsed.id, 'Currency ID is required')\n\n const scopeRecord = await (ctx.container.resolve('em') as EntityManager).fork().findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!scopeRecord) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, scopeRecord)\n const scope = scopeOf(scopeRecord)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let record!: Currency\n let changes: Record<string, { from: unknown; to: unknown }> = {}\n let promoted = false\n let demotedBaseCurrencyIds: string[] = []\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!current) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, current)\n record = current\n\n if (parsed.code && parsed.code !== record.code) {\n const existing = await em.findOne(Currency, {\n code: parsed.code,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n id: { $ne: record.id },\n deletedAt: null,\n })\n if (existing) {\n throw conflict('Currency code already exists for this organization.')\n }\n }\n\n if (parsed.isBase === false && record.isBase) {\n throw await translatedHttpError(\n 400,\n 'currencies.errors.baseCurrencyUnset',\n 'Choose another currency as the base instead of clearing it.',\n )\n }\n\n const allChanges = buildChanges(\n record as unknown as Record<string, unknown>,\n parsed,\n UPDATABLE_CURRENCY_FIELDS,\n )\n changes = Object.fromEntries(\n Object.entries(allChanges).filter(([, change]) => change.to !== undefined),\n ) as Record<string, { from: unknown; to: unknown }>\n\n promoted = changes.isBase?.to === true\n if (promoted) {\n demotedBaseCurrencyIds = await findLiveBaseCurrencyIds(em, scope, record.id)\n }\n },\n () => {\n if (Object.keys(changes).length === 0) return\n const assignments = Object.fromEntries(\n Object.entries(changes).map(([key, change]) => [key, change.to]),\n )\n Object.assign(record, assignments, { updatedAt: new Date() })\n },\n () => setBaseFlag(em, scope, demotedBaseCurrencyIds, false),\n ],\n { transaction: true },\n )\n\n const result: CurrencyCommandResult = promoted\n ? { currencyId: record.id, base: { promoted: true, demotedBaseCurrencyIds } }\n : { currencyId: record.id }\n if (Object.keys(changes).length === 0) {\n return result\n }\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return result\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadCurrencySnapshot(em, result.currencyId, ctx)\n },\n buildLog: async ({ input, snapshots, result }) => {\n const before = snapshots.before as CurrencySnapshot | undefined\n const after = snapshots.after as CurrencySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n const base = result?.base ?? NO_BASE_TRANSITION\n return {\n actionLabel: translate('currencies.audit.update', 'Update currency'),\n resourceKind: 'currencies.currency',\n resourceId: after.id,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotBefore: before ?? undefined,\n snapshotAfter: after,\n payload: { undo: { before, after, base } },\n redoInput: base.promoted ? input : omitIsBase(input),\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const before = payload?.before ?? null\n if (!before) return\n const scope = scopeOf(before)\n const promoted = payload?.base ? payload.base.promoted : isPromotionByLegacyLog(payload)\n const predecessorIds = payload?.base?.demotedBaseCurrencyIds ?? []\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const demotedByLegacyLog = isDemotionByLegacyLog(payload)\n let record: Currency | null = null\n let restoredBaseId: string | null = null\n let repromote = false\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await readScopedSnapshotRecord(em, before)\n if (!current) return\n if (promoted && current.isBase && !current.deletedAt) {\n restoredBaseId = await resolvePredecessorOrRefuse(em, scope, predecessorIds)\n }\n if (demotedByLegacyLog && !current.isBase && !current.deletedAt) {\n repromote = (await findLiveBaseCurrencyIds(em, scope)).length === 0\n }\n record = current\n },\n () => {\n if (!record) return\n Object.assign(record, {\n code: before.code,\n name: before.name,\n symbol: before.symbol,\n decimalPlaces: before.decimalPlaces,\n thousandsSeparator: before.thousandsSeparator,\n decimalSeparator: before.decimalSeparator,\n isActive: before.isActive,\n updatedAt: new Date(),\n })\n if (restoredBaseId) record.isBase = false\n if (repromote) record.isBase = true\n },\n () => (restoredBaseId ? setBaseFlag(em, scope, [restoredBaseId], true) : undefined),\n ],\n { transaction: true },\n )\n },\n}\n\nconst deleteCurrencyCommand: CommandHandler<CurrencyDeleteInput, { currencyId: string }> = {\n id: 'currencies.currencies.delete',\n async prepare(input, ctx) {\n requireId(input.id, 'Currency ID is required')\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadCurrencySnapshot(em, input.id, ctx)\n return { before }\n },\n async execute(input, ctx) {\n const parsed = currencyDeleteSchema.parse(input)\n requireId(parsed.id, 'Currency ID is required')\n\n const scopeRecord = await (ctx.container.resolve('em') as EntityManager).fork().findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!scopeRecord) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, scopeRecord)\n const scope = scopeOf(scopeRecord)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let record!: Currency\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!current) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, current)\n\n if (current.isBase) {\n throw new CrudHttpError(400, { error: 'Cannot delete the base currency' })\n }\n\n const activeRatesCount = await countActiveRatesForCode(em, scope, current.code)\n if (activeRatesCount > 0) {\n throw new CrudHttpError(400, {\n error: `Cannot delete currency ${current.code} because it has ${activeRatesCount} active exchange rate(s). Please delete or deactivate the exchange rates first.`,\n })\n }\n record = current\n },\n () => {\n record.deletedAt = new Date()\n record.isActive = false\n },\n ],\n { transaction: true },\n )\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return { currencyId: record.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as CurrencySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.delete', 'Delete currency'),\n resourceKind: 'currencies.currency',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: { undo: { before } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const before = payload?.before ?? null\n if (!before) return\n const scope = scopeOf(before)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let record: Currency | null = null\n let reviveAsNonBase = false\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await readScopedSnapshotRecord(em, before)\n if (!current) return\n if (current.isBase) {\n const otherBaseIds = await findLiveBaseCurrencyIds(em, scope, current.id)\n reviveAsNonBase = otherBaseIds.length > 0\n }\n record = current\n },\n () => {\n if (!record) return\n record.deletedAt = null\n record.isActive = before.isActive\n if (reviveAsNonBase) record.isBase = false\n record.updatedAt = new Date()\n },\n ],\n { transaction: true },\n )\n },\n}\n\nregisterCommand(createCurrencyCommand)\nregisterCommand(updateCurrencyCommand)\nregisterCommand(deleteCurrencyCommand)\n\nexport { createCurrencyCommand, updateCurrencyCommand, deleteCurrencyCommand }\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -110,6 +110,7 @@ async function seedExampleCurrencies(em, scope) {
110
110
  for (const entry of existingEntries) {
111
111
  existingMap.set(entry.code, entry);
112
112
  }
113
+ const hasLiveBase = existingEntries.some((entry) => entry.isBase && !entry.deletedAt);
113
114
  let touched = false;
114
115
  for (const curr of SEED_CURRENCIES) {
115
116
  const current = existingMap.get(curr.code);
@@ -126,6 +127,7 @@ async function seedExampleCurrencies(em, scope) {
126
127
  tenantId: scope.tenantId,
127
128
  organizationId: scope.organizationId,
128
129
  ...curr,
130
+ isBase: curr.isBase && !hasLiveBase,
129
131
  createdAt: /* @__PURE__ */ new Date(),
130
132
  updatedAt: /* @__PURE__ */ new Date()
131
133
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/currencies/lib/seeds.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Currency } from '../data/entities'\n\nexport type CurrencySeedScope = { tenantId: string; organizationId: string }\n\ntype CurrencySeed = {\n code: string\n name: string\n decimalPlaces: number\n symbol: string\n decimalSeparator: string\n thousandsSeparator: string\n isBase: boolean\n isActive: boolean\n}\n\nconst SEED_CURRENCIES: CurrencySeed[] = [\n {\n code: 'USD',\n name: 'US Dollar',\n decimalPlaces: 2,\n symbol: '$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: true,\n isActive: true,\n },\n {\n code: 'EUR',\n name: 'Euro',\n decimalPlaces: 2,\n symbol: '\u20AC',\n decimalSeparator: ',',\n thousandsSeparator: '.',\n isBase: false,\n isActive: true,\n },\n {\n code: 'JPY',\n name: 'Japanese Yen',\n decimalPlaces: 0,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'GBP',\n name: 'British Pound',\n decimalPlaces: 2,\n symbol: '\u00A3',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CHF',\n name: 'Swiss Franc',\n decimalPlaces: 2,\n symbol: 'Fr',\n decimalSeparator: '.',\n thousandsSeparator: \"'\",\n isBase: false,\n isActive: true,\n },\n {\n code: 'CAD',\n name: 'Canadian Dollar',\n decimalPlaces: 2,\n symbol: 'C$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'AUD',\n name: 'Australian Dollar',\n decimalPlaces: 2,\n symbol: 'A$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CNY',\n name: 'Chinese Yuan',\n decimalPlaces: 2,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CNH',\n name: 'Chinese Yuan (Offshore)',\n decimalPlaces: 2,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'PLN',\n name: 'Polish Zloty',\n decimalPlaces: 2,\n symbol: 'z\u0142',\n decimalSeparator: ',',\n thousandsSeparator: ' ',\n isBase: false,\n isActive: true,\n },\n]\n\nexport async function seedExampleCurrencies(em: EntityManager, scope: CurrencySeedScope): Promise<boolean> {\n const existingEntries = await em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const existingMap = new Map<string, Currency>()\n for (const entry of existingEntries) {\n existingMap.set(entry.code, entry)\n }\n\n let touched = false\n for (const curr of SEED_CURRENCIES) {\n const current = existingMap.get(curr.code)\n if (current) {\n if (current.name !== curr.name) {\n current.name = curr.name\n current.updatedAt = new Date()\n em.persist(current)\n touched = true\n }\n continue\n }\n const entry = em.create(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n ...curr,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n touched = true\n }\n\n if (touched) {\n await em.flush()\n }\n return touched\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Currency } from '../data/entities'\n\nexport type CurrencySeedScope = { tenantId: string; organizationId: string }\n\ntype CurrencySeed = {\n code: string\n name: string\n decimalPlaces: number\n symbol: string\n decimalSeparator: string\n thousandsSeparator: string\n isBase: boolean\n isActive: boolean\n}\n\nconst SEED_CURRENCIES: CurrencySeed[] = [\n {\n code: 'USD',\n name: 'US Dollar',\n decimalPlaces: 2,\n symbol: '$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: true,\n isActive: true,\n },\n {\n code: 'EUR',\n name: 'Euro',\n decimalPlaces: 2,\n symbol: '\u20AC',\n decimalSeparator: ',',\n thousandsSeparator: '.',\n isBase: false,\n isActive: true,\n },\n {\n code: 'JPY',\n name: 'Japanese Yen',\n decimalPlaces: 0,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'GBP',\n name: 'British Pound',\n decimalPlaces: 2,\n symbol: '\u00A3',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CHF',\n name: 'Swiss Franc',\n decimalPlaces: 2,\n symbol: 'Fr',\n decimalSeparator: '.',\n thousandsSeparator: \"'\",\n isBase: false,\n isActive: true,\n },\n {\n code: 'CAD',\n name: 'Canadian Dollar',\n decimalPlaces: 2,\n symbol: 'C$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'AUD',\n name: 'Australian Dollar',\n decimalPlaces: 2,\n symbol: 'A$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CNY',\n name: 'Chinese Yuan',\n decimalPlaces: 2,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CNH',\n name: 'Chinese Yuan (Offshore)',\n decimalPlaces: 2,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'PLN',\n name: 'Polish Zloty',\n decimalPlaces: 2,\n symbol: 'z\u0142',\n decimalSeparator: ',',\n thousandsSeparator: ' ',\n isBase: false,\n isActive: true,\n },\n]\n\nexport async function seedExampleCurrencies(em: EntityManager, scope: CurrencySeedScope): Promise<boolean> {\n const existingEntries = await em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const existingMap = new Map<string, Currency>()\n for (const entry of existingEntries) {\n existingMap.set(entry.code, entry)\n }\n const hasLiveBase = existingEntries.some((entry) => entry.isBase && !entry.deletedAt)\n\n let touched = false\n for (const curr of SEED_CURRENCIES) {\n const current = existingMap.get(curr.code)\n if (current) {\n if (current.name !== curr.name) {\n current.name = curr.name\n current.updatedAt = new Date()\n em.persist(current)\n touched = true\n }\n continue\n }\n const entry = em.create(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n ...curr,\n isBase: curr.isBase && !hasLiveBase,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n touched = true\n }\n\n if (touched) {\n await em.flush()\n }\n return touched\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  import { loginSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
5
5
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
6
6
  import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
7
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
7
8
  import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
8
9
  import { getClientIp } from "@open-mercato/shared/lib/ratelimit/helpers";
9
10
  import {
@@ -18,6 +19,9 @@ import {
18
19
  } from "@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext";
19
20
  const metadata = { requireAuth: false };
20
21
  const TIMING_EQUALIZATION_HASH = "$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i";
22
+ function reportLoginEventError(error) {
23
+ getTelemetryRuntime()?.reportError(error, { module: "customer_accounts", code: "customer_accounts.login_event_failed" });
24
+ }
21
25
  async function POST(req) {
22
26
  let body;
23
27
  try {
@@ -56,23 +60,23 @@ async function POST(req) {
56
60
  const user = await customerUserService.findByEmail(email, tenantId);
57
61
  if (!user || !user.passwordHash) {
58
62
  await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
59
- void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_credentials", tenantId }).catch(() => void 0);
63
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_credentials", tenantId }).catch(reportLoginEventError);
60
64
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
61
65
  }
62
66
  if (!user.isActive) {
63
67
  await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
64
- void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "inactive", tenantId }).catch(() => void 0);
68
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "inactive", tenantId }).catch(reportLoginEventError);
65
69
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
66
70
  }
67
71
  if (customerUserService.checkLockout(user)) {
68
72
  await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
69
- void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "locked", tenantId }).catch(() => void 0);
73
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "locked", tenantId }).catch(reportLoginEventError);
70
74
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
71
75
  }
72
76
  const passwordValid = await customerUserService.verifyPassword(user, password);
73
77
  if (!passwordValid) {
74
78
  await customerUserService.incrementFailedAttempts(user);
75
- void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_password", tenantId }).catch(() => void 0);
79
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_password", tenantId }).catch(reportLoginEventError);
76
80
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
77
81
  }
78
82
  if (!user.emailVerifiedAt) {
@@ -80,10 +84,13 @@ async function POST(req) {
80
84
  email,
81
85
  reason: "email_not_verified",
82
86
  tenantId
83
- }).catch(() => void 0);
87
+ }).catch(reportLoginEventError);
88
+ return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
89
+ }
90
+ if (!await customerUserService.resetFailedAttemptsUnlessLocked(user)) {
91
+ void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "locked", tenantId }).catch(reportLoginEventError);
84
92
  return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
85
93
  }
86
- await customerUserService.resetFailedAttempts(user);
87
94
  await customerUserService.updateLastLoginAt(user);
88
95
  if (compoundKey) {
89
96
  await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig);
@@ -100,7 +107,7 @@ async function POST(req) {
100
107
  email: user.email,
101
108
  tenantId,
102
109
  organizationId: user.organizationId
103
- }).catch(() => void 0);
110
+ }).catch(reportLoginEventError);
104
111
  const res = NextResponse.json({
105
112
  ok: true,
106
113
  user: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/api/login.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the real-account and unknown/inactive/locked/no-hash login branches\n// so account existence cannot be inferred from a timing side channel. Mirrors signup.ts.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n\n // Equalize response timing and error responses across every failed-login branch so an\n // attacker cannot enumerate which emails have an account in this tenant. Unknown,\n // password-less, inactive, locked, wrong-password, and unverified accounts all run a\n // bcrypt comparison and return the same generic 401 \u2014 lockout/deactivation guidance is\n // conveyed out-of-band (e.g. email), never in the synchronous response.\n if (!user || !user.passwordHash) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.isActive) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'inactive', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (customerUserService.checkLockout(user)) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n const passwordValid = await customerUserService.verifyPassword(user, password)\n if (!passwordValid) {\n await customerUserService.incrementFailedAttempts(user)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_password', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.emailVerifiedAt) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', {\n email,\n reason: 'email_not_verified',\n tenantId,\n }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n await customerUserService.resetFailedAttempts(user)\n await customerUserService.updateLastLoginAt(user)\n\n if (compoundKey) {\n await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)\n }\n\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId,\n organizationId: user.organizationId,\n })\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt, session } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.login.success', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId: user.organizationId,\n }).catch(() => undefined)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n },\n resolvedFeatures,\n })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Authenticate customer credentials',\n description: 'Validates customer credentials and issues JWT + session cookies.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: loginSchema,\n description: 'Login payload with email and password.',\n },\n responses: [\n { status: 200, description: 'Login successful', schema: loginSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Invalid credentials', schema: errorSchema },\n { status: 429, description: 'Too many login attempts', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer login',\n description: 'Handles customer authentication and session issuance.',\n methods: { POST: methodDoc },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the real-account and unknown/inactive/locked/no-hash login branches\n// so account existence cannot be inferred from a timing side channel. Mirrors signup.ts.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\n\nfunction reportLoginEventError(error: unknown): void {\n getTelemetryRuntime()?.reportError(error, { module: 'customer_accounts', code: 'customer_accounts.login_event_failed' })\n}\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n\n // Equalize response timing and error responses across every failed-login branch so an\n // attacker cannot enumerate which emails have an account in this tenant. Unknown,\n // password-less, inactive, locked, wrong-password, and unverified accounts all run a\n // bcrypt comparison and return the same generic 401 \u2014 lockout/deactivation guidance is\n // conveyed out-of-band (e.g. email), never in the synchronous response.\n if (!user || !user.passwordHash) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.isActive) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'inactive', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (customerUserService.checkLockout(user)) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n const passwordValid = await customerUserService.verifyPassword(user, password)\n if (!passwordValid) {\n await customerUserService.incrementFailedAttempts(user)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_password', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.emailVerifiedAt) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', {\n email,\n reason: 'email_not_verified',\n tenantId,\n }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!(await customerUserService.resetFailedAttemptsUnlessLocked(user))) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n await customerUserService.updateLastLoginAt(user)\n\n if (compoundKey) {\n await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)\n }\n\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId,\n organizationId: user.organizationId,\n })\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt, session } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.login.success', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId: user.organizationId,\n }).catch(reportLoginEventError)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n },\n resolvedFeatures,\n })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Authenticate customer credentials',\n description: 'Validates customer credentials and issues JWT + session cookies.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: loginSchema,\n description: 'Login payload with email and password.',\n },\n responses: [\n { status: 200, description: 'Login successful', schema: loginSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Invalid credentials', schema: errorSchema },\n { status: 429, description: 'Too many login attempts', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer login',\n description: 'Handles customer authentication and session issuance.',\n methods: { POST: methodDoc },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -64,22 +64,22 @@ class CustomerUserService {
64
64
  return false;
65
65
  }
66
66
  async incrementFailedAttempts(user) {
67
- const newCount = (user.failedLoginAttempts || 0) + 1;
68
- const updates = { failedLoginAttempts: newCount };
69
- if (newCount >= MAX_FAILED_ATTEMPTS) {
70
- updates.lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS);
71
- }
72
- await this.em.nativeUpdate(CustomerUser, { id: user.id }, updates);
73
- user.failedLoginAttempts = newCount;
74
- if (updates.lockedUntil) user.lockedUntil = updates.lockedUntil;
67
+ const lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS);
68
+ await this.em.getKysely().updateTable("customer_users").set((eb) => ({
69
+ failed_login_attempts: eb("failed_login_attempts", "+", 1),
70
+ locked_until: eb.case().when("failed_login_attempts", ">=", MAX_FAILED_ATTEMPTS - 1).then(eb.fn("greatest", [eb.ref("locked_until"), eb.val(lockedUntil)])).else(eb.ref("locked_until")).end()
71
+ })).where("id", "=", user.id).execute();
72
+ }
73
+ async resetFailedAttemptsUnlessLocked(user) {
74
+ const now = /* @__PURE__ */ new Date();
75
+ const row = await this.em.getKysely().updateTable("customer_users").set({ failed_login_attempts: 0, locked_until: null }).where("id", "=", user.id).where((eb) => eb.or([eb("locked_until", "is", null), eb("locked_until", "<=", now)])).returning("id").executeTakeFirst();
76
+ return row !== void 0;
75
77
  }
76
78
  async resetFailedAttempts(user) {
77
79
  await this.em.nativeUpdate(CustomerUser, { id: user.id }, {
78
80
  failedLoginAttempts: 0,
79
81
  lockedUntil: null
80
82
  });
81
- user.failedLoginAttempts = 0;
82
- user.lockedUntil = null;
83
83
  }
84
84
  async updatePassword(user, newPassword, em) {
85
85
  const passwordHash = await hash(newPassword, BCRYPT_COST);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/services/customerUserService.ts"],
4
- "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { hash, compare } from 'bcryptjs'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { hashForLookup, lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst BCRYPT_COST = 10\nconst MAX_FAILED_ATTEMPTS = 5\nconst LOCKOUT_DURATION_MS = 15 * 60 * 1000 // 15 minutes\n\nexport class CustomerUserService {\n constructor(private em: EntityManager) {}\n\n async createUser(\n email: string,\n password: string,\n displayName: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<CustomerUser> {\n const passwordHash = await hash(password, BCRYPT_COST)\n const emailHash = hashForLookup(email)\n const user = this.em.create(CustomerUser, {\n email: email.toLowerCase().trim(),\n emailHash,\n passwordHash,\n displayName,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n failedLoginAttempts: 0,\n createdAt: new Date(),\n } as any)\n return user as CustomerUser\n }\n\n async findByEmail(email: string, tenantId: string): Promise<CustomerUser | null> {\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n {\n emailHash: { $in: lookupHashCandidates(email) },\n tenantId,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId },\n )\n }\n\n async findById(\n id: string,\n tenantId: string,\n organizationId?: string | null,\n ): Promise<CustomerUser | null> {\n const where: Record<string, unknown> = { id, tenantId, deletedAt: null }\n if (organizationId !== undefined) where.organizationId = organizationId\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n where as any,\n undefined,\n { tenantId, organizationId },\n )\n }\n\n async verifyPassword(user: CustomerUser, password: string): Promise<boolean> {\n if (!user.passwordHash) return false\n return compare(password, user.passwordHash)\n }\n\n async updateLastLoginAt(user: CustomerUser): Promise<void> {\n const now = new Date()\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, { lastLoginAt: now })\n user.lastLoginAt = now\n }\n\n checkLockout(user: CustomerUser): boolean {\n if (!user.lockedUntil) return false\n if (user.lockedUntil.getTime() > Date.now()) return true\n return false\n }\n\n async incrementFailedAttempts(user: CustomerUser): Promise<void> {\n const newCount = (user.failedLoginAttempts || 0) + 1\n const updates: Record<string, unknown> = { failedLoginAttempts: newCount }\n if (newCount >= MAX_FAILED_ATTEMPTS) {\n updates.lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS)\n }\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, updates)\n user.failedLoginAttempts = newCount\n if (updates.lockedUntil) user.lockedUntil = updates.lockedUntil as Date\n }\n\n async resetFailedAttempts(user: CustomerUser): Promise<void> {\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, {\n failedLoginAttempts: 0,\n lockedUntil: null,\n })\n user.failedLoginAttempts = 0\n user.lockedUntil = null\n }\n\n async updatePassword(user: CustomerUser, newPassword: string, em?: EntityManager): Promise<void> {\n const passwordHash = await hash(newPassword, BCRYPT_COST)\n await (em ?? this.em).nativeUpdate(CustomerUser, {\n id: user.id,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n }, { passwordHash })\n user.passwordHash = passwordHash\n }\n\n // `display_name` is encrypted at rest. `nativeUpdate` issues raw SQL and fires none of the\n // flush hooks the tenant-encryption subscriber depends on, so writing it that way persists\n // plaintext PII into a ciphertext column (#3837). Assign it on the managed entity and flush\n // so `beforeUpdate` encrypts the value on its way to the database.\n async updateProfile(user: CustomerUser, data: { displayName?: string }): Promise<void> {\n if (data.displayName === undefined) return\n const managed = await findOneWithDecryption(\n this.em,\n CustomerUser,\n { id: user.id, tenantId: user.tenantId, organizationId: user.organizationId, deletedAt: null } as any,\n undefined,\n { tenantId: user.tenantId, organizationId: user.organizationId },\n )\n if (!managed) return\n managed.displayName = data.displayName\n await this.em.flush()\n user.displayName = data.displayName\n }\n\n async softDelete(\n userId: string,\n scope?: { tenantId: string; organizationId: string | null },\n ): Promise<void> {\n const where: Record<string, unknown> = { id: userId }\n if (scope) {\n where.tenantId = scope.tenantId\n where.organizationId = scope.organizationId\n }\n await this.em.nativeUpdate(CustomerUser, where, {\n deletedAt: new Date(),\n isActive: false,\n })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { hash, compare } from 'bcryptjs'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { hashForLookup, lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst BCRYPT_COST = 10\nconst MAX_FAILED_ATTEMPTS = 5\nconst LOCKOUT_DURATION_MS = 15 * 60 * 1000 // 15 minutes\n\ntype CustomerUserLockoutDatabase = {\n customer_users: {\n id: string\n failed_login_attempts: number\n locked_until: Date | null\n }\n}\n\nexport class CustomerUserService {\n constructor(private em: EntityManager) {}\n\n async createUser(\n email: string,\n password: string,\n displayName: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<CustomerUser> {\n const passwordHash = await hash(password, BCRYPT_COST)\n const emailHash = hashForLookup(email)\n const user = this.em.create(CustomerUser, {\n email: email.toLowerCase().trim(),\n emailHash,\n passwordHash,\n displayName,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n failedLoginAttempts: 0,\n createdAt: new Date(),\n } as any)\n return user as CustomerUser\n }\n\n async findByEmail(email: string, tenantId: string): Promise<CustomerUser | null> {\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n {\n emailHash: { $in: lookupHashCandidates(email) },\n tenantId,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId },\n )\n }\n\n async findById(\n id: string,\n tenantId: string,\n organizationId?: string | null,\n ): Promise<CustomerUser | null> {\n const where: Record<string, unknown> = { id, tenantId, deletedAt: null }\n if (organizationId !== undefined) where.organizationId = organizationId\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n where as any,\n undefined,\n { tenantId, organizationId },\n )\n }\n\n async verifyPassword(user: CustomerUser, password: string): Promise<boolean> {\n if (!user.passwordHash) return false\n return compare(password, user.passwordHash)\n }\n\n async updateLastLoginAt(user: CustomerUser): Promise<void> {\n const now = new Date()\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, { lastLoginAt: now })\n user.lastLoginAt = now\n }\n\n checkLockout(user: CustomerUser): boolean {\n if (!user.lockedUntil) return false\n if (user.lockedUntil.getTime() > Date.now()) return true\n return false\n }\n\n async incrementFailedAttempts(user: CustomerUser): Promise<void> {\n const lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS)\n await this.em.getKysely<CustomerUserLockoutDatabase>()\n .updateTable('customer_users')\n .set((eb) => ({\n failed_login_attempts: eb('failed_login_attempts', '+', 1),\n locked_until: eb\n .case()\n .when('failed_login_attempts', '>=', MAX_FAILED_ATTEMPTS - 1)\n .then(eb.fn<Date>('greatest', [eb.ref('locked_until'), eb.val(lockedUntil)]))\n .else(eb.ref('locked_until'))\n .end(),\n }))\n .where('id', '=', user.id)\n .execute()\n }\n\n async resetFailedAttemptsUnlessLocked(user: CustomerUser): Promise<boolean> {\n const now = new Date()\n const row = await this.em.getKysely<CustomerUserLockoutDatabase>()\n .updateTable('customer_users')\n .set({ failed_login_attempts: 0, locked_until: null })\n .where('id', '=', user.id)\n .where((eb) => eb.or([eb('locked_until', 'is', null), eb('locked_until', '<=', now)]))\n .returning('id')\n .executeTakeFirst()\n return row !== undefined\n }\n\n async resetFailedAttempts(user: CustomerUser): Promise<void> {\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, {\n failedLoginAttempts: 0,\n lockedUntil: null,\n })\n }\n\n async updatePassword(user: CustomerUser, newPassword: string, em?: EntityManager): Promise<void> {\n const passwordHash = await hash(newPassword, BCRYPT_COST)\n await (em ?? this.em).nativeUpdate(CustomerUser, {\n id: user.id,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n }, { passwordHash })\n user.passwordHash = passwordHash\n }\n\n // `display_name` is encrypted at rest. `nativeUpdate` issues raw SQL and fires none of the\n // flush hooks the tenant-encryption subscriber depends on, so writing it that way persists\n // plaintext PII into a ciphertext column (#3837). Assign it on the managed entity and flush\n // so `beforeUpdate` encrypts the value on its way to the database.\n async updateProfile(user: CustomerUser, data: { displayName?: string }): Promise<void> {\n if (data.displayName === undefined) return\n const managed = await findOneWithDecryption(\n this.em,\n CustomerUser,\n { id: user.id, tenantId: user.tenantId, organizationId: user.organizationId, deletedAt: null } as any,\n undefined,\n { tenantId: user.tenantId, organizationId: user.organizationId },\n )\n if (!managed) return\n managed.displayName = data.displayName\n await this.em.flush()\n user.displayName = data.displayName\n }\n\n async softDelete(\n userId: string,\n scope?: { tenantId: string; organizationId: string | null },\n ): Promise<void> {\n const where: Record<string, unknown> = { id: userId }\n if (scope) {\n where.tenantId = scope.tenantId\n where.organizationId = scope.organizationId\n }\n await this.em.nativeUpdate(CustomerUser, where, {\n deletedAt: new Date(),\n isActive: false,\n })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -22,6 +22,11 @@ import {
22
22
  CustomerPersonProfile,
23
23
  CustomerTagAssignment
24
24
  } from "../data/entities.js";
25
+ import {
26
+ captureInteractionRestoreDetails,
27
+ omitInteractionRestoreDetails,
28
+ restoreInteractionRestoreDetails
29
+ } from "./interactionRestoreDetails.js";
25
30
  import {
26
31
  companyCreateSchema,
27
32
  companyUpdateSchema
@@ -98,7 +103,7 @@ function companyEntityIndexEntry(entity) {
98
103
  organizationId: entity.organizationId
99
104
  };
100
105
  }
101
- async function loadCompanySnapshot(em, id) {
106
+ async function loadCompanySnapshot(em, id, options = {}) {
102
107
  const entity = await em.findOne(CustomerEntity, { id, deletedAt: null });
103
108
  if (!entity || entity.kind !== "company") return null;
104
109
  const profile = await em.findOne(CustomerCompanyProfile, { entity });
@@ -248,6 +253,7 @@ async function loadCompanySnapshot(em, id) {
248
253
  createdAt: interaction.createdAt,
249
254
  updatedAt: interaction.updatedAt,
250
255
  deletedAt: interaction.deletedAt ?? null,
256
+ ...options.includeInteractionRestoreDetails ? captureInteractionRestoreDetails(interaction) : {},
251
257
  custom: await loadCustomFieldSnapshot(em, {
252
258
  entityId: INTERACTION_ENTITY_ID,
253
259
  recordId: interaction.id,
@@ -792,7 +798,7 @@ const deleteCompanyCommand = {
792
798
  async prepare(input, ctx) {
793
799
  const id = requireId(input, "Company id required");
794
800
  const em = ctx.container.resolve("em");
795
- const snapshot = await loadCompanySnapshot(em, id);
801
+ const snapshot = await loadCompanySnapshot(em, id, { includeInteractionRestoreDetails: true });
796
802
  return snapshot ? { before: snapshot } : {};
797
803
  },
798
804
  async execute(input, ctx) {
@@ -966,7 +972,7 @@ const deleteCompanyCommand = {
966
972
  resourceId: before.entity.id,
967
973
  tenantId: before.entity.tenantId,
968
974
  organizationId: before.entity.organizationId,
969
- snapshotBefore: before,
975
+ snapshotBefore: omitInteractionRestoreDetails(before),
970
976
  payload: {
971
977
  undo: {
972
978
  before
@@ -1193,6 +1199,7 @@ const deleteCompanyCommand = {
1193
1199
  source: interaction.source,
1194
1200
  appearanceIcon: interaction.appearanceIcon,
1195
1201
  appearanceColor: interaction.appearanceColor,
1202
+ ...restoreInteractionRestoreDetails(interaction),
1196
1203
  createdAt: interaction.createdAt,
1197
1204
  updatedAt: interaction.updatedAt,
1198
1205
  deletedAt: interaction.deletedAt