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:
co-authored by
Claude Opus 5.5
parent
8c5d971d1f
commit
29084904e9
@@ -6,6 +6,7 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type { Ctx } from '../lib/context';
|
||||
import { toNumber } from '../lib/normalize';
|
||||
import { agentSalesWhere, customerSalesWhere } from '../../../src/modules/sales-agent/agent-orders';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
const TOLERANCE = new Prisma.Decimal('0.0001');
|
||||
@@ -89,3 +90,58 @@ export async function reconcile(ctx: Ctx) {
|
||||
);
|
||||
ctx.report.note(S, `Imported variants with an imageId: ${String(imageIds)} (must be 0 — PROTECTED.md §B).`);
|
||||
}
|
||||
/**
|
||||
* «میزان فروش» for every migrated customer and expert, against legacy's own
|
||||
* formula (CustomerController::updateGramSell, soft-deleted orders excluded):
|
||||
* SUM(orders.input) WHERE customer_id = X OR reagent_id = X
|
||||
* computed live from legacy's rows — and, separately, legacy's STORED
|
||||
* `customers.grams_sell`, which the legacy screens print. The stored value is
|
||||
* a cache refreshed only on some order events, so where it disagrees with the
|
||||
* formula the legacy screen itself is out of date; each such row is listed.
|
||||
*
|
||||
* Legacy rows the import merged into one target row (duplicate mobiles) are
|
||||
* compared as one: their legacy figures are summed.
|
||||
*/
|
||||
export async function reconcileSales(ctx: Ctx) {
|
||||
const S = 'reconciliation — sales («میزان فروش»)';
|
||||
const rows = await ctx.q<Row>(
|
||||
`SELECT c.id, c.grams_sell AS kept,
|
||||
(SELECT COALESCE(SUM(o.input), 0) FROM orders o
|
||||
WHERE o.deleted_at IS NULL AND (o.customer_id = c.id OR o.reagent_id = c.id)) AS live
|
||||
FROM customers c WHERE c.deleted_at IS NULL`,
|
||||
);
|
||||
const groups = new Map<string, { where: Prisma.OrderWhereInput; live: number; ids: string[] }>();
|
||||
let stale = 0;
|
||||
for (const r of rows) {
|
||||
const agentId = ctx.map.get('expert', r.id);
|
||||
const customerId = ctx.map.get('customer', r.id);
|
||||
if (!agentId && !customerId) continue;
|
||||
const key = agentId ? `expert:${agentId}` : `customer:${customerId!}`;
|
||||
const g = groups.get(key) ?? {
|
||||
where: agentId ? agentSalesWhere(agentId) : customerSalesWhere(customerId!),
|
||||
live: 0,
|
||||
ids: [],
|
||||
};
|
||||
g.live += toNumber(r.live);
|
||||
g.ids.push(String(r.id));
|
||||
groups.set(key, g);
|
||||
|
||||
const live = new Prisma.Decimal(toNumber(r.live).toFixed(4));
|
||||
const kept = new Prisma.Decimal(toNumber(r.kept).toFixed(4));
|
||||
if (kept.sub(live).abs().gt(TOLERANCE)) {
|
||||
stale++;
|
||||
ctx.report.note(S, `legacy's stored figure is out of date for #${String(r.id)}: legacy screen shows ${kept.toString()}, legacy's own formula gives ${live.toString()}`);
|
||||
}
|
||||
}
|
||||
let ok = 0, bad = 0;
|
||||
for (const [key, g] of groups) {
|
||||
const agg = await ctx.db.order.aggregate({ where: g.where, _sum: { totalGoldWeightGrams: true } });
|
||||
const target = agg._sum.totalGoldWeightGrams ?? new Prisma.Decimal(0);
|
||||
const live = new Prisma.Decimal(g.live.toFixed(4));
|
||||
if (live.sub(target).abs().lte(TOLERANCE)) ok++;
|
||||
else { bad++; ctx.report.note(S, `⚠ ${key.split(':')[0]} legacy #${g.ids.join(' + #')}: legacy formula ${live.toString()} / target ${target.toString()}`); }
|
||||
}
|
||||
ctx.report.count(S, 'matches legacy formula (±0.0001 g)', ok);
|
||||
ctx.report.count(S, 'DIFFERS from legacy formula', bad);
|
||||
ctx.report.count(S, "legacy's stored screen value is stale (legacy disagrees with itself)", stale);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user