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
+23
View File
@@ -179,6 +179,8 @@ model AdminUser {
memberCode String? // legacy mcode
referredCustomers Customer[] @relation("ReferralAgent")
/// Orders credited to this sales expert — see Order.salesAgentId.
creditedOrders Order[] @relation("OrderSalesAgent")
supervisedWorkshops Workshop[] @relation("WorkshopSupervisor")
branchAllocations BranchAllocation[] @relation("AllocationAuthor")
postedLedgerEntries LedgerEntry[]
@@ -421,6 +423,9 @@ model Customer {
/// account (see SalesExpertProfile.tradingCustomerId). Empty for every
/// ordinary customer.
salesExpertTradingFor SalesExpertProfile? @relation("SalesExpertTradingAccount")
/// Legacy orders that named THIS customer as their referrer — see
/// Order.referrerCustomerId. History only.
referredOrders Order[] @relation("OrderReferrerCustomer")
// Credit limit + cached ledger balances (source of truth is LedgerEntry; these
// are denormalized for fast reads and reconciled by a scheduled job).
@@ -736,6 +741,22 @@ model Order {
isSuspendedOverCreditLimit Boolean @default(false)
deliveredAt DateTime? // setting this is what triggers debt recognition
/// The sales expert this order is credited to, fixed when it is placed:
/// the customer's referring expert AT THAT MOMENT. Legacy `orders.reagent_id`,
/// set once at creation (ApiMobileController.php:601) and never moved, so a
/// customer changing expert later does not move past sales or commission
/// (owner ruling 2026-10-08). «میزان فروش» counts from here, never from the
/// customer's current referrer.
salesAgentId String?
salesAgent AdminUser? @relation("OrderSalesAgent", fields: [salesAgentId], references: [id], onDelete: SetNull)
/// History only: a legacy order whose `reagent_id` named a customer who is
/// not a sales expert (one such customer, 17 orders in 2022). Legacy counted
/// those orders in that customer's own «میزان فروش»; this keeps that figure
/// exact. Never set for a new order.
referrerCustomerId String?
referrerCustomer Customer? @relation("OrderReferrerCustomer", fields: [referrerCustomerId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -746,6 +767,8 @@ model Order {
@@index([customerId])
@@index([status])
@@index([salesAgentId])
@@index([referrerCustomerId])
}
model OrderItem {