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:
s.esmaeili
2026-10-08 11:26:55 +03:30
co-authored by Claude Opus 5.5
parent 4b7b2473fc
commit 699657e483
21 changed files with 766 additions and 22 deletions
@@ -14,6 +14,7 @@ import { TableQueryDto } from '../admin-table/table-query.dto';
import { CurrentAdmin, RequirePermission } from '../auth/guards';
import type { AuthenticatedAdmin } from '../auth/jwt.strategy';
import { isAccountingSettingKey } from '../accounting-sync/accounting-config';
import { PRICING_SETTING_PREFIX } from '../pricing/wage.service';
/**
* Cities, SMS, notifications, newsletter, activity log and settings — the
@@ -228,7 +229,7 @@ export class AdminMiscController {
// `custom.config.statics.slider`) simply by calling `const/mobile_main_slider`
// directly, since the underlying Setting row is keyed the same either way.
private assertNotSliderKey(key: string) {
if (key === 'mobile_main_slider' || isAccountingSettingKey(key)) {
if (key === 'mobile_main_slider' || isAccountingSettingKey(key) || key.trim().toLowerCase().startsWith(PRICING_SETTING_PREFIX)) {
throw new ForbiddenException('این تنظیم فقط از مسیر اختصاصی خود قابل دسترسی است');
}
}