@yoryoboy/bi-mcp 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/mcp-use.json +6 -3
  3. package/dist/scripts/profile-mercadolibre-billing.js +634 -0
  4. package/dist/scripts/profile-mercadolibre-billing.js.map +7 -0
  5. package/dist/src/config/__tests__/tiendanube-profile-store.test.js +50 -0
  6. package/dist/src/config/__tests__/tiendanube-profile-store.test.js.map +7 -0
  7. package/dist/src/config/tiendanube-profile-store.js +31 -0
  8. package/dist/src/config/tiendanube-profile-store.js.map +7 -0
  9. package/dist/src/google-ads/report-utils.js +78 -0
  10. package/dist/src/google-ads/report-utils.js.map +7 -0
  11. package/dist/src/search-console/search-console-utils.js +275 -0
  12. package/dist/src/search-console/search-console-utils.js.map +7 -0
  13. package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report-ventas.test.js +358 -0
  14. package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report-ventas.test.js.map +7 -0
  15. package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report.test.js +96 -0
  16. package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report.test.js.map +7 -0
  17. package/dist/src/services/mercadolibre/mercadolibre-claim-return-lookup.js +15 -0
  18. package/dist/src/services/mercadolibre/mercadolibre-claim-return-lookup.js.map +7 -0
  19. package/dist/src/services/mercadolibre/mercadolibre-observability.js +12 -0
  20. package/dist/src/services/mercadolibre/mercadolibre-observability.js.map +7 -0
  21. package/dist/src/services/mercadolibre/mercadolibre-sale-reference.js +7 -0
  22. package/dist/src/services/mercadolibre/mercadolibre-sale-reference.js.map +7 -0
  23. package/dist/src/services/mercadolibre/mercadolibre-sales-report.js +553 -0
  24. package/dist/src/services/mercadolibre/mercadolibre-sales-report.js.map +7 -0
  25. package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js +165 -0
  26. package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js.map +7 -0
  27. package/dist/src/tools/config/__tests__/list-profiles.test.js +144 -1
  28. package/dist/src/tools/config/__tests__/list-profiles.test.js.map +2 -2
  29. package/dist/src/tools/config/list-profiles.js +50 -6
  30. package/dist/src/tools/config/list-profiles.js.map +2 -2
  31. package/dist/src/tools/mercadolibre/__tests__/get-sales-report-foundation.test.js +417 -0
  32. package/dist/src/tools/mercadolibre/__tests__/get-sales-report-foundation.test.js.map +7 -0
  33. package/dist/src/tools/mercadolibre/get-sales-report.js +474 -0
  34. package/dist/src/tools/mercadolibre/get-sales-report.js.map +7 -0
  35. package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js +233 -0
  36. package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js.map +7 -0
  37. package/dist/src/tools/vtex/catalog-admin-batch.js +298 -0
  38. package/dist/src/tools/vtex/catalog-admin-batch.js.map +7 -0
  39. package/dist/tests/meli/meli-sales-report-buyers-mcp-e2e.test.js +137 -0
  40. package/dist/tests/meli/meli-sales-report-buyers-mcp-e2e.test.js.map +7 -0
  41. package/dist/tests/meli/meli-sales-report-mcp-e2e-server.js +24 -0
  42. package/dist/tests/meli/meli-sales-report-mcp-e2e-server.js.map +7 -0
  43. package/dist/tests/meli/meli-sales-report-mcp-e2e.test.js +207 -0
  44. package/dist/tests/meli/meli-sales-report-mcp-e2e.test.js.map +7 -0
  45. package/dist/tests/meli/meli-sales-report-public-mcp-e2e-server.js +10 -0
  46. package/dist/tests/meli/meli-sales-report-public-mcp-e2e-server.js.map +7 -0
  47. package/dist/tests/meli/meli-sales-report-public-registration.test.js +98 -0
  48. package/dist/tests/meli/meli-sales-report-public-registration.test.js.map +7 -0
  49. package/dist/tests/smoke/process-harness.js +89 -0
  50. package/dist/tests/smoke/process-harness.js.map +7 -0
  51. package/dist/tests/smoke/process-harness.test.js +39 -0
  52. package/dist/tests/smoke/process-harness.test.js.map +7 -0
  53. package/package.json +1 -1
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../scripts/profile-mercadolibre-billing.ts"],
4
+ "sourcesContent": ["import { mkdir, writeFile } from \"node:fs/promises\";\nimport process from \"node:process\";\n\nimport {\n formatMercadoLibreError,\n getMercadoLibreApiContext,\n isRetryableMercadoLibreError,\n MercadoLibreApiError,\n} from \"../src/services/mercadolibre/mercadolibre-api.js\";\nimport {\n buildMercadoLibreBillingPeriodDetailsPath,\n buildMercadoLibreBillingOrderDetailsPath,\n clampMercadoLibreBillingOrderIdsBatchSize,\n MERCADO_LIBRE_BILLING_ORDER_IDS_MAX_PER_REQUEST,\n scopeToGroupPath,\n type BillingProvisionScope,\n} from \"../src/services/mercadolibre/mercadolibre-billing.js\";\nimport { loadLocalEnv, parseCliArgs } from \"./_helpers.js\";\n\ntype Mode = \"period\" | \"order-ids\" | \"both\";\n\ninterface ScriptConfig {\n profileId: string;\n periodKey: string;\n documentType: \"BILL\" | \"CREDIT_NOTE\";\n mode: Mode;\n scope: BillingProvisionScope;\n pageLimit: number;\n maxPages: number;\n batchSize: number;\n delayMs: number;\n maxRetries: number;\n orderIds: string[];\n outputPath: string;\n}\n\ninterface RequestSummary {\n startedAt: string;\n endedAt: string;\n durationMs: number;\n strategy: \"period\" | \"order-ids\";\n step: string;\n attempt: number;\n endpoint: string;\n scope: BillingProvisionScope;\n groupPath: string;\n params: Record<string, unknown>;\n status: number | null;\n resultCount: number | null;\n total: number | null;\n lastId: string | null;\n responseShape: string[];\n retryAfterMs: number | null;\n retryAfterHeader: string | null;\n error?: ErrorSummary;\n}\n\ninterface ErrorSummary {\n name: string;\n message: string;\n status: number | null;\n code: string | null;\n retryAfterMs: number | null;\n retryable: boolean | null;\n}\n\ninterface StrategyStats {\n requestsAttempted: number;\n requestsSucceeded: number;\n requestsFailed: number;\n transportAttempts: number;\n rateLimit429Count: number;\n rowsRecovered: number;\n rawRowsRecovered: number;\n elapsedMs: number;\n totalLatencyMs: number;\n rowKeys: Set<string>;\n lastIdsSeen: string[];\n requestLogs: RequestSummary[];\n coverageComplete: boolean | null;\n stopReason: string | null;\n}\n\ninterface RunSummary {\n requestsAttempted: number;\n requestsSucceeded: number;\n requestsFailed: number;\n transportAttempts: number;\n rateLimit429Count: number;\n rowsRecovered: number;\n averageLatencyMs: number;\n estimatedThroughputRowsPerSec: number;\n recommendationHint: string;\n}\n\ninterface RunArtifact {\n run: {\n startedAt: string;\n endedAt: string;\n durationMs: number;\n config: ScriptConfig;\n };\n summary: RunSummary;\n period?: {\n strategy: \"period\";\n stats: Omit<StrategyStats, \"rowKeys\"> & { rowsRecovered: number };\n };\n orderIds?: {\n strategy: \"order-ids\";\n stats: Omit<StrategyStats, \"rowKeys\"> & { rowsRecovered: number };\n };\n requestLogs: RequestSummary[];\n}\n\ninterface ProfiledRequestConfig {\n strategy: \"period\" | \"order-ids\";\n step: string;\n attempt: number;\n endpoint: string;\n scope: BillingProvisionScope;\n groupPath: string;\n params: Record<string, unknown>;\n}\n\ntype ProfiledRequestBaseConfig = Omit<ProfiledRequestConfig, \"attempt\">;\n\nconst HELP = `Mercado Libre billing profiler\n\nUsage:\n pnpm exec tsx scripts/profile-mercadolibre-billing.ts --profile-id <id> --period-key <key> [options]\n\nRequired:\n --profile-id, MELI_PROFILE_ID, MERCADOLIBRE_PROFILE_ID\n --period-key, MELI_PERIOD_KEY\n\nOptional:\n --document-type BILL|CREDIT_NOTE Default: BILL\n --mode period|order-ids|both Default: both\n --scope ml|mp|flex|full Default: full\n --limit <n> Default: 1000\n --max-pages <n> Default: 10\n --batch-size <n> Default: 60 (capped at 60 per request)\n --delay-ms <n> Default: 150\n --max-retries <n> Default: 0\n --order-ids <id,id,...> Required when mode includes order-ids\n --output <path> Default: /tmp/opencode/mercadolibre-billing-profile-<timestamp>.json\n --help\n\nExamples:\n pnpm exec tsx scripts/profile-mercadolibre-billing.ts \\\n --profile-id profile_123 \\\n --period-key 2025-01 \\\n --mode both \\\n --order-ids 123,456,789\n`;\n\nfunction printHelp() {\n console.log(HELP);\n}\n\nfunction sleep(ms: number) {\n return new Promise<void>((resolve) => setTimeout(resolve, ms));\n}\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction toPositiveInteger(value: string | undefined, fallback: number, name: string): number {\n if (!value || !value.trim()) {\n return fallback;\n }\n\n const parsed = Number(value);\n if (!Number.isInteger(parsed) || parsed < 0) {\n throw new Error(`Invalid ${name}: expected a non-negative integer, got ${value}`);\n }\n\n return parsed;\n}\n\nfunction normalizeMode(value: string | undefined): Mode {\n const candidate = value?.trim().toLowerCase();\n if (candidate === \"period\" || candidate === \"order-ids\" || candidate === \"both\") {\n return candidate;\n }\n\n if (!candidate) {\n return \"both\";\n }\n\n throw new Error(`Invalid mode: ${value}`);\n}\n\nfunction normalizeScope(value: string | undefined): BillingProvisionScope {\n const candidate = value?.trim().toLowerCase();\n if (candidate === \"ml\" || candidate === \"mp\" || candidate === \"flex\" || candidate === \"full\") {\n return candidate;\n }\n\n if (!candidate) {\n return \"full\";\n }\n\n throw new Error(`Invalid scope: ${value}`);\n}\n\nfunction normalizeDocumentType(value: string | undefined): \"BILL\" | \"CREDIT_NOTE\" {\n const candidate = value?.trim().toUpperCase();\n if (candidate === \"BILL\" || candidate === \"CREDIT_NOTE\") {\n return candidate;\n }\n\n if (!candidate) {\n return \"BILL\";\n }\n\n throw new Error(`Invalid document type: ${value}`);\n}\n\nfunction parseOrderIds(raw: string | undefined): string[] {\n if (!raw) {\n return [];\n }\n\n return raw\n .split(/[\\s,]+/g)\n .map((value) => value.trim())\n .filter(Boolean);\n}\n\nfunction readConfigValue(args: ReturnType<typeof parseCliArgs>, name: string, ...envNames: string[]) {\n const cliValue = args.values[name]?.trim();\n if (cliValue) {\n return cliValue;\n }\n\n for (const envName of envNames) {\n const envValue = process.env[envName]?.trim();\n if (envValue) {\n return envValue;\n }\n }\n\n return undefined;\n}\n\nfunction parseConfig(): ScriptConfig {\n const args = parseCliArgs(process.argv.slice(2));\n\n if (args.flags.has(\"help\") || args.flags.has(\"h\")) {\n printHelp();\n process.exit(0);\n }\n\n const profileId = readConfigValue(args, \"profile-id\", \"MELI_PROFILE_ID\", \"MERCADOLIBRE_PROFILE_ID\");\n const periodKey = readConfigValue(args, \"period-key\", \"MELI_PERIOD_KEY\");\n const documentType = normalizeDocumentType(readConfigValue(args, \"document-type\", \"MELI_DOCUMENT_TYPE\"));\n const mode = normalizeMode(readConfigValue(args, \"mode\", \"MELI_MODE\"));\n const scope = normalizeScope(readConfigValue(args, \"scope\", \"MELI_SCOPE\"));\n const pageLimit = toPositiveInteger(readConfigValue(args, \"limit\", \"MELI_LIMIT\"), 1000, \"limit\");\n const maxPages = toPositiveInteger(readConfigValue(args, \"max-pages\", \"MELI_MAX_PAGES\"), 10, \"max-pages\");\n const batchSize = toPositiveInteger(readConfigValue(args, \"batch-size\", \"MELI_BATCH_SIZE\"), 60, \"batch-size\");\n const delayMs = toPositiveInteger(readConfigValue(args, \"delay-ms\", \"MELI_DELAY_MS\"), 150, \"delay-ms\");\n const maxRetries = toPositiveInteger(readConfigValue(args, \"max-retries\", \"MELI_MAX_RETRIES\"), 0, \"max-retries\");\n const orderIds = parseOrderIds(readConfigValue(args, \"order-ids\", \"MELI_ORDER_IDS\"));\n const outputPath =\n readConfigValue(args, \"output\", \"MELI_OUTPUT_PATH\") ??\n `/tmp/opencode/mercadolibre-billing-profile-${new Date().toISOString().replace(/[:.]/g, \"-\")}.json`;\n\n if (!profileId) {\n throw new Error(\"Missing required profile id. Pass --profile-id or set MELI_PROFILE_ID.\");\n }\n\n if (!periodKey) {\n throw new Error(\"Missing required period key. Pass --period-key or set MELI_PERIOD_KEY.\");\n }\n\n if ((mode === \"order-ids\" || mode === \"both\") && orderIds.length === 0) {\n throw new Error(\"Order IDs are required when mode includes order-ids. Pass --order-ids or MELI_ORDER_IDS.\");\n }\n\n return {\n profileId,\n periodKey,\n documentType,\n mode,\n scope,\n pageLimit,\n maxPages,\n batchSize,\n delayMs,\n maxRetries,\n orderIds,\n outputPath,\n };\n}\n\nfunction createStrategyStats(): StrategyStats {\n return {\n requestsAttempted: 0,\n requestsSucceeded: 0,\n requestsFailed: 0,\n transportAttempts: 0,\n rateLimit429Count: 0,\n rowsRecovered: 0,\n rawRowsRecovered: 0,\n elapsedMs: 0,\n totalLatencyMs: 0,\n rowKeys: new Set<string>(),\n lastIdsSeen: [],\n requestLogs: [],\n coverageComplete: null,\n stopReason: null,\n };\n}\n\nfunction getHeaderValue(headers: Record<string, unknown> | undefined, name: string): string | null {\n if (!headers) {\n return null;\n }\n\n const raw = headers[name.toLowerCase()] ?? headers[name] ?? headers[name.toUpperCase()];\n if (Array.isArray(raw)) {\n return raw.length > 0 ? String(raw[0]) : null;\n }\n\n return raw == null ? null : String(raw);\n}\n\nfunction parseRetryAfterHeaderMs(value: string | null): number | null {\n if (!value) {\n return null;\n }\n\n const seconds = Number(value);\n if (Number.isFinite(seconds) && seconds > 0) {\n return seconds * 1000;\n }\n\n const dateMs = new Date(value).getTime();\n if (Number.isFinite(dateMs)) {\n return Math.max(0, dateMs - Date.now());\n }\n\n return null;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value != null && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction normalizeCursor(value: unknown): string | null {\n if (value == null || value === 0 || value === \"0\") {\n return null;\n }\n\n const normalized = String(value).trim();\n return normalized ? normalized : null;\n}\n\nfunction getRowKey(strategy: \"period\" | \"order-ids\", row: Record<string, unknown>): string | null {\n if (strategy === \"period\") {\n const chargeInfo = asRecord(row.charge_info);\n const detailId = String(chargeInfo.detail_id ?? row.detail_id ?? \"\").trim();\n if (detailId) {\n return `detail:${detailId}`;\n }\n }\n\n const orderId = String(row.order_id ?? \"\").trim();\n if (orderId) {\n return `order:${orderId}`;\n }\n\n const detailId = String(row.detail_id ?? \"\").trim();\n if (detailId) {\n return `detail:${detailId}`;\n }\n\n return null;\n}\n\nfunction summarizeError(error: unknown): ErrorSummary {\n if (error instanceof MercadoLibreApiError) {\n return {\n name: error.name,\n message: error.message,\n status: error.status ?? null,\n code: error.code ?? null,\n retryAfterMs: error.retryAfterMs ?? null,\n retryable: isRetryableMercadoLibreError(error),\n };\n }\n\n if (error instanceof Error) {\n return {\n name: error.name,\n message: formatMercadoLibreError(error),\n status: null,\n code: null,\n retryAfterMs: null,\n retryable: null,\n };\n }\n\n return {\n name: \"UnknownError\",\n message: formatMercadoLibreError(error),\n status: null,\n code: null,\n retryAfterMs: null,\n retryable: null,\n };\n}\n\nfunction logRequest(summary: RequestSummary) {\n console.log(JSON.stringify(summary));\n}\n\nasync function profiledGet<T extends Record<string, unknown>>(\n api: Awaited<ReturnType<typeof getMercadoLibreApiContext>>[\"api\"],\n config: ProfiledRequestConfig\n): Promise<{ ok: true; data: T; log: RequestSummary } | { ok: false; log: RequestSummary }> {\n const startedAt = nowIso();\n const startedMs = Date.now();\n\n try {\n const response = await api.get<T>(config.endpoint, { params: config.params });\n const endedAt = nowIso();\n const durationMs = Date.now() - startedMs;\n const data = response.data;\n const responseRecord = asRecord(data);\n const results = Array.isArray(responseRecord.results) ? responseRecord.results : [];\n const lastId = normalizeCursor(responseRecord.last_id);\n const total = Number.isFinite(Number(responseRecord.total)) ? Number(responseRecord.total) : null;\n const retryAfterHeader = getHeaderValue(response.headers as Record<string, unknown>, \"retry-after\");\n const log: RequestSummary = {\n startedAt,\n endedAt,\n durationMs,\n strategy: config.strategy,\n step: config.step,\n attempt: config.attempt,\n endpoint: config.endpoint,\n scope: config.scope,\n groupPath: config.groupPath,\n params: config.params,\n status: response.status,\n resultCount: results.length,\n total,\n lastId,\n responseShape: Object.keys(responseRecord).sort(),\n retryAfterMs: parseRetryAfterHeaderMs(retryAfterHeader),\n retryAfterHeader,\n };\n\n logRequest(log);\n return { ok: true, data, log };\n } catch (error) {\n const endedAt = nowIso();\n const durationMs = Date.now() - startedMs;\n const summary = summarizeError(error);\n const log: RequestSummary = {\n startedAt,\n endedAt,\n durationMs,\n strategy: config.strategy,\n step: config.step,\n attempt: config.attempt,\n endpoint: config.endpoint,\n scope: config.scope,\n groupPath: config.groupPath,\n params: config.params,\n status: summary.status,\n resultCount: null,\n total: null,\n lastId: null,\n responseShape: [],\n retryAfterMs: summary.retryAfterMs,\n retryAfterHeader: null,\n error: summary,\n };\n\n logRequest(log);\n return { ok: false, log };\n }\n}\n\nasync function waitBetweenRequests(delayMs: number) {\n if (delayMs > 0) {\n await sleep(delayMs);\n }\n}\n\nasync function requestWithRetries<T extends Record<string, unknown>>(\n api: Awaited<ReturnType<typeof getMercadoLibreApiContext>>[\"api\"],\n config: ProfiledRequestBaseConfig,\n maxRetries: number,\n delayMs: number\n): Promise<{ ok: true; data: T; logs: RequestSummary[] } | { ok: false; logs: RequestSummary[] }> {\n const logs: RequestSummary[] = [];\n\n for (let attempt = 1; attempt <= maxRetries + 1; attempt += 1) {\n const result = await profiledGet<T>(api, { ...config, attempt });\n logs.push(result.log);\n\n if (result.ok) {\n return { ok: true, data: result.data, logs };\n }\n\n const error = result.log.error;\n const retryable = error?.retryable ?? false;\n const isLastAttempt = attempt >= maxRetries + 1;\n\n if (isLastAttempt || !retryable) {\n return { ok: false, logs };\n }\n\n const backoffMs = Math.max(delayMs, error?.retryAfterMs ?? 0, 200 * attempt);\n await sleep(backoffMs);\n }\n\n return { ok: false, logs };\n}\n\nfunction summarizeResponseRows(strategy: \"period\" | \"order-ids\", rows: Record<string, unknown>[], stats: StrategyStats) {\n stats.rawRowsRecovered += rows.length;\n\n for (const row of rows) {\n const key = getRowKey(strategy, row);\n if (!key) {\n continue;\n }\n\n stats.rowKeys.add(key);\n }\n\n stats.rowsRecovered = stats.rowKeys.size;\n}\n\nfunction buildStrategyReport(stats: StrategyStats) {\n return {\n requestsAttempted: stats.requestsAttempted,\n requestsSucceeded: stats.requestsSucceeded,\n requestsFailed: stats.requestsFailed,\n transportAttempts: stats.transportAttempts,\n rateLimit429Count: stats.rateLimit429Count,\n rowsRecovered: stats.rowsRecovered,\n rawRowsRecovered: stats.rawRowsRecovered,\n elapsedMs: stats.elapsedMs,\n totalLatencyMs: stats.totalLatencyMs,\n averageLatencyMs: stats.requestsSucceeded > 0 ? Math.round(stats.totalLatencyMs / stats.requestsSucceeded) : 0,\n coverageComplete: stats.coverageComplete,\n stopReason: stats.stopReason,\n lastIdsSeen: stats.lastIdsSeen,\n requestLogs: stats.requestLogs,\n };\n}\n\nfunction recommendStrategy(period?: ReturnType<typeof buildStrategyReport>, orderIds?: ReturnType<typeof buildStrategyReport>) {\n if (!period && !orderIds) {\n return \"No strategy executed.\";\n }\n\n if (period && !orderIds) {\n return period.rateLimit429Count > 0\n ? \"Period cursor worked, but 429s appeared. Reduce page size or add delay before scaling up.\"\n : \"Period cursor is the only observed strategy. Use it as the baseline and keep batch size conservative.\";\n }\n\n if (!period && orderIds) {\n return orderIds.rateLimit429Count > 0\n ? \"Order-ID batches worked, but 429s appeared. Lower the batch size and add more delay.\"\n : \"Order-ID batches are the only observed strategy. Use them when the report generator already has the target order set.\";\n }\n\n if (!period || !orderIds) {\n return \"No recommendation available.\";\n }\n\n if (period.rateLimit429Count !== orderIds.rateLimit429Count) {\n return period.rateLimit429Count < orderIds.rateLimit429Count\n ? \"Prefer period pagination: it produced fewer 429s under the current pacing.\"\n : \"Prefer order-id batching: it produced fewer 429s under the current pacing.\";\n }\n\n const periodThroughput = period.elapsedMs > 0 ? period.rowsRecovered / (period.elapsedMs / 1000) : 0;\n const orderThroughput = orderIds.elapsedMs > 0 ? orderIds.rowsRecovered / (orderIds.elapsedMs / 1000) : 0;\n\n if (Math.abs(periodThroughput - orderThroughput) > 0.01) {\n return periodThroughput > orderThroughput\n ? \"Prefer period pagination: it recovered rows faster with the current pacing.\"\n : \"Prefer order-id batching: it recovered rows faster with the current pacing.\";\n }\n\n if (Boolean(period.coverageComplete) !== Boolean(orderIds.coverageComplete)) {\n return period.coverageComplete\n ? \"Prefer period pagination: it completed coverage while order-id batching did not.\"\n : \"Prefer order-id batching: it completed coverage while period pagination did not.\";\n }\n\n return \"Both strategies behaved similarly. Use period pagination for bulk coverage and order-id batching for targeted subsets.\";\n}\n\nasync function profilePeriodStrategy(\n api: Awaited<ReturnType<typeof getMercadoLibreApiContext>>[\"api\"],\n config: ScriptConfig,\n startedAtMs: number\n) {\n const stats = createStrategyStats();\n let fromId: string | null = null;\n let page = 0;\n let coverageComplete = true;\n\n while (page < config.maxPages) {\n page += 1;\n stats.requestsAttempted += 1;\n\n const groupPath = scopeToGroupPath(config.scope);\n const endpoint = buildMercadoLibreBillingPeriodDetailsPath(config.periodKey, config.scope);\n const params: Record<string, unknown> = {\n document_type: config.documentType,\n limit: config.pageLimit,\n };\n\n if (fromId) {\n params.from_id = fromId;\n }\n\n const result = await requestWithRetries<Record<string, unknown>>(\n api,\n {\n strategy: \"period\",\n step: `page-${page}`,\n endpoint,\n scope: config.scope,\n groupPath,\n params,\n },\n config.maxRetries,\n config.delayMs\n );\n\n stats.requestLogs.push(...result.logs);\n stats.transportAttempts += result.logs.length;\n\n stats.rateLimit429Count += result.logs.filter((log) => log.status === 429).length;\n\n if (!result.ok) {\n stats.requestsFailed += 1;\n stats.stopReason = `Stopped on page ${page} after a failed request`;\n coverageComplete = false;\n break;\n }\n\n stats.requestsSucceeded += 1;\n const lastLog = result.logs[result.logs.length - 1];\n stats.totalLatencyMs += lastLog?.durationMs ?? 0;\n\n const responseRecord = asRecord(result.data);\n const rows = Array.isArray(responseRecord.results)\n ? (responseRecord.results.filter((entry): entry is Record<string, unknown> => entry != null && typeof entry === \"object\" && !Array.isArray(entry)) as Record<string, unknown>[])\n : [];\n summarizeResponseRows(\"period\", rows, stats);\n\n const nextLastId = normalizeCursor(responseRecord.last_id);\n if (nextLastId) {\n stats.lastIdsSeen.push(nextLastId);\n }\n\n if (!nextLastId) {\n stats.stopReason = \"Reached terminal last_id\";\n break;\n }\n\n if (nextLastId === fromId) {\n stats.stopReason = \"Cursor stopped advancing\";\n coverageComplete = false;\n break;\n }\n\n if (rows.length === 0) {\n stats.stopReason = \"Empty page returned before terminal cursor\";\n coverageComplete = false;\n break;\n }\n\n fromId = nextLastId;\n await waitBetweenRequests(config.delayMs);\n }\n\n if (page >= config.maxPages && stats.stopReason == null) {\n coverageComplete = false;\n stats.stopReason = `Reached max-pages=${config.maxPages}`;\n }\n\n stats.coverageComplete = coverageComplete;\n stats.elapsedMs = Date.now() - startedAtMs;\n\n return buildStrategyReport(stats);\n}\n\nfunction chunkArray<T>(values: T[], size: number): T[][] {\n const chunks: T[][] = [];\n if (size <= 0) {\n return chunks;\n }\n\n for (let index = 0; index < values.length; index += size) {\n chunks.push(values.slice(index, index + size));\n }\n\n return chunks;\n}\n\nasync function profileOrderIdsStrategy(\n api: Awaited<ReturnType<typeof getMercadoLibreApiContext>>[\"api\"],\n config: ScriptConfig,\n startedAtMs: number\n) {\n const stats = createStrategyStats();\n const effectiveBatchSize = clampMercadoLibreBillingOrderIdsBatchSize(config.batchSize);\n const chunks = chunkArray(config.orderIds, effectiveBatchSize);\n const limitedChunks = chunks.slice(0, config.maxPages);\n\n if (effectiveBatchSize !== config.batchSize) {\n console.log(\n JSON.stringify({\n event: \"order_ids_batch_size_capped\",\n requested: config.batchSize,\n effective: effectiveBatchSize,\n cap: MERCADO_LIBRE_BILLING_ORDER_IDS_MAX_PER_REQUEST,\n })\n );\n }\n\n for (let index = 0; index < limitedChunks.length; index += 1) {\n const chunk = limitedChunks[index];\n stats.requestsAttempted += 1;\n\n const groupPath = \"ML\";\n const endpoint = buildMercadoLibreBillingOrderDetailsPath();\n const params: Record<string, unknown> = {\n document_type: config.documentType,\n order_ids: chunk.join(\",\"),\n };\n\n const result = await requestWithRetries<Record<string, unknown>>(\n api,\n {\n strategy: \"order-ids\",\n step: `batch-${index + 1}`,\n endpoint,\n scope: \"ml\",\n groupPath,\n params,\n },\n config.maxRetries,\n config.delayMs\n );\n\n stats.requestLogs.push(...result.logs);\n stats.transportAttempts += result.logs.length;\n\n stats.rateLimit429Count += result.logs.filter((log) => log.status === 429).length;\n\n if (!result.ok) {\n stats.requestsFailed += 1;\n stats.stopReason = `Stopped on order batch ${index + 1} after a failed request`;\n stats.coverageComplete = false;\n break;\n }\n\n stats.requestsSucceeded += 1;\n const lastLog = result.logs[result.logs.length - 1];\n stats.totalLatencyMs += lastLog?.durationMs ?? 0;\n\n const responseRecord = asRecord(result.data);\n const rows = Array.isArray(responseRecord.results)\n ? (responseRecord.results.filter((entry): entry is Record<string, unknown> => entry != null && typeof entry === \"object\" && !Array.isArray(entry)) as Record<string, unknown>[])\n : [];\n summarizeResponseRows(\"order-ids\", rows, stats);\n\n const nextLastId = normalizeCursor(responseRecord.last_id);\n if (nextLastId) {\n stats.lastIdsSeen.push(nextLastId);\n }\n\n await waitBetweenRequests(config.delayMs);\n }\n\n if (limitedChunks.length < chunks.length && stats.stopReason == null) {\n stats.stopReason = `Reached max-pages=${config.maxPages} before exhausting all order-id batches`;\n stats.coverageComplete = false;\n } else if (stats.coverageComplete == null) {\n stats.coverageComplete = stats.requestsFailed === 0;\n }\n\n stats.elapsedMs = Date.now() - startedAtMs;\n\n return buildStrategyReport(stats);\n}\n\nasync function main() {\n loadLocalEnv();\n\n const config = parseConfig();\n const startedAt = nowIso();\n const startedAtMs = Date.now();\n\n const { api } = await getMercadoLibreApiContext(config.profileId);\n\n const requestLogs: RequestSummary[] = [];\n let periodReport: ReturnType<typeof buildStrategyReport> | undefined;\n let orderIdsReport: ReturnType<typeof buildStrategyReport> | undefined;\n\n console.log(\n JSON.stringify({\n event: \"run_start\",\n startedAt,\n config: {\n profileId: config.profileId,\n periodKey: config.periodKey,\n documentType: config.documentType,\n mode: config.mode,\n scope: config.scope,\n pageLimit: config.pageLimit,\n maxPages: config.maxPages,\n batchSize: config.batchSize,\n orderIdsBatchSizeCap: MERCADO_LIBRE_BILLING_ORDER_IDS_MAX_PER_REQUEST,\n delayMs: config.delayMs,\n maxRetries: config.maxRetries,\n orderIdsCount: config.orderIds.length,\n outputPath: config.outputPath,\n },\n })\n );\n\n if (config.mode === \"period\" || config.mode === \"both\") {\n periodReport = await profilePeriodStrategy(api, config, startedAtMs);\n requestLogs.push(...periodReport.requestLogs);\n }\n\n if (config.mode === \"order-ids\" || config.mode === \"both\") {\n orderIdsReport = await profileOrderIdsStrategy(api, config, startedAtMs);\n requestLogs.push(...orderIdsReport.requestLogs);\n }\n\n const endedAt = nowIso();\n const durationMs = Date.now() - startedAtMs;\n\n const summary: RunSummary = {\n requestsAttempted: (periodReport?.requestsAttempted ?? 0) + (orderIdsReport?.requestsAttempted ?? 0),\n requestsSucceeded: (periodReport?.requestsSucceeded ?? 0) + (orderIdsReport?.requestsSucceeded ?? 0),\n requestsFailed: (periodReport?.requestsFailed ?? 0) + (orderIdsReport?.requestsFailed ?? 0),\n transportAttempts: (periodReport?.transportAttempts ?? 0) + (orderIdsReport?.transportAttempts ?? 0),\n rateLimit429Count: (periodReport?.rateLimit429Count ?? 0) + (orderIdsReport?.rateLimit429Count ?? 0),\n rowsRecovered: (periodReport?.rowsRecovered ?? 0) + (orderIdsReport?.rowsRecovered ?? 0),\n averageLatencyMs: requestLogs.length > 0 ? Math.round(requestLogs.reduce((sum, log) => sum + log.durationMs, 0) / requestLogs.length) : 0,\n estimatedThroughputRowsPerSec: durationMs > 0 ? Math.round((((periodReport?.rowsRecovered ?? 0) + (orderIdsReport?.rowsRecovered ?? 0)) / (durationMs / 1000)) * 100) / 100 : 0,\n recommendationHint: recommendStrategy(periodReport, orderIdsReport),\n };\n\n const artifact: RunArtifact = {\n run: {\n startedAt,\n endedAt,\n durationMs,\n config,\n },\n summary,\n requestLogs,\n };\n\n if (periodReport) {\n artifact.period = {\n strategy: \"period\",\n stats: periodReport,\n };\n }\n\n if (orderIdsReport) {\n artifact.orderIds = {\n strategy: \"order-ids\",\n stats: orderIdsReport,\n };\n }\n\n await mkdir(\"/tmp/opencode\", { recursive: true });\n await writeFile(config.outputPath, `${JSON.stringify(artifact, null, 2)}\\n`, \"utf8\");\n\n console.log(JSON.stringify({ event: \"artifact_written\", path: config.outputPath, durationMs }));\n console.log(JSON.stringify(artifact, null, 2));\n}\n\nmain().catch((error: unknown) => {\n console.error(\"Mercado Libre billing profiler failed\");\n console.error(formatMercadoLibreError(error, \"Mercado Libre billing profiler failed\"));\n if (error instanceof Error && error.stack) {\n console.error(error.stack);\n }\n process.exitCode = 1;\n});\n"],
5
+ "mappings": "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",
6
+ "names": ["detailId"]
7
+ }
@@ -0,0 +1,50 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const queryMock = vi.fn();
3
+ vi.mock("../../db/client.js", () => ({
4
+ getDb: () => ({ query: queryMock })
5
+ }));
6
+ const { listTiendanubeConnections } = await import("../tiendanube-profile-store.js");
7
+ describe("Tiendanube profile store", () => {
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+ });
11
+ it("reads only public connection state required by profile reporting", async () => {
12
+ queryMock.mockResolvedValue({
13
+ rows: [
14
+ {
15
+ profile_id: "profile-1",
16
+ store_id: "store-42",
17
+ status: "active",
18
+ last_validated_at: /* @__PURE__ */ new Date("2026-01-03T00:00:00.000Z")
19
+ }
20
+ ]
21
+ });
22
+ await expect(listTiendanubeConnections()).resolves.toEqual([
23
+ {
24
+ profileId: "profile-1",
25
+ storeId: "store-42",
26
+ status: "active",
27
+ lastValidatedAt: /* @__PURE__ */ new Date("2026-01-03T00:00:00.000Z")
28
+ }
29
+ ]);
30
+ const query = queryMock.mock.calls[0][0];
31
+ expect(query).toContain("profile_tiendanube_connections");
32
+ expect(query).toContain("store_id");
33
+ expect(query).toContain("status");
34
+ expect(query).toContain("last_validated_at");
35
+ expect(query).not.toContain("encrypted_access_token");
36
+ expect(query).not.toContain("last_error");
37
+ });
38
+ it("degrades a missing Dashboard relation to no connections", async () => {
39
+ queryMock.mockRejectedValue(
40
+ new Error('relation "profile_tiendanube_connections" does not exist')
41
+ );
42
+ await expect(listTiendanubeConnections()).resolves.toEqual([]);
43
+ });
44
+ it("rethrows unrelated database failures", async () => {
45
+ const failure = new Error("database unavailable");
46
+ queryMock.mockRejectedValue(failure);
47
+ await expect(listTiendanubeConnections()).rejects.toBe(failure);
48
+ });
49
+ });
50
+ //# sourceMappingURL=tiendanube-profile-store.test.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/config/__tests__/tiendanube-profile-store.test.ts"],
4
+ "sourcesContent": ["import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nconst queryMock = vi.fn();\n\nvi.mock(\"../../db/client.js\", () => ({\n getDb: () => ({ query: queryMock }),\n}));\n\nconst { listTiendanubeConnections } = await import(\n \"../tiendanube-profile-store.js\"\n);\n\ndescribe(\"Tiendanube profile store\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n it(\"reads only public connection state required by profile reporting\", async () => {\n queryMock.mockResolvedValue({\n rows: [\n {\n profile_id: \"profile-1\",\n store_id: \"store-42\",\n status: \"active\",\n last_validated_at: new Date(\"2026-01-03T00:00:00.000Z\"),\n },\n ],\n });\n\n await expect(listTiendanubeConnections()).resolves.toEqual([\n {\n profileId: \"profile-1\",\n storeId: \"store-42\",\n status: \"active\",\n lastValidatedAt: new Date(\"2026-01-03T00:00:00.000Z\"),\n },\n ]);\n\n const query = queryMock.mock.calls[0][0] as string;\n expect(query).toContain(\"profile_tiendanube_connections\");\n expect(query).toContain(\"store_id\");\n expect(query).toContain(\"status\");\n expect(query).toContain(\"last_validated_at\");\n expect(query).not.toContain(\"encrypted_access_token\");\n expect(query).not.toContain(\"last_error\");\n });\n\n it(\"degrades a missing Dashboard relation to no connections\", async () => {\n queryMock.mockRejectedValue(\n new Error('relation \"profile_tiendanube_connections\" does not exist'),\n );\n\n await expect(listTiendanubeConnections()).resolves.toEqual([]);\n });\n\n it(\"rethrows unrelated database failures\", async () => {\n const failure = new Error(\"database unavailable\");\n queryMock.mockRejectedValue(failure);\n\n await expect(listTiendanubeConnections()).rejects.toBe(failure);\n });\n});\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,UAAU,QAAQ,IAAI,UAAU;AAErD,MAAM,YAAY,GAAG,GAAG;AAExB,GAAG,KAAK,sBAAsB,OAAO;AAAA,EACnC,OAAO,OAAO,EAAE,OAAO,UAAU;AACnC,EAAE;AAEF,MAAM,EAAE,0BAA0B,IAAI,MAAM,OAC1C,gCACF;AAEA,SAAS,4BAA4B,MAAM;AACzC,aAAW,MAAM;AACf,OAAG,cAAc;AAAA,EACnB,CAAC;AAED,KAAG,oEAAoE,YAAY;AACjF,cAAU,kBAAkB;AAAA,MAC1B,MAAM;AAAA,QACJ;AAAA,UACE,YAAY;AAAA,UACZ,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,mBAAmB,oBAAI,KAAK,0BAA0B;AAAA,QACxD;AAAA,MACF;AAAA,IACF,CAAC;AAED,UAAM,OAAO,0BAA0B,CAAC,EAAE,SAAS,QAAQ;AAAA,MACzD;AAAA,QACE,WAAW;AAAA,QACX,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,iBAAiB,oBAAI,KAAK,0BAA0B;AAAA,MACtD;AAAA,IACF,CAAC;AAED,UAAM,QAAQ,UAAU,KAAK,MAAM,CAAC,EAAE,CAAC;AACvC,WAAO,KAAK,EAAE,UAAU,gCAAgC;AACxD,WAAO,KAAK,EAAE,UAAU,UAAU;AAClC,WAAO,KAAK,EAAE,UAAU,QAAQ;AAChC,WAAO,KAAK,EAAE,UAAU,mBAAmB;AAC3C,WAAO,KAAK,EAAE,IAAI,UAAU,wBAAwB;AACpD,WAAO,KAAK,EAAE,IAAI,UAAU,YAAY;AAAA,EAC1C,CAAC;AAED,KAAG,2DAA2D,YAAY;AACxE,cAAU;AAAA,MACR,IAAI,MAAM,0DAA0D;AAAA,IACtE;AAEA,UAAM,OAAO,0BAA0B,CAAC,EAAE,SAAS,QAAQ,CAAC,CAAC;AAAA,EAC/D,CAAC;AAED,KAAG,wCAAwC,YAAY;AACrD,UAAM,UAAU,IAAI,MAAM,sBAAsB;AAChD,cAAU,kBAAkB,OAAO;AAEnC,UAAM,OAAO,0BAA0B,CAAC,EAAE,QAAQ,KAAK,OAAO;AAAA,EAChE,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,31 @@
1
+ import { getDb } from "../db/client.js";
2
+ function isMissingRelationError(error) {
3
+ return error instanceof Error && /does not exist/i.test(error.message);
4
+ }
5
+ function mapTiendanubeConnectionRow(row) {
6
+ return {
7
+ profileId: row.profile_id,
8
+ storeId: row.store_id,
9
+ status: row.status,
10
+ lastValidatedAt: row.last_validated_at
11
+ };
12
+ }
13
+ async function listTiendanubeConnections() {
14
+ try {
15
+ const result = await getDb().query(
16
+ `
17
+ select profile_id, store_id, status, last_validated_at
18
+ from profile_tiendanube_connections
19
+ order by profile_id asc
20
+ `
21
+ );
22
+ return result.rows.map(mapTiendanubeConnectionRow);
23
+ } catch (error) {
24
+ if (isMissingRelationError(error)) return [];
25
+ throw error;
26
+ }
27
+ }
28
+ export {
29
+ listTiendanubeConnections
30
+ };
31
+ //# sourceMappingURL=tiendanube-profile-store.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/config/tiendanube-profile-store.ts"],
4
+ "sourcesContent": ["import { getDb } from \"../db/client.js\";\n\nexport type TiendanubeConnectionStatus =\n | \"active\"\n | \"disabled\"\n | \"expired\"\n | \"error\"\n | \"pending\";\n\nexport interface TiendanubeConnectionRecord {\n profileId: string;\n storeId: string;\n status: TiendanubeConnectionStatus;\n lastValidatedAt: Date | null;\n}\n\ninterface TiendanubeConnectionRow {\n profile_id: string;\n store_id: string;\n status: TiendanubeConnectionStatus;\n last_validated_at: Date | null;\n}\n\nfunction isMissingRelationError(error: unknown): boolean {\n return error instanceof Error && /does not exist/i.test(error.message);\n}\n\nfunction mapTiendanubeConnectionRow(\n row: TiendanubeConnectionRow,\n): TiendanubeConnectionRecord {\n return {\n profileId: row.profile_id,\n storeId: row.store_id,\n status: row.status,\n lastValidatedAt: row.last_validated_at,\n };\n}\n\nexport async function listTiendanubeConnections(): Promise<\n TiendanubeConnectionRecord[]\n> {\n try {\n const result = await getDb().query<TiendanubeConnectionRow>(\n `\n select profile_id, store_id, status, last_validated_at\n from profile_tiendanube_connections\n order by profile_id asc\n `,\n );\n\n return result.rows.map(mapTiendanubeConnectionRow);\n } catch (error) {\n if (isMissingRelationError(error)) return [];\n throw error;\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,aAAa;AAuBtB,SAAS,uBAAuB,OAAyB;AACvD,SAAO,iBAAiB,SAAS,kBAAkB,KAAK,MAAM,OAAO;AACvE;AAEA,SAAS,2BACP,KAC4B;AAC5B,SAAO;AAAA,IACL,WAAW,IAAI;AAAA,IACf,SAAS,IAAI;AAAA,IACb,QAAQ,IAAI;AAAA,IACZ,iBAAiB,IAAI;AAAA,EACvB;AACF;AAEA,eAAsB,4BAEpB;AACA,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,EAAE;AAAA,MAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKF;AAEA,WAAO,OAAO,KAAK,IAAI,0BAA0B;AAAA,EACnD,SAAS,OAAO;AACd,QAAI,uBAAuB,KAAK,EAAG,QAAO,CAAC;AAC3C,UAAM;AAAA,EACR;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ getDefaultGoogleAdsLoginCustomerId,
3
+ getGoogleAdsDeveloperToken
4
+ } from "../config/google.js";
5
+ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
6
+ function normalizeCustomerId(customerId) {
7
+ const normalized = customerId.trim().replace(/-/g, "");
8
+ if (!/^\d+$/.test(normalized)) {
9
+ throw new Error("Invalid Google Ads customer ID. Use digits only, with or without hyphens.");
10
+ }
11
+ return normalized;
12
+ }
13
+ function resolveGoogleAdsCustomerId(customerId) {
14
+ if (!customerId?.trim()) {
15
+ throw new Error("Missing Google Ads customer ID. Provide customerId.");
16
+ }
17
+ return normalizeCustomerId(customerId);
18
+ }
19
+ function resolveGoogleAdsLoginCustomerId(loginCustomerId) {
20
+ const explicitCustomerId = loginCustomerId?.trim();
21
+ if (explicitCustomerId) {
22
+ return normalizeCustomerId(explicitCustomerId);
23
+ }
24
+ return getDefaultGoogleAdsLoginCustomerId();
25
+ }
26
+ function resolveGoogleAdsDeveloperToken() {
27
+ return getGoogleAdsDeveloperToken();
28
+ }
29
+ function parseGoogleAdsMetricValue(value) {
30
+ if (typeof value === "number" && Number.isFinite(value)) {
31
+ return value;
32
+ }
33
+ if (typeof value === "string") {
34
+ const parsedValue = Number(value);
35
+ return Number.isFinite(parsedValue) ? parsedValue : 0;
36
+ }
37
+ return 0;
38
+ }
39
+ function microsToCurrency(value, decimals = 2) {
40
+ return round(parseGoogleAdsMetricValue(value) / 1e6, decimals);
41
+ }
42
+ function round(value, decimals = 2) {
43
+ if (!Number.isFinite(value)) {
44
+ return 0;
45
+ }
46
+ const factor = 10 ** decimals;
47
+ return Math.round(value * factor) / factor;
48
+ }
49
+ function toPercent(numerator, denominator) {
50
+ if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {
51
+ return 0;
52
+ }
53
+ return numerator / denominator * 100;
54
+ }
55
+ function getFieldValue(row, path, fallback) {
56
+ const segments = path.split(".");
57
+ let current = row;
58
+ for (const segment of segments) {
59
+ if (!current || typeof current !== "object") {
60
+ return fallback;
61
+ }
62
+ current = current[segment];
63
+ }
64
+ return current ?? fallback;
65
+ }
66
+ export {
67
+ dateRegex,
68
+ getFieldValue,
69
+ microsToCurrency,
70
+ normalizeCustomerId,
71
+ parseGoogleAdsMetricValue,
72
+ resolveGoogleAdsCustomerId,
73
+ resolveGoogleAdsDeveloperToken,
74
+ resolveGoogleAdsLoginCustomerId,
75
+ round,
76
+ toPercent
77
+ };
78
+ //# sourceMappingURL=report-utils.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/google-ads/report-utils.ts"],
4
+ "sourcesContent": ["import {\n getDefaultGoogleAdsLoginCustomerId,\n getGoogleAdsDeveloperToken,\n} from \"../config/google.js\";\nimport type { GoogleAdsMetricValue, GoogleAdsSearchRow } from \"../services/google-ads/google-ads-client.js\";\n\nexport const dateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport function normalizeCustomerId(customerId: string): string {\n const normalized = customerId.trim().replace(/-/g, \"\");\n if (!/^\\d+$/.test(normalized)) {\n throw new Error(\"Invalid Google Ads customer ID. Use digits only, with or without hyphens.\");\n }\n\n return normalized;\n}\n\nexport function resolveGoogleAdsCustomerId(customerId?: string): string {\n if (!customerId?.trim()) {\n throw new Error(\"Missing Google Ads customer ID. Provide customerId.\");\n }\n\n return normalizeCustomerId(customerId);\n}\n\nexport function resolveGoogleAdsLoginCustomerId(loginCustomerId?: string): string | undefined {\n const explicitCustomerId = loginCustomerId?.trim();\n if (explicitCustomerId) {\n return normalizeCustomerId(explicitCustomerId);\n }\n\n return getDefaultGoogleAdsLoginCustomerId();\n}\n\nexport function resolveGoogleAdsDeveloperToken(): string {\n return getGoogleAdsDeveloperToken();\n}\n\nexport function parseGoogleAdsMetricValue(value: GoogleAdsMetricValue | undefined): number {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n\n if (typeof value === \"string\") {\n const parsedValue = Number(value);\n return Number.isFinite(parsedValue) ? parsedValue : 0;\n }\n\n return 0;\n}\n\nexport function microsToCurrency(value: GoogleAdsMetricValue | undefined, decimals = 2): number {\n return round(parseGoogleAdsMetricValue(value) / 1_000_000, decimals);\n}\n\nexport function round(value: number, decimals = 2): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n\n const factor = 10 ** decimals;\n return Math.round(value * factor) / factor;\n}\n\nexport function toPercent(numerator: number, denominator: number): number {\n if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {\n return 0;\n }\n\n return (numerator / denominator) * 100;\n}\n\nexport function getFieldValue<T>(row: GoogleAdsSearchRow, path: string, fallback?: T): T | undefined {\n const segments = path.split(\".\");\n let current: unknown = row;\n\n for (const segment of segments) {\n if (!current || typeof current !== \"object\") {\n return fallback;\n }\n\n current = (current as Record<string, unknown>)[segment];\n }\n\n return (current as T | undefined) ?? fallback;\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAGA,MAAM,YAAY;AAElB,SAAS,oBAAoB,YAA4B;AAC9D,QAAM,aAAa,WAAW,KAAK,EAAE,QAAQ,MAAM,EAAE;AACrD,MAAI,CAAC,QAAQ,KAAK,UAAU,GAAG;AAC7B,UAAM,IAAI,MAAM,2EAA2E;AAAA,EAC7F;AAEA,SAAO;AACT;AAEO,SAAS,2BAA2B,YAA6B;AACtE,MAAI,CAAC,YAAY,KAAK,GAAG;AACvB,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AAEA,SAAO,oBAAoB,UAAU;AACvC;AAEO,SAAS,gCAAgC,iBAA8C;AAC5F,QAAM,qBAAqB,iBAAiB,KAAK;AACjD,MAAI,oBAAoB;AACtB,WAAO,oBAAoB,kBAAkB;AAAA,EAC/C;AAEA,SAAO,mCAAmC;AAC5C;AAEO,SAAS,iCAAyC;AACvD,SAAO,2BAA2B;AACpC;AAEO,SAAS,0BAA0B,OAAiD;AACzF,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG;AACvD,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,cAAc,OAAO,KAAK;AAChC,WAAO,OAAO,SAAS,WAAW,IAAI,cAAc;AAAA,EACtD;AAEA,SAAO;AACT;AAEO,SAAS,iBAAiB,OAAyC,WAAW,GAAW;AAC9F,SAAO,MAAM,0BAA0B,KAAK,IAAI,KAAW,QAAQ;AACrE;AAEO,SAAS,MAAM,OAAe,WAAW,GAAW;AACzD,MAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,MAAM;AACrB,SAAO,KAAK,MAAM,QAAQ,MAAM,IAAI;AACtC;AAEO,SAAS,UAAU,WAAmB,aAA6B;AACxE,MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,CAAC,OAAO,SAAS,WAAW,KAAK,eAAe,GAAG;AACpF,WAAO;AAAA,EACT;AAEA,SAAQ,YAAY,cAAe;AACrC;AAEO,SAAS,cAAiB,KAAyB,MAAc,UAA6B;AACnG,QAAM,WAAW,KAAK,MAAM,GAAG;AAC/B,MAAI,UAAmB;AAEvB,aAAW,WAAW,UAAU;AAC9B,QAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,aAAO;AAAA,IACT;AAEA,cAAW,QAAoC,OAAO;AAAA,EACxD;AAEA,SAAQ,WAA6B;AACvC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,275 @@
1
+ import { z } from "zod";
2
+ const searchConsoleDateRegex = /^\d{4}-\d{2}-\d{2}$/;
3
+ const searchConsoleSearchTypes = ["web", "image", "video", "news", "discover"];
4
+ const searchConsoleDimensions = [
5
+ "query",
6
+ "page",
7
+ "country",
8
+ "device",
9
+ "date",
10
+ "searchAppearance"
11
+ ];
12
+ const searchConsoleFilterOperators = [
13
+ "contains",
14
+ "equals",
15
+ "notContains",
16
+ "notEquals",
17
+ "includingRegex",
18
+ "excludingRegex"
19
+ ];
20
+ const searchConsoleAggregationTypes = ["auto", "byPage", "byProperty"];
21
+ const searchConsoleDimensionFilterSchema = z.object({
22
+ dimension: z.enum(searchConsoleDimensions).describe("Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)"),
23
+ operator: z.enum(searchConsoleFilterOperators).describe("Filter operator supported by Search Console."),
24
+ expression: z.string().min(1).describe("Filter value or regex expression.")
25
+ });
26
+ const siteUrlSchemaField = z.string().optional().describe(
27
+ "Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool expects the site to be configured elsewhere."
28
+ );
29
+ const profileIdSchemaField = z.string().optional().describe("Optional business profile identifier for future multi-profile resolution. Defaults to the active profile when supported.");
30
+ const startDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Start date in YYYY-MM-DD format.");
31
+ const endDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("End date in YYYY-MM-DD format.");
32
+ const currentStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Current comparison period start date in YYYY-MM-DD format.");
33
+ const currentEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Current comparison period end date in YYYY-MM-DD format.");
34
+ const previousStartDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Previous comparison period start date in YYYY-MM-DD format.");
35
+ const previousEndDateSchemaField = z.string().regex(searchConsoleDateRegex).describe("Previous comparison period end date in YYYY-MM-DD format.");
36
+ const rowLimitSchemaField = z.number().int().min(1).max(25e3).optional().describe("Maximum number of rows to return. Search Console supports up to 25,000 rows per request.");
37
+ const startRowSchemaField = z.number().int().min(0).optional().describe("Zero-based row offset for paginated Search Console requests.");
38
+ const searchTypeSchemaField = z.enum(searchConsoleSearchTypes).optional().describe("Search type to query. Defaults to web.");
39
+ const brandTermsSchemaField = z.array(z.string().min(1)).max(50).optional().describe("Brand tokens used to separate branded from non-branded queries.");
40
+ function resolveSearchConsoleSiteUrl(siteUrl, profileId) {
41
+ const explicitSiteUrl = siteUrl?.trim();
42
+ if (explicitSiteUrl) {
43
+ return explicitSiteUrl;
44
+ }
45
+ if (profileId?.trim()) {
46
+ throw new Error(
47
+ `Missing Search Console site URL. profileId "${profileId}" was provided, but business-data based site resolution is not implemented yet. Pass siteUrl explicitly or configure Search Console site resolution first.`
48
+ );
49
+ }
50
+ throw new Error(
51
+ "Missing Search Console site URL. Pass siteUrl explicitly or first call gsc_list_accessible_sites to discover available properties."
52
+ );
53
+ }
54
+ function inferSearchConsolePropertyType(siteUrl) {
55
+ if (siteUrl.startsWith("sc-domain:")) {
56
+ return "domain";
57
+ }
58
+ if (/^https?:\/\//.test(siteUrl)) {
59
+ return "url_prefix";
60
+ }
61
+ return "unknown";
62
+ }
63
+ function normalizePermissionLevel(permissionLevel) {
64
+ return permissionLevel?.trim().toLowerCase() || void 0;
65
+ }
66
+ function normalizeSearchConsoleSiteEntry(siteEntry) {
67
+ return {
68
+ site_url: siteEntry.siteUrl,
69
+ property_type: inferSearchConsolePropertyType(siteEntry.siteUrl),
70
+ permission_level: normalizePermissionLevel(siteEntry.permissionLevel)
71
+ };
72
+ }
73
+ function toPercent(numerator, denominator) {
74
+ if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {
75
+ return 0;
76
+ }
77
+ return numerator / denominator * 100;
78
+ }
79
+ function round(value, decimals = 2) {
80
+ if (!Number.isFinite(value)) {
81
+ return 0;
82
+ }
83
+ const factor = 10 ** decimals;
84
+ return Math.round(value * factor) / factor;
85
+ }
86
+ function normalizeSearchConsoleRow(row, dimensions = []) {
87
+ const clicks = row.clicks ?? 0;
88
+ const impressions = row.impressions ?? 0;
89
+ const ctr = typeof row.ctr === "number" ? row.ctr * 100 : toPercent(clicks, impressions);
90
+ const keys = row.keys ?? [];
91
+ return {
92
+ keys,
93
+ dimensions: dimensions.reduce((accumulator, dimension, index) => {
94
+ accumulator[dimension] = keys[index] ?? "";
95
+ return accumulator;
96
+ }, {}),
97
+ clicks: round(clicks, 2),
98
+ impressions: round(impressions, 2),
99
+ ctr_percent: round(ctr, 2),
100
+ position: round(row.position ?? 0, 2)
101
+ };
102
+ }
103
+ function buildSearchConsoleQueryBody(input) {
104
+ const dimensionFilterGroups = input.dimensionFilters && input.dimensionFilters.length > 0 ? [
105
+ {
106
+ groupType: "and",
107
+ filters: input.dimensionFilters
108
+ }
109
+ ] : void 0;
110
+ return {
111
+ startDate: input.startDate,
112
+ endDate: input.endDate,
113
+ dimensions: input.dimensions,
114
+ type: input.searchType ?? "web",
115
+ dimensionFilterGroups,
116
+ aggregationType: input.aggregationType,
117
+ rowLimit: input.rowLimit,
118
+ startRow: input.startRow
119
+ };
120
+ }
121
+ function buildPaginationMetadata(input) {
122
+ const startRow = input.startRow ?? 0;
123
+ const rowLimit = input.rowLimit;
124
+ const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit;
125
+ const nextStartRow = isPartial ? startRow + input.returnedRows : void 0;
126
+ return {
127
+ is_partial: isPartial,
128
+ start_row: startRow,
129
+ row_limit: rowLimit,
130
+ returned_rows: input.returnedRows,
131
+ next_start_row: nextStartRow,
132
+ continuation_instructions: nextStartRow ? "This Search Console response may be truncated at row_limit. If you need additional rows, call the same tool again with startRow set to next_start_row." : void 0
133
+ };
134
+ }
135
+ function parseBrandTerms(brandTerms) {
136
+ return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean);
137
+ }
138
+ function isBrandedQuery(query, brandTerms) {
139
+ const normalizedQuery = query.trim().toLowerCase();
140
+ const normalizedBrandTerms = parseBrandTerms(brandTerms);
141
+ if (!normalizedQuery || normalizedBrandTerms.length === 0) {
142
+ return false;
143
+ }
144
+ return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm));
145
+ }
146
+ function classifyBrandLabel(query, brandTerms) {
147
+ return isBrandedQuery(query, brandTerms) ? "brand" : "non_brand";
148
+ }
149
+ function computeChangePercent(currentValue, previousValue) {
150
+ if (!Number.isFinite(currentValue) || !Number.isFinite(previousValue)) {
151
+ return null;
152
+ }
153
+ if (previousValue === 0) {
154
+ if (currentValue === 0) {
155
+ return 0;
156
+ }
157
+ return null;
158
+ }
159
+ return round((currentValue - previousValue) / previousValue * 100, 2);
160
+ }
161
+ function sumSearchConsoleRows(rows) {
162
+ const totals = rows.reduce(
163
+ (accumulator, row) => {
164
+ accumulator.clicks += row.clicks;
165
+ accumulator.impressions += row.impressions;
166
+ accumulator.weightedPosition += row.position * row.impressions;
167
+ return accumulator;
168
+ },
169
+ {
170
+ clicks: 0,
171
+ impressions: 0,
172
+ weightedPosition: 0
173
+ }
174
+ );
175
+ return {
176
+ clicks: round(totals.clicks, 2),
177
+ impressions: round(totals.impressions, 2),
178
+ ctrPercent: round(toPercent(totals.clicks, totals.impressions), 2),
179
+ averagePosition: totals.impressions > 0 ? round(totals.weightedPosition / totals.impressions, 2) : 0
180
+ };
181
+ }
182
+ function buildComparisonMap(rows, keyBuilder) {
183
+ return new Map(rows.map((row) => [keyBuilder(row), row]));
184
+ }
185
+ function tokenizeQuery(query) {
186
+ return query.toLowerCase().split(/[^a-z0-9áéíóúüñ]+/i).map((token) => token.trim()).filter(Boolean);
187
+ }
188
+ function looksLikeCommercialQuery(query) {
189
+ const tokens = tokenizeQuery(query);
190
+ const commercialTerms = /* @__PURE__ */ new Set([
191
+ "comprar",
192
+ "precio",
193
+ "precios",
194
+ "cuanto",
195
+ "oferta",
196
+ "ofertas",
197
+ "envio",
198
+ "shop",
199
+ "tienda",
200
+ "modelo",
201
+ "medida",
202
+ "ml",
203
+ "cm",
204
+ "kg"
205
+ ]);
206
+ return tokens.some((token) => commercialTerms.has(token));
207
+ }
208
+ function looksLikeProductQuery(query, productTerms) {
209
+ const normalizedProductTerms = parseBrandTerms(productTerms);
210
+ const normalizedQuery = query.toLowerCase();
211
+ if (normalizedProductTerms.some((term) => normalizedQuery.includes(term))) {
212
+ return true;
213
+ }
214
+ return looksLikeCommercialQuery(query) || tokenizeQuery(query).length >= 3;
215
+ }
216
+ function classifyQuickWinType(position, ctrPercent) {
217
+ if (position <= 8 && ctrPercent <= 3) {
218
+ return "ctr";
219
+ }
220
+ return "position";
221
+ }
222
+ function scoreHighImpressionLowClickOpportunity(input) {
223
+ const impressionScore = Math.min(input.impressions / 100, 60);
224
+ const ctrPenalty = Math.max(0, 15 - input.ctrPercent) * 2;
225
+ const positionBonus = input.position > 0 && input.position <= 20 ? Math.max(0, 20 - input.position) : 0;
226
+ return round(impressionScore + ctrPenalty + positionBonus, 2);
227
+ }
228
+ function scoreQuickWinOpportunity(input) {
229
+ const impressionWeight = Math.min(input.impressions / 150, 50);
230
+ const ctrWeight = Math.max(0, 10 - input.ctrPercent) * 3;
231
+ const positionWeight = input.position > 0 && input.position <= 20 ? Math.max(0, 18 - input.position) * 1.8 : 0;
232
+ return round(impressionWeight + ctrWeight + positionWeight, 2);
233
+ }
234
+ export {
235
+ brandTermsSchemaField,
236
+ buildComparisonMap,
237
+ buildPaginationMetadata,
238
+ buildSearchConsoleQueryBody,
239
+ classifyBrandLabel,
240
+ classifyQuickWinType,
241
+ computeChangePercent,
242
+ currentEndDateSchemaField,
243
+ currentStartDateSchemaField,
244
+ endDateSchemaField,
245
+ inferSearchConsolePropertyType,
246
+ isBrandedQuery,
247
+ looksLikeCommercialQuery,
248
+ looksLikeProductQuery,
249
+ normalizePermissionLevel,
250
+ normalizeSearchConsoleRow,
251
+ normalizeSearchConsoleSiteEntry,
252
+ parseBrandTerms,
253
+ previousEndDateSchemaField,
254
+ previousStartDateSchemaField,
255
+ profileIdSchemaField,
256
+ resolveSearchConsoleSiteUrl,
257
+ round,
258
+ rowLimitSchemaField,
259
+ scoreHighImpressionLowClickOpportunity,
260
+ scoreQuickWinOpportunity,
261
+ searchConsoleAggregationTypes,
262
+ searchConsoleDateRegex,
263
+ searchConsoleDimensionFilterSchema,
264
+ searchConsoleDimensions,
265
+ searchConsoleFilterOperators,
266
+ searchConsoleSearchTypes,
267
+ searchTypeSchemaField,
268
+ siteUrlSchemaField,
269
+ startDateSchemaField,
270
+ startRowSchemaField,
271
+ sumSearchConsoleRows,
272
+ toPercent,
273
+ tokenizeQuery
274
+ };
275
+ //# sourceMappingURL=search-console-utils.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/search-console/search-console-utils.ts"],
4
+ "sourcesContent": ["import { z } from \"zod\";\n\nimport type {\n SearchConsoleDimensionFilter,\n SearchConsoleDimensionFilterGroup,\n SearchConsoleSearchAnalyticsRequest,\n SearchConsoleSearchAnalyticsRow,\n SearchConsoleSiteEntry,\n} from \"../services/search-console/search-console-client.js\";\n\nexport const searchConsoleDateRegex = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const searchConsoleSearchTypes = [\"web\", \"image\", \"video\", \"news\", \"discover\"] as const;\nexport const searchConsoleDimensions = [\n \"query\",\n \"page\",\n \"country\",\n \"device\",\n \"date\",\n \"searchAppearance\",\n] as const;\nexport const searchConsoleFilterOperators = [\n \"contains\",\n \"equals\",\n \"notContains\",\n \"notEquals\",\n \"includingRegex\",\n \"excludingRegex\",\n] as const;\nexport const searchConsoleAggregationTypes = [\"auto\", \"byPage\", \"byProperty\"] as const;\n\nexport const searchConsoleDimensionFilterSchema = z.object({\n dimension: z\n .enum(searchConsoleDimensions)\n .describe(\"Dimension to filter on in Search Console (query, page, country, device, date, or searchAppearance)\"),\n operator: z\n .enum(searchConsoleFilterOperators)\n .describe(\"Filter operator supported by Search Console.\"),\n expression: z.string().min(1).describe(\"Filter value or regex expression.\"),\n});\n\nexport const siteUrlSchemaField = z\n .string()\n .optional()\n .describe(\n \"Search Console site URL or domain property identifier (for example https://www.example.com/ or sc-domain:example.com). If omitted, the tool expects the site to be configured elsewhere.\"\n );\n\nexport const profileIdSchemaField = z\n .string()\n .optional()\n .describe(\"Optional business profile identifier for future multi-profile resolution. Defaults to the active profile when supported.\");\n\nexport const startDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Start date in YYYY-MM-DD format.\");\n\nexport const endDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"End date in YYYY-MM-DD format.\");\n\nexport const currentStartDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Current comparison period start date in YYYY-MM-DD format.\");\n\nexport const currentEndDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Current comparison period end date in YYYY-MM-DD format.\");\n\nexport const previousStartDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Previous comparison period start date in YYYY-MM-DD format.\");\n\nexport const previousEndDateSchemaField = z\n .string()\n .regex(searchConsoleDateRegex)\n .describe(\"Previous comparison period end date in YYYY-MM-DD format.\");\n\nexport const rowLimitSchemaField = z\n .number()\n .int()\n .min(1)\n .max(25000)\n .optional()\n .describe(\"Maximum number of rows to return. Search Console supports up to 25,000 rows per request.\");\n\nexport const startRowSchemaField = z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\"Zero-based row offset for paginated Search Console requests.\");\n\nexport const searchTypeSchemaField = z\n .enum(searchConsoleSearchTypes)\n .optional()\n .describe(\"Search type to query. Defaults to web.\");\n\nexport const brandTermsSchemaField = z\n .array(z.string().min(1))\n .max(50)\n .optional()\n .describe(\"Brand tokens used to separate branded from non-branded queries.\");\n\nexport function resolveSearchConsoleSiteUrl(siteUrl?: string, profileId?: string): string {\n const explicitSiteUrl = siteUrl?.trim();\n if (explicitSiteUrl) {\n return explicitSiteUrl;\n }\n\n if (profileId?.trim()) {\n throw new Error(\n `Missing Search Console site URL. profileId \"${profileId}\" was provided, but business-data based site resolution is not implemented yet. Pass siteUrl explicitly or configure Search Console site resolution first.`\n );\n }\n\n throw new Error(\n \"Missing Search Console site URL. Pass siteUrl explicitly or first call gsc_list_accessible_sites to discover available properties.\"\n );\n}\n\nexport function inferSearchConsolePropertyType(\n siteUrl: string\n): \"domain\" | \"url_prefix\" | \"unknown\" {\n if (siteUrl.startsWith(\"sc-domain:\")) {\n return \"domain\";\n }\n\n if (/^https?:\\/\\//.test(siteUrl)) {\n return \"url_prefix\";\n }\n\n return \"unknown\";\n}\n\nexport function normalizePermissionLevel(permissionLevel?: string): string | undefined {\n return permissionLevel?.trim().toLowerCase() || undefined;\n}\n\nexport function normalizeSearchConsoleSiteEntry(siteEntry: SearchConsoleSiteEntry) {\n return {\n site_url: siteEntry.siteUrl,\n property_type: inferSearchConsolePropertyType(siteEntry.siteUrl),\n permission_level: normalizePermissionLevel(siteEntry.permissionLevel),\n };\n}\n\nexport function toPercent(numerator: number, denominator: number): number {\n if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {\n return 0;\n }\n\n return (numerator / denominator) * 100;\n}\n\nexport function round(value: number, decimals = 2): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n\n const factor = 10 ** decimals;\n return Math.round(value * factor) / factor;\n}\n\nexport function normalizeSearchConsoleRow(\n row: SearchConsoleSearchAnalyticsRow,\n dimensions: readonly string[] = []\n) {\n const clicks = row.clicks ?? 0;\n const impressions = row.impressions ?? 0;\n const ctr = typeof row.ctr === \"number\" ? row.ctr * 100 : toPercent(clicks, impressions);\n const keys = row.keys ?? [];\n\n return {\n keys,\n dimensions: dimensions.reduce<Record<string, string>>((accumulator, dimension, index) => {\n accumulator[dimension] = keys[index] ?? \"\";\n return accumulator;\n }, {}),\n clicks: round(clicks, 2),\n impressions: round(impressions, 2),\n ctr_percent: round(ctr, 2),\n position: round(row.position ?? 0, 2),\n };\n}\n\nexport function buildSearchConsoleQueryBody(input: {\n startDate: string;\n endDate: string;\n dimensions?: string[];\n searchType?: string;\n dimensionFilters?: SearchConsoleDimensionFilter[];\n aggregationType?: string;\n rowLimit?: number;\n startRow?: number;\n}): SearchConsoleSearchAnalyticsRequest {\n const dimensionFilterGroups: SearchConsoleDimensionFilterGroup[] | undefined =\n input.dimensionFilters && input.dimensionFilters.length > 0\n ? [\n {\n groupType: \"and\",\n filters: input.dimensionFilters,\n },\n ]\n : undefined;\n\n return {\n startDate: input.startDate,\n endDate: input.endDate,\n dimensions: input.dimensions,\n type: input.searchType ?? \"web\",\n dimensionFilterGroups,\n aggregationType: input.aggregationType,\n rowLimit: input.rowLimit,\n startRow: input.startRow,\n };\n}\n\nexport function buildPaginationMetadata(input: {\n startRow?: number;\n rowLimit?: number;\n returnedRows: number;\n}) {\n const startRow = input.startRow ?? 0;\n const rowLimit = input.rowLimit;\n const isPartial = Boolean(rowLimit) && input.returnedRows === rowLimit;\n const nextStartRow = isPartial ? startRow + input.returnedRows : undefined;\n\n return {\n is_partial: isPartial,\n start_row: startRow,\n row_limit: rowLimit,\n returned_rows: input.returnedRows,\n next_start_row: nextStartRow,\n continuation_instructions: nextStartRow\n ? \"This Search Console response may be truncated at row_limit. If you need additional rows, call the same tool again with startRow set to next_start_row.\"\n : undefined,\n };\n}\n\nexport function parseBrandTerms(brandTerms?: string[]): string[] {\n return (brandTerms ?? []).map((term) => term.trim().toLowerCase()).filter(Boolean);\n}\n\nexport function isBrandedQuery(query: string, brandTerms?: string[]): boolean {\n const normalizedQuery = query.trim().toLowerCase();\n const normalizedBrandTerms = parseBrandTerms(brandTerms);\n\n if (!normalizedQuery || normalizedBrandTerms.length === 0) {\n return false;\n }\n\n return normalizedBrandTerms.some((brandTerm) => normalizedQuery.includes(brandTerm));\n}\n\nexport function classifyBrandLabel(query: string, brandTerms?: string[]): \"brand\" | \"non_brand\" {\n return isBrandedQuery(query, brandTerms) ? \"brand\" : \"non_brand\";\n}\n\nexport function computeChangePercent(currentValue: number, previousValue: number): number | null {\n if (!Number.isFinite(currentValue) || !Number.isFinite(previousValue)) {\n return null;\n }\n\n if (previousValue === 0) {\n if (currentValue === 0) {\n return 0;\n }\n\n return null;\n }\n\n return round(((currentValue - previousValue) / previousValue) * 100, 2);\n}\n\nexport function sumSearchConsoleRows(\n rows: Array<ReturnType<typeof normalizeSearchConsoleRow>>\n): {\n clicks: number;\n impressions: number;\n ctrPercent: number;\n averagePosition: number;\n} {\n const totals = rows.reduce(\n (accumulator, row) => {\n accumulator.clicks += row.clicks;\n accumulator.impressions += row.impressions;\n accumulator.weightedPosition += row.position * row.impressions;\n return accumulator;\n },\n {\n clicks: 0,\n impressions: 0,\n weightedPosition: 0,\n }\n );\n\n return {\n clicks: round(totals.clicks, 2),\n impressions: round(totals.impressions, 2),\n ctrPercent: round(toPercent(totals.clicks, totals.impressions), 2),\n averagePosition: totals.impressions > 0 ? round(totals.weightedPosition / totals.impressions, 2) : 0,\n };\n}\n\nexport function buildComparisonMap(\n rows: Array<ReturnType<typeof normalizeSearchConsoleRow>>,\n keyBuilder: (row: ReturnType<typeof normalizeSearchConsoleRow>) => string\n) {\n return new Map(rows.map((row) => [keyBuilder(row), row]));\n}\n\nexport function tokenizeQuery(query: string): string[] {\n return query\n .toLowerCase()\n .split(/[^a-z0-9\u00E1\u00E9\u00ED\u00F3\u00FA\u00FC\u00F1]+/i)\n .map((token) => token.trim())\n .filter(Boolean);\n}\n\nexport function looksLikeCommercialQuery(query: string): boolean {\n const tokens = tokenizeQuery(query);\n const commercialTerms = new Set([\n \"comprar\",\n \"precio\",\n \"precios\",\n \"cuanto\",\n \"oferta\",\n \"ofertas\",\n \"envio\",\n \"shop\",\n \"tienda\",\n \"modelo\",\n \"medida\",\n \"ml\",\n \"cm\",\n \"kg\",\n ]);\n\n return tokens.some((token) => commercialTerms.has(token));\n}\n\nexport function looksLikeProductQuery(query: string, productTerms?: string[]): boolean {\n const normalizedProductTerms = parseBrandTerms(productTerms);\n const normalizedQuery = query.toLowerCase();\n\n if (normalizedProductTerms.some((term) => normalizedQuery.includes(term))) {\n return true;\n }\n\n return looksLikeCommercialQuery(query) || tokenizeQuery(query).length >= 3;\n}\n\nexport function classifyQuickWinType(position: number, ctrPercent: number): \"ctr\" | \"position\" {\n if (position <= 8 && ctrPercent <= 3) {\n return \"ctr\";\n }\n\n return \"position\";\n}\n\nexport function scoreHighImpressionLowClickOpportunity(input: {\n impressions: number;\n ctrPercent: number;\n position: number;\n}): number {\n const impressionScore = Math.min(input.impressions / 100, 60);\n const ctrPenalty = Math.max(0, 15 - input.ctrPercent) * 2;\n const positionBonus = input.position > 0 && input.position <= 20 ? Math.max(0, 20 - input.position) : 0;\n\n return round(impressionScore + ctrPenalty + positionBonus, 2);\n}\n\nexport function scoreQuickWinOpportunity(input: {\n impressions: number;\n ctrPercent: number;\n position: number;\n}): number {\n const impressionWeight = Math.min(input.impressions / 150, 50);\n const ctrWeight = Math.max(0, 10 - input.ctrPercent) * 3;\n const positionWeight = input.position > 0 && input.position <= 20 ? Math.max(0, 18 - input.position) * 1.8 : 0;\n\n return round(impressionWeight + ctrWeight + positionWeight, 2);\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }