Experts (AdminUser) and customers (Customer) sign in to the same apps by mobile but live in different tables, so the database cannot make the number unique across them. A number could be given to an expert and to a customer, and the login then silently merged the two (the customer row was adopted as the expert's trading account), which left the expert on a customer session and the panel's name edits unreachable. The reference rejected a duplicate at entry (`unique:customers`, the agent being a customers row too); this does the same where a number is typed in: - customer create/update (wholesale, banker, walk-in): refused when an ACTIVE expert holds the number; - expert create/update: refused when a customer holds it (the expert's own trading account excepted); - the app's self sign-up: refused when an active expert holds it. Only a number being created or CHANGED is checked, so existing duplicates keep signing in and stay editable; the login's "expert wins" adoption is untouched. Plain equality on the two unique columns, normalised with faToEn, so both lookups are index hits. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
118 lines
5.2 KiB
TypeScript
118 lines
5.2 KiB
TypeScript
import { Prisma } from '@prisma/client';
|
|
import { AdminUsersService } from './admin-users.service';
|
|
|
|
/**
|
|
* Editing a sales expert in the panel must reach the app: the app shows the
|
|
* expert their own account through the linked trading Customer row, which is
|
|
* only seeded once at first sign-in.
|
|
*/
|
|
describe('AdminUsersService — expert edit reaches the trading account', () => {
|
|
const expertRow = {
|
|
id: 'exp1',
|
|
role: 'SALES_EXPERT',
|
|
fullName: 'سعید اسماعیلی',
|
|
firstName: 'سعید',
|
|
lastName: 'اسماعیلی',
|
|
mobile: '09902560722',
|
|
wagePercent: new Prisma.Decimal(12),
|
|
isActive: true,
|
|
salesExpertProfile: { tradingCustomerId: 'cust1', gramLevel: new Prisma.Decimal(0) },
|
|
};
|
|
|
|
function build(customerHolder: { id: string } | null = null) {
|
|
const customerUpdate = jest.fn(async () => ({}));
|
|
const tx = {
|
|
adminUser: {
|
|
update: jest.fn(async ({ data }: { data: Record<string, unknown> }) => ({
|
|
...expertRow,
|
|
...(data.firstName !== undefined ? { firstName: data.firstName, fullName: `${data.firstName} ${data.lastName}` } : {}),
|
|
...(data.lastName !== undefined ? { lastName: data.lastName } : {}),
|
|
...(data.mobile !== undefined ? { mobile: data.mobile } : {}),
|
|
...(data.wagePercent !== undefined ? { wagePercent: data.wagePercent } : {}),
|
|
salesExpertProfile: {
|
|
...expertRow.salesExpertProfile,
|
|
...((data.salesExpertProfile as { upsert?: { update?: { gramLevel?: Prisma.Decimal } } } | undefined)
|
|
?.upsert?.update?.gramLevel !== undefined
|
|
? { gramLevel: (data.salesExpertProfile as { upsert: { update: { gramLevel: Prisma.Decimal } } }).upsert.update.gramLevel }
|
|
: {}),
|
|
},
|
|
})),
|
|
},
|
|
customer: {
|
|
update: customerUpdate,
|
|
findUnique: jest.fn(async () => customerHolder),
|
|
// the cross-table check: does a customer (not the expert's own account) hold the number
|
|
findFirst: jest.fn(async () => customerHolder),
|
|
},
|
|
};
|
|
const prisma = {
|
|
...tx,
|
|
admin: { findUnique: jest.fn(async () => null) },
|
|
adminUser: {
|
|
...tx.adminUser,
|
|
findFirst: jest.fn(async () => expertRow),
|
|
// the expert-table mobile clash check: no other expert holds the number
|
|
findUnique: jest.fn(async () => null),
|
|
},
|
|
$transaction: jest.fn(async (fn: (t: typeof tx) => unknown) => fn(tx)),
|
|
};
|
|
const service = new AdminUsersService(prisma as never, {} as never, {} as never);
|
|
return { service, customerUpdate };
|
|
}
|
|
|
|
it('copies a name change to the trading account', async () => {
|
|
const { service, customerUpdate } = build();
|
|
await service.update('exp1', { fname: 'سعید1', lname: 'اسماعیلی1' });
|
|
expect(customerUpdate).toHaveBeenCalledWith({
|
|
where: { id: 'cust1' },
|
|
data: { firstName: 'سعید1', lastName: 'اسماعیلی1' },
|
|
});
|
|
});
|
|
|
|
it('copies the credit limit and wage percent the operator changed', async () => {
|
|
const { service, customerUpdate } = build();
|
|
await service.update('exp1', { gram_level: '50', percent: '3' });
|
|
const data = (customerUpdate.mock.calls[0] as unknown as [{ data: Record<string, Prisma.Decimal> }])[0].data;
|
|
expect(data.goldCreditLimitGrams.toString()).toBe('50');
|
|
expect(data.makingChargeOffsetPercent.toString()).toBe('3');
|
|
expect(data).not.toHaveProperty('firstName');
|
|
});
|
|
|
|
it('copies a new mobile only when no other customer holds it', async () => {
|
|
const free = build(null);
|
|
await free.service.update('exp1', { mobile: '09120001111' });
|
|
expect(free.customerUpdate).toHaveBeenCalledWith({ where: { id: 'cust1' }, data: { mobile: '09120001111' } });
|
|
|
|
});
|
|
|
|
it('refuses a new mobile a customer already holds, and writes nothing', async () => {
|
|
const taken = build({ id: 'someone-else' });
|
|
await expect(taken.service.update('exp1', { mobile: '09120001111' })).rejects.toThrow(
|
|
'این شماره موبایل قبلاً برای یک مشتری ثبت شده است',
|
|
);
|
|
expect(taken.customerUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not ask about a mobile that is not changing, so an existing duplicate stays editable', async () => {
|
|
const { service, customerUpdate } = build({ id: 'someone-else' });
|
|
await expect(service.update('exp1', { mobile: '09902560722', fname: 'سعید1', lname: 'اسماعیلی1' })).resolves.toBeDefined();
|
|
expect(customerUpdate).toHaveBeenCalledWith({
|
|
where: { id: 'cust1' },
|
|
data: { firstName: 'سعید1', lastName: 'اسماعیلی1' },
|
|
});
|
|
});
|
|
|
|
it('refuses to create an expert on a number a customer holds', async () => {
|
|
const { service } = build({ id: 'someone-else' });
|
|
await expect(
|
|
service.create({ fname: 'علی', lname: 'رضایی', mobile: '09125550000', password: 'secret1', role: 'SALES_EXPERT' }),
|
|
).rejects.toThrow('این شماره موبایل قبلاً برای یک مشتری ثبت شده است');
|
|
});
|
|
|
|
it('writes nothing to the trading account when nothing it mirrors changed', async () => {
|
|
const { service, customerUpdate } = build();
|
|
await service.update('exp1', { address: 'تهران' });
|
|
expect(customerUpdate).not.toHaveBeenCalled();
|
|
});
|
|
});
|