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