Files

245 lines
9.8 KiB
JavaScript
Raw Permalink Normal View History

/**
* B2B admin WRITE paths (D19 step 2) — the money-touching operations the
* wholesale desk performs daily, which a read-only scan cannot cover:
*
* place an order for a customer from the admin panel
* record a payment document (cash) and confirm it
*
* Both move a customer's gold/rial debt, so each assertion checks the LEDGER,
* not just the HTTP status. Everything created here is removed again, and the
* customer's balances are compared before and after to prove nothing was left
* behind.
*
* Usage: node scripts/smoke-admin-b2b-writes.cjs [apiBaseUrl]
*/
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const API = (process.argv[2] || 'http://localhost:3000/api').replace(/\/$/, '');
let pass = 0;
let fail = 0;
const failures = [];
const chk = (c, m) => {
if (c) {
pass++;
console.log(' PASS ' + m);
} else {
fail++;
failures.push(m);
console.log(' FAIL ' + m);
}
};
const step = (t) => console.log('\n' + t);
let H = {};
const call = async (method, p, body) => {
const res = await fetch(`${API}/${p}`, {
method,
headers: H,
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await res.text();
let payload = null;
try {
payload = JSON.parse(text);
} catch {
/* empty */
}
return { status: res.status, ok: res.ok, body: payload, raw: text };
};
const createdOrders = [];
const createdEntries = [];
(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 };
// A WHOLESALE customer — retail is out of scope (D13).
const customer = await prisma.customer.findFirst({
where: { kind: { not: 'RETAIL' }, isActive: true, isArchived: false },
select: { id: true, fullName: true, goldDebtGrams: true, fiatDebtRial: true },
});
if (!customer) {
console.log('no wholesale customer available');
process.exit(1);
}
console.log(`Customer under test: ${customer.fullName}`);
console.log(` gold debt before: ${customer.goldDebtGrams} rial debt before: ${customer.fiatDebtRial}`);
const variant = await prisma.productVariant.findFirst({
where: { OR: [{ qty: { gt: 0 } }, { isPreOrder: true }] },
select: { id: true, title: true, rashWeightGrams: true, defaultWagePercent: true },
});
if (!variant) {
console.log('no orderable variant available');
process.exit(1);
}
// ── 1. ADMIN PLACES AN ORDER ────────────────────────────────────────────
step('1 — PLACE A WHOLESALE ORDER FROM THE ADMIN PANEL');
const wage = Number(variant.defaultWagePercent);
const order = await call('POST', 'admin/orders/create', {
customer_id: customer.id,
order_items: [{ vid: variant.id, qty: 2, percent: wage }],
note: 'سفارش آزمایشی تحویل',
});
chk(order.ok, `POST admin/orders/create -> ${order.status}` + (order.ok ? '' : ` ${order.raw.slice(0, 200)}`));
const orderId = order.body?.id ?? order.body?.order?.id;
if (orderId) createdOrders.push(orderId);
chk(Boolean(orderId), 'an order id came back');
if (orderId) {
const row = await prisma.order.findUnique({
where: { id: orderId },
include: { items: true },
});
chk(row?.customerId === customer.id, 'the order is attached to the chosen customer');
chk(row?.items.length === 1, 'the line item was saved');
chk(row?.items[0]?.quantity === 2, 'the quantity (2) was saved');
// The reference formula, per unit: rash * (1 + (wage - discount)/100)
const rash = Number(variant.rashWeightGrams);
const expectedPerUnit = rash * (1 + wage / 100);
const actualPerUnit = Number(row?.items[0]?.computedWeightGrams ?? 0);
chk(
Math.abs(actualPerUnit - expectedPerUnit) < 0.00005,
`making charge per unit: ${rash}g +${wage}% = ${expectedPerUnit.toFixed(4)} (got ${actualPerUnit})`,
);
chk(
Math.abs(Number(row?.totalGoldWeightGrams ?? 0) - expectedPerUnit * 2) < 0.00005,
`the order total is the per-unit weight x2 (${(expectedPerUnit * 2).toFixed(4)})`,
);
step('1b — the order appears in the wholesale grid');
const grid = await call('POST', 'admin/orders/wholesale/list', { page: 1, limit: 50 });
chk(grid.ok, `POST admin/orders/wholesale/list -> ${grid.status}`);
chk(
(grid.body?.rows ?? []).some((r) => r.id === orderId),
'the new order is listed',
);
step('1c — its detail opens');
const detail = await call('GET', `admin/orders/${orderId}`);
chk(detail.ok, `GET admin/orders/:id -> ${detail.status}`);
chk(Array.isArray(detail.body?.activity), 'the status timeline came back');
}
// ── 2. RECORD A PAYMENT DOCUMENT ────────────────────────────────────────
step('2 — RECORD A CASH PAYMENT DOCUMENT');
const before = await prisma.customer.findUnique({
where: { id: customer.id },
select: { goldDebtGrams: true, fiatDebtRial: true },
});
// type 4 = «پولی» (cash), which takes `price` + `price_per_gram`
// (addFinDoc.vue:33,104-115). Type 2 is «حواله» and takes a weight in
// `value` — sending cash fields with type 2 yields a zero amount and a 400.
const doc = await call('POST', 'admin/documents', {
type: 4,
customer_id: customer.id,
price: 10000000,
price_per_gram: 50000000,
note: 'پرداخت آزمایشی تحویل',
});
chk(doc.ok, `POST admin/documents -> ${doc.status}` + (doc.ok ? '' : ` ${doc.raw.slice(0, 200)}`));
const docId = doc.body?.id;
if (docId) createdEntries.push(docId);
chk(Boolean(docId), 'a document id came back');
if (docId) {
const entry = await prisma.ledgerEntry.findUnique({ where: { id: docId } });
chk(Boolean(entry), 'a ledger entry was written');
chk(entry?.customerId === customer.id, 'it is attached to the right customer');
chk(entry?.confirmed === false, 'it starts UNCONFIRMED, as the reference does');
step('2b — it appears in the documents grid');
const dlist = await call('POST', 'admin/documents/list', { page: 1, limit: 50 });
chk(dlist.ok, `POST admin/documents/list -> ${dlist.status}`);
chk((dlist.body?.rows ?? []).some((r) => r.id === docId), 'the new document is listed');
step('2c — CONFIRM it, and the customer balance moves');
const conf = await call('POST', `admin/documents/${docId}/confirm`);
chk(conf.ok, `POST admin/documents/:id/confirm -> ${conf.status}`);
const afterConfirm = await prisma.ledgerEntry.findUnique({ where: { id: docId } });
chk(afterConfirm?.confirmed === true, 'the entry is now confirmed');
const balAfter = await prisma.customer.findUnique({
where: { id: customer.id },
select: { goldDebtGrams: true, fiatDebtRial: true },
});
const moved =
String(balAfter?.goldDebtGrams) !== String(before?.goldDebtGrams) ||
String(balAfter?.fiatDebtRial) !== String(before?.fiatDebtRial);
chk(moved, `confirming moved the balance (gold ${before?.goldDebtGrams} -> ${balAfter?.goldDebtGrams})`);
step('2d — confirm is a TOGGLE, as the reference has it');
const unconf = await call('POST', `admin/documents/${docId}/confirm`);
chk(unconf.ok, `second confirm -> ${unconf.status}`);
const toggled = await prisma.ledgerEntry.findUnique({ where: { id: docId } });
chk(toggled?.confirmed === false, 'it toggled back to unconfirmed');
const balBack = await prisma.customer.findUnique({
where: { id: customer.id },
select: { goldDebtGrams: true, fiatDebtRial: true },
});
chk(
String(balBack?.goldDebtGrams) === String(before?.goldDebtGrams) &&
String(balBack?.fiatDebtRial) === String(before?.fiatDebtRial),
'un-confirming restored the balance exactly',
);
}
// ── CLEANUP ─────────────────────────────────────────────────────────────
step('CLEANUP');
if (createdEntries.length) {
await prisma.ledgerEntry.deleteMany({ where: { id: { in: createdEntries } } });
}
if (createdOrders.length) {
await prisma.orderItem.deleteMany({ where: { orderId: { in: createdOrders } } });
await prisma.orderStatusEvent.deleteMany({ where: { orderId: { in: createdOrders } } });
await prisma.ledgerEntry.deleteMany({ where: { orderId: { in: createdOrders } } });
await prisma.order.deleteMany({ where: { id: { in: createdOrders } } });
}
const finalBal = await prisma.customer.findUnique({
where: { id: customer.id },
select: { goldDebtGrams: true, fiatDebtRial: true },
});
chk(
String(finalBal?.goldDebtGrams) === String(customer.goldDebtGrams) &&
String(finalBal?.fiatDebtRial) === String(customer.fiatDebtRial),
`the customer's balances are exactly as they started ` +
`(gold ${finalBal?.goldDebtGrams}, rial ${finalBal?.fiatDebtRial})`,
);
console.log(
` deleted ${createdOrders.length} order(s) and ${createdEntries.length} ledger entry/entries ` +
'created by this run. No pre-existing data was removed.',
);
console.log(`\n${'='.repeat(60)}`);
console.log(`B2B WRITE SMOKE: passed ${pass} failed ${fail}`);
if (fail) {
console.log('\nFailures:');
failures.forEach((f) => console.log(' - ' + f));
}
console.log('='.repeat(60));
await prisma.$disconnect();
process.exit(fail ? 1 : 0);
})().catch(async (e) => {
console.error('\nABORTED:', e);
await prisma.$disconnect();
process.exit(1);
});