Files
hedena_gold/apps/api/scripts/scan-admin-b2b.cjs
T
s.esmaeiliandClaude Opus 5 6d0f5c5359 test(api): B2B admin scan + write-path smoke, both green
scan-admin-b2b.cjs (34 ok, 0 broken) hits every read endpoint the wholesale desk
uses daily. smoke-admin-b2b-writes.cjs (25/25) covers the money-touching writes:
admin order placement and a cash payment document, asserting the ledger and the
customer balance rather than the HTTP status.

Verified: the making charge computes exactly, a document is created unconfirmed,
confirming moves the gold debt, and confirming again toggles back and restores
the balance to the byte.

Retail/storefront surfaces were deliberately not scanned (D13).

No B2B breakage found. Three first-draft failures were test bugs, corrected
against the real callers: cities/list -> admin/cities/list, by-ids takes
products_id, and document type 4 is cash (2 is a transfer).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 00:25:53 +03:30

169 lines
7.3 KiB
JavaScript

/**
* B2B admin scan (D19 step 2) — hits every read endpoint the wholesale side of
* the admin panel uses on a normal working day, looking for things that are
* OUTRIGHT BROKEN (500s, crashes, empty required shapes).
*
* Scope per the owner: wholesale customers, sales experts, products and
* variants, wholesale orders, documents. Retail/storefront surfaces
* (single*, wallet, comments) are D13 — preserved, not scanned.
*
* This is a breakage scan, not a parity audit: a 200 with plausible data
* passes. Anything non-2xx is reported with its body so it can be triaged.
*
* Usage: node scripts/scan-admin-b2b.cjs [apiBaseUrl]
*/
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const API = (process.argv[2] || 'http://localhost:3000/api').replace(/\/$/, '');
const broken = [];
const ok = [];
let H = {};
async function probe(label, method, path, body) {
let res;
try {
res = await fetch(`${API}/${path}`, {
method,
headers: H,
body: body === undefined ? undefined : JSON.stringify(body),
});
} catch (e) {
broken.push({ label, method, path, status: 'NETWORK', detail: String(e) });
console.log(` ERR ${label} (${String(e)})`);
return null;
}
let payload = null;
const text = await res.text();
try {
payload = JSON.parse(text);
} catch {
/* non-JSON */
}
if (res.ok) {
ok.push(label);
const n = Array.isArray(payload)
? payload.length
: Array.isArray(payload?.rows)
? payload.rows.length
: null;
console.log(` ok ${label}${n === null ? '' : ` (${n} rows)`}`);
} else {
const detail =
(payload && (payload.message || payload.error)) || text.slice(0, 200);
broken.push({ label, method, path, status: res.status, detail });
console.log(` FAIL ${label} -> ${res.status} ${JSON.stringify(detail).slice(0, 160)}`);
}
return payload;
}
const page = { page: 1, limit: 5 };
(async () => {
const login = await fetch(`${API}/admin/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: '09128081392', password: '09128081392' }),
});
const auth = await login.json();
if (!auth.api_token) {
console.log('login failed');
process.exit(1);
}
H = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + auth.api_token };
console.log('\n── WHOLESALE CUSTOMERS ──────────────────────────────');
const omde = await probe('customers: omde list', 'POST', 'admin/customers-omde/list', page);
await probe('customers: banker list', 'POST', 'admin/customers-omde/banker/list', page);
await probe('customers: shopkeeper list', 'POST', 'admin/customers-omde/shopkeeper/list', page);
await probe('customers: non-verified list', 'POST', 'admin/customers-omde/non-verified/list', page);
await probe('customers: archived list', 'POST', 'admin/customers-omde/archived/list', page);
await probe('customers: referrer dropdown', 'GET', 'admin/customers-omde/query/reagent');
const aCustomer = omde?.rows?.[0];
if (aCustomer) {
await probe('customer: detail', 'GET', `admin/customers-omde/${aCustomer.id}`);
await probe('customer: dashboard', 'GET', `admin/customers-omde/dashboard/${aCustomer.id}`);
await probe('customer: orders tab', 'GET', `admin/customers-omde/${aCustomer.id}/orders`);
await probe('customer: documents tab', 'GET', `admin/customers-omde/${aCustomer.id}/documents`);
}
console.log('\n── SALES EXPERTS ────────────────────────────────────');
const reagents = await probe('experts: list', 'POST', 'admin/customers-omde/reagent/list', page);
const anExpert = reagents?.rows?.[0];
if (anExpert) {
await probe('expert: detail', 'GET', `admin/customers-omde/reagent/${anExpert.id}`);
await probe('expert: as admin user', 'GET', `admin/users/${anExpert.id}`);
}
console.log('\n── PRODUCTS & VARIANTS ──────────────────────────────');
const prods = await probe('products: list', 'POST', 'admin/products/list', page);
await probe('products: variants list', 'POST', 'admin/products/variants/list', page);
await probe('products: brands', 'GET', 'admin/products/brands');
await probe('products: brands table', 'POST', 'admin/products/brands-table', page);
await probe('products: collections', 'GET', 'admin/products/collections');
await probe('products: show-type options', 'GET', 'admin/products/show-type-options');
const aProduct = prods?.rows?.[0];
if (aProduct) {
await probe('product: detail (edit form)', 'GET', `admin/products/${aProduct.id}`);
await probe('product: its variants', 'GET', `admin/products/${aProduct.id}/variants`);
// The order builder posts `products_id` (stores/cart.ts:102), not `ids`.
const byIds = await probe('product: by-ids', 'POST', 'admin/products/by-ids', {
products_id: [aProduct.id],
});
if (byIds && Array.isArray(byIds) && byIds.length === 0) {
broken.push({
label: 'product: by-ids returned nothing for a known id',
method: 'POST',
path: 'admin/products/by-ids',
status: '200-empty',
detail: `asked for ${aProduct.id}, got []`,
});
console.log(' FAIL product: by-ids returned [] for a product that exists');
}
}
console.log('\n── WHOLESALE ORDERS ─────────────────────────────────');
const wOrders = await probe('orders: wholesale list', 'POST', 'admin/orders/wholesale/list', page);
await probe('orders: tamin list', 'POST', 'admin/orders/tamin/list', page);
await probe('orders: status options', 'GET', 'admin/orders/status-options');
await probe('orders: type options', 'GET', 'admin/orders/type-options');
const anOrder = wOrders?.rows?.[0];
if (anOrder) {
await probe('order: detail', 'GET', `admin/orders/${anOrder.id}`);
}
console.log('\n── DOCUMENTS ────────────────────────────────────────');
const docs = await probe('documents: list', 'POST', 'admin/documents/list', page);
const aDoc = docs?.rows?.[0];
if (aDoc) await probe('document: detail', 'GET', `admin/documents/${aDoc.id}`);
console.log('\n── SUPPORTING LOOKUPS THE B2B FORMS NEED ────────────');
await probe('cities: list', 'POST', 'admin/cities/list', { page: 1, limit: 500 });
await probe('branches: list', 'POST', 'admin/branches/list', page);
await probe('workshops: list', 'POST', 'admin/workshops/list', page);
await probe('pre-orders: list', 'POST', 'admin/pre-orders/list', page);
await probe('users: list', 'POST', 'admin/users/list', page);
console.log(`\n${'='.repeat(62)}`);
console.log(`B2B SCAN: ${ok.length} ok, ${broken.length} broken`);
if (broken.length) {
console.log('\nBROKEN:');
for (const b of broken) {
console.log(` - [${b.status}] ${b.label}`);
console.log(` ${b.method} ${b.path}`);
console.log(` ${JSON.stringify(b.detail).slice(0, 300)}`);
}
}
console.log('='.repeat(62));
await prisma.$disconnect();
process.exit(broken.length ? 1 : 0);
})().catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});