Files
tehran_tala/apps/user-app-v2/src/composables/useProduct.ts
T
s.esmaeiliandClaude Opus 5.5 699657e483 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>
2026-10-08 11:26:55 +03:30

91 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Adapts our actual ProductSummary (apps/user-app/src/api/client.ts) into the
// same output shape legacy's Mixins/product/index.ts `useProduct()` produced.
// The legacy card components it was originally shaped for (ProductHorizontal/
// ProductVertical/ProductVerticalFull) are gone, replaced by the redesigned
// components/product/ProductCard.vue, components/home/ShowcaseCard.vue and
// components/product/CartItem.vue — all of which consume this same contract.
//
// Honest gaps vs. legacy (no backend equivalent exists yet — apps/api was not
// touched to add these):
// - promotions/discounts: legacy matched a customer's order against admin
// promotion + ignore-list rules (getCandidPromotions/getPromotion). No such
// 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` 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 {
return (v.qty ?? 0) > 0 || Boolean(v.isPreOrder)
}
function getCheapestVariant(variants: ProductVariantSummary[] | undefined): ProductVariantSummary | null {
if (!variants || variants.length === 0) return null
// Prefer something the customer can actually order: defaulting to an
// out-of-stock variant means the page opens on a disabled button even though
// other weights are available. Falls back to the full list when nothing is
// orderable, so the page still shows a selection.
const pool = variants.filter(isVariantOrderable)
const candidates = pool.length ? pool : variants
return candidates.reduce((cheapest, v) =>
parseFloat(v.defaultWagePercent) < parseFloat(cheapest.defaultWagePercent) ? v : cheapest,
)
}
export function useProduct(productItem: ProductSummary, initialVariant?: ProductVariantSummary) {
const product = ref<ProductSummary>(productItem)
const image = computed(() => product.value.images?.[0]?.url)
const id = computed(() => product.value.id)
const title = computed(() => product.value.title)
const variants = computed(() => product.value.variants)
const selectedVariant: Ref<ProductVariantSummary | null> = ref(initialVariant ?? getCheapestVariant(unref(variants)))
const min_gram = computed(() => selectedVariant.value ? parseFloat(selectedVariant.value.rashWeightGrams) : 0)
// 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
// it is flagged pre-order — the same rule the admin cart uses.
const qty = computed(() => selectedVariant.value?.qty ?? 0)
const isPreOrder = computed(() => selectedVariant.value?.isPreOrder ?? false)
const is_exist = computed(() => qty.value > 0 || isPreOrder.value)
const productCode = computed(() => product.value.productCode ?? null)
const has_warranty = ref(false) // no warranty field on ProductVariant yet
return {
product,
image,
id,
title,
productCode,
variants,
selectedVariant,
min_gram,
wage,
hasDiscountWage,
qty,
isPreOrder,
is_exist,
has_warranty,
}
}