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
@@ -2,6 +2,7 @@ import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/commo
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import type { AuthenticatedAdmin } from '../auth/jwt.strategy';
import { agentSalesWhere } from './agent-orders';
/**
* Backs the sales expert's ("کارشناس فروش") own screens in the customer apps.
@@ -45,12 +46,12 @@ export class SalesAgentService {
* Legacy kept this denormalised in `customers.grams_sell`, recomputed by
* `CustomerController::updateGramSell` as
* SUM(orders.input) WHERE customer_id = X OR reagent_id = X
* with NO order-status filter. Computed live here instead of denormalised.
* with NO order-status filter. Computed live here instead of denormalised;
* both arms are kept — see agent-orders.ts.
*
* Two deliberate fidelity notes:
* - The `OR customer_id = X` arm is structurally empty now: an agent is an
* AdminUser and cannot own an Order, so only the referred-customer arm can
* ever contribute. Kept as a comment rather than silently dropped.
* - The `OR customer_id = X` arm is the expert's trading account (D21): an
* AdminUser cannot own an Order, so their own orders sit on that Customer.
* - No status filter, matching legacy. The admin panel disagrees with itself
* here — `AdminCustomersService.listReagents` aggregates unfiltered while
* `reagentDetail` filters on the SOLD statuses — so this matches
@@ -59,7 +60,7 @@ export class SalesAgentService {
*/
private async gramsSoldFor(agentId: string) {
const agg = await this.prisma.order.aggregate({
where: { customer: { referredByAgentId: agentId } },
where: agentSalesWhere(agentId),
_sum: { totalGoldWeightGrams: true },
});
return agg._sum.totalGoldWeightGrams ?? new Prisma.Decimal(0);