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
@@ -5,6 +5,7 @@ import { CatalogService } from '../catalog/catalog.service';
|
||||
import { LedgerService } from '../accounting/ledger.service';
|
||||
import { AccountingHooksService } from '../accounting-sync/accounting-hooks.service';
|
||||
import { CreateOrderDto } from './dto/create-order.dto';
|
||||
import { WageService } from '../pricing/wage.service';
|
||||
|
||||
interface ComputedItem {
|
||||
productVariantId: string;
|
||||
@@ -25,6 +26,7 @@ export class OrdersService {
|
||||
private readonly catalog: CatalogService,
|
||||
private readonly ledger: LedgerService,
|
||||
private readonly accounting: AccountingHooksService,
|
||||
private readonly wage: WageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -65,12 +67,25 @@ export class OrdersService {
|
||||
);
|
||||
}
|
||||
|
||||
// MON-08 (owner ruling 2026-10-08): an app order is priced by the server's
|
||||
// making-charge formula (pricing/wage-formula.ts — legacy's by default), the
|
||||
// same figure the app showed. Whatever the app sends for wage, discount or
|
||||
// rial charge is ignored, so a tampered request cannot set its own price.
|
||||
// An operator ordering from the panel keeps the numbers they entered, as
|
||||
// legacy's order builder did.
|
||||
const quotes =
|
||||
opts.placedBy === 'customer'
|
||||
? await this.wage.quote(customerId, dto.items.map((i) => i.productVariantId))
|
||||
: null;
|
||||
|
||||
const computedItems: ComputedItem[] = await Promise.all(
|
||||
dto.items.map(async (item) => {
|
||||
const variant = await this.catalog.getVariantOrThrow(item.productVariantId);
|
||||
const rashWeight = new Prisma.Decimal(variant.rashWeightGrams);
|
||||
const wagePercent = new Prisma.Decimal(item.wagePercent);
|
||||
const discountPercent = new Prisma.Decimal(item.discountPercent ?? 0);
|
||||
const quote = quotes?.get(variant.id);
|
||||
if (quotes && !quote) throw new NotFoundException(`Variant ${variant.id} not found`);
|
||||
const wagePercent = new Prisma.Decimal(quote ? quote.wagePercentSnapshot : item.wagePercent);
|
||||
const discountPercent = new Prisma.Decimal(quote ? quote.discountPercentSnapshot : (item.discountPercent ?? 0));
|
||||
const netPercent = wagePercent.sub(discountPercent);
|
||||
const computedWeightGrams = rashWeight.mul(netPercent.div(100).add(1));
|
||||
|
||||
@@ -81,7 +96,8 @@ export class OrdersService {
|
||||
wagePercentSnapshot: wagePercent,
|
||||
discountPercentSnapshot: discountPercent,
|
||||
computedWeightGrams,
|
||||
flatRialCharge: new Prisma.Decimal(item.flatRialCharge ?? 0),
|
||||
// Legacy app: rial_pay is always 0 (cart.vue); only an operator sets one.
|
||||
flatRialCharge: new Prisma.Decimal(quotes ? 0 : (item.flatRialCharge ?? 0)),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user