/** * D12 rule E0 round-trip test for the SALES EXPERT entity, plus the two grid * filters D17 required. * * All Persian input is written here as literals and sent as UTF-8 JSON, never * through a shell argument (D15). */ const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); // Port 3000 is API_PORT in the repo-root .env; overridable for another setup. // (Was hardcoded to 3077, a port from an earlier dev session, so the script // could only ever connect by accident.) const API = (process.argv[2] || 'http://localhost:3000/api').replace(/\/$/, ''); let pass = 0, fail = 0; const chk = (c, m) => { c ? pass++ : fail++; console.log((c ? ' PASS ' : ' FAIL ') + m); }; const FIRST = 'محمد علی'; const LAST = 'احمدی'; async function login() { const r = await fetch(API + '/admin/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: '09128081392', password: '09128081392' }), }); return (await r.json()).api_token; } const snap = async (id) => { const u = await prisma.adminUser.findUnique({ where: { id }, include: { salesExpertProfile: true }, }); const { updatedAt, ...profile } = u.salesExpertProfile ?? {}; return JSON.stringify({ fullName: u.fullName, firstName: u.firstName, lastName: u.lastName, mobile: u.mobile, role: u.role, wagePercent: String(u.wagePercent), address: u.address, storeName: u.storeName, profile: { ...profile, gramLevel: String(profile.gramLevel) }, }); }; (async () => { const token = await login(); const H = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }; const city = await prisma.city.findFirst({ select: { id: true, name: true } }); const city2 = await prisma.city.findFirst({ where: { id: { not: city.id } }, select: { id: true, name: true } }); const mobile = '0912' + String(Date.now()).slice(-7); console.log('STEP 1 — create a sales expert with EVERY field populated'); const createBody = { fname: FIRST, lname: LAST, mobile, password: 'Test12345', role: 'SALES_EXPERT', percent: 4.5, city_id: city.id, gram_level: 250.5, phone: '02188887777', ncode: '1234567890', address: 'تهران، خیابان ولیعصر', store_name: 'طلای محمد', pasazh_name: 'پاساژ بزرگ', mcode: 'SE-001', }; const cr = await fetch(API + '/admin/users', { method: 'POST', headers: H, body: JSON.stringify(createBody) }); const created = await cr.json(); if (!created.id) { console.log(' create failed:', JSON.stringify(created).slice(0, 300)); process.exit(1); } const id = created.id; console.log(' id:', id); const row = await prisma.adminUser.findUnique({ where: { id }, include: { salesExpertProfile: true } }); chk(row.firstName === FIRST, `firstName stored as «${FIRST}» — a two-token first name is NOT re-split`); chk(row.lastName === LAST, `lastName stored as «${LAST}»`); chk(row.fullName === `${FIRST} ${LAST}`, 'fullName rebuilt by the trigger'); chk(!!row.salesExpertProfile, 'SalesExpertProfile auto-created on create'); chk(row.salesExpertProfile?.cityId === city.id, 'profile.cityId stored'); chk(String(row.salesExpertProfile?.gramLevel) === '250.5', 'profile.gramLevel stored (250.5)'); chk(row.salesExpertProfile?.phone === '02188887777', 'profile.phone stored'); chk(row.salesExpertProfile?.nationalCode === '1234567890', 'profile.nationalCode stored'); console.log('\nSTEP 2 — open the edit form: every field populated, separately'); const g = await (await fetch(API + `/admin/users/${id}`, { headers: H })).json(); chk(g.fname === FIRST, `fname === «${FIRST}» (NOT the whole name)`); chk(g.lname === LAST, `lname === «${LAST}» (NOT empty)`); chk(Number(g.percent) === 4.5, 'percent === 4.5'); const listed = await (await fetch(API + '/admin/customers-omde/reagent/list', { method: 'POST', headers: H, body: JSON.stringify({ page: 1, limit: 200 }), })).json(); const mine = (listed.rows ?? []).find((x) => x.id === id); chk(!!mine, 'the new expert appears in the reagent grid'); chk(mine?.fname === FIRST && mine?.lname === LAST, 'grid shows the real name parts'); chk(mine?.city_id === city.id, 'grid returns a REAL city_id (was hardcoded null)'); chk(mine?.city?.title === city.name, 'grid returns the city object for the column'); chk(String(mine?.gram_level) === '250.5', 'grid returns a REAL gram_level (was hardcoded null)'); chk(mine?.phone === '02188887777', 'grid returns phone'); chk(mine?.ncode === '1234567890', 'grid returns ncode'); console.log('\nSTEP 3 — save with NO changes: the row must be byte-identical'); const before = await snap(id); await fetch(API + `/admin/users/${id}`, { method: 'PUT', headers: H, body: JSON.stringify({ fname: g.fname, lname: g.lname, mobile: g.mobile, role: 'SALES_EXPERT', percent: Number(g.percent), city_id: mine.city_id, gram_level: Number(mine.gram_level), phone: mine.phone, ncode: mine.ncode, address: g.address, store_name: g.store_name, pasazh_name: g.pasazh_name, mcode: g.mcode, }), }); const after = await snap(id); chk(before === after, 'no-op save changed nothing'); if (before !== after) console.log(' before: ' + before + '\n after : ' + after); console.log('\nSTEP 4a — change ONLY lastName'); await fetch(API + `/admin/users/${id}`, { method: 'PUT', headers: H, body: JSON.stringify({ lname: 'محمدی' }), }); const s4 = JSON.parse(await snap(id)); const b4 = JSON.parse(after); chk(s4.lastName === 'محمدی' && s4.firstName === FIRST, `only lastName changed; firstName still «${FIRST}»`); chk(s4.fullName === `${FIRST} محمدی`, 'fullName rebuilt by the trigger'); chk(JSON.stringify(s4.profile) === JSON.stringify(b4.profile), 'the profile was untouched'); console.log('\nSTEP 4b — change ONLY gramLevel'); const b5 = JSON.parse(await snap(id)); await fetch(API + `/admin/users/${id}`, { method: 'PUT', headers: H, body: JSON.stringify({ gram_level: 999.25 }), }); const s5 = JSON.parse(await snap(id)); const diff = Object.keys(s5.profile).filter((k) => String(s5.profile[k]) !== String(b5.profile[k])); chk(diff.length === 1 && diff[0] === 'gramLevel', 'only gramLevel changed: ' + diff.join(',')); chk(s5.firstName === b5.firstName && s5.lastName === b5.lastName, 'the name was untouched'); console.log('\nSTEP 5 — the two grid FILTERS'); const byCity = await (await fetch(API + '/admin/customers-omde/reagent/list', { method: 'POST', headers: H, body: JSON.stringify({ page: 1, limit: 200, filters: [{ field: 'city_id', op: '=', value: city.id }] }), })).json(); chk((byCity.rows ?? []).some((x) => x.id === id), `city filter (${city.name}) returns the expert`); chk((byCity.rows ?? []).every((x) => x.city_id === city.id), 'city filter returns ONLY that city'); if (city2) { const otherCity = await (await fetch(API + '/admin/customers-omde/reagent/list', { method: 'POST', headers: H, body: JSON.stringify({ page: 1, limit: 200, filters: [{ field: 'city_id', op: '=', value: city2.id }] }), })).json(); chk(!(otherCity.rows ?? []).some((x) => x.id === id), `filtering by a DIFFERENT city (${city2.name}) excludes it`); } const byGram = await (await fetch(API + '/admin/customers-omde/reagent/list', { method: 'POST', headers: H, body: JSON.stringify({ page: 1, limit: 200, filters: [{ field: 'gram_level', op: '=', value: 999.25 }] }), })).json(); chk((byGram.rows ?? []).some((x) => x.id === id), 'gram_level filter returns the expert'); chk((byGram.rows ?? []).every((x) => String(x.gram_level) === '999.25'), 'gram_level filter returns ONLY matches'); console.log('\nSTEP 6 — an expert with NO profile still lists (left join)'); const orphan = await prisma.adminUser.create({ data: { fullName: 'بدون پروفایل', firstName: 'بدون', lastName: 'پروفایل', mobile: '0913' + String(Date.now()).slice(-7), passwordHash: 'x', role: 'SALES_EXPERT' }, }); const l2 = await (await fetch(API + '/admin/customers-omde/reagent/list', { method: 'POST', headers: H, body: JSON.stringify({ page: 1, limit: 200 }), })).json(); const o = (l2.rows ?? []).find((x) => x.id === orphan.id); chk(!!o, 'an expert without a profile still appears in the grid'); chk(o?.city_id === null && o?.gram_level === null, 'its city/gram cells are null, not an error'); console.log('\nSTEP 7 — the referrer dropdown lists every active expert'); const opts = await (await fetch(API + '/admin/customers-omde/query/reagent', { headers: H })).json(); chk(opts.some((x) => x.id === id), 'the new expert is selectable as a referrer'); chk(opts.some((x) => x.id === orphan.id), 'the profile-less expert is selectable too'); await prisma.salesExpertProfile.deleteMany({ where: { adminUserId: { in: [id, orphan.id] } } }); await prisma.adminUser.deleteMany({ where: { id: { in: [id, orphan.id] } } }); console.log(`\nCleanup: deleted 2 test AdminUser rows (${id}, ${orphan.id}) and their profiles.`); console.log(`\npassed: ${pass} failed: ${fail}`); await prisma.$disconnect(); process.exit(fail ? 1 : 0); })().catch(async (e) => { console.error(e); await prisma.$disconnect(); process.exit(1); });