@open-mercato/core 0.6.7-develop.6660.1.90e1e2eef6 → 0.6.7-develop.6669.1.40b669666b

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 (54) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/communication_channels/lib/pg-errors.js +1 -7
  3. package/dist/modules/communication_channels/lib/pg-errors.js.map +2 -2
  4. package/dist/modules/customer_accounts/migrations/Migration20260724120000_customer_accounts.js +56 -0
  5. package/dist/modules/customer_accounts/migrations/Migration20260724120000_customer_accounts.js.map +7 -0
  6. package/dist/modules/directory/api/organizations/route.js +1 -1
  7. package/dist/modules/directory/api/organizations/route.js.map +1 -1
  8. package/dist/modules/entities/api/definitions.js +2 -0
  9. package/dist/modules/entities/api/definitions.js.map +2 -2
  10. package/dist/modules/entities/lib/helpers.js +1 -0
  11. package/dist/modules/entities/lib/helpers.js.map +2 -2
  12. package/dist/modules/query_index/api/openapi.js +2 -2
  13. package/dist/modules/query_index/api/openapi.js.map +2 -2
  14. package/dist/modules/query_index/api/status.js +6 -4
  15. package/dist/modules/query_index/api/status.js.map +2 -2
  16. package/dist/modules/query_index/cli.js +31 -28
  17. package/dist/modules/query_index/cli.js.map +2 -2
  18. package/dist/modules/query_index/components/QueryIndexesTable.js +4 -1
  19. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  20. package/dist/modules/query_index/lib/batch.js +163 -44
  21. package/dist/modules/query_index/lib/batch.js.map +3 -3
  22. package/dist/modules/query_index/lib/jobs.js +3 -2
  23. package/dist/modules/query_index/lib/jobs.js.map +2 -2
  24. package/dist/modules/query_index/lib/reindexer.js +50 -16
  25. package/dist/modules/query_index/lib/reindexer.js.map +2 -2
  26. package/dist/modules/query_index/lib/stale.js +3 -0
  27. package/dist/modules/query_index/lib/stale.js.map +2 -2
  28. package/dist/modules/sales/api/quotes/accept/route.js +7 -2
  29. package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
  30. package/dist/modules/sales/api/quotes/public/[token]/route.js +2 -1
  31. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  32. package/dist/modules/sales/lib/publicQuoteTenantScope.js +24 -0
  33. package/dist/modules/sales/lib/publicQuoteTenantScope.js.map +7 -0
  34. package/package.json +7 -7
  35. package/src/modules/communication_channels/lib/pg-errors.ts +3 -10
  36. package/src/modules/customer_accounts/migrations/Migration20260724120000_customer_accounts.ts +96 -0
  37. package/src/modules/directory/api/organizations/route.ts +1 -1
  38. package/src/modules/entities/api/definitions.ts +2 -0
  39. package/src/modules/entities/lib/helpers.ts +1 -0
  40. package/src/modules/query_index/api/openapi.ts +2 -2
  41. package/src/modules/query_index/api/status.ts +14 -5
  42. package/src/modules/query_index/cli.ts +36 -28
  43. package/src/modules/query_index/components/QueryIndexesTable.tsx +7 -4
  44. package/src/modules/query_index/i18n/de.json +2 -0
  45. package/src/modules/query_index/i18n/en.json +2 -0
  46. package/src/modules/query_index/i18n/es.json +2 -0
  47. package/src/modules/query_index/i18n/pl.json +2 -0
  48. package/src/modules/query_index/lib/batch.ts +244 -67
  49. package/src/modules/query_index/lib/jobs.ts +11 -0
  50. package/src/modules/query_index/lib/reindexer.ts +69 -16
  51. package/src/modules/query_index/lib/stale.ts +10 -0
  52. package/src/modules/sales/api/quotes/accept/route.ts +9 -2
  53. package/src/modules/sales/api/quotes/public/[token]/route.ts +2 -1
  54. package/src/modules/sales/lib/publicQuoteTenantScope.ts +49 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/lib/reindexer.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { resolveRegisteredEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { decryptIndexDocForSearch, encryptIndexDocForStorage } from '@open-mercato/shared/lib/encryption/indexDoc'\nimport { upsertIndexBatch, type AnyRow } from './batch'\nimport { refreshCoverageSnapshot, writeCoverageCounts, applyCoverageAdjustments } from './coverage'\nimport { prepareJob, updateJobProgress, finalizeJob, type JobScope } from './jobs'\nimport { purgeOrphans } from './stale'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\nimport { isSearchDebugEnabled } from './search-tokens'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('query_index').child({ component: 'reindexer' })\n\nexport type ReindexJobOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n force?: boolean\n batchSize?: number\n emitVectorizeEvents?: boolean\n eventBus?: {\n emitEvent(event: string, payload: any, options?: any): Promise<void>\n }\n partitionCount?: number\n partitionIndex?: number\n resetCoverage?: boolean\n onProgress?: (info: { processed: number; total: number; chunkSize: number }) => void\n vectorService?: VectorIndexService | null\n}\n\nexport type ReindexJobResult = {\n processed: number\n total: number\n tenantScopes: Array<string | null>\n scopes: Array<{ tenantId: string | null; organizationId: string | null }>\n}\n\nexport const DEFAULT_REINDEX_PARTITIONS = 5\nconst DEFAULT_BATCH_SIZE = 500\nconst deriveOrgFromId = new Set<string>(['directory:organization'])\nconst COVERAGE_REFRESH_THROTTLE_MS = 5 * 60 * 1000\nconst lastCoverageReset = new Map<string, number>()\n\nconst REINDEX_DECRYPT_DEBUG_KEYS = ['display_name', 'first_name', 'last_name', 'brand_name', 'legal_name', 'primary_email', 'primary_phone'] as const\n\nexport type ReindexDecryptDebugPayload = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n keys: string[]\n}\n\nexport function buildReindexDecryptDebugPayload(\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n): ReindexDecryptDebugPayload {\n const presentKeys: string[] = []\n for (const key of REINDEX_DECRYPT_DEBUG_KEYS) {\n const value = doc[key]\n if (key in doc && value != null && value !== '') presentKeys.push(key)\n }\n return {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n keys: presentKeys,\n }\n}\n\nasync function cleanupLegacyJobScopes(\n db: Kysely<any>,\n options: {\n entityType: string\n organizationId: string | null\n tenantId: string | null\n activePartitionCount: number | null\n },\n): Promise<void> {\n await db\n .deleteFrom('entity_index_jobs' as any)\n .where('entity_type' as any, '=', options.entityType)\n .where(sql<boolean>`organization_id is not distinct from ${options.organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${options.tenantId}`)\n .where(sql<boolean>`partition_count is distinct from ${options.activePartitionCount}`)\n .execute()\n}\n\nfunction toNumber(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\nasync function getColumnSet(db: Kysely<any>, tableName: string): Promise<Set<string>> {\n try {\n const rows = await db\n .selectFrom('information_schema.columns' as any)\n .select(['column_name' as any])\n .where(sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', tableName)\n .execute() as Array<{ column_name: string }>\n return new Set(rows.map((row) => String(row.column_name).toLowerCase()))\n } catch {\n return new Set<string>()\n }\n}\n\nexport async function reindexEntity(\n em: EntityManager,\n options: ReindexJobOptions,\n): Promise<ReindexJobResult> {\n const entityType = String(options?.entityType || '')\n if (!entityType) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const tenantIdInput = options?.tenantId\n const tenantId = tenantIdInput === 'undefined' ? undefined : tenantIdInput\n const organizationIdInput = options?.organizationId\n const organizationId = organizationIdInput === 'undefined' ? undefined : organizationIdInput\n const force = options?.force === true\n const batchSize = Number.isFinite(options?.batchSize) && options!.batchSize! > 0\n ? Math.max(1, Math.trunc(options!.batchSize!))\n : DEFAULT_BATCH_SIZE\n const emitVectorize = options?.emitVectorizeEvents === true\n const eventBus = options?.eventBus\n const vectorService = options?.vectorService ?? null\n const partitionCountRaw = Number.isFinite(options?.partitionCount)\n ? Math.max(1, Math.trunc(options!.partitionCount!))\n : 1\n const usingPartitions = partitionCountRaw > 1\n const partitionIndexRaw = Number.isFinite(options?.partitionIndex)\n ? Math.max(0, Math.trunc(options!.partitionIndex!))\n : 0\n const partitionIndex = usingPartitions\n ? Math.min(partitionIndexRaw, partitionCountRaw - 1)\n : null\n const resetCoverage = options?.resetCoverage ?? (!usingPartitions || partitionIndex === 0)\n\n const db = (em as any).getKysely() as Kysely<any>\n // Resolve the source table strictly via registered MikroORM metadata. We must\n // never fall back to a pluralized guess derived from the caller-supplied id\n // here: doing so would let a principal with `query_index.reindex` point the\n // reindexer at arbitrary tables (e.g. `auth_users`, `users`) and read their\n // rows into the index, bypassing tenant scoping and entity-level encryption.\n const table = resolveRegisteredEntityTableName(em, entityType)\n if (!table || entityType === 'query_index:search_token' || table === 'search_tokens') {\n if (!table) {\n logger.warn('Refusing to reindex unregistered entity type', {\n entityType,\n })\n }\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const columns = await getColumnSet(db, table)\n const hasOrgCol = columns.has('organization_id')\n const hasTenantCol = columns.has('tenant_id')\n const hasDeletedCol = columns.has('deleted_at')\n\n const jobScope: JobScope = {\n entityType,\n organizationId: organizationId ?? null,\n tenantId: tenantId ?? null,\n partitionIndex,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n }\n\n if (!force) {\n const activeJob = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['id' as any])\n .where('entity_type' as any, '=', entityType)\n .where('finished_at' as any, 'is', null as any)\n .where(sql<boolean>`organization_id is not distinct from ${null}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .executeTakeFirst()\n if (activeJob) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n }\n\n if (resetCoverage) {\n await cleanupLegacyJobScopes(db, {\n entityType,\n organizationId: jobScope.organizationId ?? null,\n tenantId: jobScope.tenantId ?? null,\n activePartitionCount: jobScope.partitionCount ?? null,\n })\n }\n\n const scopeKey = (tenantValue: string | null, orgValue: string | null) => `${tenantValue ?? '__null__'}|${orgValue ?? '__null__'}`\n\n const applyBaseWhere = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n if (hasDeletedCol) chain = chain.where('b.deleted_at' as any, 'is', null as any)\n if (tenantId !== undefined && hasTenantCol) {\n chain = tenantId === null\n ? chain.where('b.tenant_id' as any, 'is', null as any)\n : chain.where('b.tenant_id' as any, '=', tenantId)\n }\n if (organizationId !== undefined && hasOrgCol) {\n chain = organizationId === null\n ? chain.where('b.organization_id' as any, 'is', null as any)\n : chain.where('b.organization_id' as any, '=', organizationId)\n }\n if (usingPartitions && partitionIndex !== null) {\n chain = chain.where(sql<boolean>`mod(abs(hashtext(b.id::text)), ${partitionCountRaw}) = ${partitionIndex}`)\n }\n return chain\n }\n\n type ScopeStats = { tenantId: string | null; organizationId: string | null; count: number }\n const baseCounts = new Map<string, ScopeStats>()\n const registerBaseCount = (tenantValue: string | null, orgValue: string | null, count: number) => {\n const key = scopeKey(tenantValue, orgValue)\n baseCounts.set(key, { tenantId: tenantValue, organizationId: orgValue, count })\n }\n\n const groupByTenant = hasTenantCol && tenantId === undefined\n const groupByOrg = hasOrgCol && organizationId === undefined\n\n if (groupByTenant || groupByOrg) {\n let groupQuery = applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n )\n if (groupByTenant) {\n groupQuery = groupQuery.select('b.tenant_id as tenant_id' as any).groupBy('b.tenant_id' as any)\n }\n if (groupByOrg) {\n groupQuery = groupQuery.select('b.organization_id as organization_id' as any).groupBy('b.organization_id' as any)\n }\n const rows = await groupQuery.execute() as Array<Record<string, unknown>>\n for (const row of rows) {\n const bucketTenant = groupByTenant\n ? ((row as any)?.tenant_id ?? null)\n : (tenantId === undefined ? null : tenantId ?? null)\n const bucketOrg = groupByOrg\n ? ((row as any)?.organization_id ?? null)\n : (organizationId === undefined ? null : organizationId ?? null)\n registerBaseCount(bucketTenant, bucketOrg, toNumber((row as any)?.count))\n }\n } else {\n const row = await applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n ).executeTakeFirst() as { count: unknown } | undefined\n const bucketTenant = tenantId === undefined ? null : tenantId ?? null\n const bucketOrg = organizationId === undefined ? null : organizationId ?? null\n registerBaseCount(bucketTenant, bucketOrg, toNumber(row?.count))\n }\n\n const total = Array.from(baseCounts.values()).reduce((acc, value) => acc + (Number.isFinite(value.count) ? value.count : 0), 0)\n await prepareJob(db, jobScope, 'reindexing', { totalCount: total })\n const jobRow = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['started_at' as any])\n .where('entity_type' as any, '=', entityType)\n .where('organization_id' as any, 'is', null as any)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .orderBy('started_at' as any, 'desc')\n .executeTakeFirst() as { started_at: Date | string } | undefined\n const jobStartedAt = jobRow?.started_at ? new Date(jobRow.started_at) : new Date()\n const deriveOrg = deriveOrgFromId.has(entityType)\n ? (row: AnyRow) => String(row.id)\n : undefined\n\n const scopeOverrides: { tenantId?: string; orgId?: string } = {}\n if (tenantId !== undefined && tenantId !== null) {\n scopeOverrides.tenantId = String(tenantId)\n }\n if (organizationId !== undefined && organizationId !== null) {\n scopeOverrides.orgId = String(organizationId)\n }\n\n const scopeEntries = Array.from(baseCounts.values()).map((entry) => ({\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n }))\n const tenantScopes = Array.from(\n new Set(scopeEntries.map((entry) => entry.tenantId ?? null)),\n )\n\n let processed = 0\n let lastId: string | null = null\n\n options?.onProgress?.({ processed, total, chunkSize: 0 })\n\n if (resetCoverage) {\n if (force) {\n try {\n let purgeQuery = db\n .deleteFrom('entity_indexes' as any)\n .where('entity_type' as any, '=', entityType)\n if (tenantId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n }\n if (organizationId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`organization_id is not distinct from ${organizationId ?? null}`)\n }\n await purgeQuery.execute()\n } catch (error) {\n logger.warn('Failed to purge index rows before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n\n if (emitVectorize && eventBus) {\n if (tenantId !== undefined) {\n const payload: Record<string, unknown> = {\n entityType,\n tenantId: tenantId ?? null,\n }\n if (organizationId !== undefined) payload.organizationId = organizationId ?? null\n try {\n await eventBus.emitEvent('query_index.vectorize_purge', payload)\n } catch (err) {\n logger.warn('Failed to queue vector purge before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: err instanceof Error ? err.message : err,\n })\n }\n } else {\n logger.warn('Skipping vector purge for force reindex without tenant scope', {\n entityType,\n })\n }\n }\n }\n\n const nowTs = Date.now()\n for (const scope of baseCounts.values()) {\n const key = `${entityType}|${scopeKey(scope.tenantId, scope.organizationId)}`\n const last = lastCoverageReset.get(key) ?? 0\n if (force || nowTs - last >= COVERAGE_REFRESH_THROTTLE_MS) {\n await writeCoverageCounts(em, {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n }, {\n baseCount: scope.count,\n indexedCount: 0,\n vectorCount: emitVectorize ? 0 : undefined,\n })\n lastCoverageReset.set(key, nowTs)\n }\n }\n }\n\n try {\n while (true) {\n let query = applyBaseWhere(\n db\n .selectFrom(`${table} as b` as any)\n .selectAll('b' as any)\n .orderBy('b.id' as any, 'asc')\n .limit(batchSize),\n )\n if (lastId !== null) {\n query = query.where('b.id' as any, '>', lastId)\n }\n const rows = await query.execute() as AnyRow[]\n if (!rows.length) break\n\n const encryption = resolveTenantEncryptionService(em as any)\n const dekKeyCache = new Map<string | null, string | null>()\n const encryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n return await encryptIndexDocForStorage(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n )\n }\n const decryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n const result = await decryptIndexDocForSearch(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n dekKeyCache,\n )\n if (isSearchDebugEnabled()) {\n logger.debug('Reindex decrypt', buildReindexDecryptDebugPayload(targetEntity, result as Record<string, unknown>, scope))\n }\n return result\n }\n\n await upsertIndexBatch(db, entityType, rows, scopeOverrides, { deriveOrganizationId: deriveOrg, encryptDoc, decryptDoc })\n\n const coverageDeltas = new Map<string, { tenantId: string | null; organizationId: string | null; delta: number }>()\n for (const row of rows) {\n const scopeTenant = tenantId !== undefined\n ? tenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n const scopeOrg = organizationId !== undefined\n ? organizationId ?? null\n : (hasOrgCol ? ((row as AnyRow).organization_id ?? null) : (deriveOrg ? deriveOrg(row) ?? null : null))\n const key = scopeKey(scopeTenant ?? null, scopeOrg ?? null)\n const existingDelta = coverageDeltas.get(key)\n if (existingDelta) existingDelta.delta += 1\n else coverageDeltas.set(key, {\n tenantId: scopeTenant ?? null,\n organizationId: scopeOrg ?? null,\n delta: 1,\n })\n }\n if (coverageDeltas.size > 0) {\n await applyCoverageAdjustments(\n em,\n Array.from(coverageDeltas.values()).map((entry) => ({\n entityType,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n withDeleted: false,\n deltaBase: 0,\n deltaIndex: entry.delta,\n })),\n )\n }\n\n if (emitVectorize && eventBus) {\n await Promise.all(\n rows.map((row) => {\n const scopeOrg = organizationId !== undefined\n ? organizationId ?? null\n : hasOrgCol\n ? ((row as AnyRow).organization_id ?? null)\n : (deriveOrg ? deriveOrg(row) ?? null : null)\n const scopeTenant = tenantId !== undefined\n ? tenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n return eventBus\n .emitEvent('query_index.vectorize_one', {\n entityType,\n recordId: String(row.id),\n organizationId: scopeOrg,\n tenantId: scopeTenant,\n })\n .catch(() => undefined)\n }),\n )\n }\n\n processed += rows.length\n lastId = String(rows[rows.length - 1]!.id)\n options?.onProgress?.({ processed, total, chunkSize: rows.length })\n await updateJobProgress(db, jobScope, rows.length)\n }\n\n await purgeOrphans(db, {\n entityType,\n tenantId,\n organizationId,\n partitionIndex: usingPartitions ? partitionIndex : null,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n startedAt: jobStartedAt,\n })\n\n if (force && vectorService && (!usingPartitions || partitionIndex === null)) {\n try {\n await vectorService.removeOrphans({\n entityId: entityType,\n tenantId,\n organizationId,\n olderThan: jobStartedAt,\n })\n } catch (error) {\n logger.warn('Failed to prune vector orphans after reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n for (const scope of scopeEntries) {\n await refreshCoverageSnapshot(\n em,\n {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n },\n )\n }\n } finally {\n await finalizeJob(db, jobScope)\n }\n\n return {\n processed,\n total,\n scopes: scopeEntries,\n tenantScopes,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { resolveRegisteredEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { decryptIndexDocForSearch, encryptIndexDocForStorage } from '@open-mercato/shared/lib/encryption/indexDoc'\nimport {\n upsertIndexBatch,\n assertIndexBatchWritesLanded,\n createEmptyUpsertIndexBatchResult,\n mergeUpsertIndexBatchResults,\n QueryIndexBatchWriteError,\n type AnyRow,\n} from './batch'\nimport { refreshCoverageSnapshot, writeCoverageCounts, applyCoverageAdjustments } from './coverage'\nimport { prepareJob, updateJobProgress, finalizeJob, type JobScope } from './jobs'\nimport { purgeOrphans } from './stale'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\nimport { isSearchDebugEnabled } from './search-tokens'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('query_index').child({ component: 'reindexer' })\n\nexport type ReindexJobOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n force?: boolean\n batchSize?: number\n emitVectorizeEvents?: boolean\n eventBus?: {\n emitEvent(event: string, payload: any, options?: any): Promise<void>\n }\n partitionCount?: number\n partitionIndex?: number\n resetCoverage?: boolean\n onProgress?: (info: { processed: number; total: number; chunkSize: number }) => void\n vectorService?: VectorIndexService | null\n}\n\nexport type ReindexJobResult = {\n processed: number\n total: number\n tenantScopes: Array<string | null>\n scopes: Array<{ tenantId: string | null; organizationId: string | null }>\n}\n\nexport const DEFAULT_REINDEX_PARTITIONS = 5\nconst DEFAULT_BATCH_SIZE = 500\n/**\n * Above this many failed records the purge exclusion list stops being a sane query, so\n * the purge is skipped entirely instead. Failing closed keeps stale rows; the alternative\n * deletes index entries the run failed to rebuild.\n */\nconst MAX_PURGE_EXCLUSIONS = 1000\nconst deriveOrgFromId = new Set<string>(['directory:organization'])\nconst COVERAGE_REFRESH_THROTTLE_MS = 5 * 60 * 1000\nconst lastCoverageReset = new Map<string, number>()\n\nconst REINDEX_DECRYPT_DEBUG_KEYS = ['display_name', 'first_name', 'last_name', 'brand_name', 'legal_name', 'primary_email', 'primary_phone'] as const\n\nexport type ReindexDecryptDebugPayload = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n keys: string[]\n}\n\nexport function buildReindexDecryptDebugPayload(\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n): ReindexDecryptDebugPayload {\n const presentKeys: string[] = []\n for (const key of REINDEX_DECRYPT_DEBUG_KEYS) {\n const value = doc[key]\n if (key in doc && value != null && value !== '') presentKeys.push(key)\n }\n return {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n keys: presentKeys,\n }\n}\n\nasync function cleanupLegacyJobScopes(\n db: Kysely<any>,\n options: {\n entityType: string\n organizationId: string | null\n tenantId: string | null\n activePartitionCount: number | null\n },\n): Promise<void> {\n await db\n .deleteFrom('entity_index_jobs' as any)\n .where('entity_type' as any, '=', options.entityType)\n .where(sql<boolean>`organization_id is not distinct from ${options.organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${options.tenantId}`)\n .where(sql<boolean>`partition_count is distinct from ${options.activePartitionCount}`)\n .execute()\n}\n\nfunction toNumber(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\nasync function getColumnSet(db: Kysely<any>, tableName: string): Promise<Set<string>> {\n try {\n const rows = await db\n .selectFrom('information_schema.columns' as any)\n .select(['column_name' as any])\n .where(sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', tableName)\n .execute() as Array<{ column_name: string }>\n return new Set(rows.map((row) => String(row.column_name).toLowerCase()))\n } catch {\n return new Set<string>()\n }\n}\n\nexport async function reindexEntity(\n em: EntityManager,\n options: ReindexJobOptions,\n): Promise<ReindexJobResult> {\n const entityType = String(options?.entityType || '')\n if (!entityType) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const tenantIdInput = options?.tenantId\n const tenantId = tenantIdInput === 'undefined' ? undefined : tenantIdInput\n const organizationIdInput = options?.organizationId\n const organizationId = organizationIdInput === 'undefined' ? undefined : organizationIdInput\n const force = options?.force === true\n const batchSize = Number.isFinite(options?.batchSize) && options!.batchSize! > 0\n ? Math.max(1, Math.trunc(options!.batchSize!))\n : DEFAULT_BATCH_SIZE\n const emitVectorize = options?.emitVectorizeEvents === true\n const eventBus = options?.eventBus\n const vectorService = options?.vectorService ?? null\n const partitionCountRaw = Number.isFinite(options?.partitionCount)\n ? Math.max(1, Math.trunc(options!.partitionCount!))\n : 1\n const usingPartitions = partitionCountRaw > 1\n const partitionIndexRaw = Number.isFinite(options?.partitionIndex)\n ? Math.max(0, Math.trunc(options!.partitionIndex!))\n : 0\n const partitionIndex = usingPartitions\n ? Math.min(partitionIndexRaw, partitionCountRaw - 1)\n : null\n const resetCoverage = options?.resetCoverage ?? (!usingPartitions || partitionIndex === 0)\n\n const db = (em as any).getKysely() as Kysely<any>\n // Resolve the source table strictly via registered MikroORM metadata. We must\n // never fall back to a pluralized guess derived from the caller-supplied id\n // here: doing so would let a principal with `query_index.reindex` point the\n // reindexer at arbitrary tables (e.g. `auth_users`, `users`) and read their\n // rows into the index, bypassing tenant scoping and entity-level encryption.\n const table = resolveRegisteredEntityTableName(em, entityType)\n if (!table || entityType === 'query_index:search_token' || table === 'search_tokens') {\n if (!table) {\n logger.warn('Refusing to reindex unregistered entity type', {\n entityType,\n })\n }\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const columns = await getColumnSet(db, table)\n const hasOrgCol = columns.has('organization_id')\n const hasTenantCol = columns.has('tenant_id')\n const hasDeletedCol = columns.has('deleted_at')\n\n const jobScope: JobScope = {\n entityType,\n organizationId: organizationId ?? null,\n tenantId: tenantId ?? null,\n partitionIndex,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n }\n\n if (!force) {\n const activeJob = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['id' as any])\n .where('entity_type' as any, '=', entityType)\n .where('finished_at' as any, 'is', null as any)\n .where(sql<boolean>`organization_id is not distinct from ${null}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .executeTakeFirst()\n if (activeJob) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n }\n\n if (resetCoverage) {\n await cleanupLegacyJobScopes(db, {\n entityType,\n organizationId: jobScope.organizationId ?? null,\n tenantId: jobScope.tenantId ?? null,\n activePartitionCount: jobScope.partitionCount ?? null,\n })\n }\n\n const scopeKey = (tenantValue: string | null, orgValue: string | null) => `${tenantValue ?? '__null__'}|${orgValue ?? '__null__'}`\n\n const applyBaseWhere = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n if (hasDeletedCol) chain = chain.where('b.deleted_at' as any, 'is', null as any)\n if (tenantId !== undefined && hasTenantCol) {\n chain = tenantId === null\n ? chain.where('b.tenant_id' as any, 'is', null as any)\n : chain.where('b.tenant_id' as any, '=', tenantId)\n }\n if (organizationId !== undefined && hasOrgCol) {\n chain = organizationId === null\n ? chain.where('b.organization_id' as any, 'is', null as any)\n : chain.where('b.organization_id' as any, '=', organizationId)\n }\n if (usingPartitions && partitionIndex !== null) {\n chain = chain.where(sql<boolean>`mod(abs(hashtext(b.id::text)), ${partitionCountRaw}) = ${partitionIndex}`)\n }\n return chain\n }\n\n type ScopeStats = { tenantId: string | null; organizationId: string | null; count: number }\n const baseCounts = new Map<string, ScopeStats>()\n const registerBaseCount = (tenantValue: string | null, orgValue: string | null, count: number) => {\n const key = scopeKey(tenantValue, orgValue)\n baseCounts.set(key, { tenantId: tenantValue, organizationId: orgValue, count })\n }\n\n const groupByTenant = hasTenantCol && tenantId === undefined\n const groupByOrg = hasOrgCol && organizationId === undefined\n\n if (groupByTenant || groupByOrg) {\n let groupQuery = applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n )\n if (groupByTenant) {\n groupQuery = groupQuery.select('b.tenant_id as tenant_id' as any).groupBy('b.tenant_id' as any)\n }\n if (groupByOrg) {\n groupQuery = groupQuery.select('b.organization_id as organization_id' as any).groupBy('b.organization_id' as any)\n }\n const rows = await groupQuery.execute() as Array<Record<string, unknown>>\n for (const row of rows) {\n const bucketTenant = groupByTenant\n ? ((row as any)?.tenant_id ?? null)\n : (tenantId === undefined ? null : tenantId ?? null)\n const bucketOrg = groupByOrg\n ? ((row as any)?.organization_id ?? null)\n : (organizationId === undefined ? null : organizationId ?? null)\n registerBaseCount(bucketTenant, bucketOrg, toNumber((row as any)?.count))\n }\n } else {\n const row = await applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n ).executeTakeFirst() as { count: unknown } | undefined\n const bucketTenant = tenantId === undefined ? null : tenantId ?? null\n const bucketOrg = organizationId === undefined ? null : organizationId ?? null\n registerBaseCount(bucketTenant, bucketOrg, toNumber(row?.count))\n }\n\n const total = Array.from(baseCounts.values()).reduce((acc, value) => acc + (Number.isFinite(value.count) ? value.count : 0), 0)\n await prepareJob(db, jobScope, 'reindexing', { totalCount: total })\n const jobRow = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['started_at' as any])\n .where('entity_type' as any, '=', entityType)\n .where('organization_id' as any, 'is', null as any)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .orderBy('started_at' as any, 'desc')\n .executeTakeFirst() as { started_at: Date | string } | undefined\n const jobStartedAt = jobRow?.started_at ? new Date(jobRow.started_at) : new Date()\n const deriveOrg = deriveOrgFromId.has(entityType)\n ? (row: AnyRow) => String(row.id)\n : undefined\n\n const scopeOverrides: { tenantId?: string; orgId?: string } = {}\n if (tenantId !== undefined && tenantId !== null) {\n scopeOverrides.tenantId = String(tenantId)\n }\n if (organizationId !== undefined && organizationId !== null) {\n scopeOverrides.orgId = String(organizationId)\n }\n\n const scopeEntries = Array.from(baseCounts.values()).map((entry) => ({\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n }))\n const tenantScopes = Array.from(\n new Set(scopeEntries.map((entry) => entry.tenantId ?? null)),\n )\n\n let processed = 0\n let lastId: string | null = null\n let jobFailed = false\n const writeTotals = createEmptyUpsertIndexBatchResult()\n\n options?.onProgress?.({ processed, total, chunkSize: 0 })\n\n if (resetCoverage) {\n if (force) {\n try {\n let purgeQuery = db\n .deleteFrom('entity_indexes' as any)\n .where('entity_type' as any, '=', entityType)\n if (tenantId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n }\n if (organizationId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`organization_id is not distinct from ${organizationId ?? null}`)\n }\n await purgeQuery.execute()\n } catch (error) {\n logger.warn('Failed to purge index rows before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n\n if (emitVectorize && eventBus) {\n if (tenantId !== undefined) {\n const payload: Record<string, unknown> = {\n entityType,\n tenantId: tenantId ?? null,\n }\n if (organizationId !== undefined) payload.organizationId = organizationId ?? null\n try {\n await eventBus.emitEvent('query_index.vectorize_purge', payload)\n } catch (err) {\n logger.warn('Failed to queue vector purge before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: err instanceof Error ? err.message : err,\n })\n }\n } else {\n logger.warn('Skipping vector purge for force reindex without tenant scope', {\n entityType,\n })\n }\n }\n }\n\n const nowTs = Date.now()\n for (const scope of baseCounts.values()) {\n const key = `${entityType}|${scopeKey(scope.tenantId, scope.organizationId)}`\n const last = lastCoverageReset.get(key) ?? 0\n if (force || nowTs - last >= COVERAGE_REFRESH_THROTTLE_MS) {\n await writeCoverageCounts(em, {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n }, {\n baseCount: scope.count,\n indexedCount: 0,\n vectorCount: emitVectorize ? 0 : undefined,\n })\n lastCoverageReset.set(key, nowTs)\n }\n }\n }\n\n try {\n while (true) {\n let query = applyBaseWhere(\n db\n .selectFrom(`${table} as b` as any)\n .selectAll('b' as any)\n .orderBy('b.id' as any, 'asc')\n .limit(batchSize),\n )\n if (lastId !== null) {\n query = query.where('b.id' as any, '>', lastId)\n }\n const rows = await query.execute() as AnyRow[]\n if (!rows.length) break\n\n const encryption = resolveTenantEncryptionService(em as any)\n const dekKeyCache = new Map<string | null, string | null>()\n const encryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n return await encryptIndexDocForStorage(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n )\n }\n const decryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n const result = await decryptIndexDocForSearch(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n dekKeyCache,\n )\n if (isSearchDebugEnabled()) {\n logger.debug('Reindex decrypt', buildReindexDecryptDebugPayload(targetEntity, result as Record<string, unknown>, scope))\n }\n return result\n }\n\n const batchResult = await upsertIndexBatch(db, entityType, rows, scopeOverrides, { deriveOrganizationId: deriveOrg, encryptDoc, decryptDoc })\n mergeUpsertIndexBatchResults(writeTotals, batchResult)\n\n // A whole batch failing is infrastructural (pool exhausted, disk full, KMS down),\n // not a poison record. Abort now instead of grinding through the rest of the table.\n if (batchResult.written === 0 && batchResult.attempted > 0) {\n throw new QueryIndexBatchWriteError(entityType, writeTotals)\n }\n\n const failedInBatch = new Set(batchResult.failedRecordIds)\n const writtenRows = failedInBatch.size ? rows.filter((row) => !failedInBatch.has(String(row.id))) : rows\n\n const coverageDeltas = new Map<string, { tenantId: string | null; organizationId: string | null; delta: number }>()\n for (const row of writtenRows) {\n const scopeTenant = tenantId !== undefined\n ? tenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n const scopeOrg = organizationId !== undefined\n ? organizationId ?? null\n : (hasOrgCol ? ((row as AnyRow).organization_id ?? null) : (deriveOrg ? deriveOrg(row) ?? null : null))\n const key = scopeKey(scopeTenant ?? null, scopeOrg ?? null)\n const existingDelta = coverageDeltas.get(key)\n if (existingDelta) existingDelta.delta += 1\n else coverageDeltas.set(key, {\n tenantId: scopeTenant ?? null,\n organizationId: scopeOrg ?? null,\n delta: 1,\n })\n }\n if (coverageDeltas.size > 0) {\n await applyCoverageAdjustments(\n em,\n Array.from(coverageDeltas.values()).map((entry) => ({\n entityType,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n withDeleted: false,\n deltaBase: 0,\n deltaIndex: entry.delta,\n })),\n )\n }\n\n if (emitVectorize && eventBus) {\n await Promise.all(\n writtenRows.map((row) => {\n const scopeOrg = organizationId !== undefined\n ? organizationId ?? null\n : hasOrgCol\n ? ((row as AnyRow).organization_id ?? null)\n : (deriveOrg ? deriveOrg(row) ?? null : null)\n const scopeTenant = tenantId !== undefined\n ? tenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n return eventBus\n .emitEvent('query_index.vectorize_one', {\n entityType,\n recordId: String(row.id),\n organizationId: scopeOrg,\n tenantId: scopeTenant,\n })\n .catch(() => undefined)\n }),\n )\n }\n\n processed += batchResult.written\n lastId = String(rows[rows.length - 1]!.id)\n options?.onProgress?.({ processed, total, chunkSize: batchResult.written })\n await updateJobProgress(db, jobScope, batchResult.written)\n }\n\n // Records this run failed to write still look untouched to the purge predicate, so\n // without the exclusion the purge would delete the index rows it just failed to\n // rebuild \u2014 turning a stale entry into a missing one.\n const purgeExclusions = writeTotals.failedRecordIds\n if (purgeExclusions.length > MAX_PURGE_EXCLUSIONS) {\n logger.warn('Skipping orphan purge after widespread write failures', {\n entityType,\n failedRecords: purgeExclusions.length,\n })\n } else {\n await purgeOrphans(db, {\n entityType,\n tenantId,\n organizationId,\n partitionIndex: usingPartitions ? partitionIndex : null,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n startedAt: jobStartedAt,\n excludeRecordIds: purgeExclusions,\n })\n }\n\n if (force && vectorService && (!usingPartitions || partitionIndex === null)) {\n try {\n await vectorService.removeOrphans({\n entityId: entityType,\n tenantId,\n organizationId,\n olderThan: jobStartedAt,\n })\n } catch (error) {\n logger.warn('Failed to prune vector orphans after reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n for (const scope of scopeEntries) {\n await refreshCoverageSnapshot(\n em,\n {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n },\n )\n }\n\n // Deliberately after the coverage refresh: the authoritative recount is what keeps\n // indexed_count truthful, and it is most worth having when a run has just failed.\n // Throwing here fails the queue job so the loss is visible instead of silent.\n if (writeTotals.searchTokenFailures > 0) {\n logger.warn('Search token writes failed during reindex', {\n entityType,\n batches: writeTotals.searchTokenFailures,\n })\n }\n assertIndexBatchWritesLanded(entityType, writeTotals)\n } catch (error) {\n jobFailed = true\n throw error\n } finally {\n // Still finalized on failure: the scope stays wedged behind the active-job guard\n // while finished_at is null. The status carries the outcome instead.\n await finalizeJob(db, jobScope, jobFailed ? { status: 'failed' } : {})\n }\n\n return {\n processed,\n total,\n scopes: scopeEntries,\n tenantScopes,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -12,6 +12,9 @@ async function purgeOrphans(db, options) {
12
12
  q = q.where(sql`mod(abs(hashtext(entity_id::text)), ${partitionCount}) = ${partitionIndex}`);
13
13
  }
14
14
  q = q.where("updated_at", "<", startedAt);
15
+ if (options.excludeRecordIds?.length) {
16
+ q = q.where("entity_id", "not in", options.excludeRecordIds);
17
+ }
15
18
  await q.execute();
16
19
  }
17
20
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/lib/stale.ts"],
4
- "sourcesContent": ["import { type Kysely, sql } from 'kysely'\n\ntype PurgeOrphansOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n partitionIndex: number | null\n partitionCount: number | null\n startedAt: Date\n}\n\nexport async function purgeOrphans(\n db: Kysely<any>,\n options: PurgeOrphansOptions,\n): Promise<void> {\n const { entityType, tenantId, partitionIndex, partitionCount, startedAt } = options\n let q = db.deleteFrom('entity_indexes' as any).where('entity_type' as any, '=', entityType)\n if (tenantId !== undefined) {\n q = q.where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n }\n if (options.organizationId !== undefined) {\n q = q.where(sql<boolean>`organization_id is not distinct from ${options.organizationId ?? null}`)\n }\n if (partitionIndex != null && partitionCount != null) {\n q = q.where(sql<boolean>`mod(abs(hashtext(entity_id::text)), ${partitionCount}) = ${partitionIndex}`)\n }\n q = q.where('updated_at' as any, '<', startedAt as any)\n await q.execute()\n}\n"],
5
- "mappings": "AAAA,SAAsB,WAAW;AAWjC,eAAsB,aACpB,IACA,SACe;AACf,QAAM,EAAE,YAAY,UAAU,gBAAgB,gBAAgB,UAAU,IAAI;AAC5E,MAAI,IAAI,GAAG,WAAW,gBAAuB,EAAE,MAAM,eAAsB,KAAK,UAAU;AAC1F,MAAI,aAAa,QAAW;AAC1B,QAAI,EAAE,MAAM,qCAA8C,YAAY,IAAI,EAAE;AAAA,EAC9E;AACA,MAAI,QAAQ,mBAAmB,QAAW;AACxC,QAAI,EAAE,MAAM,2CAAoD,QAAQ,kBAAkB,IAAI,EAAE;AAAA,EAClG;AACA,MAAI,kBAAkB,QAAQ,kBAAkB,MAAM;AACpD,QAAI,EAAE,MAAM,0CAAmD,cAAc,OAAO,cAAc,EAAE;AAAA,EACtG;AACA,MAAI,EAAE,MAAM,cAAqB,KAAK,SAAgB;AACtD,QAAM,EAAE,QAAQ;AAClB;",
4
+ "sourcesContent": ["import { type Kysely, sql } from 'kysely'\n\ntype PurgeOrphansOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n partitionIndex: number | null\n partitionCount: number | null\n startedAt: Date\n /**\n * Records whose index rows must be preserved even though this run did not refresh\n * them. A row that failed to be written still looks untouched to the `updated_at`\n * predicate below, so without this the purge would delete the very entries the run\n * failed to rebuild.\n */\n excludeRecordIds?: string[]\n}\n\nexport async function purgeOrphans(\n db: Kysely<any>,\n options: PurgeOrphansOptions,\n): Promise<void> {\n const { entityType, tenantId, partitionIndex, partitionCount, startedAt } = options\n let q = db.deleteFrom('entity_indexes' as any).where('entity_type' as any, '=', entityType)\n if (tenantId !== undefined) {\n q = q.where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n }\n if (options.organizationId !== undefined) {\n q = q.where(sql<boolean>`organization_id is not distinct from ${options.organizationId ?? null}`)\n }\n if (partitionIndex != null && partitionCount != null) {\n q = q.where(sql<boolean>`mod(abs(hashtext(entity_id::text)), ${partitionCount}) = ${partitionIndex}`)\n }\n q = q.where('updated_at' as any, '<', startedAt as any)\n if (options.excludeRecordIds?.length) {\n q = q.where('entity_id' as any, 'not in', options.excludeRecordIds)\n }\n await q.execute()\n}\n"],
5
+ "mappings": "AAAA,SAAsB,WAAW;AAkBjC,eAAsB,aACpB,IACA,SACe;AACf,QAAM,EAAE,YAAY,UAAU,gBAAgB,gBAAgB,UAAU,IAAI;AAC5E,MAAI,IAAI,GAAG,WAAW,gBAAuB,EAAE,MAAM,eAAsB,KAAK,UAAU;AAC1F,MAAI,aAAa,QAAW;AAC1B,QAAI,EAAE,MAAM,qCAA8C,YAAY,IAAI,EAAE;AAAA,EAC9E;AACA,MAAI,QAAQ,mBAAmB,QAAW;AACxC,QAAI,EAAE,MAAM,2CAAoD,QAAQ,kBAAkB,IAAI,EAAE;AAAA,EAClG;AACA,MAAI,kBAAkB,QAAQ,kBAAkB,MAAM;AACpD,QAAI,EAAE,MAAM,0CAAmD,cAAc,OAAO,cAAc,EAAE;AAAA,EACtG;AACA,MAAI,EAAE,MAAM,cAAqB,KAAK,SAAgB;AACtD,MAAI,QAAQ,kBAAkB,QAAQ;AACpC,QAAI,EAAE,MAAM,aAAoB,UAAU,QAAQ,gBAAgB;AAAA,EACpE;AACA,QAAM,EAAE,QAAQ;AAClB;",
6
6
  "names": []
7
7
  }
@@ -15,6 +15,7 @@ import { SalesOrder, SalesQuote } from "../../../data/entities.js";
15
15
  import { quoteAcceptSchema } from "../../../data/validators.js";
16
16
  import { sendEmail } from "@open-mercato/shared/lib/email/send";
17
17
  import { resolveStatusEntryIdByValue } from "../../../lib/statusHelpers.js";
18
+ import { resolveEffectiveTenantId } from "../../../lib/publicQuoteTenantScope.js";
18
19
  import { QuoteAcceptedAdminEmail } from "../../../emails/QuoteAcceptedAdminEmail.js";
19
20
  import { createLogger } from "@open-mercato/shared/lib/logger";
20
21
  const logger = createLogger("sales");
@@ -53,7 +54,11 @@ async function POST(req) {
53
54
  const container = await createRequestContainer();
54
55
  const em = container.resolve("em").fork();
55
56
  const hashedToken = hashAuthToken(token);
56
- const tenantScope = auth?.tenantId ? { tenantId: auth.tenantId } : void 0;
57
+ const effectiveTenantId = resolveEffectiveTenantId(auth);
58
+ if (auth && effectiveTenantId === null && auth.isApiKey !== true) {
59
+ throw new CrudHttpError(404, { error: translate("sales.quotes.accept.notFound", "Quote not found.") });
60
+ }
61
+ const tenantScope = effectiveTenantId ? { tenantId: effectiveTenantId } : void 0;
57
62
  const commandBus = container.resolve("commandBus");
58
63
  const { quote, orderId } = await em.transactional(async (trx) => {
59
64
  const findQuoteByToken = (acceptanceToken) => findOneWithDecryption(
@@ -61,7 +66,7 @@ async function POST(req) {
61
66
  SalesQuote,
62
67
  {
63
68
  acceptanceToken,
64
- ...auth?.tenantId ? { tenantId: auth.tenantId } : {},
69
+ ...effectiveTenantId ? { tenantId: effectiveTenantId } : {},
65
70
  deletedAt: null
66
71
  },
67
72
  { lockMode: LockMode.PESSIMISTIC_WRITE },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/sales/api/quotes/accept/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { LockMode } from '@mikro-orm/core'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { getCachedRateLimiterService } from '@open-mercato/core/bootstrap'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_FALLBACK_KEY, rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { validateSameOriginMutationRequest } from './originGuard'\nimport { hashAuthToken } from '../../../../auth/lib/tokenHash'\nimport { SalesOrder, SalesQuote } from '../../../data/entities'\nimport { quoteAcceptSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { QuoteAcceptedAdminEmail } from '../../../emails/QuoteAcceptedAdminEmail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype ConvertToOrderResult = {\n result?: { orderId?: string } | null\n orderId?: string\n}\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nconst quoteAcceptRateLimitConfig = readEndpointRateLimitConfig('SALES_QUOTES_ACCEPT', {\n points: 10,\n duration: 60,\n blockDuration: 300,\n keyPrefix: 'sales_quote_accept',\n})\n\nexport async function POST(req: Request) {\n try {\n const { translate } = await resolveTranslations()\n const sameOriginViolation = validateSameOriginMutationRequest(req)\n if (sameOriginViolation) {\n return NextResponse.json(\n { error: translate('sales.quotes.accept.forbidden', 'Cross-site quote acceptance is not allowed.') },\n { status: 403 },\n )\n }\n\n const rateLimiterService = getCachedRateLimiterService()\n const clientIp = rateLimiterService ? getClientIp(req, rateLimiterService.trustProxyDepth) : null\n if (rateLimiterService) {\n const rateLimitResponse = await checkRateLimit(\n rateLimiterService,\n quoteAcceptRateLimitConfig,\n clientIp ?? RATE_LIMIT_FALLBACK_KEY,\n translate('api.errors.rateLimit', 'Too many requests. Please try again later.'),\n )\n if (rateLimitResponse) return rateLimitResponse\n }\n\n const { token } = quoteAcceptSchema.parse(await req.json().catch(() => ({})))\n const auth = await getAuthFromRequest(req)\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n const hashedToken = hashAuthToken(token)\n const tenantScope = auth?.tenantId ? { tenantId: auth.tenantId } : undefined\n\n const commandBus = container.resolve('commandBus') as CommandBus\n\n // Lock the quote, flip it to confirmed, and convert it to an order inside a\n // single transaction. The conversion command reuses this transaction (and its\n // PESSIMISTIC_WRITE lock) via ctx.transactionalEm, so the status flip and the\n // order creation are atomic: if conversion fails the whole transaction rolls\n // back, leaving the quote in its prior 'sent' state with no partial order and\n // no need for an out-of-band compensating write.\n const { quote, orderId } = await em.transactional(async (trx) => {\n const findQuoteByToken = (acceptanceToken: string) =>\n findOneWithDecryption(\n trx,\n SalesQuote,\n {\n acceptanceToken,\n ...(auth?.tenantId ? { tenantId: auth.tenantId } : {}),\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n tenantScope,\n )\n const quote = await findQuoteByToken(hashedToken)\n if (!quote) {\n throw notFound(translate('sales.quotes.accept.notFound', 'Quote not found.'))\n }\n\n const now = new Date()\n if (quote.validUntil && quote.validUntil.getTime() < now.getTime()) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.accept.expired', 'This quote has expired.') })\n }\n\n if ((quote.status ?? null) !== 'sent') {\n throw new CrudHttpError(400, {\n error: translate('sales.quotes.accept.invalidStatus', 'This quote cannot be accepted in its current status.'),\n })\n }\n\n quote.status = 'confirmed'\n quote.statusEntryId = await resolveStatusEntryIdByValue(trx, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'confirmed',\n })\n quote.updatedAt = now\n trx.persist(quote)\n await trx.flush()\n\n const ctx: CommandRuntimeContext = {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: quote.organizationId,\n organizationIds: [quote.organizationId],\n request: req,\n transactionalEm: trx,\n }\n\n const result = (await commandBus.execute('sales.quotes.convert_to_order', { input: { quoteId: quote.id }, ctx })) as ConvertToOrderResult | null\n const orderId = result?.result?.orderId ?? result?.orderId ?? quote.id\n\n return { quote, orderId }\n })\n\n const order = await findOneWithDecryption(em, SalesOrder, { id: orderId, deletedAt: null }, {}, tenantScope)\n const orderNumber = order?.orderNumber ?? orderId\n\n // Admin notification should not block acceptance.\n const adminEmail = process.env.ADMIN_EMAIL || ''\n if (adminEmail) {\n try {\n const appUrl = process.env.APP_URL || ''\n const orderUrl = appUrl ? `${appUrl.replace(/\\/$/, '')}/backend/sales/orders/${orderId}` : `/backend/sales/orders/${orderId}`\n\n const copy = {\n preview: translate('sales.quotes.accept.adminEmail.preview', 'Quote {quoteNumber} accepted', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.accept.adminEmail.heading', 'Quote {quoteNumber} accepted', { quoteNumber: quote.quoteNumber }),\n body: translate('sales.quotes.accept.adminEmail.body', 'The customer accepted quote {quoteNumber}. An order has been created: {orderNumber}.', {\n quoteNumber: quote.quoteNumber,\n orderNumber,\n }),\n cta: translate('sales.quotes.accept.adminEmail.cta', 'View order'),\n footer: translate('sales.quotes.accept.adminEmail.footer', 'Open Mercato'),\n }\n\n await sendEmail({\n to: adminEmail,\n subject: translate('sales.quotes.accept.adminSubject', 'Quote {quoteNumber} accepted \u2192 Order {orderNumber}', {\n quoteNumber: quote.quoteNumber,\n orderNumber,\n }),\n react: QuoteAcceptedAdminEmail({ orderUrl, copy }),\n })\n } catch (err) {\n logger.error('sales.quotes.accept.adminEmail failed', { err })\n }\n }\n\n return NextResponse.json({ orderId, orderNumber })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('sales.quotes.accept failed', { err })\n return NextResponse.json({ error: translate('sales.quotes.accept.failed', 'Failed to accept quote.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Accept a quote (public)',\n methods: {\n POST: {\n summary: 'Accept quote and convert to order',\n requestBody: {\n contentType: 'application/json',\n schema: quoteAcceptSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Quote accepted and order created',\n schema: z.object({ orderId: z.string().uuid(), orderNumber: z.string() }),\n },\n { status: 400, description: 'Invalid or expired quote', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Cross-site request rejected', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Quote not found', schema: z.object({ error: z.string() }) },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { LockMode } from '@mikro-orm/core'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { getCachedRateLimiterService } from '@open-mercato/core/bootstrap'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_FALLBACK_KEY, rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { validateSameOriginMutationRequest } from './originGuard'\nimport { hashAuthToken } from '../../../../auth/lib/tokenHash'\nimport { SalesOrder, SalesQuote } from '../../../data/entities'\nimport { quoteAcceptSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { resolveEffectiveTenantId } from '../../../lib/publicQuoteTenantScope'\nimport { QuoteAcceptedAdminEmail } from '../../../emails/QuoteAcceptedAdminEmail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype ConvertToOrderResult = {\n result?: { orderId?: string } | null\n orderId?: string\n}\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nconst quoteAcceptRateLimitConfig = readEndpointRateLimitConfig('SALES_QUOTES_ACCEPT', {\n points: 10,\n duration: 60,\n blockDuration: 300,\n keyPrefix: 'sales_quote_accept',\n})\n\nexport async function POST(req: Request) {\n try {\n const { translate } = await resolveTranslations()\n const sameOriginViolation = validateSameOriginMutationRequest(req)\n if (sameOriginViolation) {\n return NextResponse.json(\n { error: translate('sales.quotes.accept.forbidden', 'Cross-site quote acceptance is not allowed.') },\n { status: 403 },\n )\n }\n\n const rateLimiterService = getCachedRateLimiterService()\n const clientIp = rateLimiterService ? getClientIp(req, rateLimiterService.trustProxyDepth) : null\n if (rateLimiterService) {\n const rateLimitResponse = await checkRateLimit(\n rateLimiterService,\n quoteAcceptRateLimitConfig,\n clientIp ?? RATE_LIMIT_FALLBACK_KEY,\n translate('api.errors.rateLimit', 'Too many requests. Please try again later.'),\n )\n if (rateLimitResponse) return rateLimitResponse\n }\n\n const { token } = quoteAcceptSchema.parse(await req.json().catch(() => ({})))\n const auth = await getAuthFromRequest(req)\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n const hashedToken = hashAuthToken(token)\n const effectiveTenantId = resolveEffectiveTenantId(auth)\n // A session whose tenant cannot be resolved must not fall through to an unscoped lookup.\n // Anonymous callers (no auth) and tenant-less API keys stay unscoped by design.\n if (auth && effectiveTenantId === null && auth.isApiKey !== true) {\n throw new CrudHttpError(404, { error: translate('sales.quotes.accept.notFound', 'Quote not found.') })\n }\n const tenantScope = effectiveTenantId ? { tenantId: effectiveTenantId } : undefined\n\n const commandBus = container.resolve('commandBus') as CommandBus\n\n // Lock the quote, flip it to confirmed, and convert it to an order inside a\n // single transaction. The conversion command reuses this transaction (and its\n // PESSIMISTIC_WRITE lock) via ctx.transactionalEm, so the status flip and the\n // order creation are atomic: if conversion fails the whole transaction rolls\n // back, leaving the quote in its prior 'sent' state with no partial order and\n // no need for an out-of-band compensating write.\n const { quote, orderId } = await em.transactional(async (trx) => {\n const findQuoteByToken = (acceptanceToken: string) =>\n findOneWithDecryption(\n trx,\n SalesQuote,\n {\n acceptanceToken,\n ...(effectiveTenantId ? { tenantId: effectiveTenantId } : {}),\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n tenantScope,\n )\n const quote = await findQuoteByToken(hashedToken)\n if (!quote) {\n throw notFound(translate('sales.quotes.accept.notFound', 'Quote not found.'))\n }\n\n const now = new Date()\n if (quote.validUntil && quote.validUntil.getTime() < now.getTime()) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.accept.expired', 'This quote has expired.') })\n }\n\n if ((quote.status ?? null) !== 'sent') {\n throw new CrudHttpError(400, {\n error: translate('sales.quotes.accept.invalidStatus', 'This quote cannot be accepted in its current status.'),\n })\n }\n\n quote.status = 'confirmed'\n quote.statusEntryId = await resolveStatusEntryIdByValue(trx, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'confirmed',\n })\n quote.updatedAt = now\n trx.persist(quote)\n await trx.flush()\n\n const ctx: CommandRuntimeContext = {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: quote.organizationId,\n organizationIds: [quote.organizationId],\n request: req,\n transactionalEm: trx,\n }\n\n const result = (await commandBus.execute('sales.quotes.convert_to_order', { input: { quoteId: quote.id }, ctx })) as ConvertToOrderResult | null\n const orderId = result?.result?.orderId ?? result?.orderId ?? quote.id\n\n return { quote, orderId }\n })\n\n const order = await findOneWithDecryption(em, SalesOrder, { id: orderId, deletedAt: null }, {}, tenantScope)\n const orderNumber = order?.orderNumber ?? orderId\n\n // Admin notification should not block acceptance.\n const adminEmail = process.env.ADMIN_EMAIL || ''\n if (adminEmail) {\n try {\n const appUrl = process.env.APP_URL || ''\n const orderUrl = appUrl ? `${appUrl.replace(/\\/$/, '')}/backend/sales/orders/${orderId}` : `/backend/sales/orders/${orderId}`\n\n const copy = {\n preview: translate('sales.quotes.accept.adminEmail.preview', 'Quote {quoteNumber} accepted', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.accept.adminEmail.heading', 'Quote {quoteNumber} accepted', { quoteNumber: quote.quoteNumber }),\n body: translate('sales.quotes.accept.adminEmail.body', 'The customer accepted quote {quoteNumber}. An order has been created: {orderNumber}.', {\n quoteNumber: quote.quoteNumber,\n orderNumber,\n }),\n cta: translate('sales.quotes.accept.adminEmail.cta', 'View order'),\n footer: translate('sales.quotes.accept.adminEmail.footer', 'Open Mercato'),\n }\n\n await sendEmail({\n to: adminEmail,\n subject: translate('sales.quotes.accept.adminSubject', 'Quote {quoteNumber} accepted \u2192 Order {orderNumber}', {\n quoteNumber: quote.quoteNumber,\n orderNumber,\n }),\n react: QuoteAcceptedAdminEmail({ orderUrl, copy }),\n })\n } catch (err) {\n logger.error('sales.quotes.accept.adminEmail failed', { err })\n }\n }\n\n return NextResponse.json({ orderId, orderNumber })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('sales.quotes.accept failed', { err })\n return NextResponse.json({ error: translate('sales.quotes.accept.failed', 'Failed to accept quote.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Accept a quote (public)',\n methods: {\n POST: {\n summary: 'Accept quote and convert to order',\n requestBody: {\n contentType: 'application/json',\n schema: quoteAcceptSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Quote accepted and order created',\n schema: z.object({ orderId: z.string().uuid(), orderNumber: z.string() }),\n },\n { status: 400, description: 'Invalid or expired quote', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Cross-site request rejected', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Quote not found', schema: z.object({ error: z.string() }) },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["quote", "orderId"]
7
7
  }
@@ -12,6 +12,7 @@ import {
12
12
  } from "../../../../data/entities.js";
13
13
  import { canonicalizeUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
14
14
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
15
+ import { isForeignTenantActor } from "../../../../lib/publicQuoteTenantScope.js";
15
16
  import { createLogger } from "@open-mercato/shared/lib/logger";
16
17
  const logger = createLogger("sales");
17
18
  const paramsSchema = z.object({
@@ -35,7 +36,7 @@ async function GET(req, ctx) {
35
36
  throw notFound(translate("sales.quotes.public.notFound", "Quote not found."));
36
37
  }
37
38
  const auth = await getAuthFromRequest(req);
38
- if (auth?.tenantId && quote.tenantId !== auth.tenantId) {
39
+ if (isForeignTenantActor(auth, quote.tenantId)) {
39
40
  throw notFound(translate("sales.quotes.public.notFound", "Quote not found."));
40
41
  }
41
42
  const now = /* @__PURE__ */ new Date();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/sales/api/quotes/public/%5Btoken%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport { resolveTranslations } from \"@open-mercato/shared/lib/i18n/server\";\nimport { isCrudHttpError, notFound } from \"@open-mercato/shared/lib/crud/errors\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { findOneWithDecryption, findWithDecryption } from \"@open-mercato/shared/lib/encryption/find\";\nimport { hashAuthToken } from \"../../../../../auth/lib/tokenHash\";\nimport {\n SalesQuote,\n SalesQuoteLine,\n SalesQuoteAdjustment,\n} from \"../../../../data/entities\";\nimport { canonicalizeUnitCode } from \"@open-mercato/shared/lib/units/unitCodes\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nconst paramsSchema = z.object({\n token: z.string().uuid(),\n});\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nexport async function GET(req: Request, ctx: { params: { token: string } }) {\n try {\n const { token } = paramsSchema.parse(ctx.params ?? {});\n const container = await createRequestContainer();\n const em = container.resolve(\"em\") as EntityManager;\n const hashedToken = hashAuthToken(token);\n const quote = await findOneWithDecryption(em, SalesQuote, {\n acceptanceToken: hashedToken,\n deletedAt: null,\n });\n const { translate } = await resolveTranslations();\n if (!quote) {\n throw notFound(translate(\"sales.quotes.public.notFound\", \"Quote not found.\"));\n }\n\n const auth = await getAuthFromRequest(req);\n if (auth?.tenantId && quote.tenantId !== auth.tenantId) {\n throw notFound(translate(\"sales.quotes.public.notFound\", \"Quote not found.\"));\n }\n\n const now = new Date();\n const isExpired =\n !!quote.validUntil && quote.validUntil.getTime() < now.getTime();\n\n const [lines, adjustments] = await Promise.all([\n findWithDecryption(\n em,\n SalesQuoteLine,\n { quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId, deletedAt: null },\n { orderBy: { lineNumber: \"asc\" } },\n ),\n findWithDecryption(\n em,\n SalesQuoteAdjustment,\n { quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId },\n { orderBy: { position: \"asc\" } },\n ),\n ]);\n\n return NextResponse.json({\n quote: {\n quoteNumber: quote.quoteNumber,\n currencyCode: quote.currencyCode,\n validFrom: quote.validFrom?.toISOString() ?? null,\n validUntil: quote.validUntil?.toISOString() ?? null,\n status: quote.status ?? null,\n subtotalNetAmount: quote.subtotalNetAmount,\n subtotalGrossAmount: quote.subtotalGrossAmount,\n discountTotalAmount: quote.discountTotalAmount,\n taxTotalAmount: quote.taxTotalAmount,\n grandTotalNetAmount: quote.grandTotalNetAmount,\n grandTotalGrossAmount: quote.grandTotalGrossAmount,\n },\n lines: lines.map((line) => ({\n lineNumber: line.lineNumber ?? null,\n kind: line.kind,\n name: line.name ?? null,\n description: line.description ?? null,\n quantity: line.quantity,\n quantityUnit: canonicalizeUnitCode(line.quantityUnit) ?? null,\n normalizedQuantity: line.normalizedQuantity ?? line.quantity,\n normalizedUnit:\n canonicalizeUnitCode(line.normalizedUnit ?? line.quantityUnit) ??\n null,\n uomSnapshot: line.uomSnapshot\n ? {\n baseUnitCode: line.uomSnapshot.baseUnitCode ?? null,\n enteredUnitCode: line.uomSnapshot.enteredUnitCode ?? null,\n }\n : null,\n currencyCode: line.currencyCode,\n unitPriceNet: line.unitPriceNet,\n unitPriceGross: line.unitPriceGross,\n discountAmount: line.discountAmount,\n discountPercent: line.discountPercent,\n taxRate: line.taxRate,\n taxAmount: line.taxAmount,\n totalNetAmount: line.totalNetAmount,\n totalGrossAmount: line.totalGrossAmount,\n unitPriceReference: (() => {\n if (!line.uomSnapshot) return null;\n const ref = line.uomSnapshot.unitPriceReference;\n if (!ref) return null;\n return {\n enabled: ref.enabled ?? null,\n referenceUnitCode: ref.referenceUnitCode ?? null,\n baseQuantity: ref.baseQuantity ?? null,\n grossPerReference: ref.grossPerReference ?? null,\n netPerReference: ref.netPerReference ?? null,\n };\n })(),\n })),\n adjustments: adjustments.map((adj) => ({\n scope: adj.scope,\n kind: adj.kind,\n label: adj.label ?? adj.code ?? null,\n rate: adj.rate,\n amountNet: adj.amountNet,\n amountGross: adj.amountGross,\n currencyCode: adj.currencyCode ?? null,\n position: adj.position ?? null,\n quoteLineId: adj.quoteLine?.id ?? null,\n })),\n isExpired,\n });\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status });\n }\n const { translate } = await resolveTranslations();\n logger.error('sales.quotes.public failed', { err });\n return NextResponse.json(\n {\n error: translate(\"sales.quotes.public.failed\", \"Failed to load quote.\"),\n },\n { status: 400 },\n );\n }\n}\n\nconst publicQuoteResponseSchema = z.object({\n quote: z.object({\n quoteNumber: z.string(),\n currencyCode: z.string(),\n validFrom: z.string().nullable(),\n validUntil: z.string().nullable(),\n status: z.string().nullable(),\n subtotalNetAmount: z.string(),\n subtotalGrossAmount: z.string(),\n discountTotalAmount: z.string(),\n taxTotalAmount: z.string(),\n grandTotalNetAmount: z.string(),\n grandTotalGrossAmount: z.string(),\n }),\n lines: z.array(\n z.object({\n lineNumber: z.number().nullable(),\n kind: z.string(),\n name: z.string().nullable(),\n description: z.string().nullable(),\n quantity: z.string(),\n quantityUnit: z.string().nullable(),\n normalizedQuantity: z.string(),\n normalizedUnit: z.string().nullable(),\n uomSnapshot: z\n .object({\n baseUnitCode: z.string().nullable(),\n enteredUnitCode: z.string().nullable(),\n })\n .nullable()\n .optional(),\n currencyCode: z.string(),\n unitPriceNet: z.string(),\n unitPriceGross: z.string(),\n discountAmount: z.string(),\n discountPercent: z.string(),\n taxRate: z.string(),\n taxAmount: z.string(),\n totalNetAmount: z.string(),\n totalGrossAmount: z.string(),\n unitPriceReference: z\n .object({\n enabled: z.boolean().nullable().optional(),\n referenceUnitCode: z.string().nullable().optional(),\n baseQuantity: z.string().nullable().optional(),\n grossPerReference: z.string().nullable().optional(),\n netPerReference: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n }),\n ),\n adjustments: z.array(\n z.object({\n scope: z.string().nullable(),\n kind: z.string().nullable(),\n label: z.string().nullable(),\n rate: z.string().nullable(),\n amountNet: z.string().nullable(),\n amountGross: z.string().nullable(),\n currencyCode: z.string().nullable(),\n position: z.number().nullable(),\n quoteLineId: z.string().uuid().nullable(),\n }),\n ),\n isExpired: z.boolean(),\n});\n\nexport const openApi: OpenApiRouteDoc = {\n tag: \"Sales\",\n summary: \"View a quote (public)\",\n pathParams: z.object({ token: z.string().uuid() }),\n methods: {\n GET: {\n summary: \"Get quote details by acceptance token\",\n responses: [\n {\n status: 200,\n description: \"Quote details\",\n schema: publicQuoteResponseSchema,\n },\n {\n status: 404,\n description: \"Quote not found\",\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n};\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,iBAAiB,gBAAgB;AAG1C,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AACrC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,OAAO;AAEnC,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,OAAO,EAAE,OAAO,EAAE,KAAK;AACzB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM;AAC5B;AAEA,eAAsB,IAAI,KAAc,KAAoC;AAC1E,MAAI;AACF,UAAM,EAAE,MAAM,IAAI,aAAa,MAAM,IAAI,UAAU,CAAC,CAAC;AACrD,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,UAAM,cAAc,cAAc,KAAK;AACvC,UAAM,QAAQ,MAAM,sBAAsB,IAAI,YAAY;AAAA,MACxD,iBAAiB;AAAA,MACjB,WAAW;AAAA,IACb,CAAC;AACD,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAI,CAAC,OAAO;AACV,YAAM,SAAS,UAAU,gCAAgC,kBAAkB,CAAC;AAAA,IAC9E;AAEA,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,MAAM,YAAY,MAAM,aAAa,KAAK,UAAU;AACtD,YAAM,SAAS,UAAU,gCAAgC,kBAAkB,CAAC;AAAA,IAC9E;AAEA,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,YACJ,CAAC,CAAC,MAAM,cAAc,MAAM,WAAW,QAAQ,IAAI,IAAI,QAAQ;AAEjE,UAAM,CAAC,OAAO,WAAW,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC7C;AAAA,QACE;AAAA,QACA;AAAA,QACA,EAAE,OAAO,MAAM,IAAI,gBAAgB,MAAM,gBAAgB,UAAU,MAAM,UAAU,WAAW,KAAK;AAAA,QACnG,EAAE,SAAS,EAAE,YAAY,MAAM,EAAE;AAAA,MACnC;AAAA,MACA;AAAA,QACE;AAAA,QACA;AAAA,QACA,EAAE,OAAO,MAAM,IAAI,gBAAgB,MAAM,gBAAgB,UAAU,MAAM,SAAS;AAAA,QAClF,EAAE,SAAS,EAAE,UAAU,MAAM,EAAE;AAAA,MACjC;AAAA,IACF,CAAC;AAED,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO;AAAA,QACL,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,QACpB,WAAW,MAAM,WAAW,YAAY,KAAK;AAAA,QAC7C,YAAY,MAAM,YAAY,YAAY,KAAK;AAAA,QAC/C,QAAQ,MAAM,UAAU;AAAA,QACxB,mBAAmB,MAAM;AAAA,QACzB,qBAAqB,MAAM;AAAA,QAC3B,qBAAqB,MAAM;AAAA,QAC3B,gBAAgB,MAAM;AAAA,QACtB,qBAAqB,MAAM;AAAA,QAC3B,uBAAuB,MAAM;AAAA,MAC/B;AAAA,MACA,OAAO,MAAM,IAAI,CAAC,UAAU;AAAA,QAC1B,YAAY,KAAK,cAAc;AAAA,QAC/B,MAAM,KAAK;AAAA,QACX,MAAM,KAAK,QAAQ;AAAA,QACnB,aAAa,KAAK,eAAe;AAAA,QACjC,UAAU,KAAK;AAAA,QACf,cAAc,qBAAqB,KAAK,YAAY,KAAK;AAAA,QACzD,oBAAoB,KAAK,sBAAsB,KAAK;AAAA,QACpD,gBACE,qBAAqB,KAAK,kBAAkB,KAAK,YAAY,KAC7D;AAAA,QACF,aAAa,KAAK,cACd;AAAA,UACE,cAAc,KAAK,YAAY,gBAAgB;AAAA,UAC/C,iBAAiB,KAAK,YAAY,mBAAmB;AAAA,QACvD,IACA;AAAA,QACJ,cAAc,KAAK;AAAA,QACnB,cAAc,KAAK;AAAA,QACnB,gBAAgB,KAAK;AAAA,QACrB,gBAAgB,KAAK;AAAA,QACrB,iBAAiB,KAAK;AAAA,QACtB,SAAS,KAAK;AAAA,QACd,WAAW,KAAK;AAAA,QAChB,gBAAgB,KAAK;AAAA,QACrB,kBAAkB,KAAK;AAAA,QACvB,qBAAqB,MAAM;AACzB,cAAI,CAAC,KAAK,YAAa,QAAO;AAC9B,gBAAM,MAAM,KAAK,YAAY;AAC7B,cAAI,CAAC,IAAK,QAAO;AACjB,iBAAO;AAAA,YACL,SAAS,IAAI,WAAW;AAAA,YACxB,mBAAmB,IAAI,qBAAqB;AAAA,YAC5C,cAAc,IAAI,gBAAgB;AAAA,YAClC,mBAAmB,IAAI,qBAAqB;AAAA,YAC5C,iBAAiB,IAAI,mBAAmB;AAAA,UAC1C;AAAA,QACF,GAAG;AAAA,MACL,EAAE;AAAA,MACF,aAAa,YAAY,IAAI,CAAC,SAAS;AAAA,QACrC,OAAO,IAAI;AAAA,QACX,MAAM,IAAI;AAAA,QACV,OAAO,IAAI,SAAS,IAAI,QAAQ;AAAA,QAChC,MAAM,IAAI;AAAA,QACV,WAAW,IAAI;AAAA,QACf,aAAa,IAAI;AAAA,QACjB,cAAc,IAAI,gBAAgB;AAAA,QAClC,UAAU,IAAI,YAAY;AAAA,QAC1B,aAAa,IAAI,WAAW,MAAM;AAAA,MACpC,EAAE;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,GAAG;AACxB,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,WAAO,MAAM,8BAA8B,EAAE,IAAI,CAAC;AAClD,WAAO,aAAa;AAAA,MAClB;AAAA,QACE,OAAO,UAAU,8BAA8B,uBAAuB;AAAA,MACxE;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEA,MAAM,4BAA4B,EAAE,OAAO;AAAA,EACzC,OAAO,EAAE,OAAO;AAAA,IACd,aAAa,EAAE,OAAO;AAAA,IACtB,cAAc,EAAE,OAAO;AAAA,IACvB,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,IAC/B,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,IAChC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,IAC5B,mBAAmB,EAAE,OAAO;AAAA,IAC5B,qBAAqB,EAAE,OAAO;AAAA,IAC9B,qBAAqB,EAAE,OAAO;AAAA,IAC9B,gBAAgB,EAAE,OAAO;AAAA,IACzB,qBAAqB,EAAE,OAAO;AAAA,IAC9B,uBAAuB,EAAE,OAAO;AAAA,EAClC,CAAC;AAAA,EACD,OAAO,EAAE;AAAA,IACP,EAAE,OAAO;AAAA,MACP,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,MAChC,MAAM,EAAE,OAAO;AAAA,MACf,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,MAC1B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,MACjC,UAAU,EAAE,OAAO;AAAA,MACnB,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,MAClC,oBAAoB,EAAE,OAAO;AAAA,MAC7B,gBAAgB,EAAE,OAAO,EAAE,SAAS;AAAA,MACpC,aAAa,EACV,OAAO;AAAA,QACN,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,QAClC,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,MACvC,CAAC,EACA,SAAS,EACT,SAAS;AAAA,MACZ,cAAc,EAAE,OAAO;AAAA,MACvB,cAAc,EAAE,OAAO;AAAA,MACvB,gBAAgB,EAAE,OAAO;AAAA,MACzB,gBAAgB,EAAE,OAAO;AAAA,MACzB,iBAAiB,EAAE,OAAO;AAAA,MAC1B,SAAS,EAAE,OAAO;AAAA,MAClB,WAAW,EAAE,OAAO;AAAA,MACpB,gBAAgB,EAAE,OAAO;AAAA,MACzB,kBAAkB,EAAE,OAAO;AAAA,MAC3B,oBAAoB,EACjB,OAAO;AAAA,QACN,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,QACzC,mBAAmB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,QAClD,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,QAC7C,mBAAmB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,QAClD,iBAAiB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,MAClD,CAAC,EACA,SAAS,EACT,SAAS;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EACA,aAAa,EAAE;AAAA,IACb,EAAE,OAAO;AAAA,MACP,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,MAC3B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,MAC1B,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,MAC3B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,MAC1B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,MAC/B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,MACjC,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,MAClC,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,MAC9B,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,IAC1C,CAAC;AAAA,EACH;AAAA,EACA,WAAW,EAAE,QAAQ;AACvB,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,EACjD,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport { resolveTranslations } from \"@open-mercato/shared/lib/i18n/server\";\nimport { isCrudHttpError, notFound } from \"@open-mercato/shared/lib/crud/errors\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { findOneWithDecryption, findWithDecryption } from \"@open-mercato/shared/lib/encryption/find\";\nimport { hashAuthToken } from \"../../../../../auth/lib/tokenHash\";\nimport {\n SalesQuote,\n SalesQuoteLine,\n SalesQuoteAdjustment,\n} from \"../../../../data/entities\";\nimport { canonicalizeUnitCode } from \"@open-mercato/shared/lib/units/unitCodes\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { isForeignTenantActor } from \"../../../../lib/publicQuoteTenantScope\";\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nconst paramsSchema = z.object({\n token: z.string().uuid(),\n});\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nexport async function GET(req: Request, ctx: { params: { token: string } }) {\n try {\n const { token } = paramsSchema.parse(ctx.params ?? {});\n const container = await createRequestContainer();\n const em = container.resolve(\"em\") as EntityManager;\n const hashedToken = hashAuthToken(token);\n const quote = await findOneWithDecryption(em, SalesQuote, {\n acceptanceToken: hashedToken,\n deletedAt: null,\n });\n const { translate } = await resolveTranslations();\n if (!quote) {\n throw notFound(translate(\"sales.quotes.public.notFound\", \"Quote not found.\"));\n }\n\n const auth = await getAuthFromRequest(req);\n if (isForeignTenantActor(auth, quote.tenantId)) {\n throw notFound(translate(\"sales.quotes.public.notFound\", \"Quote not found.\"));\n }\n\n const now = new Date();\n const isExpired =\n !!quote.validUntil && quote.validUntil.getTime() < now.getTime();\n\n const [lines, adjustments] = await Promise.all([\n findWithDecryption(\n em,\n SalesQuoteLine,\n { quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId, deletedAt: null },\n { orderBy: { lineNumber: \"asc\" } },\n ),\n findWithDecryption(\n em,\n SalesQuoteAdjustment,\n { quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId },\n { orderBy: { position: \"asc\" } },\n ),\n ]);\n\n return NextResponse.json({\n quote: {\n quoteNumber: quote.quoteNumber,\n currencyCode: quote.currencyCode,\n validFrom: quote.validFrom?.toISOString() ?? null,\n validUntil: quote.validUntil?.toISOString() ?? null,\n status: quote.status ?? null,\n subtotalNetAmount: quote.subtotalNetAmount,\n subtotalGrossAmount: quote.subtotalGrossAmount,\n discountTotalAmount: quote.discountTotalAmount,\n taxTotalAmount: quote.taxTotalAmount,\n grandTotalNetAmount: quote.grandTotalNetAmount,\n grandTotalGrossAmount: quote.grandTotalGrossAmount,\n },\n lines: lines.map((line) => ({\n lineNumber: line.lineNumber ?? null,\n kind: line.kind,\n name: line.name ?? null,\n description: line.description ?? null,\n quantity: line.quantity,\n quantityUnit: canonicalizeUnitCode(line.quantityUnit) ?? null,\n normalizedQuantity: line.normalizedQuantity ?? line.quantity,\n normalizedUnit:\n canonicalizeUnitCode(line.normalizedUnit ?? line.quantityUnit) ??\n null,\n uomSnapshot: line.uomSnapshot\n ? {\n baseUnitCode: line.uomSnapshot.baseUnitCode ?? null,\n enteredUnitCode: line.uomSnapshot.enteredUnitCode ?? null,\n }\n : null,\n currencyCode: line.currencyCode,\n unitPriceNet: line.unitPriceNet,\n unitPriceGross: line.unitPriceGross,\n discountAmount: line.discountAmount,\n discountPercent: line.discountPercent,\n taxRate: line.taxRate,\n taxAmount: line.taxAmount,\n totalNetAmount: line.totalNetAmount,\n totalGrossAmount: line.totalGrossAmount,\n unitPriceReference: (() => {\n if (!line.uomSnapshot) return null;\n const ref = line.uomSnapshot.unitPriceReference;\n if (!ref) return null;\n return {\n enabled: ref.enabled ?? null,\n referenceUnitCode: ref.referenceUnitCode ?? null,\n baseQuantity: ref.baseQuantity ?? null,\n grossPerReference: ref.grossPerReference ?? null,\n netPerReference: ref.netPerReference ?? null,\n };\n })(),\n })),\n adjustments: adjustments.map((adj) => ({\n scope: adj.scope,\n kind: adj.kind,\n label: adj.label ?? adj.code ?? null,\n rate: adj.rate,\n amountNet: adj.amountNet,\n amountGross: adj.amountGross,\n currencyCode: adj.currencyCode ?? null,\n position: adj.position ?? null,\n quoteLineId: adj.quoteLine?.id ?? null,\n })),\n isExpired,\n });\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status });\n }\n const { translate } = await resolveTranslations();\n logger.error('sales.quotes.public failed', { err });\n return NextResponse.json(\n {\n error: translate(\"sales.quotes.public.failed\", \"Failed to load quote.\"),\n },\n { status: 400 },\n );\n }\n}\n\nconst publicQuoteResponseSchema = z.object({\n quote: z.object({\n quoteNumber: z.string(),\n currencyCode: z.string(),\n validFrom: z.string().nullable(),\n validUntil: z.string().nullable(),\n status: z.string().nullable(),\n subtotalNetAmount: z.string(),\n subtotalGrossAmount: z.string(),\n discountTotalAmount: z.string(),\n taxTotalAmount: z.string(),\n grandTotalNetAmount: z.string(),\n grandTotalGrossAmount: z.string(),\n }),\n lines: z.array(\n z.object({\n lineNumber: z.number().nullable(),\n kind: z.string(),\n name: z.string().nullable(),\n description: z.string().nullable(),\n quantity: z.string(),\n quantityUnit: z.string().nullable(),\n normalizedQuantity: z.string(),\n normalizedUnit: z.string().nullable(),\n uomSnapshot: z\n .object({\n baseUnitCode: z.string().nullable(),\n enteredUnitCode: z.string().nullable(),\n })\n .nullable()\n .optional(),\n currencyCode: z.string(),\n unitPriceNet: z.string(),\n unitPriceGross: z.string(),\n discountAmount: z.string(),\n discountPercent: z.string(),\n taxRate: z.string(),\n taxAmount: z.string(),\n totalNetAmount: z.string(),\n totalGrossAmount: z.string(),\n unitPriceReference: z\n .object({\n enabled: z.boolean().nullable().optional(),\n referenceUnitCode: z.string().nullable().optional(),\n baseQuantity: z.string().nullable().optional(),\n grossPerReference: z.string().nullable().optional(),\n netPerReference: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n }),\n ),\n adjustments: z.array(\n z.object({\n scope: z.string().nullable(),\n kind: z.string().nullable(),\n label: z.string().nullable(),\n rate: z.string().nullable(),\n amountNet: z.string().nullable(),\n amountGross: z.string().nullable(),\n currencyCode: z.string().nullable(),\n position: z.number().nullable(),\n quoteLineId: z.string().uuid().nullable(),\n }),\n ),\n isExpired: z.boolean(),\n});\n\nexport const openApi: OpenApiRouteDoc = {\n tag: \"Sales\",\n summary: \"View a quote (public)\",\n pathParams: z.object({ token: z.string().uuid() }),\n methods: {\n GET: {\n summary: \"Get quote details by acceptance token\",\n responses: [\n {\n status: 200,\n description: \"Quote details\",\n schema: publicQuoteResponseSchema,\n },\n {\n status: 404,\n description: \"Quote not found\",\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,24 @@
1
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2
+ function normalizeTenantValue(value) {
3
+ if (typeof value !== "string") return null;
4
+ const trimmed = value.trim();
5
+ if (!UUID_RE.test(trimmed)) return null;
6
+ return trimmed.toLowerCase();
7
+ }
8
+ function resolveEffectiveTenantId(auth) {
9
+ if (!auth) return null;
10
+ const selectedTenantId = normalizeTenantValue(auth.tenantId);
11
+ if (selectedTenantId !== null) return selectedTenantId;
12
+ return normalizeTenantValue(auth.actorTenantId);
13
+ }
14
+ function isForeignTenantActor(auth, documentTenantId) {
15
+ if (!auth) return false;
16
+ const effectiveTenantId = resolveEffectiveTenantId(auth);
17
+ if (effectiveTenantId === null) return auth.isApiKey !== true;
18
+ return effectiveTenantId !== normalizeTenantValue(documentTenantId);
19
+ }
20
+ export {
21
+ isForeignTenantActor,
22
+ resolveEffectiveTenantId
23
+ };
24
+ //# sourceMappingURL=publicQuoteTenantScope.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/sales/lib/publicQuoteTenantScope.ts"],
4
+ "sourcesContent": ["import type { AuthContext } from '@open-mercato/shared/lib/auth/server'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n\nfunction normalizeTenantValue(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (!UUID_RE.test(trimmed)) return null\n return trimmed.toLowerCase()\n}\n\n/**\n * The tenant a signed-in actor's reads and writes are scoped to, or null when none can be\n * resolved (including anonymous requests).\n *\n * `applySuperAdminScope` rewrites `auth.tenantId` from the `om_selected_tenant` cookie and\n * preserves the actor's own tenant under `actorTenantId`. An explicit selection is an intentional\n * scope and wins; an empty cookie (\"all tenants\") sets `auth.tenantId` to null on an otherwise\n * fully authenticated session, so the actor's own tenant is the fallback. `auth.tenantId` alone\n * cannot decide the scope, which is what defeated the public quote guard in #4309.\n *\n * A value that is not a well-formed UUID is treated as unresolvable rather than passed through to\n * a `uuid` column filter, since the cookie is attacker-controllable by the actor themselves.\n */\nexport function resolveEffectiveTenantId(auth: AuthContext): string | null {\n if (!auth) return null\n const selectedTenantId = normalizeTenantValue(auth.tenantId)\n if (selectedTenantId !== null) return selectedTenantId\n return normalizeTenantValue((auth as { actorTenantId?: unknown }).actorTenantId)\n}\n\n/**\n * True when the request carries a session belonging to a tenant other than the document's.\n *\n * An authenticated session whose tenant cannot be resolved counts as foreign \u2014 treating\n * \"tenant unknown\" as \"allow\" is the #4309 bug. Two cases are deliberately NOT foreign:\n *\n * - Anonymous requests. The public quote link is meant to work without a session, and denying\n * here would break the endpoint's entire purpose.\n * - API keys with no tenant. `ApiKey.tenantId` is nullable, so a global key legitimately resolves\n * to no tenant. Denying it would reject a more-privileged caller than the anonymous one that\n * the same link already serves.\n */\nexport function isForeignTenantActor(auth: AuthContext, documentTenantId: unknown): boolean {\n if (!auth) return false\n const effectiveTenantId = resolveEffectiveTenantId(auth)\n if (effectiveTenantId === null) return auth.isApiKey !== true\n return effectiveTenantId !== normalizeTenantValue(documentTenantId)\n}\n"],
5
+ "mappings": "AAEA,MAAM,UAAU;AAEhB,SAAS,qBAAqB,OAA+B;AAC3D,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAQ,KAAK,OAAO,EAAG,QAAO;AACnC,SAAO,QAAQ,YAAY;AAC7B;AAeO,SAAS,yBAAyB,MAAkC;AACzE,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,mBAAmB,qBAAqB,KAAK,QAAQ;AAC3D,MAAI,qBAAqB,KAAM,QAAO;AACtC,SAAO,qBAAsB,KAAqC,aAAa;AACjF;AAcO,SAAS,qBAAqB,MAAmB,kBAAoC;AAC1F,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,oBAAoB,yBAAyB,IAAI;AACvD,MAAI,sBAAsB,KAAM,QAAO,KAAK,aAAa;AACzD,SAAO,sBAAsB,qBAAqB,gBAAgB;AACpE;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.7-develop.6660.1.90e1e2eef6",
3
+ "version": "0.6.7-develop.6669.1.40b669666b",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -254,16 +254,16 @@
254
254
  "zod": "^4.4.3"
255
255
  },
256
256
  "peerDependencies": {
257
- "@open-mercato/ai-assistant": "0.6.7-develop.6660.1.90e1e2eef6",
258
- "@open-mercato/shared": "0.6.7-develop.6660.1.90e1e2eef6",
259
- "@open-mercato/ui": "0.6.7-develop.6660.1.90e1e2eef6",
257
+ "@open-mercato/ai-assistant": "0.6.7-develop.6669.1.40b669666b",
258
+ "@open-mercato/shared": "0.6.7-develop.6669.1.40b669666b",
259
+ "@open-mercato/ui": "0.6.7-develop.6669.1.40b669666b",
260
260
  "react": "^19.0.0",
261
261
  "react-dom": "^19.0.0"
262
262
  },
263
263
  "devDependencies": {
264
- "@open-mercato/ai-assistant": "0.6.7-develop.6660.1.90e1e2eef6",
265
- "@open-mercato/shared": "0.6.7-develop.6660.1.90e1e2eef6",
266
- "@open-mercato/ui": "0.6.7-develop.6660.1.90e1e2eef6",
264
+ "@open-mercato/ai-assistant": "0.6.7-develop.6669.1.40b669666b",
265
+ "@open-mercato/shared": "0.6.7-develop.6669.1.40b669666b",
266
+ "@open-mercato/ui": "0.6.7-develop.6669.1.40b669666b",
267
267
  "@testing-library/dom": "^10.4.1",
268
268
  "@testing-library/jest-dom": "^6.9.1",
269
269
  "@testing-library/react": "^16.3.1",
@@ -1,12 +1,5 @@
1
1
  /**
2
- * Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of
3
- * the ORM/driver layer that surfaces it. Shared across the hub's commands and
4
- * lib helpers so duplicate-insert handling stays consistent module-wide.
2
+ * @deprecated Import `isUniqueViolation` from `@open-mercato/shared/lib/db/pg-errors` instead.
3
+ * Kept as a re-export so existing hub call sites keep working.
5
4
  */
6
- export function isUniqueViolation(err: unknown): boolean {
7
- if (!err || typeof err !== 'object') return false
8
- const code = (err as { code?: string }).code
9
- if (code === '23505') return true // Postgres unique_violation
10
- const message = (err as { message?: string }).message
11
- return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)
12
- }
5
+ export { isUniqueViolation } from '@open-mercato/shared/lib/db/pg-errors'
@@ -0,0 +1,96 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ /**
4
+ * Builds the one-off repair for `customer_users.customer_entity_id` (#4473).
5
+ *
6
+ * `customerEntityId` is the CRM *company* FK and the portal company scope key —
7
+ * the portal Users page, portal invitations, and the company detail "Portal
8
+ * users" group all filter on it. Earlier invite flows (#4362) could store a
9
+ * person entity id, or an entity from another organization, in that column.
10
+ * The `autoLinkCrm` subscriber normalizes it (#4457), but it only runs on
11
+ * `customer_accounts.user.created`, which is never re-emitted for a row that
12
+ * already exists — so users created before that fix keep the bad FK forever.
13
+ * They stay scoped to the wrong company and every admin edit that resubmits the
14
+ * pre-filled value fails with "Company not found".
15
+ *
16
+ * The repair mirrors the subscriber's normalization exactly: touch only rows
17
+ * whose FK does NOT resolve to an in-org, non-deleted `company` entity, and for
18
+ * those either recover the real company from the person's profile (when the FK
19
+ * points at an in-org person whose company is itself in-org) or clear it.
20
+ *
21
+ * `updated_at` is bumped on every repaired row on purpose: it is the optimistic
22
+ * lock version. Without the bump a client holding the pre-repair version could
23
+ * resubmit the poisoned value and silently undo the fix instead of getting a
24
+ * 409 conflict.
25
+ *
26
+ * The whole statement is wrapped in a `to_regclass` guard because the `customers`
27
+ * tables it reads belong to a different module. `dbMigrate` walks enabled modules
28
+ * in alphabetical order, so `customer_accounts` migrations run BEFORE `customers`
29
+ * ones — on a fresh database these tables do not exist yet. A database in that
30
+ * state has no poisoned rows to repair either, so skipping is the correct no-op.
31
+ * `catalog/Migration20251116191744` already ships the same guarded `do $$` idiom.
32
+ *
33
+ * This SQL is hand-written rather than produced by `yarn db:generate`, which the
34
+ * review checklist otherwise requires. The generator only diffs entities against
35
+ * the schema snapshot, so it cannot emit a data repair, and there is no schema
36
+ * change here to diff: no column, index, or constraint is touched, which is why
37
+ * `.snapshot-open-mercato.json` is deliberately left untouched by this migration.
38
+ *
39
+ * @public Exported for testing
40
+ */
41
+ export function buildRepairPoisonedCustomerEntityLinksSql(): string {
42
+ return `do $$
43
+ begin
44
+ if to_regclass('customer_entities') is null or to_regclass('customer_people') is null then
45
+ return;
46
+ end if;
47
+
48
+ update "customer_users" cu
49
+ set "customer_entity_id" = (
50
+ select company."id"
51
+ from "customer_entities" person
52
+ join "customer_people" profile
53
+ on profile."entity_id" = person."id"
54
+ and profile."tenant_id" = person."tenant_id"
55
+ and profile."organization_id" = person."organization_id"
56
+ join "customer_entities" company
57
+ on company."id" = profile."company_entity_id"
58
+ and company."tenant_id" = person."tenant_id"
59
+ and company."organization_id" = person."organization_id"
60
+ and company."kind" = 'company'
61
+ and company."deleted_at" is null
62
+ where person."id" = cu."customer_entity_id"
63
+ and person."tenant_id" = cu."tenant_id"
64
+ and person."organization_id" = cu."organization_id"
65
+ and person."kind" = 'person'
66
+ and person."deleted_at" is null
67
+ order by profile."created_at", profile."id"
68
+ limit 1
69
+ ),
70
+ "updated_at" = now()
71
+ where cu."customer_entity_id" is not null
72
+ and not exists (
73
+ select 1
74
+ from "customer_entities" company
75
+ where company."id" = cu."customer_entity_id"
76
+ and company."tenant_id" = cu."tenant_id"
77
+ and company."organization_id" = cu."organization_id"
78
+ and company."kind" = 'company'
79
+ and company."deleted_at" is null
80
+ );
81
+ end
82
+ $$;`;
83
+ }
84
+
85
+ export class Migration20260724120000_customer_accounts extends Migration {
86
+
87
+ override up(): void | Promise<void> {
88
+ this.addSql(buildRepairPoisonedCustomerEntityLinksSql());
89
+ }
90
+
91
+ override down(): void | Promise<void> {
92
+ // Forward-only: the poisoned values are not recorded anywhere, so there is
93
+ // nothing to restore. Re-poisoning the column would reintroduce the bug.
94
+ }
95
+
96
+ }
@@ -52,7 +52,7 @@ type TreeNode = {
52
52
 
53
53
  const viewSchema = z.object({
54
54
  page: z.coerce.number().min(1).default(1),
55
- pageSize: z.coerce.number().min(1).max(200).default(50),
55
+ pageSize: z.coerce.number().min(1).max(100).default(50),
56
56
  search: z.string().optional(),
57
57
  view: z.enum(['options', 'manage', 'tree']).default('options'),
58
58
  ids: z.string().optional(),
@@ -421,6 +421,8 @@ export async function GET(req: Request) {
421
421
  // attachments config passthrough
422
422
  maxAttachmentSizeMb: typeof d.configJson?.maxAttachmentSizeMb === 'number' ? d.configJson.maxAttachmentSizeMb : undefined,
423
423
  acceptExtensions: Array.isArray(d.configJson?.acceptExtensions) ? d.configJson.acceptExtensions : undefined,
424
+ // phone config passthrough
425
+ defaultCountryIso2: typeof d.configJson?.defaultCountryIso2 === 'string' ? d.configJson.defaultCountryIso2 : undefined,
424
426
  entityId,
425
427
  fieldset: normalizedFieldset ?? effectiveFieldsets[0],
426
428
  fieldsets: effectiveFieldsets.length > 0 ? effectiveFieldsets : undefined,
@@ -30,6 +30,7 @@ function columnFromKind(kind: string): keyof CustomFieldValue {
30
30
  case 'select':
31
31
  case 'currency':
32
32
  case 'dictionary':
33
+ case 'phone':
33
34
  return 'valueText'
34
35
  case 'multiline':
35
36
  return 'valueMultiline'