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>
169 lines
7.3 KiB
JavaScript
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);
|
|
});
|