@open-mercato/core 0.7.1-develop.7102.1.b41f7e3e51 → 0.7.1-develop.7104.1.1dd769bf1a

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 4361 entry points
1
+ [build:core] found 4362 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 218 entry points
4
4
  [build:core:generated] built successfully
@@ -7,6 +7,7 @@ import { resolveDealsOrganizationIds } from "../../../lib/dealsOrganizationScope
7
7
  import { resolveOptionalBaseCurrencyCode } from "../../../lib/optionalBaseCurrency.js";
8
8
  import { parseBooleanFromUnknown } from "@open-mercato/shared/lib/boolean";
9
9
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
10
+ import { readQueryParamList } from "@open-mercato/shared/lib/crud/query-params";
10
11
  import { isTenantDataEncryptionEnabled } from "@open-mercato/shared/lib/encryption/toggles";
11
12
  import { fetchStuckDealIds } from "../../../lib/stuckDeals.js";
12
13
  import { expandDealStatusAliases } from "../../../lib/dealStatus.js";
@@ -75,11 +76,8 @@ const openApi = {
75
76
  }
76
77
  };
77
78
  function readArrayParam(searchParams, key) {
78
- const all = searchParams.getAll(key);
79
- if (!all.length) return null;
80
- const flat = all.flatMap((v) => v.split(","));
81
- const trimmed = flat.map((s) => s.trim()).filter(Boolean);
82
- return trimmed.length ? trimmed : null;
79
+ const values = readQueryParamList(searchParams, key);
80
+ return values.length ? values : null;
83
81
  }
84
82
  function restrictToIds(where, values, ids) {
85
83
  if (ids.length === 0) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/api/deals/aggregate/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager as CoreEntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PgEntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveDealsOrganizationIds } from '../../../lib/dealsOrganizationScope'\nimport { resolveOptionalBaseCurrencyCode } from '../../../lib/optionalBaseCurrency'\nimport type { ExchangeRateService } from '@open-mercato/core/modules/currencies/services/exchangeRateService'\nimport { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { fetchStuckDealIds } from '../../../lib/stuckDeals'\nimport { expandDealStatusAliases } from '../../../lib/dealStatus'\nimport { findMatchingEntityIdsBySearchTokensAcrossSources } from '../../utils'\nimport { E } from '#generated/entities.ids.generated'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },\n}\n\nconst querySchema = z.object({\n pipelineId: z.string().uuid().optional(),\n search: z.string().optional(),\n status: z.array(z.string().max(50)).max(20).optional(),\n ownerUserId: z.array(z.string().uuid()).optional(),\n personId: z.array(z.string().uuid()).optional(),\n companyId: z.array(z.string().uuid()).optional(),\n isStuck: z.preprocess((value) => {\n const parsed = parseBooleanFromUnknown(value)\n return parsed === null ? value : parsed\n }, z.boolean()).optional(),\n isOverdue: z.preprocess((value) => {\n const parsed = parseBooleanFromUnknown(value)\n return parsed === null ? value : parsed\n }, z.boolean()).optional(),\n expectedCloseAtFrom: z.string().optional(),\n expectedCloseAtTo: z.string().optional(),\n})\n\ntype StageBreakdownByCurrency = {\n currency: string\n total: number\n count: number\n}\n\ntype StageAggregate = {\n stageId: string\n count: number\n openCount: number\n totalInBaseCurrency: number\n byCurrency: StageBreakdownByCurrency[]\n /**\n * `true` when every currency present in `byCurrency` was either the base currency or\n * had a usable exchange rate at request time. `false` when at least one currency in\n * `byCurrency` could not be resolved to base \u2014 the lane header should surface a\n * \"partial\" indicator in that case so the converted total isn't read as authoritative.\n */\n convertedAll: boolean\n /**\n * Currencies present in `byCurrency` that had NO exchange rate to the base currency.\n * Empty when conversion was complete or there is no base currency configured. The\n * client uses this to disclose which slice of the value is excluded from `totalInBaseCurrency`.\n */\n missingRateCurrencies: string[]\n}\n\ntype AggregateResponse = {\n baseCurrencyCode: string | null\n perStage: StageAggregate[]\n}\n\nconst stageBreakdownByCurrencySchema = z.object({\n currency: z.string(),\n total: z.number(),\n count: z.number(),\n})\n\nconst stageAggregateSchema = z.object({\n stageId: z.string(),\n count: z.number(),\n openCount: z.number(),\n totalInBaseCurrency: z.number(),\n byCurrency: z.array(stageBreakdownByCurrencySchema),\n convertedAll: z.boolean(),\n missingRateCurrencies: z.array(z.string()),\n})\n\nconst aggregateResponseSchema = z.object({\n baseCurrencyCode: z.string().nullable(),\n perStage: z.array(stageAggregateSchema),\n})\n\nconst aggregateErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Deals aggregate per pipeline stage',\n methods: {\n GET: {\n summary: 'Per-stage counts and currency totals for kanban lane headers',\n description:\n 'Returns per-stage counts and totals for deals, with values converted to the tenant base currency where rates are available. Used to power kanban lane headers without loading every deal.',\n query: querySchema,\n responses: [\n { status: 200, description: 'Per-stage aggregate payload', schema: aggregateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: aggregateErrorSchema },\n { status: 401, description: 'Unauthorized', schema: aggregateErrorSchema },\n ],\n },\n },\n}\n\nfunction readArrayParam(searchParams: URLSearchParams, key: string): string[] | null {\n const all = searchParams.getAll(key)\n if (!all.length) return null\n const flat = all.flatMap((v) => v.split(','))\n const trimmed = flat.map((s) => s.trim()).filter(Boolean)\n return trimmed.length ? trimmed : null\n}\n\nfunction restrictToIds(where: string[], values: Array<string | number | null>, ids: string[]) {\n if (ids.length === 0) {\n where.push('id = ?')\n values.push('00000000-0000-0000-0000-000000000000')\n return\n }\n const placeholders = ids.map(() => '?').join(',')\n where.push(`id IN (${placeholders})`)\n values.push(...ids)\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const params = url.searchParams\n const parsed = querySchema.safeParse({\n pipelineId: params.get('pipelineId') ?? undefined,\n search: params.get('search') ?? undefined,\n status: readArrayParam(params, 'status') ?? undefined,\n ownerUserId: readArrayParam(params, 'ownerUserId') ?? undefined,\n personId: readArrayParam(params, 'personId') ?? undefined,\n companyId: readArrayParam(params, 'companyId') ?? undefined,\n isStuck: params.get('isStuck') ?? undefined,\n isOverdue: params.get('isOverdue') ?? undefined,\n expectedCloseAtFrom: params.get('expectedCloseAtFrom') ?? undefined,\n expectedCloseAtTo: params.get('expectedCloseAtTo') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query parameters' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve<CoreEntityManager>('em')\n\n // Resolve organization scope from the request so multi-org operators can aggregate\n // deals for any org their RBAC scope permits (matching the deal detail route at\n // [id]/route.ts:360). Falls back to `auth.orgId` when no scope cookie is set or\n // when rbacService cannot be resolved (e.g. in unit tests with a minimal container).\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const effectiveTenantId = scope.tenantId ?? auth.tenantId\n if (!effectiveTenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const orgFilterIds = await resolveDealsOrganizationIds({ em, scope, auth, tenantId: effectiveTenantId })\n\n // Raw SQL is used here intentionally \u2014 the route only projects non-encrypted columns\n // (`pipeline_stage_id`, `value_amount`, `value_currency`, `status`, plus filters). It\n // avoids the per-row decryption cost that would be paid by `findWithDecryption` for an\n // aggregate that never reads `title`/`description`. The search path still relies on\n // the token index above to find matching deals when encrypted columns are involved.\n const baseCurrencyCode = await resolveOptionalBaseCurrencyCode(\n container,\n effectiveTenantId,\n orgFilterIds[0],\n )\n\n // Build WHERE clause shared between count + sum queries\n const orgPlaceholders = orgFilterIds.map(() => '?').join(',')\n const where: string[] = [\n 'tenant_id = ?',\n `organization_id IN (${orgPlaceholders})`,\n 'deleted_at IS NULL',\n ]\n const values: Array<string | number | null> = [effectiveTenantId, ...orgFilterIds]\n\n if (parsed.data.pipelineId) {\n where.push('pipeline_id = ?')\n values.push(parsed.data.pipelineId)\n }\n const search = parsed.data.search?.trim()\n if (search) {\n const searchCtx: CrudCtx = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: orgFilterIds[0],\n organizationIds: orgFilterIds,\n request: req,\n }\n const matchingIds = await findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx: searchCtx,\n query: search,\n sources: [\n {\n entityType: E.customers.customer_deal,\n fields: [\n 'title',\n 'description',\n 'status',\n 'pipeline_stage',\n 'source',\n 'value_amount',\n 'value_currency',\n 'cf:competitive_risk',\n 'cf:implementation_complexity',\n ],\n },\n ],\n })\n if (matchingIds !== null && matchingIds.length > 0) {\n restrictToIds(where, values, matchingIds)\n } else if (isTenantDataEncryptionEnabled()) {\n // `customers:customer_deal.title` and `.description` are declared in\n // `encryption.ts` as encrypted columns. A raw `ILIKE` over the ciphertext would\n // silently match nothing on encrypted tenants and produce misleading lane totals,\n // so we collapse the result to zero rows instead \u2014 consistent with the list\n // endpoint's behavior when the token index has nothing to offer.\n restrictToIds(where, values, [])\n } else {\n const searchPattern = `%${escapeLikePattern(search)}%`\n where.push(\"(title ILIKE ? ESCAPE '\\\\' OR description ILIKE ? ESCAPE '\\\\')\")\n values.push(searchPattern, searchPattern)\n }\n }\n if (parsed.data.status && parsed.data.status.length) {\n // Non-empty input always expands to a non-empty set, so this always narrows.\n const expandedStatuses = expandDealStatusAliases(parsed.data.status)\n const placeholders = expandedStatuses.map(() => '?').join(',')\n where.push(`status IN (${placeholders})`)\n values.push(...expandedStatuses)\n }\n if (parsed.data.ownerUserId && parsed.data.ownerUserId.length) {\n const placeholders = parsed.data.ownerUserId.map(() => '?').join(',')\n where.push(`owner_user_id IN (${placeholders})`)\n values.push(...parsed.data.ownerUserId)\n }\n if (parsed.data.expectedCloseAtFrom) {\n where.push('expected_close_at >= ?')\n values.push(parsed.data.expectedCloseAtFrom)\n }\n if (parsed.data.expectedCloseAtTo) {\n where.push('expected_close_at <= ?')\n values.push(parsed.data.expectedCloseAtTo)\n }\n if (parsed.data.isOverdue) {\n // Mirror the list route's precedence: the caller-supplied status filter wins, and\n // status='open' is injected only when none was provided.\n const hasCallerStatus = !!parsed.data.status?.length\n if (hasCallerStatus) {\n where.push('expected_close_at < CURRENT_DATE')\n } else {\n where.push(\"expected_close_at < CURRENT_DATE AND status = 'open'\")\n }\n }\n if (parsed.data.isStuck) {\n // Reuse the list endpoint's stuck-deal lookup so kanban headers, lane counts, and the\n // cards rendered inside each lane agree on the same definition of \"stuck\". Empty result\n // \u2192 narrow to a sentinel UUID so the WHERE clause collapses to zero rows.\n const stuckIds = await fetchStuckDealIds(em as unknown as PgEntityManager, orgFilterIds[0], effectiveTenantId)\n restrictToIds(where, values, stuckIds)\n }\n if (parsed.data.personId && parsed.data.personId.length) {\n const placeholders = parsed.data.personId.map(() => '?').join(',')\n where.push(`EXISTS (SELECT 1 FROM customer_deal_people dp WHERE dp.deal_id = customer_deals.id AND dp.person_entity_id IN (${placeholders}))`)\n values.push(...parsed.data.personId)\n }\n if (parsed.data.companyId && parsed.data.companyId.length) {\n const placeholders = parsed.data.companyId.map(() => '?').join(',')\n where.push(`EXISTS (SELECT 1 FROM customer_deal_companies dc WHERE dc.deal_id = customer_deals.id AND dc.company_entity_id IN (${placeholders}))`)\n values.push(...parsed.data.companyId)\n }\n\n const whereSql = where.join(' AND ')\n\n // Aggregate by (stage, currency). Treat null stage as the literal `__unassigned`.\n const rows = await em.getConnection().execute<\n Array<{\n stage_id: string | null\n currency: string | null\n total: string | number | null\n count: string | number\n open_count: string | number\n }>\n >(\n `SELECT\n pipeline_stage_id AS stage_id,\n UPPER(COALESCE(value_currency, '')) AS currency,\n COALESCE(SUM(value_amount), 0) AS total,\n COUNT(*) AS count,\n COUNT(*) FILTER (WHERE status = 'open') AS open_count\n FROM customer_deals\n WHERE ${whereSql}\n GROUP BY pipeline_stage_id, UPPER(COALESCE(value_currency, ''))`,\n values,\n )\n\n // Reduce to per-stage aggregates\n const stageMap = new Map<string, StageAggregate>()\n for (const row of rows) {\n const stageId = row.stage_id ?? '__unassigned'\n const total = Number(row.total ?? 0)\n const count = Number(row.count ?? 0)\n const openCount = Number(row.open_count ?? 0)\n const currency = (row.currency ?? '').toString().trim()\n\n if (!stageMap.has(stageId)) {\n stageMap.set(stageId, {\n stageId,\n count: 0,\n openCount: 0,\n totalInBaseCurrency: 0,\n byCurrency: [],\n convertedAll: true,\n missingRateCurrencies: [],\n })\n }\n const agg = stageMap.get(stageId)!\n agg.count += count\n agg.openCount += openCount\n // Include any row that has a real currency code, even when its summed amount is\n // zero \u2014 `byCurrency` should agree with `count` so the per-currency breakdown\n // doesn't silently drop deals whose amount happens to be zero. Rows with no\n // currency code at all still get folded into the stage totals via `count`.\n if (currency.length > 0) {\n agg.byCurrency.push({ currency, total, count })\n }\n }\n\n // Convert to base currency where possible\n if (baseCurrencyCode) {\n const exchange = container.resolve('exchangeRateService') as ExchangeRateService | undefined\n const today = new Date()\n const distinctCurrencies = new Set<string>()\n for (const agg of stageMap.values()) {\n for (const row of agg.byCurrency) {\n if (row.currency && row.currency !== baseCurrencyCode) {\n distinctCurrencies.add(row.currency)\n }\n }\n }\n\n const rateCache = new Map<string, number>()\n if (exchange && distinctCurrencies.size > 0) {\n const pairs = Array.from(distinctCurrencies).map((c) => ({\n fromCurrencyCode: c,\n toCurrencyCode: baseCurrencyCode,\n }))\n try {\n const results = await exchange.getRates({\n pairs,\n date: today,\n scope: { tenantId: effectiveTenantId, organizationId: orgFilterIds[0] },\n options: { maxDaysBack: 60, autoFetch: false },\n })\n for (const [key, rateResult] of results) {\n if (rateResult.rates.length > 0) {\n // Pick the first matching rate (sources are equivalent for display purposes)\n const rate = Number(rateResult.rates[0].rate)\n if (Number.isFinite(rate) && rate > 0) {\n rateCache.set(key, rate)\n }\n }\n }\n } catch (err) {\n // Swallow \u2014 partial totals are still useful and we'll fall back to currency-native\n // sums. Logging at warn level so operators can correlate missing-rate disclosures in\n // the UI (`convertedAll: false` / `missingRateCurrencies`) with the underlying error.\n logger.warn('exchange-rate lookup failed; falling back to per-currency totals', { component: 'deals.aggregate', err })\n }\n }\n\n for (const agg of stageMap.values()) {\n let totalBase = 0\n let convertedAll = true\n const missingRateCurrencies: string[] = []\n for (const row of agg.byCurrency) {\n if (!row.currency) continue\n if (row.currency === baseCurrencyCode) {\n totalBase += row.total\n continue\n }\n const key = `${row.currency}/${baseCurrencyCode}`\n const rate = rateCache.get(key)\n if (rate !== undefined) {\n totalBase += row.total * rate\n } else {\n convertedAll = false\n if (!missingRateCurrencies.includes(row.currency)) {\n missingRateCurrencies.push(row.currency)\n }\n }\n }\n // Even when some rates are missing, totalInBaseCurrency reflects the converted slice.\n // `convertedAll`/`missingRateCurrencies` let the client distinguish a complete\n // conversion from a partial one and disclose which currencies were excluded.\n agg.totalInBaseCurrency = Math.round(totalBase)\n agg.convertedAll = convertedAll\n agg.missingRateCurrencies = missingRateCurrencies\n }\n } else {\n // No base currency configured for the tenant \u2014 surface this by marking every stage as\n // \"not converted\" with all currencies flagged as missing rates. This way the client UI\n // can fall back to a per-currency breakdown without trying to render a board total.\n for (const agg of stageMap.values()) {\n const present = agg.byCurrency.map((row) => row.currency).filter((c): c is string => !!c)\n if (present.length > 0) {\n agg.convertedAll = false\n agg.missingRateCurrencies = Array.from(new Set(present))\n }\n }\n }\n\n // Sort byCurrency rows by total descending for stable display\n for (const agg of stageMap.values()) {\n agg.byCurrency.sort((a, b) => b.total - a.total)\n }\n\n const response: AggregateResponse = {\n baseCurrencyCode,\n perStage: Array.from(stageMap.values()),\n }\n\n return NextResponse.json(response)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager as CoreEntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PgEntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveDealsOrganizationIds } from '../../../lib/dealsOrganizationScope'\nimport { resolveOptionalBaseCurrencyCode } from '../../../lib/optionalBaseCurrency'\nimport type { ExchangeRateService } from '@open-mercato/core/modules/currencies/services/exchangeRateService'\nimport { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { readQueryParamList } from '@open-mercato/shared/lib/crud/query-params'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { fetchStuckDealIds } from '../../../lib/stuckDeals'\nimport { expandDealStatusAliases } from '../../../lib/dealStatus'\nimport { findMatchingEntityIdsBySearchTokensAcrossSources } from '../../utils'\nimport { E } from '#generated/entities.ids.generated'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },\n}\n\nconst querySchema = z.object({\n pipelineId: z.string().uuid().optional(),\n search: z.string().optional(),\n status: z.array(z.string().max(50)).max(20).optional(),\n ownerUserId: z.array(z.string().uuid()).optional(),\n personId: z.array(z.string().uuid()).optional(),\n companyId: z.array(z.string().uuid()).optional(),\n isStuck: z.preprocess((value) => {\n const parsed = parseBooleanFromUnknown(value)\n return parsed === null ? value : parsed\n }, z.boolean()).optional(),\n isOverdue: z.preprocess((value) => {\n const parsed = parseBooleanFromUnknown(value)\n return parsed === null ? value : parsed\n }, z.boolean()).optional(),\n expectedCloseAtFrom: z.string().optional(),\n expectedCloseAtTo: z.string().optional(),\n})\n\ntype StageBreakdownByCurrency = {\n currency: string\n total: number\n count: number\n}\n\ntype StageAggregate = {\n stageId: string\n count: number\n openCount: number\n totalInBaseCurrency: number\n byCurrency: StageBreakdownByCurrency[]\n /**\n * `true` when every currency present in `byCurrency` was either the base currency or\n * had a usable exchange rate at request time. `false` when at least one currency in\n * `byCurrency` could not be resolved to base \u2014 the lane header should surface a\n * \"partial\" indicator in that case so the converted total isn't read as authoritative.\n */\n convertedAll: boolean\n /**\n * Currencies present in `byCurrency` that had NO exchange rate to the base currency.\n * Empty when conversion was complete or there is no base currency configured. The\n * client uses this to disclose which slice of the value is excluded from `totalInBaseCurrency`.\n */\n missingRateCurrencies: string[]\n}\n\ntype AggregateResponse = {\n baseCurrencyCode: string | null\n perStage: StageAggregate[]\n}\n\nconst stageBreakdownByCurrencySchema = z.object({\n currency: z.string(),\n total: z.number(),\n count: z.number(),\n})\n\nconst stageAggregateSchema = z.object({\n stageId: z.string(),\n count: z.number(),\n openCount: z.number(),\n totalInBaseCurrency: z.number(),\n byCurrency: z.array(stageBreakdownByCurrencySchema),\n convertedAll: z.boolean(),\n missingRateCurrencies: z.array(z.string()),\n})\n\nconst aggregateResponseSchema = z.object({\n baseCurrencyCode: z.string().nullable(),\n perStage: z.array(stageAggregateSchema),\n})\n\nconst aggregateErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Deals aggregate per pipeline stage',\n methods: {\n GET: {\n summary: 'Per-stage counts and currency totals for kanban lane headers',\n description:\n 'Returns per-stage counts and totals for deals, with values converted to the tenant base currency where rates are available. Used to power kanban lane headers without loading every deal.',\n query: querySchema,\n responses: [\n { status: 200, description: 'Per-stage aggregate payload', schema: aggregateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: aggregateErrorSchema },\n { status: 401, description: 'Unauthorized', schema: aggregateErrorSchema },\n ],\n },\n },\n}\n\nfunction readArrayParam(searchParams: URLSearchParams, key: string): string[] | null {\n const values = readQueryParamList(searchParams, key)\n return values.length ? values : null\n}\n\nfunction restrictToIds(where: string[], values: Array<string | number | null>, ids: string[]) {\n if (ids.length === 0) {\n where.push('id = ?')\n values.push('00000000-0000-0000-0000-000000000000')\n return\n }\n const placeholders = ids.map(() => '?').join(',')\n where.push(`id IN (${placeholders})`)\n values.push(...ids)\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const params = url.searchParams\n const parsed = querySchema.safeParse({\n pipelineId: params.get('pipelineId') ?? undefined,\n search: params.get('search') ?? undefined,\n status: readArrayParam(params, 'status') ?? undefined,\n ownerUserId: readArrayParam(params, 'ownerUserId') ?? undefined,\n personId: readArrayParam(params, 'personId') ?? undefined,\n companyId: readArrayParam(params, 'companyId') ?? undefined,\n isStuck: params.get('isStuck') ?? undefined,\n isOverdue: params.get('isOverdue') ?? undefined,\n expectedCloseAtFrom: params.get('expectedCloseAtFrom') ?? undefined,\n expectedCloseAtTo: params.get('expectedCloseAtTo') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query parameters' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve<CoreEntityManager>('em')\n\n // Resolve organization scope from the request so multi-org operators can aggregate\n // deals for any org their RBAC scope permits (matching the deal detail route at\n // [id]/route.ts:360). Falls back to `auth.orgId` when no scope cookie is set or\n // when rbacService cannot be resolved (e.g. in unit tests with a minimal container).\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const effectiveTenantId = scope.tenantId ?? auth.tenantId\n if (!effectiveTenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const orgFilterIds = await resolveDealsOrganizationIds({ em, scope, auth, tenantId: effectiveTenantId })\n\n // Raw SQL is used here intentionally \u2014 the route only projects non-encrypted columns\n // (`pipeline_stage_id`, `value_amount`, `value_currency`, `status`, plus filters). It\n // avoids the per-row decryption cost that would be paid by `findWithDecryption` for an\n // aggregate that never reads `title`/`description`. The search path still relies on\n // the token index above to find matching deals when encrypted columns are involved.\n const baseCurrencyCode = await resolveOptionalBaseCurrencyCode(\n container,\n effectiveTenantId,\n orgFilterIds[0],\n )\n\n // Build WHERE clause shared between count + sum queries\n const orgPlaceholders = orgFilterIds.map(() => '?').join(',')\n const where: string[] = [\n 'tenant_id = ?',\n `organization_id IN (${orgPlaceholders})`,\n 'deleted_at IS NULL',\n ]\n const values: Array<string | number | null> = [effectiveTenantId, ...orgFilterIds]\n\n if (parsed.data.pipelineId) {\n where.push('pipeline_id = ?')\n values.push(parsed.data.pipelineId)\n }\n const search = parsed.data.search?.trim()\n if (search) {\n const searchCtx: CrudCtx = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: orgFilterIds[0],\n organizationIds: orgFilterIds,\n request: req,\n }\n const matchingIds = await findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx: searchCtx,\n query: search,\n sources: [\n {\n entityType: E.customers.customer_deal,\n fields: [\n 'title',\n 'description',\n 'status',\n 'pipeline_stage',\n 'source',\n 'value_amount',\n 'value_currency',\n 'cf:competitive_risk',\n 'cf:implementation_complexity',\n ],\n },\n ],\n })\n if (matchingIds !== null && matchingIds.length > 0) {\n restrictToIds(where, values, matchingIds)\n } else if (isTenantDataEncryptionEnabled()) {\n // `customers:customer_deal.title` and `.description` are declared in\n // `encryption.ts` as encrypted columns. A raw `ILIKE` over the ciphertext would\n // silently match nothing on encrypted tenants and produce misleading lane totals,\n // so we collapse the result to zero rows instead \u2014 consistent with the list\n // endpoint's behavior when the token index has nothing to offer.\n restrictToIds(where, values, [])\n } else {\n const searchPattern = `%${escapeLikePattern(search)}%`\n where.push(\"(title ILIKE ? ESCAPE '\\\\' OR description ILIKE ? ESCAPE '\\\\')\")\n values.push(searchPattern, searchPattern)\n }\n }\n if (parsed.data.status && parsed.data.status.length) {\n // Non-empty input always expands to a non-empty set, so this always narrows.\n const expandedStatuses = expandDealStatusAliases(parsed.data.status)\n const placeholders = expandedStatuses.map(() => '?').join(',')\n where.push(`status IN (${placeholders})`)\n values.push(...expandedStatuses)\n }\n if (parsed.data.ownerUserId && parsed.data.ownerUserId.length) {\n const placeholders = parsed.data.ownerUserId.map(() => '?').join(',')\n where.push(`owner_user_id IN (${placeholders})`)\n values.push(...parsed.data.ownerUserId)\n }\n if (parsed.data.expectedCloseAtFrom) {\n where.push('expected_close_at >= ?')\n values.push(parsed.data.expectedCloseAtFrom)\n }\n if (parsed.data.expectedCloseAtTo) {\n where.push('expected_close_at <= ?')\n values.push(parsed.data.expectedCloseAtTo)\n }\n if (parsed.data.isOverdue) {\n // Mirror the list route's precedence: the caller-supplied status filter wins, and\n // status='open' is injected only when none was provided.\n const hasCallerStatus = !!parsed.data.status?.length\n if (hasCallerStatus) {\n where.push('expected_close_at < CURRENT_DATE')\n } else {\n where.push(\"expected_close_at < CURRENT_DATE AND status = 'open'\")\n }\n }\n if (parsed.data.isStuck) {\n // Reuse the list endpoint's stuck-deal lookup so kanban headers, lane counts, and the\n // cards rendered inside each lane agree on the same definition of \"stuck\". Empty result\n // \u2192 narrow to a sentinel UUID so the WHERE clause collapses to zero rows.\n const stuckIds = await fetchStuckDealIds(em as unknown as PgEntityManager, orgFilterIds[0], effectiveTenantId)\n restrictToIds(where, values, stuckIds)\n }\n if (parsed.data.personId && parsed.data.personId.length) {\n const placeholders = parsed.data.personId.map(() => '?').join(',')\n where.push(`EXISTS (SELECT 1 FROM customer_deal_people dp WHERE dp.deal_id = customer_deals.id AND dp.person_entity_id IN (${placeholders}))`)\n values.push(...parsed.data.personId)\n }\n if (parsed.data.companyId && parsed.data.companyId.length) {\n const placeholders = parsed.data.companyId.map(() => '?').join(',')\n where.push(`EXISTS (SELECT 1 FROM customer_deal_companies dc WHERE dc.deal_id = customer_deals.id AND dc.company_entity_id IN (${placeholders}))`)\n values.push(...parsed.data.companyId)\n }\n\n const whereSql = where.join(' AND ')\n\n // Aggregate by (stage, currency). Treat null stage as the literal `__unassigned`.\n const rows = await em.getConnection().execute<\n Array<{\n stage_id: string | null\n currency: string | null\n total: string | number | null\n count: string | number\n open_count: string | number\n }>\n >(\n `SELECT\n pipeline_stage_id AS stage_id,\n UPPER(COALESCE(value_currency, '')) AS currency,\n COALESCE(SUM(value_amount), 0) AS total,\n COUNT(*) AS count,\n COUNT(*) FILTER (WHERE status = 'open') AS open_count\n FROM customer_deals\n WHERE ${whereSql}\n GROUP BY pipeline_stage_id, UPPER(COALESCE(value_currency, ''))`,\n values,\n )\n\n // Reduce to per-stage aggregates\n const stageMap = new Map<string, StageAggregate>()\n for (const row of rows) {\n const stageId = row.stage_id ?? '__unassigned'\n const total = Number(row.total ?? 0)\n const count = Number(row.count ?? 0)\n const openCount = Number(row.open_count ?? 0)\n const currency = (row.currency ?? '').toString().trim()\n\n if (!stageMap.has(stageId)) {\n stageMap.set(stageId, {\n stageId,\n count: 0,\n openCount: 0,\n totalInBaseCurrency: 0,\n byCurrency: [],\n convertedAll: true,\n missingRateCurrencies: [],\n })\n }\n const agg = stageMap.get(stageId)!\n agg.count += count\n agg.openCount += openCount\n // Include any row that has a real currency code, even when its summed amount is\n // zero \u2014 `byCurrency` should agree with `count` so the per-currency breakdown\n // doesn't silently drop deals whose amount happens to be zero. Rows with no\n // currency code at all still get folded into the stage totals via `count`.\n if (currency.length > 0) {\n agg.byCurrency.push({ currency, total, count })\n }\n }\n\n // Convert to base currency where possible\n if (baseCurrencyCode) {\n const exchange = container.resolve('exchangeRateService') as ExchangeRateService | undefined\n const today = new Date()\n const distinctCurrencies = new Set<string>()\n for (const agg of stageMap.values()) {\n for (const row of agg.byCurrency) {\n if (row.currency && row.currency !== baseCurrencyCode) {\n distinctCurrencies.add(row.currency)\n }\n }\n }\n\n const rateCache = new Map<string, number>()\n if (exchange && distinctCurrencies.size > 0) {\n const pairs = Array.from(distinctCurrencies).map((c) => ({\n fromCurrencyCode: c,\n toCurrencyCode: baseCurrencyCode,\n }))\n try {\n const results = await exchange.getRates({\n pairs,\n date: today,\n scope: { tenantId: effectiveTenantId, organizationId: orgFilterIds[0] },\n options: { maxDaysBack: 60, autoFetch: false },\n })\n for (const [key, rateResult] of results) {\n if (rateResult.rates.length > 0) {\n // Pick the first matching rate (sources are equivalent for display purposes)\n const rate = Number(rateResult.rates[0].rate)\n if (Number.isFinite(rate) && rate > 0) {\n rateCache.set(key, rate)\n }\n }\n }\n } catch (err) {\n // Swallow \u2014 partial totals are still useful and we'll fall back to currency-native\n // sums. Logging at warn level so operators can correlate missing-rate disclosures in\n // the UI (`convertedAll: false` / `missingRateCurrencies`) with the underlying error.\n logger.warn('exchange-rate lookup failed; falling back to per-currency totals', { component: 'deals.aggregate', err })\n }\n }\n\n for (const agg of stageMap.values()) {\n let totalBase = 0\n let convertedAll = true\n const missingRateCurrencies: string[] = []\n for (const row of agg.byCurrency) {\n if (!row.currency) continue\n if (row.currency === baseCurrencyCode) {\n totalBase += row.total\n continue\n }\n const key = `${row.currency}/${baseCurrencyCode}`\n const rate = rateCache.get(key)\n if (rate !== undefined) {\n totalBase += row.total * rate\n } else {\n convertedAll = false\n if (!missingRateCurrencies.includes(row.currency)) {\n missingRateCurrencies.push(row.currency)\n }\n }\n }\n // Even when some rates are missing, totalInBaseCurrency reflects the converted slice.\n // `convertedAll`/`missingRateCurrencies` let the client distinguish a complete\n // conversion from a partial one and disclose which currencies were excluded.\n agg.totalInBaseCurrency = Math.round(totalBase)\n agg.convertedAll = convertedAll\n agg.missingRateCurrencies = missingRateCurrencies\n }\n } else {\n // No base currency configured for the tenant \u2014 surface this by marking every stage as\n // \"not converted\" with all currencies flagged as missing rates. This way the client UI\n // can fall back to a per-currency breakdown without trying to render a board total.\n for (const agg of stageMap.values()) {\n const present = agg.byCurrency.map((row) => row.currency).filter((c): c is string => !!c)\n if (present.length > 0) {\n agg.convertedAll = false\n agg.missingRateCurrencies = Array.from(new Set(present))\n }\n }\n }\n\n // Sort byCurrency rows by total descending for stable display\n for (const agg of stageMap.values()) {\n agg.byCurrency.sort((a, b) => b.total - a.total)\n }\n\n const response: AggregateResponse = {\n baseCurrencyCode,\n perStage: Array.from(stageMap.values()),\n }\n\n return NextResponse.json(response)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
6
+ import { readQueryParamList } from "@open-mercato/shared/lib/crud/query-params";
6
7
  import { SortDir } from "@open-mercato/shared/lib/query/types";
7
8
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
8
9
  import {
@@ -129,10 +130,8 @@ function toIsoStringOrNull(value) {
129
130
  return toStringOrNull(value);
130
131
  }
131
132
  function readArrayParam(searchParams, key) {
132
- const all = searchParams.getAll(key);
133
- if (!all.length) return null;
134
- const trimmed = all.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
135
- return trimmed.length ? trimmed : null;
133
+ const values = readQueryParamList(searchParams, key);
134
+ return values.length ? values : null;
136
135
  }
137
136
  function collectRefIds(rows, key) {
138
137
  const ids = /* @__PURE__ */ new Set();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/api/deals/map/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { SortDir, type QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerAddress,\n CustomerDealCompanyLink,\n CustomerDealPersonLink,\n} from '../../../data/entities'\nimport { buildDealListFilters, dealListQuerySchema } from '../route'\nimport { applyEntityIdRestriction, findMatchingEntityIdsBySearchTokensAcrossSources } from '../../utils'\nimport { createPagedListResponseSchema } from '../../openapi'\nimport {\n resolveDealLocations,\n type DealMapAddress,\n type DealMapLink,\n type DealMapLocation,\n} from '../../../lib/dealsMapLocation'\nimport { E } from '#generated/entities.ids.generated'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view', 'customers.activities.view'] },\n}\n\nconst mapSortFields = ['createdAt', 'updatedAt', 'title', 'value', 'probability', 'expectedCloseAt'] as const\n\nconst sortFieldMap: Record<(typeof mapSortFields)[number], string> = {\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n title: 'title',\n value: 'value_amount',\n probability: 'probability',\n expectedCloseAt: 'expected_close_at',\n}\n\nconst querySchema = dealListQuerySchema.extend({\n pageSize: z.coerce.number().min(1).max(100).default(100),\n sortField: z.enum(mapSortFields).optional(),\n})\n\nconst dealMapAssociationSchema = z.object({\n id: z.string().uuid(),\n label: z.string().nullable(),\n})\n\nconst dealMapLocationSchema = z.object({\n latitude: z.number(),\n longitude: z.number(),\n city: z.string().nullable(),\n region: z.string().nullable(),\n country: z.string().nullable(),\n source: z.enum(['company', 'person']),\n entityId: z.string().uuid(),\n addressId: z.string().uuid(),\n})\n\nconst dealMapItemSchema = z.object({\n id: z.string().uuid(),\n title: z.string().nullable(),\n status: z.string().nullable(),\n pipelineId: z.string().uuid().nullable(),\n pipelineStageId: z.string().uuid().nullable(),\n pipelineStage: z.string().nullable(),\n valueAmount: z.number().nullable(),\n valueCurrency: z.string().nullable(),\n probability: z.number().nullable(),\n expectedCloseAt: z.string().nullable(),\n ownerUserId: z.string().uuid().nullable(),\n updatedAt: z.string().nullable(),\n companies: z.array(dealMapAssociationSchema),\n people: z.array(dealMapAssociationSchema),\n location: dealMapLocationSchema.nullable(),\n})\n\nconst mapErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Deals map listing with resolved locations',\n methods: {\n GET: {\n summary: 'Paginated deals that have a resolvable map location',\n description:\n 'Returns a page of deals that have a coordinate-bearing linked company/person address, each enriched with one resolved location (company primary first, then earliest created; person addresses as fallback). Deals with no coordinate-bearing address are excluded entirely, so every item carries a non-null location in normal operation; the schema keeps location nullable only for the rare case where the address is deleted between the located-deal resolution and the page fetch.',\n query: querySchema,\n responses: [\n { status: 200, description: 'Paged located deals', schema: createPagedListResponseSchema(dealMapItemSchema) },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: mapErrorSchema },\n { status: 401, description: 'Unauthorized', schema: mapErrorSchema },\n { status: 403, description: 'Missing required features', schema: mapErrorSchema },\n ],\n },\n },\n}\n\ntype DealMapAssociation = z.infer<typeof dealMapAssociationSchema>\n\ntype LinkRow = {\n dealId: string\n entityId: string\n label: string\n createdAt: Date | string | null\n}\n\nfunction readEntityRef(ref: unknown): { id: string | null; record: Record<string, unknown> | null } {\n if (typeof ref === 'string') return { id: ref, record: null }\n if (ref && typeof ref === 'object') {\n const record = ref as Record<string, unknown>\n return { id: typeof record.id === 'string' ? record.id : null, record }\n }\n return { id: null, record: null }\n}\n\nfunction readLinkRow(link: { deal: unknown; createdAt?: Date | string | null }, linkedRef: unknown): LinkRow | null {\n const { id: dealId } = readEntityRef(link.deal)\n if (!dealId) return null\n const { id: entityId, record } = readEntityRef(linkedRef)\n if (!entityId) return null\n const label = record && typeof record.displayName === 'string' ? record.displayName : ''\n return { dealId, entityId, label, createdAt: link.createdAt ?? null }\n}\n\nfunction groupAssociations(rows: LinkRow[]): Map<string, DealMapAssociation[]> {\n const byDeal = new Map<string, DealMapAssociation[]>()\n for (const row of rows) {\n const bucket = byDeal.get(row.dealId) ?? []\n if (!bucket.some((entry) => entry.id === row.entityId)) {\n bucket.push({ id: row.entityId, label: row.label })\n byDeal.set(row.dealId, bucket)\n }\n }\n return byDeal\n}\n\nfunction toStringOrNull(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction toFiniteNumberOrNull(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\nfunction toIsoStringOrNull(value: unknown): string | null {\n if (value instanceof Date) return value.toISOString()\n return toStringOrNull(value)\n}\n\nfunction readArrayParam(searchParams: URLSearchParams, key: string): string[] | null {\n const all = searchParams.getAll(key)\n if (!all.length) return null\n const trimmed = all.flatMap((value) => value.split(',')).map((value) => value.trim()).filter(Boolean)\n return trimmed.length ? trimmed : null\n}\n\nfunction collectRefIds(rows: unknown[], key: string): string[] {\n const ids = new Set<string>()\n for (const row of rows) {\n if (!row || typeof row !== 'object') continue\n const { id } = readEntityRef((row as Record<string, unknown>)[key])\n if (id) ids.add(id)\n }\n return Array.from(ids)\n}\n\n// The deal-list filter builder may already restrict `id` (search, association, stuck, or advanced\n// filters). `applyEntityIdRestriction` overwrites an existing `$in`, so intersect the located-deal\n// set with whatever allow-list is already present before re-applying it.\nfunction readFilterIdAllowlist(filters: Record<string, unknown>): string[] | null {\n const idFilter = filters.id\n if (!idFilter || typeof idFilter !== 'object' || Array.isArray(idFilter)) return null\n const record = idFilter as { $eq?: unknown; $in?: unknown }\n if (typeof record.$eq === 'string') return [record.$eq]\n if (Array.isArray(record.$in)) return record.$in.filter((value): value is string => typeof value === 'string')\n return null\n}\n\nfunction emptyMapResponse(page: number, pageSize: number) {\n return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const params = url.searchParams\n const parsed = querySchema.safeParse({\n page: params.get('page') ?? undefined,\n pageSize: params.get('pageSize') ?? undefined,\n search: params.get('search') ?? undefined,\n status: readArrayParam(params, 'status') ?? undefined,\n pipelineId: readArrayParam(params, 'pipelineId') ?? undefined,\n pipelineStageId: params.get('pipelineStageId') ?? undefined,\n ownerUserId: readArrayParam(params, 'ownerUserId') ?? undefined,\n personId: readArrayParam(params, 'personId') ?? undefined,\n companyId: readArrayParam(params, 'companyId') ?? undefined,\n valueCurrency: readArrayParam(params, 'valueCurrency') ?? undefined,\n expectedCloseAtFrom: params.get('expectedCloseAtFrom') ?? undefined,\n expectedCloseAtTo: params.get('expectedCloseAtTo') ?? undefined,\n isStuck: params.get('isStuck') ?? undefined,\n isOverdue: params.get('isOverdue') ?? undefined,\n sortField: params.get('sortField') ?? undefined,\n sortDir: params.get('sortDir') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query parameters' }, { status: 400 })\n }\n const query = parsed.data\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const queryEngine = container.resolve<QueryEngine>('queryEngine')\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const effectiveTenantId = scope.tenantId ?? auth.tenantId\n // `null` = unrestricted access (superadmin or an \"all organizations\" grant): aggregate tenant-wide,\n // exactly like the deals List route (`makeCrudRoute`) and the query_index status route. A populated\n // array bounds the query to the caller's visible orgs; an empty array means no org visibility \u2192 401.\n // The previous guard hard-required `auth.orgId`, which is empty under the header \"All organizations\"\n // scope, so the map 401'd (and hung on the loading spinner) while List/Kanban aggregated fine (#3481).\n const orgScopeIds: string[] | null =\n scope.filterIds === null\n ? null\n : Array.isArray(scope.filterIds) && scope.filterIds.length > 0\n ? Array.from(new Set(scope.filterIds.filter((id) => typeof id === 'string' && id.length > 0)))\n : auth.orgId\n ? [auth.orgId]\n : []\n if (!effectiveTenantId || (Array.isArray(orgScopeIds) && orgScopeIds.length === 0)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n // Bounded `$in` when the caller is org-restricted; omitted entirely under unrestricted access so\n // every query stays tenant-scoped only. `fallbackOrgId` is undefined when unrestricted.\n const orgScopeFilter = orgScopeIds ? { organizationId: { $in: orgScopeIds } } : {}\n const fallbackOrgId = orgScopeIds && orgScopeIds.length > 0 ? orgScopeIds[0] : undefined\n\n // `findWithDecryption` decrypts each row with the row's OWN tenant/organization (the encryption\n // subscriber's resolveScope(target) wins); this scope is only a fallback for rows that carry no\n // scope columns. CustomerAddress always carries organization_id, so every org's rows still decrypt\n // correctly under a multi-org / unrestricted scope even though `fallbackOrgId` may be undefined.\n const decryptionScope = { tenantId: effectiveTenantId, organizationId: fallbackOrgId }\n\n // Located-only, resolved in two stages so the per-request cost stays page-bounded:\n // 1) LIGHT id-only queries (FK columns only, no decryption) determine which deals have a\n // coordinate-bearing linked company/person, so pagination + `total` operate on located deals.\n // 2) The HEAVY decrypted/populated fetch (labels + address coordinates) runs only for the deals\n // that actually land on the requested page \u2014 never the whole located universe.\n const coordinateEntityRows = await findWithDecryption(\n em,\n CustomerAddress,\n {\n latitude: { $ne: null },\n longitude: { $ne: null },\n tenantId: effectiveTenantId,\n ...orgScopeFilter,\n },\n { fields: ['entity'] },\n decryptionScope,\n )\n const locatedEntityIds = collectRefIds(coordinateEntityRows, 'entity')\n if (locatedEntityIds.length === 0) {\n return emptyMapResponse(query.page, query.pageSize)\n }\n\n const [locatedCompanyLinkRows, locatedPersonLinkRows] = await Promise.all([\n findWithDecryption(\n em,\n CustomerDealCompanyLink,\n { company: { $in: locatedEntityIds } },\n { fields: ['deal'] },\n decryptionScope,\n ),\n findWithDecryption(\n em,\n CustomerDealPersonLink,\n { person: { $in: locatedEntityIds } },\n { fields: ['deal'] },\n decryptionScope,\n ),\n ])\n const locatedDealIds = Array.from(\n new Set([\n ...collectRefIds(locatedCompanyLinkRows, 'deal'),\n ...collectRefIds(locatedPersonLinkRows, 'deal'),\n ]),\n )\n if (locatedDealIds.length === 0) {\n return emptyMapResponse(query.page, query.pageSize)\n }\n\n const ctx: CrudCtx = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: fallbackOrgId ?? null,\n organizationIds: orgScopeIds,\n request: req,\n }\n\n const filters = await buildDealListFilters(query, ctx)\n const existingAllowlist = readFilterIdAllowlist(filters)\n\n // The map card headline is the linked company/person name, which the deal-field token search\n // (title/description/status/\u2026) used by `buildDealListFilters` does NOT cover \u2014 so searching a\n // company/person name a user can plainly see on a card would otherwise return nothing. When a\n // search term is present, also resolve located deals whose linked company/person ENTITY name\n // matches, and union them with the deal-field matches. The sentinel \"no match\" id from the\n // deal-field search drops out naturally at the located intersection below, so no special-casing.\n let searchAllowlist = existingAllowlist\n if (query.search && locatedEntityIds.length > 0) {\n const nameMatchedEntityIds = await findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query: query.search,\n sources: [\n {\n entityType: E.customers.customer_entity,\n fields: ['display_name', 'primary_email', 'primary_phone', 'description'],\n },\n ],\n })\n const locatedEntitySet = new Set(locatedEntityIds)\n const locatedMatchedEntityIds = (nameMatchedEntityIds ?? []).filter((id) => locatedEntitySet.has(id))\n if (locatedMatchedEntityIds.length > 0) {\n const [companyNameLinkRows, personNameLinkRows] = await Promise.all([\n findWithDecryption(\n em,\n CustomerDealCompanyLink,\n { company: { $in: locatedMatchedEntityIds } },\n { fields: ['deal'] },\n decryptionScope,\n ),\n findWithDecryption(\n em,\n CustomerDealPersonLink,\n { person: { $in: locatedMatchedEntityIds } },\n { fields: ['deal'] },\n decryptionScope,\n ),\n ])\n let nameMatchedDealIds = [\n ...collectRefIds(companyNameLinkRows, 'deal'),\n ...collectRefIds(personNameLinkRows, 'deal'),\n ]\n if (nameMatchedDealIds.length > 0) {\n // The deal-field search in `buildDealListFilters` is intersected with the People/Companies\n // association (and stuck/`?id`) filters \u2014 they all land together in `filters.id`. The name\n // matches above carry NO such constraint, so unioning them raw would let a co-active\n // People/Companies filter be bypassed (over-broad results). Recover exactly the non-search id\n // allowlist by rebuilding the deal filters WITHOUT the search term, and intersect the name\n // matches with it so the active filter is honored: located \u2229 nonSearch \u2229 (dealField \u222A name).\n // (The map's query carries no advanced `filter[...]` params, so the consume-mutation in\n // buildDealListFilters does not affect this second build.)\n const nonSearchAllowlist = readFilterIdAllowlist(\n await buildDealListFilters({ ...query, search: undefined }, ctx),\n )\n if (nonSearchAllowlist) {\n const nonSearchLookup = new Set(nonSearchAllowlist)\n nameMatchedDealIds = nameMatchedDealIds.filter((id) => nonSearchLookup.has(id))\n }\n }\n if (nameMatchedDealIds.length > 0) {\n searchAllowlist = Array.from(new Set([...(existingAllowlist ?? []), ...nameMatchedDealIds]))\n }\n }\n }\n\n const allowlistLookup = searchAllowlist ? new Set(searchAllowlist) : null\n const restrictedDealIds = allowlistLookup\n ? locatedDealIds.filter((id) => allowlistLookup.has(id))\n : locatedDealIds\n // `restrictedDealIds` is the final located \u2229 (deal-field \u222A company/person-name) set \u2014 it already\n // captures every id constraint `buildDealListFilters` expressed (read back via\n // `readFilterIdAllowlist`). Clear the stale `filters.id` first so `applyEntityIdRestriction` does\n // not re-intersect against the deal-field \"no match\" sentinel and drop the name-matched deals.\n delete (filters as Record<string, unknown>).id\n applyEntityIdRestriction(filters, restrictedDealIds)\n\n const sortColumn = query.sortField ? sortFieldMap[query.sortField] : 'id'\n const sortDir = query.sortDir === 'desc' ? SortDir.Desc : SortDir.Asc\n // Always append `id` as a stable tiebreaker \u2014 non-unique sort columns (title, value,\n // probability, stage, or colliding timestamps) otherwise reorder between OFFSET pages and the\n // client's multi-page accumulation would skip or duplicate deals at page boundaries.\n const sort = sortColumn === 'id'\n ? [{ field: 'id', dir: sortDir }]\n : [{ field: sortColumn, dir: sortDir }, { field: 'id', dir: SortDir.Asc }]\n\n const res = await queryEngine.query(E.customers.customer_deal, {\n fields: [\n 'id',\n // `organization_id` is projected (never exposed in the response) so the query engine can decrypt\n // each row's encrypted fields (e.g. `title`) with the row's OWN org. Without it, an unrestricted\n // \"All organizations\" scope has no single fallback org to decrypt with and titles come back as\n // ciphertext \u2014 the List route projects it for the same reason (#3481).\n 'organization_id',\n 'title',\n 'status',\n 'pipeline_id',\n 'pipeline_stage_id',\n 'pipeline_stage',\n 'value_amount',\n 'value_currency',\n 'probability',\n 'expected_close_at',\n 'owner_user_id',\n 'updated_at',\n ],\n sort,\n page: { page: query.page, pageSize: query.pageSize },\n filters,\n tenantId: effectiveTenantId,\n organizationId: fallbackOrgId,\n organizationIds: orgScopeIds ?? undefined,\n })\n\n const rows = (Array.isArray(res.items) ? res.items : []).filter(\n (row): row is Record<string, unknown> => !!row && typeof row === 'object',\n )\n const dealIds = rows\n .map((row) => (typeof row.id === 'string' && row.id.trim().length ? row.id : null))\n .filter((value): value is string => value !== null)\n\n // Heavy fetch, page-bounded: pull decrypted links (for labels) and coordinate-bearing addresses\n // only for the deals on this page.\n const [companyLinks, personLinks] = dealIds.length\n ? await Promise.all([\n findWithDecryption(\n em,\n CustomerDealCompanyLink,\n { deal: { $in: dealIds } },\n { populate: ['company'] },\n decryptionScope,\n ),\n findWithDecryption(\n em,\n CustomerDealPersonLink,\n { deal: { $in: dealIds } },\n { populate: ['person'] },\n decryptionScope,\n ),\n ])\n : [[], []]\n\n const companyRows = companyLinks\n .map((link) => readLinkRow(link, link.company))\n .filter((row): row is LinkRow => row !== null)\n const personRows = personLinks\n .map((link) => readLinkRow(link, link.person))\n .filter((row): row is LinkRow => row !== null)\n const companiesByDeal = groupAssociations(companyRows)\n const peopleByDeal = groupAssociations(personRows)\n\n const linkedEntityIds = Array.from(\n new Set([...companyRows, ...personRows].map((row) => row.entityId)),\n )\n const addresses = linkedEntityIds.length\n ? await findWithDecryption(\n em,\n CustomerAddress,\n {\n entity: { $in: linkedEntityIds },\n latitude: { $ne: null },\n longitude: { $ne: null },\n tenantId: effectiveTenantId,\n ...orgScopeFilter,\n },\n {},\n decryptionScope,\n )\n : []\n\n const addressRows: DealMapAddress[] = []\n for (const address of addresses) {\n const { id: entityId } = readEntityRef(address.entity)\n if (!entityId) continue\n addressRows.push({\n id: address.id,\n entityId,\n isPrimary: address.isPrimary === true,\n latitude: address.latitude ?? null,\n longitude: address.longitude ?? null,\n city: address.city ?? null,\n region: address.region ?? null,\n country: address.country ?? null,\n createdAt: address.createdAt ?? null,\n })\n }\n\n const toLink = (row: LinkRow): DealMapLink => ({\n dealId: row.dealId,\n entityId: row.entityId,\n createdAt: row.createdAt,\n })\n const locationByDeal: Map<string, DealMapLocation | null> = resolveDealLocations(\n dealIds,\n companyRows.map(toLink),\n personRows.map(toLink),\n addressRows,\n )\n\n const items = rows\n .map((row) => {\n const id = typeof row.id === 'string' && row.id.trim().length ? row.id : null\n if (!id) return null\n return {\n id,\n title: toStringOrNull(row.title),\n status: toStringOrNull(row.status),\n pipelineId: toStringOrNull(row.pipeline_id),\n pipelineStageId: toStringOrNull(row.pipeline_stage_id),\n pipelineStage: toStringOrNull(row.pipeline_stage),\n valueAmount: toFiniteNumberOrNull(row.value_amount),\n valueCurrency: toStringOrNull(row.value_currency),\n probability: toFiniteNumberOrNull(row.probability),\n expectedCloseAt: toIsoStringOrNull(row.expected_close_at),\n ownerUserId: toStringOrNull(row.owner_user_id),\n updatedAt: toIsoStringOrNull(row.updated_at),\n companies: companiesByDeal.get(id) ?? [],\n people: peopleByDeal.get(id) ?? [],\n location: locationByDeal.get(id) ?? null,\n }\n })\n .filter((item): item is NonNullable<typeof item> => item !== null)\n\n return NextResponse.json({\n items,\n total: res.total,\n page: query.page,\n pageSize: query.pageSize,\n totalPages: Math.ceil(res.total / (query.pageSize || 1)),\n ...(res.meta?.listCountCapWarning ? { totalIsCapped: true } : {}),\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { readQueryParamList } from '@open-mercato/shared/lib/crud/query-params'\nimport { SortDir, type QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerAddress,\n CustomerDealCompanyLink,\n CustomerDealPersonLink,\n} from '../../../data/entities'\nimport { buildDealListFilters, dealListQuerySchema } from '../route'\nimport { applyEntityIdRestriction, findMatchingEntityIdsBySearchTokensAcrossSources } from '../../utils'\nimport { createPagedListResponseSchema } from '../../openapi'\nimport {\n resolveDealLocations,\n type DealMapAddress,\n type DealMapLink,\n type DealMapLocation,\n} from '../../../lib/dealsMapLocation'\nimport { E } from '#generated/entities.ids.generated'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view', 'customers.activities.view'] },\n}\n\nconst mapSortFields = ['createdAt', 'updatedAt', 'title', 'value', 'probability', 'expectedCloseAt'] as const\n\nconst sortFieldMap: Record<(typeof mapSortFields)[number], string> = {\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n title: 'title',\n value: 'value_amount',\n probability: 'probability',\n expectedCloseAt: 'expected_close_at',\n}\n\nconst querySchema = dealListQuerySchema.extend({\n pageSize: z.coerce.number().min(1).max(100).default(100),\n sortField: z.enum(mapSortFields).optional(),\n})\n\nconst dealMapAssociationSchema = z.object({\n id: z.string().uuid(),\n label: z.string().nullable(),\n})\n\nconst dealMapLocationSchema = z.object({\n latitude: z.number(),\n longitude: z.number(),\n city: z.string().nullable(),\n region: z.string().nullable(),\n country: z.string().nullable(),\n source: z.enum(['company', 'person']),\n entityId: z.string().uuid(),\n addressId: z.string().uuid(),\n})\n\nconst dealMapItemSchema = z.object({\n id: z.string().uuid(),\n title: z.string().nullable(),\n status: z.string().nullable(),\n pipelineId: z.string().uuid().nullable(),\n pipelineStageId: z.string().uuid().nullable(),\n pipelineStage: z.string().nullable(),\n valueAmount: z.number().nullable(),\n valueCurrency: z.string().nullable(),\n probability: z.number().nullable(),\n expectedCloseAt: z.string().nullable(),\n ownerUserId: z.string().uuid().nullable(),\n updatedAt: z.string().nullable(),\n companies: z.array(dealMapAssociationSchema),\n people: z.array(dealMapAssociationSchema),\n location: dealMapLocationSchema.nullable(),\n})\n\nconst mapErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Deals map listing with resolved locations',\n methods: {\n GET: {\n summary: 'Paginated deals that have a resolvable map location',\n description:\n 'Returns a page of deals that have a coordinate-bearing linked company/person address, each enriched with one resolved location (company primary first, then earliest created; person addresses as fallback). Deals with no coordinate-bearing address are excluded entirely, so every item carries a non-null location in normal operation; the schema keeps location nullable only for the rare case where the address is deleted between the located-deal resolution and the page fetch.',\n query: querySchema,\n responses: [\n { status: 200, description: 'Paged located deals', schema: createPagedListResponseSchema(dealMapItemSchema) },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: mapErrorSchema },\n { status: 401, description: 'Unauthorized', schema: mapErrorSchema },\n { status: 403, description: 'Missing required features', schema: mapErrorSchema },\n ],\n },\n },\n}\n\ntype DealMapAssociation = z.infer<typeof dealMapAssociationSchema>\n\ntype LinkRow = {\n dealId: string\n entityId: string\n label: string\n createdAt: Date | string | null\n}\n\nfunction readEntityRef(ref: unknown): { id: string | null; record: Record<string, unknown> | null } {\n if (typeof ref === 'string') return { id: ref, record: null }\n if (ref && typeof ref === 'object') {\n const record = ref as Record<string, unknown>\n return { id: typeof record.id === 'string' ? record.id : null, record }\n }\n return { id: null, record: null }\n}\n\nfunction readLinkRow(link: { deal: unknown; createdAt?: Date | string | null }, linkedRef: unknown): LinkRow | null {\n const { id: dealId } = readEntityRef(link.deal)\n if (!dealId) return null\n const { id: entityId, record } = readEntityRef(linkedRef)\n if (!entityId) return null\n const label = record && typeof record.displayName === 'string' ? record.displayName : ''\n return { dealId, entityId, label, createdAt: link.createdAt ?? null }\n}\n\nfunction groupAssociations(rows: LinkRow[]): Map<string, DealMapAssociation[]> {\n const byDeal = new Map<string, DealMapAssociation[]>()\n for (const row of rows) {\n const bucket = byDeal.get(row.dealId) ?? []\n if (!bucket.some((entry) => entry.id === row.entityId)) {\n bucket.push({ id: row.entityId, label: row.label })\n byDeal.set(row.dealId, bucket)\n }\n }\n return byDeal\n}\n\nfunction toStringOrNull(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction toFiniteNumberOrNull(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\nfunction toIsoStringOrNull(value: unknown): string | null {\n if (value instanceof Date) return value.toISOString()\n return toStringOrNull(value)\n}\n\nfunction readArrayParam(searchParams: URLSearchParams, key: string): string[] | null {\n const values = readQueryParamList(searchParams, key)\n return values.length ? values : null\n}\n\nfunction collectRefIds(rows: unknown[], key: string): string[] {\n const ids = new Set<string>()\n for (const row of rows) {\n if (!row || typeof row !== 'object') continue\n const { id } = readEntityRef((row as Record<string, unknown>)[key])\n if (id) ids.add(id)\n }\n return Array.from(ids)\n}\n\n// The deal-list filter builder may already restrict `id` (search, association, stuck, or advanced\n// filters). `applyEntityIdRestriction` overwrites an existing `$in`, so intersect the located-deal\n// set with whatever allow-list is already present before re-applying it.\nfunction readFilterIdAllowlist(filters: Record<string, unknown>): string[] | null {\n const idFilter = filters.id\n if (!idFilter || typeof idFilter !== 'object' || Array.isArray(idFilter)) return null\n const record = idFilter as { $eq?: unknown; $in?: unknown }\n if (typeof record.$eq === 'string') return [record.$eq]\n if (Array.isArray(record.$in)) return record.$in.filter((value): value is string => typeof value === 'string')\n return null\n}\n\nfunction emptyMapResponse(page: number, pageSize: number) {\n return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const params = url.searchParams\n const parsed = querySchema.safeParse({\n page: params.get('page') ?? undefined,\n pageSize: params.get('pageSize') ?? undefined,\n search: params.get('search') ?? undefined,\n status: readArrayParam(params, 'status') ?? undefined,\n pipelineId: readArrayParam(params, 'pipelineId') ?? undefined,\n pipelineStageId: params.get('pipelineStageId') ?? undefined,\n ownerUserId: readArrayParam(params, 'ownerUserId') ?? undefined,\n personId: readArrayParam(params, 'personId') ?? undefined,\n companyId: readArrayParam(params, 'companyId') ?? undefined,\n valueCurrency: readArrayParam(params, 'valueCurrency') ?? undefined,\n expectedCloseAtFrom: params.get('expectedCloseAtFrom') ?? undefined,\n expectedCloseAtTo: params.get('expectedCloseAtTo') ?? undefined,\n isStuck: params.get('isStuck') ?? undefined,\n isOverdue: params.get('isOverdue') ?? undefined,\n sortField: params.get('sortField') ?? undefined,\n sortDir: params.get('sortDir') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query parameters' }, { status: 400 })\n }\n const query = parsed.data\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const queryEngine = container.resolve<QueryEngine>('queryEngine')\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const effectiveTenantId = scope.tenantId ?? auth.tenantId\n // `null` = unrestricted access (superadmin or an \"all organizations\" grant): aggregate tenant-wide,\n // exactly like the deals List route (`makeCrudRoute`) and the query_index status route. A populated\n // array bounds the query to the caller's visible orgs; an empty array means no org visibility \u2192 401.\n // The previous guard hard-required `auth.orgId`, which is empty under the header \"All organizations\"\n // scope, so the map 401'd (and hung on the loading spinner) while List/Kanban aggregated fine (#3481).\n const orgScopeIds: string[] | null =\n scope.filterIds === null\n ? null\n : Array.isArray(scope.filterIds) && scope.filterIds.length > 0\n ? Array.from(new Set(scope.filterIds.filter((id) => typeof id === 'string' && id.length > 0)))\n : auth.orgId\n ? [auth.orgId]\n : []\n if (!effectiveTenantId || (Array.isArray(orgScopeIds) && orgScopeIds.length === 0)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n // Bounded `$in` when the caller is org-restricted; omitted entirely under unrestricted access so\n // every query stays tenant-scoped only. `fallbackOrgId` is undefined when unrestricted.\n const orgScopeFilter = orgScopeIds ? { organizationId: { $in: orgScopeIds } } : {}\n const fallbackOrgId = orgScopeIds && orgScopeIds.length > 0 ? orgScopeIds[0] : undefined\n\n // `findWithDecryption` decrypts each row with the row's OWN tenant/organization (the encryption\n // subscriber's resolveScope(target) wins); this scope is only a fallback for rows that carry no\n // scope columns. CustomerAddress always carries organization_id, so every org's rows still decrypt\n // correctly under a multi-org / unrestricted scope even though `fallbackOrgId` may be undefined.\n const decryptionScope = { tenantId: effectiveTenantId, organizationId: fallbackOrgId }\n\n // Located-only, resolved in two stages so the per-request cost stays page-bounded:\n // 1) LIGHT id-only queries (FK columns only, no decryption) determine which deals have a\n // coordinate-bearing linked company/person, so pagination + `total` operate on located deals.\n // 2) The HEAVY decrypted/populated fetch (labels + address coordinates) runs only for the deals\n // that actually land on the requested page \u2014 never the whole located universe.\n const coordinateEntityRows = await findWithDecryption(\n em,\n CustomerAddress,\n {\n latitude: { $ne: null },\n longitude: { $ne: null },\n tenantId: effectiveTenantId,\n ...orgScopeFilter,\n },\n { fields: ['entity'] },\n decryptionScope,\n )\n const locatedEntityIds = collectRefIds(coordinateEntityRows, 'entity')\n if (locatedEntityIds.length === 0) {\n return emptyMapResponse(query.page, query.pageSize)\n }\n\n const [locatedCompanyLinkRows, locatedPersonLinkRows] = await Promise.all([\n findWithDecryption(\n em,\n CustomerDealCompanyLink,\n { company: { $in: locatedEntityIds } },\n { fields: ['deal'] },\n decryptionScope,\n ),\n findWithDecryption(\n em,\n CustomerDealPersonLink,\n { person: { $in: locatedEntityIds } },\n { fields: ['deal'] },\n decryptionScope,\n ),\n ])\n const locatedDealIds = Array.from(\n new Set([\n ...collectRefIds(locatedCompanyLinkRows, 'deal'),\n ...collectRefIds(locatedPersonLinkRows, 'deal'),\n ]),\n )\n if (locatedDealIds.length === 0) {\n return emptyMapResponse(query.page, query.pageSize)\n }\n\n const ctx: CrudCtx = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: fallbackOrgId ?? null,\n organizationIds: orgScopeIds,\n request: req,\n }\n\n const filters = await buildDealListFilters(query, ctx)\n const existingAllowlist = readFilterIdAllowlist(filters)\n\n // The map card headline is the linked company/person name, which the deal-field token search\n // (title/description/status/\u2026) used by `buildDealListFilters` does NOT cover \u2014 so searching a\n // company/person name a user can plainly see on a card would otherwise return nothing. When a\n // search term is present, also resolve located deals whose linked company/person ENTITY name\n // matches, and union them with the deal-field matches. The sentinel \"no match\" id from the\n // deal-field search drops out naturally at the located intersection below, so no special-casing.\n let searchAllowlist = existingAllowlist\n if (query.search && locatedEntityIds.length > 0) {\n const nameMatchedEntityIds = await findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query: query.search,\n sources: [\n {\n entityType: E.customers.customer_entity,\n fields: ['display_name', 'primary_email', 'primary_phone', 'description'],\n },\n ],\n })\n const locatedEntitySet = new Set(locatedEntityIds)\n const locatedMatchedEntityIds = (nameMatchedEntityIds ?? []).filter((id) => locatedEntitySet.has(id))\n if (locatedMatchedEntityIds.length > 0) {\n const [companyNameLinkRows, personNameLinkRows] = await Promise.all([\n findWithDecryption(\n em,\n CustomerDealCompanyLink,\n { company: { $in: locatedMatchedEntityIds } },\n { fields: ['deal'] },\n decryptionScope,\n ),\n findWithDecryption(\n em,\n CustomerDealPersonLink,\n { person: { $in: locatedMatchedEntityIds } },\n { fields: ['deal'] },\n decryptionScope,\n ),\n ])\n let nameMatchedDealIds = [\n ...collectRefIds(companyNameLinkRows, 'deal'),\n ...collectRefIds(personNameLinkRows, 'deal'),\n ]\n if (nameMatchedDealIds.length > 0) {\n // The deal-field search in `buildDealListFilters` is intersected with the People/Companies\n // association (and stuck/`?id`) filters \u2014 they all land together in `filters.id`. The name\n // matches above carry NO such constraint, so unioning them raw would let a co-active\n // People/Companies filter be bypassed (over-broad results). Recover exactly the non-search id\n // allowlist by rebuilding the deal filters WITHOUT the search term, and intersect the name\n // matches with it so the active filter is honored: located \u2229 nonSearch \u2229 (dealField \u222A name).\n // (The map's query carries no advanced `filter[...]` params, so the consume-mutation in\n // buildDealListFilters does not affect this second build.)\n const nonSearchAllowlist = readFilterIdAllowlist(\n await buildDealListFilters({ ...query, search: undefined }, ctx),\n )\n if (nonSearchAllowlist) {\n const nonSearchLookup = new Set(nonSearchAllowlist)\n nameMatchedDealIds = nameMatchedDealIds.filter((id) => nonSearchLookup.has(id))\n }\n }\n if (nameMatchedDealIds.length > 0) {\n searchAllowlist = Array.from(new Set([...(existingAllowlist ?? []), ...nameMatchedDealIds]))\n }\n }\n }\n\n const allowlistLookup = searchAllowlist ? new Set(searchAllowlist) : null\n const restrictedDealIds = allowlistLookup\n ? locatedDealIds.filter((id) => allowlistLookup.has(id))\n : locatedDealIds\n // `restrictedDealIds` is the final located \u2229 (deal-field \u222A company/person-name) set \u2014 it already\n // captures every id constraint `buildDealListFilters` expressed (read back via\n // `readFilterIdAllowlist`). Clear the stale `filters.id` first so `applyEntityIdRestriction` does\n // not re-intersect against the deal-field \"no match\" sentinel and drop the name-matched deals.\n delete (filters as Record<string, unknown>).id\n applyEntityIdRestriction(filters, restrictedDealIds)\n\n const sortColumn = query.sortField ? sortFieldMap[query.sortField] : 'id'\n const sortDir = query.sortDir === 'desc' ? SortDir.Desc : SortDir.Asc\n // Always append `id` as a stable tiebreaker \u2014 non-unique sort columns (title, value,\n // probability, stage, or colliding timestamps) otherwise reorder between OFFSET pages and the\n // client's multi-page accumulation would skip or duplicate deals at page boundaries.\n const sort = sortColumn === 'id'\n ? [{ field: 'id', dir: sortDir }]\n : [{ field: sortColumn, dir: sortDir }, { field: 'id', dir: SortDir.Asc }]\n\n const res = await queryEngine.query(E.customers.customer_deal, {\n fields: [\n 'id',\n // `organization_id` is projected (never exposed in the response) so the query engine can decrypt\n // each row's encrypted fields (e.g. `title`) with the row's OWN org. Without it, an unrestricted\n // \"All organizations\" scope has no single fallback org to decrypt with and titles come back as\n // ciphertext \u2014 the List route projects it for the same reason (#3481).\n 'organization_id',\n 'title',\n 'status',\n 'pipeline_id',\n 'pipeline_stage_id',\n 'pipeline_stage',\n 'value_amount',\n 'value_currency',\n 'probability',\n 'expected_close_at',\n 'owner_user_id',\n 'updated_at',\n ],\n sort,\n page: { page: query.page, pageSize: query.pageSize },\n filters,\n tenantId: effectiveTenantId,\n organizationId: fallbackOrgId,\n organizationIds: orgScopeIds ?? undefined,\n })\n\n const rows = (Array.isArray(res.items) ? res.items : []).filter(\n (row): row is Record<string, unknown> => !!row && typeof row === 'object',\n )\n const dealIds = rows\n .map((row) => (typeof row.id === 'string' && row.id.trim().length ? row.id : null))\n .filter((value): value is string => value !== null)\n\n // Heavy fetch, page-bounded: pull decrypted links (for labels) and coordinate-bearing addresses\n // only for the deals on this page.\n const [companyLinks, personLinks] = dealIds.length\n ? await Promise.all([\n findWithDecryption(\n em,\n CustomerDealCompanyLink,\n { deal: { $in: dealIds } },\n { populate: ['company'] },\n decryptionScope,\n ),\n findWithDecryption(\n em,\n CustomerDealPersonLink,\n { deal: { $in: dealIds } },\n { populate: ['person'] },\n decryptionScope,\n ),\n ])\n : [[], []]\n\n const companyRows = companyLinks\n .map((link) => readLinkRow(link, link.company))\n .filter((row): row is LinkRow => row !== null)\n const personRows = personLinks\n .map((link) => readLinkRow(link, link.person))\n .filter((row): row is LinkRow => row !== null)\n const companiesByDeal = groupAssociations(companyRows)\n const peopleByDeal = groupAssociations(personRows)\n\n const linkedEntityIds = Array.from(\n new Set([...companyRows, ...personRows].map((row) => row.entityId)),\n )\n const addresses = linkedEntityIds.length\n ? await findWithDecryption(\n em,\n CustomerAddress,\n {\n entity: { $in: linkedEntityIds },\n latitude: { $ne: null },\n longitude: { $ne: null },\n tenantId: effectiveTenantId,\n ...orgScopeFilter,\n },\n {},\n decryptionScope,\n )\n : []\n\n const addressRows: DealMapAddress[] = []\n for (const address of addresses) {\n const { id: entityId } = readEntityRef(address.entity)\n if (!entityId) continue\n addressRows.push({\n id: address.id,\n entityId,\n isPrimary: address.isPrimary === true,\n latitude: address.latitude ?? null,\n longitude: address.longitude ?? null,\n city: address.city ?? null,\n region: address.region ?? null,\n country: address.country ?? null,\n createdAt: address.createdAt ?? null,\n })\n }\n\n const toLink = (row: LinkRow): DealMapLink => ({\n dealId: row.dealId,\n entityId: row.entityId,\n createdAt: row.createdAt,\n })\n const locationByDeal: Map<string, DealMapLocation | null> = resolveDealLocations(\n dealIds,\n companyRows.map(toLink),\n personRows.map(toLink),\n addressRows,\n )\n\n const items = rows\n .map((row) => {\n const id = typeof row.id === 'string' && row.id.trim().length ? row.id : null\n if (!id) return null\n return {\n id,\n title: toStringOrNull(row.title),\n status: toStringOrNull(row.status),\n pipelineId: toStringOrNull(row.pipeline_id),\n pipelineStageId: toStringOrNull(row.pipeline_stage_id),\n pipelineStage: toStringOrNull(row.pipeline_stage),\n valueAmount: toFiniteNumberOrNull(row.value_amount),\n valueCurrency: toStringOrNull(row.value_currency),\n probability: toFiniteNumberOrNull(row.probability),\n expectedCloseAt: toIsoStringOrNull(row.expected_close_at),\n ownerUserId: toStringOrNull(row.owner_user_id),\n updatedAt: toIsoStringOrNull(row.updated_at),\n companies: companiesByDeal.get(id) ?? [],\n people: peopleByDeal.get(id) ?? [],\n location: locationByDeal.get(id) ?? null,\n }\n })\n .filter((item): item is NonNullable<typeof item> => item !== null)\n\n return NextResponse.json({\n items,\n total: res.total,\n page: query.page,\n pageSize: query.pageSize,\n totalPages: Math.ceil(res.total / (query.pageSize || 1)),\n ...(res.meta?.listCountCapWarning ? { totalIsCapped: true } : {}),\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.7.1-develop.7102.1.b41f7e3e51",
3
+ "version": "0.7.1-develop.7104.1.1dd769bf1a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -252,16 +252,16 @@
252
252
  "zod": "^4.4.3"
253
253
  },
254
254
  "peerDependencies": {
255
- "@open-mercato/ai-assistant": "0.7.1-develop.7102.1.b41f7e3e51",
256
- "@open-mercato/shared": "0.7.1-develop.7102.1.b41f7e3e51",
257
- "@open-mercato/ui": "0.7.1-develop.7102.1.b41f7e3e51",
255
+ "@open-mercato/ai-assistant": "0.7.1-develop.7104.1.1dd769bf1a",
256
+ "@open-mercato/shared": "0.7.1-develop.7104.1.1dd769bf1a",
257
+ "@open-mercato/ui": "0.7.1-develop.7104.1.1dd769bf1a",
258
258
  "react": "^19.0.0",
259
259
  "react-dom": "^19.0.0"
260
260
  },
261
261
  "devDependencies": {
262
- "@open-mercato/ai-assistant": "0.7.1-develop.7102.1.b41f7e3e51",
263
- "@open-mercato/shared": "0.7.1-develop.7102.1.b41f7e3e51",
264
- "@open-mercato/ui": "0.7.1-develop.7102.1.b41f7e3e51",
262
+ "@open-mercato/ai-assistant": "0.7.1-develop.7104.1.1dd769bf1a",
263
+ "@open-mercato/shared": "0.7.1-develop.7104.1.1dd769bf1a",
264
+ "@open-mercato/ui": "0.7.1-develop.7104.1.1dd769bf1a",
265
265
  "@testing-library/dom": "^10.4.1",
266
266
  "@testing-library/jest-dom": "^7.0.0",
267
267
  "@testing-library/react": "^16.3.1",
@@ -11,6 +11,7 @@ import type { ExchangeRateService } from '@open-mercato/core/modules/currencies/
11
11
  import { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'
12
12
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
13
13
  import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
14
+ import { readQueryParamList } from '@open-mercato/shared/lib/crud/query-params'
14
15
  import { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'
15
16
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
16
17
  import { fetchStuckDealIds } from '../../../lib/stuckDeals'
@@ -122,11 +123,8 @@ export const openApi: OpenApiRouteDoc = {
122
123
  }
123
124
 
124
125
  function readArrayParam(searchParams: URLSearchParams, key: string): string[] | null {
125
- const all = searchParams.getAll(key)
126
- if (!all.length) return null
127
- const flat = all.flatMap((v) => v.split(','))
128
- const trimmed = flat.map((s) => s.trim()).filter(Boolean)
129
- return trimmed.length ? trimmed : null
126
+ const values = readQueryParamList(searchParams, key)
127
+ return values.length ? values : null
130
128
  }
131
129
 
132
130
  function restrictToIds(where: string[], values: Array<string | number | null>, ids: string[]) {
@@ -5,6 +5,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
6
  import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
7
7
  import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
8
+ import { readQueryParamList } from '@open-mercato/shared/lib/crud/query-params'
8
9
  import { SortDir, type QueryEngine } from '@open-mercato/shared/lib/query/types'
9
10
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
10
11
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
@@ -161,10 +162,8 @@ function toIsoStringOrNull(value: unknown): string | null {
161
162
  }
162
163
 
163
164
  function readArrayParam(searchParams: URLSearchParams, key: string): string[] | null {
164
- const all = searchParams.getAll(key)
165
- if (!all.length) return null
166
- const trimmed = all.flatMap((value) => value.split(',')).map((value) => value.trim()).filter(Boolean)
167
- return trimmed.length ? trimmed : null
165
+ const values = readQueryParamList(searchParams, key)
166
+ return values.length ? values : null
168
167
  }
169
168
 
170
169
  function collectRefIds(rows: unknown[], key: string): string[] {
@@ -22,31 +22,31 @@
22
22
  "portal.disabled.title": "Portal nicht verfügbar",
23
23
  "portal.footer.copyright": "© {year} Alle Rechte vorbehalten.",
24
24
  "portal.info.instructions": "Um auf das Portal Ihrer Organisation zuzugreifen, navigieren Sie zur eindeutigen URL Ihrer Organisation. Kontaktieren Sie Ihren Administrator, wenn Sie den Portal-Link nicht kennen.",
25
- "portal.invite.backToLogin": "Already have an account?",
26
- "portal.invite.confirmPassword": "Confirm Password",
25
+ "portal.invite.backToLogin": "Bereits ein Konto?",
26
+ "portal.invite.confirmPassword": "Passwort bestätigen",
27
27
  "portal.invite.confirmPassword.placeholder": "••••••••",
28
- "portal.invite.description": "Create your portal account to accept this invitation.",
29
- "portal.invite.displayName": "Display Name",
30
- "portal.invite.error.displayNameRequired": "Display name is required.",
31
- "portal.invite.error.generic": "Invitation acceptance failed. Please try again.",
32
- "portal.invite.error.invalidToken": "Invalid or expired invitation.",
33
- "portal.invite.error.noToken": "Invalid or missing invitation token.",
34
- "portal.invite.error.passwordMismatch": "Passwords do not match.",
35
- "portal.invite.error.passwordTooShort": "Password must be at least 8 characters long.",
36
- "portal.invite.loginLink": "Sign in",
37
- "portal.invite.password": "Password",
28
+ "portal.invite.description": "Erstellen Sie Ihr Portalkonto, um diese Einladung anzunehmen.",
29
+ "portal.invite.displayName": "Anzeigename",
30
+ "portal.invite.error.displayNameRequired": "Der Anzeigename ist erforderlich.",
31
+ "portal.invite.error.generic": "Annahme der Einladung fehlgeschlagen. Bitte versuchen Sie es erneut.",
32
+ "portal.invite.error.invalidToken": "Ungültige oder abgelaufene Einladung.",
33
+ "portal.invite.error.noToken": "Ungültiges oder fehlendes Einladungs-Token.",
34
+ "portal.invite.error.passwordMismatch": "Passwörter stimmen nicht überein.",
35
+ "portal.invite.error.passwordTooShort": "Das Passwort muss mindestens 8 Zeichen lang sein.",
36
+ "portal.invite.loginLink": "Anmelden",
37
+ "portal.invite.password": "Passwort",
38
38
  "portal.invite.password.placeholder": "••••••••",
39
- "portal.invite.submit": "Accept Invitation",
40
- "portal.invite.submitting": "Accepting invitation…",
41
- "portal.invite.title": "Accept Invitation",
39
+ "portal.invite.submit": "Einladung annehmen",
40
+ "portal.invite.submitting": "Einladung wird angenommen…",
41
+ "portal.invite.title": "Einladung annehmen",
42
42
  "portal.landing.cta.login": "Anmelden",
43
43
  "portal.landing.cta.signup": "Konto erstellen",
44
- "portal.landing.feature.account": "Account Management",
45
- "portal.landing.feature.account.description": "Update your profile, manage team members, and configure your preferences.",
46
- "portal.landing.feature.orders": "Orders & Invoices",
47
- "portal.landing.feature.orders.description": "Track your orders, download invoices, and view delivery status in real time.",
48
- "portal.landing.feature.security": "Secure Access",
49
- "portal.landing.feature.security.description": "Role-based permissions, session management, and full audit trail.",
44
+ "portal.landing.feature.account": "Kontoverwaltung",
45
+ "portal.landing.feature.account.description": "Aktualisieren Sie Ihr Profil, verwalten Sie Teammitglieder und konfigurieren Sie Ihre Einstellungen.",
46
+ "portal.landing.feature.orders": "Bestellungen & Rechnungen",
47
+ "portal.landing.feature.orders.description": "Verfolgen Sie Ihre Bestellungen, laden Sie Rechnungen herunter und sehen Sie den Lieferstatus in Echtzeit.",
48
+ "portal.landing.feature.security": "Sicherer Zugriff",
49
+ "portal.landing.feature.security.description": "Rollenbasierte Berechtigungen, Sitzungsverwaltung und vollständiges Audit-Protokoll.",
50
50
  "portal.landing.hero.description": "Verwalten Sie Ihr Konto, sehen Sie Bestellungen ein und greifen Sie auf alle Ihre Ressourcen an einem Ort zu.",
51
51
  "portal.landing.hero.title": "Willkommen im Kundenportal",
52
52
  "portal.login.description": "Melden Sie sich bei Ihrem Kundenkonto an.",
@@ -66,7 +66,7 @@
66
66
  "portal.nav.account": "Konto",
67
67
  "portal.nav.dashboard": "Dashboard",
68
68
  "portal.nav.home": "Portal",
69
- "portal.nav.invite": "Accept Invitation",
69
+ "portal.nav.invite": "Einladung annehmen",
70
70
  "portal.nav.login": "Anmelden",
71
71
  "portal.nav.logout": "Abmelden",
72
72
  "portal.nav.profile": "Profil",
@@ -22,31 +22,31 @@
22
22
  "portal.disabled.title": "Portal no disponible",
23
23
  "portal.footer.copyright": "© {year} Todos los derechos reservados.",
24
24
  "portal.info.instructions": "Para acceder al portal de su organización, navegue a la URL única de su organización. Contacte a su administrador si no conoce el enlace del portal.",
25
- "portal.invite.backToLogin": "Already have an account?",
26
- "portal.invite.confirmPassword": "Confirm Password",
25
+ "portal.invite.backToLogin": "¿Ya tienes cuenta?",
26
+ "portal.invite.confirmPassword": "Confirmar contraseña",
27
27
  "portal.invite.confirmPassword.placeholder": "••••••••",
28
- "portal.invite.description": "Create your portal account to accept this invitation.",
29
- "portal.invite.displayName": "Display Name",
30
- "portal.invite.error.displayNameRequired": "Display name is required.",
31
- "portal.invite.error.generic": "Invitation acceptance failed. Please try again.",
32
- "portal.invite.error.invalidToken": "Invalid or expired invitation.",
33
- "portal.invite.error.noToken": "Invalid or missing invitation token.",
34
- "portal.invite.error.passwordMismatch": "Passwords do not match.",
35
- "portal.invite.error.passwordTooShort": "Password must be at least 8 characters long.",
36
- "portal.invite.loginLink": "Sign in",
37
- "portal.invite.password": "Password",
28
+ "portal.invite.description": "Crea tu cuenta del portal para aceptar esta invitación.",
29
+ "portal.invite.displayName": "Nombre para mostrar",
30
+ "portal.invite.error.displayNameRequired": "El nombre para mostrar es obligatorio.",
31
+ "portal.invite.error.generic": "Error al aceptar la invitación. Inténtalo de nuevo.",
32
+ "portal.invite.error.invalidToken": "Invitación no válida o expirada.",
33
+ "portal.invite.error.noToken": "Token de invitación no válido o faltante.",
34
+ "portal.invite.error.passwordMismatch": "Las contraseñas no coinciden.",
35
+ "portal.invite.error.passwordTooShort": "La contraseña debe tener al menos 8 caracteres.",
36
+ "portal.invite.loginLink": "Iniciar sesión",
37
+ "portal.invite.password": "Contraseña",
38
38
  "portal.invite.password.placeholder": "••••••••",
39
- "portal.invite.submit": "Accept Invitation",
40
- "portal.invite.submitting": "Accepting invitation…",
41
- "portal.invite.title": "Accept Invitation",
39
+ "portal.invite.submit": "Aceptar invitación",
40
+ "portal.invite.submitting": "Aceptando invitación…",
41
+ "portal.invite.title": "Aceptar invitación",
42
42
  "portal.landing.cta.login": "Iniciar sesión",
43
43
  "portal.landing.cta.signup": "Crear cuenta",
44
- "portal.landing.feature.account": "Account Management",
45
- "portal.landing.feature.account.description": "Update your profile, manage team members, and configure your preferences.",
46
- "portal.landing.feature.orders": "Orders & Invoices",
47
- "portal.landing.feature.orders.description": "Track your orders, download invoices, and view delivery status in real time.",
48
- "portal.landing.feature.security": "Secure Access",
49
- "portal.landing.feature.security.description": "Role-based permissions, session management, and full audit trail.",
44
+ "portal.landing.feature.account": "Gestión de la cuenta",
45
+ "portal.landing.feature.account.description": "Actualiza tu perfil, gestiona los miembros del equipo y configura tus preferencias.",
46
+ "portal.landing.feature.orders": "Pedidos y facturas",
47
+ "portal.landing.feature.orders.description": "Haz seguimiento de tus pedidos, descarga facturas y consulta el estado de entrega en tiempo real.",
48
+ "portal.landing.feature.security": "Acceso seguro",
49
+ "portal.landing.feature.security.description": "Permisos basados en roles, gestión de sesiones y registro de auditoría completo.",
50
50
  "portal.landing.hero.description": "Administra tu cuenta, consulta pedidos y accede a todos tus recursos en un solo lugar.",
51
51
  "portal.landing.hero.title": "Bienvenido al Portal del Cliente",
52
52
  "portal.login.description": "Inicia sesión en tu cuenta de cliente.",
@@ -66,7 +66,7 @@
66
66
  "portal.nav.account": "Cuenta",
67
67
  "portal.nav.dashboard": "Panel",
68
68
  "portal.nav.home": "Portal",
69
- "portal.nav.invite": "Accept Invitation",
69
+ "portal.nav.invite": "Aceptar invitación",
70
70
  "portal.nav.login": "Iniciar sesión",
71
71
  "portal.nav.logout": "Cerrar sesión",
72
72
  "portal.nav.profile": "Perfil",
@@ -41,12 +41,12 @@
41
41
  "portal.invite.title": "Przyjmij zaproszenie",
42
42
  "portal.landing.cta.login": "Zaloguj się",
43
43
  "portal.landing.cta.signup": "Utwórz konto",
44
- "portal.landing.feature.account": "Account Management",
45
- "portal.landing.feature.account.description": "Update your profile, manage team members, and configure your preferences.",
46
- "portal.landing.feature.orders": "Orders & Invoices",
47
- "portal.landing.feature.orders.description": "Track your orders, download invoices, and view delivery status in real time.",
48
- "portal.landing.feature.security": "Secure Access",
49
- "portal.landing.feature.security.description": "Role-based permissions, session management, and full audit trail.",
44
+ "portal.landing.feature.account": "Zarządzanie kontem",
45
+ "portal.landing.feature.account.description": "Aktualizuj swój profil, zarządzaj członkami zespołu i skonfiguruj swoje preferencje.",
46
+ "portal.landing.feature.orders": "Zamówienia i faktury",
47
+ "portal.landing.feature.orders.description": "Śledź zamówienia, pobieraj faktury i sprawdzaj status dostawy w czasie rzeczywistym.",
48
+ "portal.landing.feature.security": "Bezpieczny dostęp",
49
+ "portal.landing.feature.security.description": "Uprawnienia oparte na rolach, zarządzanie sesjami i pełny dziennik audytu.",
50
50
  "portal.landing.hero.description": "Zarządzaj swoim kontem, przeglądaj zamówienia i uzyskaj dostęp do wszystkich zasobów w jednym miejscu.",
51
51
  "portal.landing.hero.title": "Witaj w Portalu Klienta",
52
52
  "portal.login.description": "Zaloguj się do swojego konta klienta.",