feat(orders): credit each order to the expert who placed it, as legacy did

Legacy fixed `orders.reagent_id` once, when the order was placed, and its
«میزان فروش» summed `customer_id = X OR reagent_id = X`. The app credited
orders to the customer's CURRENT referrer and left out an expert's own
orders, so 9 of 49 imported experts showed a different figure.

- Order.salesAgentId (+ referrerCustomerId for 17 legacy orders whose
  referrer was not an expert); additive migration, recent app orders
  backfilled from the customer's referrer
- createOrder records the expert at placement time
- one shared filter (agent-orders.ts) for the experts list, expert
  detail, dashboard and the expert app; child orders follow salesAgentId
- customer page gets its «میزان فروش» back
- import carries orders.reagent_id as is; ex-experts kept as admins keep
  their profile so their own orders stay linked
- reconcile-sales: 1,557 of 1,558 match legacy's own formula; balances
  unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-08 10:40:21 +03:30
co-authored by Claude Opus 5.5
parent 8c5d971d1f
commit 29084904e9
14 changed files with 267 additions and 26 deletions
@@ -0,0 +1,50 @@
import { Prisma } from '@prisma/client';
import { OrdersService } from './orders.service';
/**
* Owner ruling 2026-10-08: an order is credited to the customer's expert AT
* THE MOMENT IT IS PLACED (legacy `orders.reagent_id`), and a later change of
* the customer's expert never moves it.
*/
describe('OrdersService.createOrder — sales expert credit', () => {
function build(referredByAgentId: string | null) {
const created: Record<string, unknown>[] = [];
const tx = {
order: {
create: jest.fn(async ({ data }: { data: Record<string, unknown> }) => {
created.push(data);
return { id: 'o1', ...data };
}),
},
};
const prisma = {
customer: {
findUnique: jest.fn(async () => ({
id: 'c1',
referredByAgentId,
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 accounting = { orderStatusReached: jest.fn() };
const service = new OrdersService(prisma as never, catalog as never, {} as never, accounting as never);
return { service, created };
}
const dto = { items: [{ productVariantId: 'v1', quantity: 1, wagePercent: 3 }] } as never;
it("records the customer's current expert on the order", async () => {
const { service, created } = build('agent-A');
await service.createOrder(dto, 'c1');
expect(created[0].salesAgentId).toBe('agent-A');
});
it('records no expert for a customer without one', async () => {
const { service, created } = build(null);
await service.createOrder(dto, 'c1');
expect(created[0].salesAgentId).toBeNull();
});
});