feat(pricing): the server prices app orders with legacy's making-charge formula (MON-08)
Legacy: final % = variant % + customer % − the largest active promotion on the product, its category or its brand; rial charge 0. The apps sent the variant's own % only, so the new app undercharged by the customer's percent (usually 3) and ignored promotions, and the server trusted whatever the app sent. - pricing/wage-formula.ts: one formula, legacy by default, with coefficients, promotion pick, constant, floor/cap and rounding so the admin screen can change it; validated - an app order is priced by it; the app's wage, discount and rial charge are ignored; an operator's manual numbers are kept - POST customer/me/wage-quote; both apps show that figure - admin/pricing/wage-formula (config.wage): read, preview, approve with the sample it was checked on, last 20 versions kept; the setting is kept off the generic const routes Owner approval: 2026-10-08. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4b7b2473fc
commit
699657e483
@@ -30,7 +30,8 @@ describe('OrdersService.createOrder — sales expert credit', () => {
|
||||
};
|
||||
const catalog = { getVariantOrThrow: jest.fn(async () => ({ id: 'v1', rashWeightGrams: '10' })) };
|
||||
const accounting = { orderStatusReached: jest.fn() };
|
||||
const service = new OrdersService(prisma as never, catalog as never, {} as never, accounting as never);
|
||||
const wage = { quote: jest.fn() };
|
||||
const service = new OrdersService(prisma as never, catalog as never, {} as never, accounting as never, wage as never);
|
||||
return { service, created };
|
||||
}
|
||||
|
||||
@@ -49,6 +50,11 @@ describe('OrdersService.createOrder — sales expert credit', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/** A server quote of 10% for every variant. */
|
||||
const wageStub = {
|
||||
quote: jest.fn(async (_c: string, ids: string[]) => new Map(ids.map((id) => [id, { wagePercentSnapshot: '10', discountPercentSnapshot: '0' }]))),
|
||||
};
|
||||
|
||||
/** MON-05 (owner ruling 2026-10-08): «عدم سفارش» stops app orders only. */
|
||||
describe('OrdersService.createOrder — order ban', () => {
|
||||
function build(canPlaceOrders: boolean) {
|
||||
@@ -66,7 +72,7 @@ describe('OrdersService.createOrder — order ban', () => {
|
||||
$transaction: jest.fn(async (fn: (t: typeof tx) => unknown) => fn(tx)),
|
||||
};
|
||||
const catalog = { getVariantOrThrow: jest.fn(async () => ({ id: 'v1', rashWeightGrams: '10' })) };
|
||||
return new OrdersService(prisma as never, catalog as never, {} as never, { orderStatusReached: jest.fn() } as never);
|
||||
return new OrdersService(prisma as never, catalog as never, {} as never, { orderStatusReached: jest.fn() } as never, wageStub as never);
|
||||
}
|
||||
const dto = { items: [{ productVariantId: 'v1', quantity: 1, wagePercent: 3 }] } as never;
|
||||
|
||||
@@ -82,3 +88,34 @@ describe('OrdersService.createOrder — order ban', () => {
|
||||
await expect(build(true).createOrder(dto, 'c1', { placedBy: 'customer' })).resolves.toMatchObject({ id: 'o1' });
|
||||
});
|
||||
});
|
||||
|
||||
/** MON-08 (owner ruling 2026-10-08): an app order is priced by the server, never by the app. */
|
||||
describe('OrdersService.createOrder — server-side making charge', () => {
|
||||
function build() {
|
||||
const created: Record<string, any>[] = [];
|
||||
const tx = { order: { create: jest.fn(async ({ data }: { data: Record<string, any> }) => { created.push(data); return { id: 'o1', ...data }; }) } };
|
||||
const prisma = {
|
||||
customer: { findUnique: jest.fn(async () => ({ id: 'c1', canPlaceOrders: true, referredByAgentId: null, goldDebtGrams: new Prisma.Decimal(0), goldCreditLimitGrams: new Prisma.Decimal(1000) })) },
|
||||
$transaction: jest.fn(async (fn: (t: typeof tx) => unknown) => fn(tx)),
|
||||
};
|
||||
const catalog = { getVariantOrThrow: jest.fn(async () => ({ id: 'v1', rashWeightGrams: '10' })) };
|
||||
const service = new OrdersService(prisma as never, catalog as never, {} as never, { orderStatusReached: jest.fn() } as never, wageStub as never);
|
||||
return { service, created };
|
||||
}
|
||||
// A tampered request: zero making charge, a 50% discount and a rial charge.
|
||||
const tampered = { items: [{ productVariantId: 'v1', quantity: 1, wagePercent: 0, discountPercent: 50, flatRialCharge: 999 }] } as never;
|
||||
|
||||
it('ignores what the app sends and uses the server quote', async () => {
|
||||
const { service, created } = build();
|
||||
await service.createOrder(tampered, 'c1', { placedBy: 'customer' });
|
||||
expect(created[0].totalGoldWeightGrams.toString()).toBe('11');
|
||||
expect(created[0].flatRialCharge.toString()).toBe('0');
|
||||
});
|
||||
|
||||
it('keeps the numbers an operator entered in the panel', async () => {
|
||||
const { service, created } = build();
|
||||
await service.createOrder(tampered, 'c1', { placedBy: 'operator' });
|
||||
expect(created[0].totalGoldWeightGrams.toString()).toBe('5');
|
||||
expect(created[0].flatRialCharge.toString()).toBe('999');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user