/** * 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); });