Files
s.esmaeili ea86636027 Initial commit: Hedena Gold monorepo.
First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
2026-10-15 17:19:10 +03:30

227 lines
11 KiB
JavaScript

/**
* The whole delivery flow, against the DOCKER COMPOSE stack.
*
* What the owner asked to see proven inside compose:
* 1. a sales expert signs into the app with the master code
* 2. an ordinary customer signs into the app with the master code
* 3. BOTH place an order
* 4. an ADMIN sees both of those orders in the admin panel
*
* Unlike the other scripts this one talks ONLY over HTTP — it never opens a
* Prisma client — because the point is to exercise the deployed container as a
* black box, including its own boot-time migration run. Setup and teardown go
* through the admin API, the same way an operator would.
*
* Usage: node scripts/test-compose-e2e.cjs [apiBaseUrl] [masterCode]
* e.g. node scripts/test-compose-e2e.cjs http://localhost:3100/api 1379
*/
const API = (process.argv[2] || 'http://localhost:3100/api').replace(/\/$/, '');
const MASTER = process.argv[3] || '1379';
const ADMIN_USER = process.argv[4] || '09128081392';
const ADMIN_PASS = process.argv[5] || '09128081392';
let pass = 0, 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);
const call = async (method, p, body, token) => {
const res = await fetch(`${API}/${p}`, {
method,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: 'Bearer ' + token } : {}),
},
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 signIn = (mobile) =>
call('POST', 'customer/auth/verify-otp', { username: mobile, password: MASTER });
const uniq = String(Date.now()).slice(-7);
const made = { customers: [], experts: [], orders: [] };
(async () => {
// ── ADMIN ───────────────────────────────────────────────────────────────
step('0 — ADMIN signs into the panel');
const adminLogin = await call('POST', 'admin/auth/login', {
phone: ADMIN_USER,
password: ADMIN_PASS,
});
chk(adminLogin.ok, `POST admin/auth/login -> ${adminLogin.status}`);
const adminToken = adminLogin.body?.api_token;
if (!adminToken) {
console.log('\n Could not sign in as admin; pass the credentials as argv[4]/argv[5].');
console.log(' Response: ' + adminLogin.raw.slice(0, 300));
process.exit(1);
}
chk(Boolean(adminToken), 'an admin token came back');
// ── SETUP through the ADMIN API, as an operator would ───────────────────
step('SETUP — create a sales expert and a customer through the admin panel');
const expertMobile = `0913${uniq}`;
const expert = await call('POST', 'admin/users', {
fname: 'کارشناس', lname: 'کامپوز',
mobile: expertMobile, password: 'Test12345',
role: 'SALES_EXPERT', percent: 3.5, gram_level: 150,
}, adminToken);
chk(expert.ok, `create the sales expert -> ${expert.status}` + (expert.ok ? '' : ' ' + expert.raw.slice(0, 160)));
if (expert.body?.id) made.experts.push(expert.body.id);
const custMobile = `0914${uniq}`;
const cust = await call('POST', 'admin/customers-omde/create', {
fname: 'مشتری', lname: 'کامپوز', name: 'مشتری کامپوز',
mobile: custMobile, percent: 3, type: 2,
store_name: 'گالری کامپوز', gram_level: 200,
}, adminToken);
chk(cust.ok, `create the customer -> ${cust.status}` + (cust.ok ? '' : ' ' + cust.raw.slice(0, 160)));
const custId = cust.body?.id;
if (custId) made.customers.push(custId);
// A product to order.
step('SETUP — find an orderable product variant');
const products = await call('GET', 'catalog/products');
chk(products.ok, `GET catalog/products -> ${products.status}`);
const variant = (products.body ?? [])
.flatMap((p) => p.variants ?? [])
.find((v) => v && v.id);
chk(Boolean(variant), `found a variant to order (${variant?.id ?? 'none'})`);
if (!variant) {
console.log(' No product variants in this database — cannot place orders.');
process.exit(1);
}
// ── 1. THE EXPERT SIGNS IN ──────────────────────────────────────────────
step('1 — THE SALES EXPERT signs into the app with the master code');
const expReq = await call('POST', 'customer/auth/request-otp', { username: expertMobile });
chk(expReq.ok, `request-otp -> ${expReq.status}`);
const expLogin = await signIn(expertMobile);
chk(expLogin.ok, `verify-otp with the master code -> ${expLogin.status}`);
const expToken = expLogin.body?.token;
chk(Boolean(expToken), 'an app token came back');
const whoami = await call('GET', 'expert/whoami', undefined, expToken);
chk(whoami.body?.isSalesExpert === true,
`the app is told this session is an expert (${whoami.body?.isSalesExpert})`);
// ── 2. THE CUSTOMER SIGNS IN ────────────────────────────────────────────
step('2 — AN ORDINARY CUSTOMER signs into the app with the master code');
await call('POST', 'customer/auth/request-otp', { username: custMobile });
const custLogin = await signIn(custMobile);
chk(custLogin.ok, `verify-otp with the master code -> ${custLogin.status}`);
const custToken = custLogin.body?.token;
chk(Boolean(custToken), 'an app token came back');
const custWho = await call('GET', 'expert/whoami', undefined, custToken);
chk(custWho.body?.isSalesExpert === false,
`and is NOT flagged as an expert (${custWho.body?.isSalesExpert})`);
// ── 3. BOTH PLACE AN ORDER ──────────────────────────────────────────────
step('3 — BOTH place an order from the app');
// The exact payload both apps send from the cart (CartView.vue:200-211):
// wagePercent comes from the variant the cart pinned. No customerId — the
// server takes the subject from the token (D21).
const placeOrder = async (token, who) => {
const r = await call('POST', 'orders', {
items: [
{
productVariantId: variant.id,
quantity: 1,
wagePercent: parseFloat(variant.defaultWagePercent ?? '0'),
},
],
}, token);
chk(r.ok, `${who} places an order -> ${r.status}${r.ok ? ' (' + r.body?.orderNumber + ')' : ' ' + r.raw.slice(0, 160)}`);
if (r.body?.id) made.orders.push({ id: r.body.id, number: r.body.orderNumber, who });
return r.body;
};
const expOrder = await placeOrder(expToken, 'the EXPERT');
const custOrder = await placeOrder(custToken, 'the CUSTOMER');
chk(Boolean(expOrder?.orderNumber && custOrder?.orderNumber),
'both orders came back with an order number');
chk(expOrder?.orderNumber !== custOrder?.orderNumber,
'and they are two distinct orders');
// Each sees their own order in their own history, and only their own.
const expHistory = await call('GET', 'customer/me/orders', undefined, expToken);
chk(expHistory.body?.some((o) => o.id === expOrder?.id),
"the expert's own order is in the expert's history");
chk(!expHistory.body?.some((o) => o.id === custOrder?.id),
"and the customer's order is NOT");
const custHistory = await call('GET', 'customer/me/orders', undefined, custToken);
chk(custHistory.body?.some((o) => o.id === custOrder?.id),
"the customer's own order is in the customer's history");
chk(!custHistory.body?.some((o) => o.id === expOrder?.id),
"and the expert's order is NOT");
// ── 4. THE ADMIN SEES BOTH ──────────────────────────────────────────────
step('4 — THE ADMIN sees both orders in the panel');
const list = await call('POST', 'admin/orders/list', { page: 1, limit: 100 }, adminToken);
chk(list.ok, `POST admin/orders/list -> ${list.status}`);
const rows = list.body?.rows ?? list.body?.data ?? list.body?.result ?? [];
chk(Array.isArray(rows) && rows.length > 0, `the grid returned ${rows.length} row(s)`);
const findRow = (id, number) =>
rows.find((r) => r.id === id || r.orderNumber === number);
chk(Boolean(findRow(expOrder?.id, expOrder?.orderNumber)),
`the EXPERT's order ${expOrder?.orderNumber} appears in the admin grid`);
chk(Boolean(findRow(custOrder?.id, custOrder?.orderNumber)),
`the CUSTOMER's order ${custOrder?.orderNumber} appears in the admin grid`);
// And the admin can open each one.
for (const o of made.orders) {
const detail = await call('GET', `orders/admin/${o.id}`, undefined, adminToken);
chk(detail.ok, `the admin can open ${o.who}'s order ${o.number} -> ${detail.status}`);
chk(detail.body?.items?.length > 0, `and it carries its items (${detail.body?.items?.length})`);
}
// ── 5. TOKEN BOUNDARIES still hold inside compose ───────────────────────
step('5 — the token boundaries hold in the deployed container too');
const appOnAdmin = await call('POST', 'admin/orders/list', { page: 1 }, expToken);
chk(appOnAdmin.status === 401 || appOnAdmin.status === 403,
`an expert's APP token on an admin route -> ${appOnAdmin.status}`);
const custOnExpert = await call('POST', 'expert/my-customers', { search: null }, custToken);
chk(custOnExpert.status === 403 || custOnExpert.status === 401,
`a customer's token on an expert route -> ${custOnExpert.status}`);
const adminOnApp = await call('GET', 'customer/me', undefined, adminToken);
chk(adminOnApp.status === 401, `an admin token on an app route -> ${adminOnApp.status}`);
// ── CLEANUP ─────────────────────────────────────────────────────────────
step('CLEANUP');
console.log(' This run created, through the admin API:');
for (const o of made.orders) console.log(` order ${o.number} (${o.who})`);
console.log(` customer ${custMobile}, sales expert ${expertMobile}`);
console.log(' They are LEFT IN PLACE deliberately: this is the compose stack, its');
console.log(' database is separate from development, and the rows are what the');
console.log(' owner will look at in the panel. Delete them from the admin panel');
console.log(' after the demo if they are not wanted.');
console.log(`\n${'='.repeat(60)}`);
console.log(`COMPOSE END-TO-END: passed ${pass} failed ${fail}`);
if (fail) { console.log('\nFailures:'); failures.forEach((f) => console.log(' - ' + f)); }
console.log('='.repeat(60));
process.exit(fail ? 1 : 0);
})().catch((e) => {
console.error('\nABORTED:', e);
process.exit(1);
});