309 lines
13 KiB
JavaScript
309 lines
13 KiB
JavaScript
/**
|
||||
|
|
* Delivery smoke test — the admin flows the owner listed (D19 step 3):
|
|||
|
|
*
|
|||
|
|
* login → create a customer → create and edit a sales expert
|
|||
|
|
* → create and edit a product with variants
|
|||
|
|
* → view an order and change its status
|
|||
|
|
*
|
|||
|
|
* This is a WORKING check, not a parity audit: each step asserts that the
|
|||
|
|
* operation actually took effect in the database, not merely that the HTTP
|
|||
|
|
* call returned 200. A 200 with a silently-dropped field is exactly the class
|
|||
|
|
* of bug this is meant to catch.
|
|||
|
|
*
|
|||
|
|
* Every Persian string is a literal in this file and travels as UTF-8 JSON —
|
|||
|
|
* never as a shell argument (D15).
|
|||
|
|
*
|
|||
|
|
* Usage: node scripts/smoke-admin.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 = (cond, msg) => {
|
|||
|
|
if (cond) {
|
|||
|
|
pass++;
|
|||
|
|
console.log(' PASS ' + msg);
|
|||
|
|
} else {
|
|||
|
|
fail++;
|
|||
|
|
failures.push(msg);
|
|||
|
|
console.log(' FAIL ' + msg);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
const step = (t) => console.log('\n' + t);
|
|||
|
|
|
|||
|
|
/** Records created by this run, removed at the end. */
|
|||
|
|
const created = { customers: [], adminUsers: [], products: [] };
|
|||
|
|
|
|||
|
|
let H = {};
|
|||
|
|
|
|||
|
|
const call = async (method, path, body) => {
|
|||
|
|
const res = await fetch(`${API}/${path}`, {
|
|||
|
|
method,
|
|||
|
|
headers: H,
|
|||
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|||
|
|
});
|
|||
|
|
let payload = null;
|
|||
|
|
try {
|
|||
|
|
payload = await res.json();
|
|||
|
|
} catch {
|
|||
|
|
/* empty body */
|
|||
|
|
}
|
|||
|
|
return { status: res.status, ok: res.ok, body: payload };
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
(async () => {
|
|||
|
|
// ── 1. LOGIN ────────────────────────────────────────────────────────────
|
|||
|
|
step('1 — ADMIN LOGIN');
|
|||
|
|
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();
|
|||
|
|
chk(login.status === 200 || login.status === 201, `POST admin/auth/login -> ${login.status}`);
|
|||
|
|
chk(typeof auth.api_token === 'string', 'a token came back');
|
|||
|
|
if (!auth.api_token) {
|
|||
|
|
console.log('\nCannot continue without a token.');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
H = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + auth.api_token };
|
|||
|
|
|
|||
|
|
const unique = String(Date.now()).slice(-7);
|
|||
|
|
|
|||
|
|
// ── 2. CREATE A CUSTOMER ────────────────────────────────────────────────
|
|||
|
|
step('2 — CREATE A WHOLESALE CUSTOMER');
|
|||
|
|
const custMobile = '0912' + unique;
|
|||
|
|
const cust = await call('POST', 'admin/customers-omde/create', {
|
|||
|
|
fname: 'سمیرا',
|
|||
|
|
lname: 'کریمی',
|
|||
|
|
name: 'سمیرا کریمی',
|
|||
|
|
mobile: custMobile,
|
|||
|
|
percent: 3,
|
|||
|
|
type: 2,
|
|||
|
|
store_name: 'گالری سمیرا',
|
|||
|
|
gram_level: 150,
|
|||
|
|
});
|
|||
|
|
chk(cust.ok, `POST customers-omde/create -> ${cust.status}`);
|
|||
|
|
const custId = cust.body?.id;
|
|||
|
|
if (custId) created.customers.push(custId);
|
|||
|
|
chk(Boolean(custId), 'an id came back');
|
|||
|
|
|
|||
|
|
if (custId) {
|
|||
|
|
const row = await prisma.customer.findUnique({ where: { id: custId } });
|
|||
|
|
chk(row?.firstName === 'سمیرا' && row?.lastName === 'کریمی', 'the name parts were stored separately');
|
|||
|
|
chk(row?.fullName === 'سمیرا کریمی', 'fullName was rebuilt by the trigger');
|
|||
|
|
chk(String(row?.makingChargeOffsetPercent) === '3', 'percent persisted as 3');
|
|||
|
|
chk(row?.storeName === 'گالری سمیرا', 'store_name persisted');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
step('2b — Persian digits in the mobile are normalised (D19 gap 2)');
|
|||
|
|
const faMobile = '۰۹۱۳' + unique;
|
|||
|
|
const faCust = await call('POST', 'admin/customers-omde/create', {
|
|||
|
|
fname: 'نگین',
|
|||
|
|
lname: 'مرادی',
|
|||
|
|
mobile: faMobile,
|
|||
|
|
percent: 3,
|
|||
|
|
type: 2,
|
|||
|
|
});
|
|||
|
|
chk(faCust.ok, `create with Persian digits -> ${faCust.status}`);
|
|||
|
|
if (faCust.body?.id) {
|
|||
|
|
created.customers.push(faCust.body.id);
|
|||
|
|
const row = await prisma.customer.findUnique({ where: { id: faCust.body.id } });
|
|||
|
|
chk(row?.mobile === '0913' + unique, `stored as ASCII "${row?.mobile}" (not Persian digits)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 3. SALES EXPERT: CREATE, READ BACK, EDIT ────────────────────────────
|
|||
|
|
step('3 — CREATE A SALES EXPERT');
|
|||
|
|
const expMobile = '0914' + unique;
|
|||
|
|
const exp = await call('POST', 'admin/users', {
|
|||
|
|
fname: 'محمد علی',
|
|||
|
|
lname: 'احمدی',
|
|||
|
|
name: 'محمد علی احمدی',
|
|||
|
|
mobile: expMobile,
|
|||
|
|
password: 'Test12345',
|
|||
|
|
role: 'SALES_EXPERT',
|
|||
|
|
percent: 4.5,
|
|||
|
|
gram_level: 250,
|
|||
|
|
});
|
|||
|
|
chk(exp.ok, `POST admin/users -> ${exp.status}`);
|
|||
|
|
const expId = exp.body?.id;
|
|||
|
|
if (expId) created.adminUsers.push(expId);
|
|||
|
|
chk(Boolean(expId), 'an id came back');
|
|||
|
|
|
|||
|
|
if (expId) {
|
|||
|
|
const row = await prisma.adminUser.findUnique({
|
|||
|
|
where: { id: expId },
|
|||
|
|
include: { salesExpertProfile: true },
|
|||
|
|
});
|
|||
|
|
chk(row?.firstName === 'محمد علی', 'a two-token first name was NOT re-split');
|
|||
|
|
chk(row?.lastName === 'احمدی', 'lastName stored');
|
|||
|
|
chk(Boolean(row?.salesExpertProfile), 'the SalesExpertProfile was auto-created');
|
|||
|
|
|
|||
|
|
step('3b — EDIT the sales expert (one field only)');
|
|||
|
|
const edit = await call('PUT', `admin/users/${expId}`, { gram_level: 999 });
|
|||
|
|
chk(edit.ok, `PUT admin/users/:id -> ${edit.status}`);
|
|||
|
|
const after = await prisma.adminUser.findUnique({
|
|||
|
|
where: { id: expId },
|
|||
|
|
include: { salesExpertProfile: true },
|
|||
|
|
});
|
|||
|
|
chk(String(after?.salesExpertProfile?.gramLevel) === '999', 'gram_level was persisted');
|
|||
|
|
chk(after?.firstName === 'محمد علی' && after?.lastName === 'احمدی', 'the name was untouched');
|
|||
|
|
|
|||
|
|
const grid = await call('POST', 'admin/customers-omde/reagent/list', { page: 1, limit: 200 });
|
|||
|
|
chk(grid.ok, `POST reagent/list -> ${grid.status}`);
|
|||
|
|
const mine = (grid.body?.rows ?? []).find((r) => r.id === expId);
|
|||
|
|
chk(Boolean(mine), 'the expert appears in the grid');
|
|||
|
|
chk(mine?.fname === 'محمد علی' && mine?.lname === 'احمدی', 'the grid shows the real name parts');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 4. PRODUCT + VARIANTS: CREATE, READ BACK, EDIT ──────────────────────
|
|||
|
|
step('4 — CREATE A PRODUCT WITH VARIANTS');
|
|||
|
|
const prod = await call('POST', 'admin/products/create', {
|
|||
|
|
title: 'انگشتر طلا ' + unique,
|
|||
|
|
body_html: 'توضیح آزمایشی',
|
|||
|
|
product_code: 'SMK-' + unique,
|
|||
|
|
show_type: 3,
|
|||
|
|
is_active: true,
|
|||
|
|
variants: [
|
|||
|
|
{ title: 'سایز ۱', grams: 2.5, percentage_pay: 7, qty: 4 },
|
|||
|
|
{ title: 'سایز ۲', grams: 3.25, percentage_pay: 7, qty: 0, is_pre_order: true },
|
|||
|
|
],
|
|||
|
|
});
|
|||
|
|
chk(prod.ok, `POST admin/products/create -> ${prod.status}`);
|
|||
|
|
const prodId = prod.body?.id;
|
|||
|
|
if (prodId) created.products.push(prodId);
|
|||
|
|
chk(Boolean(prodId), 'an id came back');
|
|||
|
|
|
|||
|
|
if (prodId) {
|
|||
|
|
const variants = await prisma.productVariant.findMany({
|
|||
|
|
where: { productId: prodId },
|
|||
|
|
orderBy: { title: 'asc' },
|
|||
|
|
});
|
|||
|
|
chk(variants.length === 2, `both variants were created (got ${variants.length})`);
|
|||
|
|
chk(
|
|||
|
|
variants.some((v) => String(v.rashWeightGrams) === '2.5'),
|
|||
|
|
'the 2.5g weight round-tripped exactly (no float drift)',
|
|||
|
|
);
|
|||
|
|
chk(
|
|||
|
|
variants.some((v) => String(v.defaultWagePercent) === '7'),
|
|||
|
|
'the making charge (اجرت) persisted as 7',
|
|||
|
|
);
|
|||
|
|
chk(variants.every((v) => Boolean(v.sku)), 'every variant got a SKU');
|
|||
|
|
chk(
|
|||
|
|
variants.some((v) => v.isPreOrder === true),
|
|||
|
|
'the pre-order flag persisted',
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
step('4b — OPEN the product for edit');
|
|||
|
|
const show = await call('GET', `admin/products/${prodId}`);
|
|||
|
|
chk(show.ok, `GET admin/products/:id -> ${show.status}`);
|
|||
|
|
chk(show.body?.title === 'انگشتر طلا ' + unique, 'the title reads back intact');
|
|||
|
|
// The form reads `variants_all` (AddProduct.vue:488), not `variants`.
|
|||
|
|
const shownVariants = show.body?.variants_all ?? [];
|
|||
|
|
chk(shownVariants.length === 2, `the edit form receives both variants (got ${shownVariants.length})`);
|
|||
|
|
|
|||
|
|
step('4c — EDIT the product');
|
|||
|
|
const upd = await call('PUT', `admin/products/${prodId}`, { title: 'انگشتر طلا ویرایششده' });
|
|||
|
|
chk(upd.ok, `PUT admin/products/:id -> ${upd.status}`);
|
|||
|
|
const afterP = await prisma.product.findUnique({ where: { id: prodId } });
|
|||
|
|
chk(afterP?.title === 'انگشتر طلا ویرایششده', 'the new title persisted');
|
|||
|
|
chk(afterP?.productCode === 'SMK-' + unique, 'product_code was NOT wiped by the partial edit');
|
|||
|
|
|
|||
|
|
const stillThere = await prisma.productVariant.count({ where: { productId: prodId } });
|
|||
|
|
chk(stillThere === 2, `the variants survived the edit (${stillThere} of 2)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 5. ORDERS: LIST, VIEW, CHANGE STATUS ────────────────────────────────
|
|||
|
|
step('5 — ORDER LIST / DETAIL / STATUS');
|
|||
|
|
const orders = await call('POST', 'admin/orders/list', { page: 1, limit: 5 });
|
|||
|
|
chk(orders.ok, `POST admin/orders/list -> ${orders.status}`);
|
|||
|
|
|
|||
|
|
const wholesale = await call('POST', 'admin/orders/wholesale/list', { page: 1, limit: 5 });
|
|||
|
|
chk(wholesale.ok, `POST admin/orders/wholesale/list -> ${wholesale.status}`);
|
|||
|
|
|
|||
|
|
const statusOpts = await call('GET', 'admin/orders/status-options');
|
|||
|
|
chk(statusOpts.ok, `GET admin/orders/status-options -> ${statusOpts.status}`);
|
|||
|
|
|
|||
|
|
const anyOrder =
|
|||
|
|
(orders.body?.rows ?? [])[0] ?? (wholesale.body?.rows ?? [])[0] ?? null;
|
|||
|
|
|
|||
|
|
if (!anyOrder) {
|
|||
|
|
console.log(' (no order exists in this database — detail/status checks skipped)');
|
|||
|
|
} else {
|
|||
|
|
const detail = await call('GET', `admin/orders/${anyOrder.id}`);
|
|||
|
|
chk(detail.ok, `GET admin/orders/:id -> ${detail.status}`);
|
|||
|
|
chk(Boolean(detail.body?.id), 'the order detail came back');
|
|||
|
|
|
|||
|
|
const before = await prisma.order.findUnique({ where: { id: anyOrder.id } });
|
|||
|
|
const options = Array.isArray(statusOpts.body) ? statusOpts.body : [];
|
|||
|
|
// Pick a status that is NOT the current one, so the write is observable.
|
|||
|
|
const targetOpt = options.find(
|
|||
|
|
(o) => String(o.id ?? o.value) !== String(before?.status),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (!targetOpt || !before) {
|
|||
|
|
console.log(' (no alternative status available — status change skipped)');
|
|||
|
|
} else {
|
|||
|
|
const target = String(targetOpt.id ?? targetOpt.value);
|
|||
|
|
// UpdateOrderStatusDto requires `orderId` — the exact field the retail
|
|||
|
|
// List.vue screen was failing to send (fixed in this batch).
|
|||
|
|
const res = await call('POST', 'admin/orders/status', {
|
|||
|
|
orderId: anyOrder.id,
|
|||
|
|
status: target,
|
|||
|
|
});
|
|||
|
|
chk(res.ok, `POST admin/orders/status -> ${res.status}`);
|
|||
|
|
|
|||
|
|
const after = await prisma.order.findUnique({ where: { id: anyOrder.id } });
|
|||
|
|
chk(String(after?.status) === target, `the status actually changed to ${target}`);
|
|||
|
|
|
|||
|
|
// Put it back exactly as it was — this is live data, not a fixture.
|
|||
|
|
await prisma.order.update({
|
|||
|
|
where: { id: anyOrder.id },
|
|||
|
|
data: { status: before.status },
|
|||
|
|
});
|
|||
|
|
const restored = await prisma.order.findUnique({ where: { id: anyOrder.id } });
|
|||
|
|
chk(restored?.status === before.status, 'the original status was restored');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── CLEANUP ─────────────────────────────────────────────────────────────
|
|||
|
|
step('CLEANUP — removing only the rows this run created');
|
|||
|
|
if (created.products.length) {
|
|||
|
|
await prisma.productVariant.deleteMany({ where: { productId: { in: created.products } } });
|
|||
|
|
await prisma.productImage.deleteMany({ where: { productId: { in: created.products } } });
|
|||
|
|
await prisma.product.deleteMany({ where: { id: { in: created.products } } });
|
|||
|
|
}
|
|||
|
|
if (created.adminUsers.length) {
|
|||
|
|
await prisma.salesExpertProfile.deleteMany({
|
|||
|
|
where: { adminUserId: { in: created.adminUsers } },
|
|||
|
|
});
|
|||
|
|
await prisma.adminUser.deleteMany({ where: { id: { in: created.adminUsers } } });
|
|||
|
|
}
|
|||
|
|
if (created.customers.length) {
|
|||
|
|
await prisma.customer.deleteMany({ where: { id: { in: created.customers } } });
|
|||
|
|
}
|
|||
|
|
console.log(
|
|||
|
|
` deleted: ${created.products.length} product(s) + their variants/images, ` +
|
|||
|
|
`${created.adminUsers.length} admin user(s) + profiles, ` +
|
|||
|
|
`${created.customers.length} customer(s). No pre-existing data was removed.`,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
console.log(`\n${'='.repeat(60)}`);
|
|||
|
|
console.log(`ADMIN 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('\nSMOKE ABORTED:', e);
|
|||
|
|
await prisma.$disconnect();
|
|||
|
|
process.exit(1);
|
|||
|
|
});
|