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
+18 -5
View File
@@ -12,14 +12,14 @@
// data exists in the new API, so `hasDiscountWage` is always null here —
// the discount badge and "before/after" price never render, which is
// correct given there is genuinely nothing to show, not a bug.
// - `wage`: legacy added the logged-in customer's own `percent` (a
// commission/markup field on their account) on top of the variant's
// making-charge %. No login exists in this port yet, so wage is just the
// selected variant's defaultWagePercent on its own.
// - `wage` now comes from the server (wageQuoteStore), which adds the
// customer's own percent and the promotion exactly as legacy did.
// - `is_exist` (stock) and `has_warranty`: no such fields exist on our
// ProductVariant model yet — always true / false respectively.
import { computed, ref, unref, type Ref } from 'vue'
import type { ProductSummary, ProductVariantSummary } from '../api/client'
import { useSessionStore } from '../stores/session'
import { useWageQuoteStore } from '../stores/wageQuoteStore'
/** A variant can be ordered when it has stock, or when it is flagged pre-order. */
export function isVariantOrderable(v: ProductVariantSummary): boolean {
@@ -52,7 +52,20 @@ export function useProduct(productItem: ProductSummary, initialVariant?: Product
() => selectedVariant.value?.image?.url ?? product.value.images?.[0]?.url,
)
const min_gram = computed(() => selectedVariant.value ? parseFloat(selectedVariant.value.rashWeightGrams) : 0)
const wage = computed(() => (selectedVariant.value ? parseFloat(selectedVariant.value.defaultWagePercent) : null))
// The making charge the server will charge (MON-08): variant + the
// customer's own percent − the best active promotion, by the formula the
// admin «اجرت» screen controls. Signed in: the server's figure, hidden for
// the moment it loads rather than flashing a different number. Signed out
// (no customer, so no customer percent): the variant's own figure.
const session = useSessionStore()
const wageQuotes = useWageQuoteStore()
const wage = computed(() => {
const v = selectedVariant.value
if (!v) return null
if (!session.isLoggedIn) return parseFloat(v.defaultWagePercent)
wageQuotes.request(v.id)
return wageQuotes.finalPercent(v.id)
})
const hasDiscountWage = ref<number | null>(null) // no promotions backend yet — see file header
// REAL stock: ProductVariant.qty / isPreOrder exist and are returned by
// GET /catalog/products. A variant is orderable when it has stock, or when