diff --git a/apps/api/release/app.js b/apps/api/release/app.js index 4fd4b43..a686b70 100644 --- a/apps/api/release/app.js +++ b/apps/api/release/app.js @@ -163921,6 +163921,102 @@ var require_app_module = __commonJS({ } }); +// dist/prisma/diagnostics.js +var require_diagnostics = __commonJS({ + "dist/prisma/diagnostics.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.logDataDiagnostics = logDataDiagnostics; + var common_12 = require_common(); + var QUERIES = [ + { + label: "migrations: last 5 applied", + sql: `SELECT name, applied_at FROM "_goldbank_migrations" ORDER BY applied_at DESC, name DESC LIMIT 5` + }, + { + label: "customers by kind / active / archived / step", + sql: `SELECT kind, "isActive" AS active, "isArchived" AS archived, "verificationStep" AS step, count(*)::int AS n + FROM "Customer" GROUP BY 1,2,3,4 ORDER BY n DESC` + }, + { + label: 'omde list ("all" scope) \u2014 what the panel can show', + sql: `SELECT + count(*)::int AS app_shopkeeper_not_archived, + count(*) FILTER (WHERE p.id IS NOT NULL)::int AS of_which_expert_trading_accounts, + count(*) FILTER (WHERE p.id IS NULL)::int AS listed_in_omde + FROM "Customer" c + LEFT JOIN "SalesExpertProfile" p ON p."tradingCustomerId" = c.id + WHERE c.kind = 'APP_SHOPKEEPER' AND c."isArchived" = false` + }, + { + label: "expert trading accounts: created for the expert vs adopted existing customer", + sql: `SELECT + count(*)::int AS trading_accounts, + count(*) FILTER (WHERE c."createdAt" < a."createdAt")::int AS customer_older_than_expert, + count(*) FILTER (WHERE c."createdAt" >= a."createdAt")::int AS customer_newer_than_expert, + count(*) FILTER (WHERE c.mobile = a.mobile)::int AS same_mobile, + count(*) FILTER (WHERE c."referredByAgentId" IS NOT NULL)::int AS has_referrer, + count(*) FILTER (WHERE EXISTS (SELECT 1 FROM "Order" o WHERE o."customerId" = c.id))::int AS has_orders, + count(*) FILTER (WHERE c."firstName" IS DISTINCT FROM a."firstName" + OR c."lastName" IS DISTINCT FROM a."lastName")::int AS name_differs_from_expert + FROM "SalesExpertProfile" p + JOIN "Customer" c ON c.id = p."tradingCustomerId" + JOIN "AdminUser" a ON a.id = p."adminUserId"` + }, + { + label: "AdminUser by role / active (experts + pre-split admin copies)", + sql: `SELECT role, "isActive" AS active, count(*)::int AS n FROM "AdminUser" GROUP BY 1,2 ORDER BY n DESC` + }, + { + label: "experts: profile / trading account", + sql: `SELECT + count(*)::int AS experts, + count(*) FILTER (WHERE p.id IS NULL)::int AS no_profile, + count(*) FILTER (WHERE p."tradingCustomerId" IS NOT NULL)::int AS with_trading_account, + count(*) FILTER (WHERE EXISTS (SELECT 1 FROM "Customer" c WHERE c.mobile = a.mobile))::int AS mobile_also_a_customer + FROM "AdminUser" a + LEFT JOIN "SalesExpertProfile" p ON p."adminUserId" = a.id + WHERE a.role = 'SALES_EXPERT'` + }, + { + label: "experts: when created (by day, last 10 days with rows)", + sql: `SELECT date_trunc('day', "createdAt")::date AS day, count(*)::int AS n + FROM "AdminUser" WHERE role = 'SALES_EXPERT' GROUP BY 1 ORDER BY 1 DESC LIMIT 10` + }, + { + label: "customers: when created (by day, last 10 days with rows)", + sql: `SELECT date_trunc('day', "createdAt")::date AS day, count(*)::int AS n + FROM "Customer" GROUP BY 1 ORDER BY 1 DESC LIMIT 10` + }, + { + label: "customers with a referring expert", + sql: `SELECT count(*) FILTER (WHERE "referredByAgentId" IS NOT NULL)::int AS referred, + count(*)::int AS all_customers + FROM "Customer"` + }, + { + label: "Admin table", + sql: `SELECT role, "isActive" AS active, count(*)::int AS n FROM "Admin" GROUP BY 1,2 ORDER BY n DESC` + } + ]; + async function logDataDiagnostics(prisma) { + if (process.env.DIAG_LOG === "false") + return; + const logger = new common_12.Logger("DIAG"); + logger.log("[DIAG] ===== data diagnostics start (counts only, read-only) ====="); + for (const q of QUERIES) { + try { + const rows = await prisma.$queryRawUnsafe(q.sql); + logger.log(`[DIAG] ${q.label}: ${JSON.stringify(rows, (_k, v) => typeof v === "bigint" ? Number(v) : v)}`); + } catch (error) { + logger.warn(`[DIAG] ${q.label}: FAILED \u2014 ${error.message.split("\n").pop()}`); + } + } + logger.log("[DIAG] ===== data diagnostics end ====="); + } + } +}); + // dist/prisma/migrate.js var require_migrate = __commonJS({ "dist/prisma/migrate.js"(exports2) { @@ -164099,6 +164195,7 @@ var require_bootstrap = __commonJS({ var util_1 = require("util"); var common_12 = require_common(); var client_1 = require("@prisma/client"); + var diagnostics_1 = require_diagnostics(); var migrate_1 = require_migrate(); var execFileAsync = (0, util_1.promisify)(child_process_1.execFile); function resolveSeedScript() { @@ -164121,6 +164218,7 @@ var require_bootstrap = __commonJS({ } else { logger.log("No pending migrations"); } + await (0, diagnostics_1.logDataDiagnostics)(prisma); if (process.env.RUN_SEED === "false") return; const seedScript = resolveSeedScript(); diff --git a/apps/api/src/prisma/bootstrap.ts b/apps/api/src/prisma/bootstrap.ts index 42c6c94..1fec71d 100644 --- a/apps/api/src/prisma/bootstrap.ts +++ b/apps/api/src/prisma/bootstrap.ts @@ -4,6 +4,7 @@ import { join } from 'path'; import { promisify } from 'util'; import { Logger } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; +import { logDataDiagnostics } from './diagnostics'; import { runMigrations, runOnce } from './migrate'; const execFileAsync = promisify(execFile); @@ -40,6 +41,9 @@ export async function prepareDatabase(): Promise { logger.log('No pending migrations'); } + // TEMPORARY — see diagnostics.ts. Never throws. + await logDataDiagnostics(prisma); + if (process.env.RUN_SEED === 'false') return; const seedScript = resolveSeedScript(); diff --git a/apps/api/src/prisma/diagnostics.ts b/apps/api/src/prisma/diagnostics.ts new file mode 100644 index 0000000..b3bfab6 --- /dev/null +++ b/apps/api/src/prisma/diagnostics.ts @@ -0,0 +1,105 @@ +import { Logger } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +/** + * TEMPORARY boot-time data diagnostics — prints aggregate COUNTS only to the + * container log, prefixed "[DIAG]", so the production state behind two open + * issues can be read from Dokploy's log without database access: + * + * 1. «لیست مشتریان عمده» is empty after the last deploy; + * 2. a sales expert's app data vs the panel. + * + * Read-only (SELECT only). No mobile, name or id is printed. Every query is + * isolated, so one failing query (e.g. a missing table) is reported and the + * rest still run; nothing here can stop the API from booting. + * + * Disable with DIAG_LOG=false. Remove this file and its call in bootstrap.ts + * once the issues are closed. + */ +const QUERIES: { label: string; sql: string }[] = [ + { + label: 'migrations: last 5 applied', + sql: `SELECT name, applied_at FROM "_goldbank_migrations" ORDER BY applied_at DESC, name DESC LIMIT 5`, + }, + { + label: 'customers by kind / active / archived / step', + sql: `SELECT kind, "isActive" AS active, "isArchived" AS archived, "verificationStep" AS step, count(*)::int AS n + FROM "Customer" GROUP BY 1,2,3,4 ORDER BY n DESC`, + }, + { + label: 'omde list ("all" scope) — what the panel can show', + sql: `SELECT + count(*)::int AS app_shopkeeper_not_archived, + count(*) FILTER (WHERE p.id IS NOT NULL)::int AS of_which_expert_trading_accounts, + count(*) FILTER (WHERE p.id IS NULL)::int AS listed_in_omde + FROM "Customer" c + LEFT JOIN "SalesExpertProfile" p ON p."tradingCustomerId" = c.id + WHERE c.kind = 'APP_SHOPKEEPER' AND c."isArchived" = false`, + }, + { + label: 'expert trading accounts: created for the expert vs adopted existing customer', + sql: `SELECT + count(*)::int AS trading_accounts, + count(*) FILTER (WHERE c."createdAt" < a."createdAt")::int AS customer_older_than_expert, + count(*) FILTER (WHERE c."createdAt" >= a."createdAt")::int AS customer_newer_than_expert, + count(*) FILTER (WHERE c.mobile = a.mobile)::int AS same_mobile, + count(*) FILTER (WHERE c."referredByAgentId" IS NOT NULL)::int AS has_referrer, + count(*) FILTER (WHERE EXISTS (SELECT 1 FROM "Order" o WHERE o."customerId" = c.id))::int AS has_orders, + count(*) FILTER (WHERE c."firstName" IS DISTINCT FROM a."firstName" + OR c."lastName" IS DISTINCT FROM a."lastName")::int AS name_differs_from_expert + FROM "SalesExpertProfile" p + JOIN "Customer" c ON c.id = p."tradingCustomerId" + JOIN "AdminUser" a ON a.id = p."adminUserId"`, + }, + { + label: 'AdminUser by role / active (experts + pre-split admin copies)', + sql: `SELECT role, "isActive" AS active, count(*)::int AS n FROM "AdminUser" GROUP BY 1,2 ORDER BY n DESC`, + }, + { + label: 'experts: profile / trading account', + sql: `SELECT + count(*)::int AS experts, + count(*) FILTER (WHERE p.id IS NULL)::int AS no_profile, + count(*) FILTER (WHERE p."tradingCustomerId" IS NOT NULL)::int AS with_trading_account, + count(*) FILTER (WHERE EXISTS (SELECT 1 FROM "Customer" c WHERE c.mobile = a.mobile))::int AS mobile_also_a_customer + FROM "AdminUser" a + LEFT JOIN "SalesExpertProfile" p ON p."adminUserId" = a.id + WHERE a.role = 'SALES_EXPERT'`, + }, + { + label: 'experts: when created (by day, last 10 days with rows)', + sql: `SELECT date_trunc('day', "createdAt")::date AS day, count(*)::int AS n + FROM "AdminUser" WHERE role = 'SALES_EXPERT' GROUP BY 1 ORDER BY 1 DESC LIMIT 10`, + }, + { + label: 'customers: when created (by day, last 10 days with rows)', + sql: `SELECT date_trunc('day', "createdAt")::date AS day, count(*)::int AS n + FROM "Customer" GROUP BY 1 ORDER BY 1 DESC LIMIT 10`, + }, + { + label: 'customers with a referring expert', + sql: `SELECT count(*) FILTER (WHERE "referredByAgentId" IS NOT NULL)::int AS referred, + count(*)::int AS all_customers + FROM "Customer"`, + }, + { + label: 'Admin table', + sql: `SELECT role, "isActive" AS active, count(*)::int AS n FROM "Admin" GROUP BY 1,2 ORDER BY n DESC`, + }, +]; + +export async function logDataDiagnostics(prisma: PrismaClient): Promise { + if (process.env.DIAG_LOG === 'false') return; + const logger = new Logger('DIAG'); + + logger.log('[DIAG] ===== data diagnostics start (counts only, read-only) ====='); + for (const q of QUERIES) { + try { + const rows = await prisma.$queryRawUnsafe[]>(q.sql); + logger.log(`[DIAG] ${q.label}: ${JSON.stringify(rows, (_k, v) => (typeof v === 'bigint' ? Number(v) : v))}`); + } catch (error) { + logger.warn(`[DIAG] ${q.label}: FAILED — ${(error as Error).message.split('\n').pop()}`); + } + } + logger.log('[DIAG] ===== data diagnostics end ====='); +}