Files

173 lines
9.1 KiB
JavaScript
Raw Permalink Normal View History

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