Compare commits
2
Commits
600ce06d15
...
ab39f6abed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab39f6abed | ||
|
|
ee43d664d5 |
+160
-9
@@ -101621,17 +101621,70 @@ var require_admin_users_service = __commonJS({
|
||||
}
|
||||
};
|
||||
}
|
||||
return this.prisma.adminUser.update({
|
||||
where: { id },
|
||||
data: patch,
|
||||
include: {
|
||||
userType: true,
|
||||
branch: true,
|
||||
avatarFile: true,
|
||||
salesExpertProfile: { include: { city: true } }
|
||||
}
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.adminUser.update({
|
||||
where: { id },
|
||||
data: patch,
|
||||
include: {
|
||||
userType: true,
|
||||
branch: true,
|
||||
avatarFile: true,
|
||||
salesExpertProfile: { include: { city: true } }
|
||||
}
|
||||
});
|
||||
await this.syncTradingAccount(tx, updated, existing.mobile, {
|
||||
name: patch.firstName !== void 0 || patch.lastName !== void 0 || patch.fullName !== void 0,
|
||||
mobile: patch.mobile !== void 0,
|
||||
gramLevel: data.gram_level !== void 0,
|
||||
percent: data.percent !== void 0
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Mirror an expert edit onto their trading Customer row (D21).
|
||||
*
|
||||
* The apps show an expert their OWN account through that row (/customer/me),
|
||||
* which is seeded from the expert once, at first sign-in, and was never
|
||||
* updated again — so a name or credit-limit change made here never reached
|
||||
* the app. In the reference the agent and that account were ONE row
|
||||
* (customers.type=1), so an edit changed both; this restores that.
|
||||
*
|
||||
* Only the fields the operator actually changed are written. The mobile is
|
||||
* copied only when no other customer holds it (the column is unique);
|
||||
* otherwise it is left and logged rather than failing the expert's save.
|
||||
*/
|
||||
async syncTradingAccount(tx, expert, previousMobile, changed) {
|
||||
const tradingId = expert.salesExpertProfile?.tradingCustomerId;
|
||||
if (!tradingId)
|
||||
return;
|
||||
const data = {};
|
||||
if (changed.name) {
|
||||
if (expert.firstName !== null || expert.lastName !== null) {
|
||||
data.firstName = expert.firstName;
|
||||
data.lastName = expert.lastName;
|
||||
} else {
|
||||
data.fullName = expert.fullName;
|
||||
}
|
||||
}
|
||||
if (changed.mobile && expert.mobile !== previousMobile) {
|
||||
const holder = await tx.customer.findUnique({ where: { mobile: expert.mobile }, select: { id: true } });
|
||||
if (!holder) {
|
||||
data.mobile = expert.mobile;
|
||||
} else if (holder.id !== tradingId) {
|
||||
this.logger.warn(`Expert ${expert.id}: new mobile ${expert.mobile} already belongs to customer ${holder.id}; trading account ${tradingId} keeps its old mobile.`);
|
||||
}
|
||||
}
|
||||
if (changed.gramLevel && expert.salesExpertProfile) {
|
||||
data.goldCreditLimitGrams = expert.salesExpertProfile.gramLevel;
|
||||
}
|
||||
if (changed.percent) {
|
||||
data.makingChargeOffsetPercent = expert.wagePercent ?? new client_1.Prisma.Decimal(0);
|
||||
}
|
||||
if (Object.keys(data).length > 0) {
|
||||
await tx.customer.update({ where: { id: tradingId }, data });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Legacy `DELETE users/{id}`.
|
||||
*
|
||||
@@ -163868,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) {
|
||||
@@ -164046,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() {
|
||||
@@ -164068,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();
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { AdminUsersService } from './admin-users.service';
|
||||
|
||||
/**
|
||||
* Editing a sales expert in the panel must reach the app: the app shows the
|
||||
* expert their own account through the linked trading Customer row, which is
|
||||
* only seeded once at first sign-in.
|
||||
*/
|
||||
describe('AdminUsersService — expert edit reaches the trading account', () => {
|
||||
const expertRow = {
|
||||
id: 'exp1',
|
||||
role: 'SALES_EXPERT',
|
||||
fullName: 'سعید اسماعیلی',
|
||||
firstName: 'سعید',
|
||||
lastName: 'اسماعیلی',
|
||||
mobile: '09902560722',
|
||||
wagePercent: new Prisma.Decimal(12),
|
||||
isActive: true,
|
||||
salesExpertProfile: { tradingCustomerId: 'cust1', gramLevel: new Prisma.Decimal(0) },
|
||||
};
|
||||
|
||||
function build(customerHolder: { id: string } | null = null) {
|
||||
const customerUpdate = jest.fn(async () => ({}));
|
||||
const tx = {
|
||||
adminUser: {
|
||||
update: jest.fn(async ({ data }: { data: Record<string, unknown> }) => ({
|
||||
...expertRow,
|
||||
...(data.firstName !== undefined ? { firstName: data.firstName, fullName: `${data.firstName} ${data.lastName}` } : {}),
|
||||
...(data.lastName !== undefined ? { lastName: data.lastName } : {}),
|
||||
...(data.mobile !== undefined ? { mobile: data.mobile } : {}),
|
||||
...(data.wagePercent !== undefined ? { wagePercent: data.wagePercent } : {}),
|
||||
salesExpertProfile: {
|
||||
...expertRow.salesExpertProfile,
|
||||
...((data.salesExpertProfile as { upsert?: { update?: { gramLevel?: Prisma.Decimal } } } | undefined)
|
||||
?.upsert?.update?.gramLevel !== undefined
|
||||
? { gramLevel: (data.salesExpertProfile as { upsert: { update: { gramLevel: Prisma.Decimal } } }).upsert.update.gramLevel }
|
||||
: {}),
|
||||
},
|
||||
})),
|
||||
},
|
||||
customer: { update: customerUpdate, findUnique: jest.fn(async () => customerHolder) },
|
||||
};
|
||||
const prisma = {
|
||||
...tx,
|
||||
admin: { findUnique: jest.fn(async () => null) },
|
||||
adminUser: {
|
||||
...tx.adminUser,
|
||||
findFirst: jest.fn(async () => expertRow),
|
||||
// the expert-table mobile clash check: no other expert holds the number
|
||||
findUnique: jest.fn(async () => null),
|
||||
},
|
||||
$transaction: jest.fn(async (fn: (t: typeof tx) => unknown) => fn(tx)),
|
||||
};
|
||||
const service = new AdminUsersService(prisma as never, {} as never, {} as never);
|
||||
return { service, customerUpdate };
|
||||
}
|
||||
|
||||
it('copies a name change to the trading account', async () => {
|
||||
const { service, customerUpdate } = build();
|
||||
await service.update('exp1', { fname: 'سعید1', lname: 'اسماعیلی1' });
|
||||
expect(customerUpdate).toHaveBeenCalledWith({
|
||||
where: { id: 'cust1' },
|
||||
data: { firstName: 'سعید1', lastName: 'اسماعیلی1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('copies the credit limit and wage percent the operator changed', async () => {
|
||||
const { service, customerUpdate } = build();
|
||||
await service.update('exp1', { gram_level: '50', percent: '3' });
|
||||
const data = (customerUpdate.mock.calls[0] as unknown as [{ data: Record<string, Prisma.Decimal> }])[0].data;
|
||||
expect(data.goldCreditLimitGrams.toString()).toBe('50');
|
||||
expect(data.makingChargeOffsetPercent.toString()).toBe('3');
|
||||
expect(data).not.toHaveProperty('firstName');
|
||||
});
|
||||
|
||||
it('copies a new mobile only when no other customer holds it', async () => {
|
||||
const free = build(null);
|
||||
await free.service.update('exp1', { mobile: '09120001111' });
|
||||
expect(free.customerUpdate).toHaveBeenCalledWith({ where: { id: 'cust1' }, data: { mobile: '09120001111' } });
|
||||
|
||||
const taken = build({ id: 'someone-else' });
|
||||
await taken.service.update('exp1', { mobile: '09120001111' });
|
||||
expect(taken.customerUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('writes nothing to the trading account when nothing it mirrors changed', async () => {
|
||||
const { service, customerUpdate } = build();
|
||||
await service.update('exp1', { address: 'تهران' });
|
||||
expect(customerUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -519,18 +519,89 @@ export class AdminUsersService {
|
||||
|
||||
// Experts sign in to the apps, whose sessions are not admin-panel sessions,
|
||||
// so there is nothing to revoke here.
|
||||
return this.prisma.adminUser.update({
|
||||
where: { id },
|
||||
data: patch,
|
||||
include: {
|
||||
userType: true,
|
||||
branch: true,
|
||||
avatarFile: true,
|
||||
salesExpertProfile: { include: { city: true } },
|
||||
},
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.adminUser.update({
|
||||
where: { id },
|
||||
data: patch,
|
||||
include: {
|
||||
userType: true,
|
||||
branch: true,
|
||||
avatarFile: true,
|
||||
salesExpertProfile: { include: { city: true } },
|
||||
},
|
||||
});
|
||||
await this.syncTradingAccount(tx, updated, existing.mobile, {
|
||||
name: patch.firstName !== undefined || patch.lastName !== undefined || patch.fullName !== undefined,
|
||||
mobile: patch.mobile !== undefined,
|
||||
gramLevel: data.gram_level !== undefined,
|
||||
percent: data.percent !== undefined,
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror an expert edit onto their trading Customer row (D21).
|
||||
*
|
||||
* The apps show an expert their OWN account through that row (/customer/me),
|
||||
* which is seeded from the expert once, at first sign-in, and was never
|
||||
* updated again — so a name or credit-limit change made here never reached
|
||||
* the app. In the reference the agent and that account were ONE row
|
||||
* (customers.type=1), so an edit changed both; this restores that.
|
||||
*
|
||||
* Only the fields the operator actually changed are written. The mobile is
|
||||
* copied only when no other customer holds it (the column is unique);
|
||||
* otherwise it is left and logged rather than failing the expert's save.
|
||||
*/
|
||||
private async syncTradingAccount(
|
||||
tx: Prisma.TransactionClient,
|
||||
expert: {
|
||||
id: string;
|
||||
fullName: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
mobile: string;
|
||||
wagePercent: Prisma.Decimal | null;
|
||||
salesExpertProfile: { tradingCustomerId: string | null; gramLevel: Prisma.Decimal } | null;
|
||||
},
|
||||
previousMobile: string,
|
||||
changed: { name: boolean; mobile: boolean; gramLevel: boolean; percent: boolean },
|
||||
) {
|
||||
const tradingId = expert.salesExpertProfile?.tradingCustomerId;
|
||||
if (!tradingId) return;
|
||||
|
||||
const data: Prisma.CustomerUpdateInput = {};
|
||||
if (changed.name) {
|
||||
// Write the parts; the customer_sync_full_name trigger rebuilds fullName.
|
||||
if (expert.firstName !== null || expert.lastName !== null) {
|
||||
data.firstName = expert.firstName;
|
||||
data.lastName = expert.lastName;
|
||||
} else {
|
||||
data.fullName = expert.fullName;
|
||||
}
|
||||
}
|
||||
if (changed.mobile && expert.mobile !== previousMobile) {
|
||||
const holder = await tx.customer.findUnique({ where: { mobile: expert.mobile }, select: { id: true } });
|
||||
if (!holder) {
|
||||
data.mobile = expert.mobile;
|
||||
} else if (holder.id !== tradingId) {
|
||||
this.logger.warn(
|
||||
`Expert ${expert.id}: new mobile ${expert.mobile} already belongs to customer ${holder.id}; trading account ${tradingId} keeps its old mobile.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (changed.gramLevel && expert.salesExpertProfile) {
|
||||
data.goldCreditLimitGrams = expert.salesExpertProfile.gramLevel;
|
||||
}
|
||||
if (changed.percent) {
|
||||
data.makingChargeOffsetPercent = expert.wagePercent ?? new Prisma.Decimal(0);
|
||||
}
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
await tx.customer.update({ where: { id: tradingId }, data });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy `DELETE users/{id}`.
|
||||
*
|
||||
|
||||
@@ -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<void> {
|
||||
logger.log('No pending migrations');
|
||||
}
|
||||
|
||||
// TEMPORARY — see diagnostics.ts. Never throws.
|
||||
await logDataDiagnostics(prisma);
|
||||
|
||||
if (process.env.RUN_SEED === 'false') return;
|
||||
|
||||
const seedScript = resolveSeedScript();
|
||||
|
||||
@@ -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<void> {
|
||||
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<Record<string, unknown>[]>(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 =====');
|
||||
}
|
||||
Reference in New Issue
Block a user