This commit is contained in:
2026-07-16 15:21:53 +03:30
parent 5d390fb225
commit 362040a649
5 changed files with 352 additions and 37 deletions
@@ -26,7 +26,7 @@ const _attrs = computed(() => {
const props = withDefaults(defineProps<Props>(), {
value: '',
format: 'YYYY/MM/DD'
format: 'YYYY-MM-DD'
})
const emit = defineEmits<{
+9
View File
@@ -17,16 +17,25 @@ export interface Product {
percent_fee_sell: number | undefined
cash_fee_buy: number | undefined
cash_fee_sell: number | undefined
cash_fee_buy_is_percent?: boolean | null
cash_fee_sell_is_percent?: boolean | null
reading_method: any
created_at: any
updated_at: any
deleted_at: any
price?:number | undefined
price_profit?: number | null
price_offset_percent?: number | null
sellPrice?:number | undefined
description: string
wallet: Wallet
similar?:Product[]
inventory:number
app_price?: boolean | number | null
direct_price?: number | null
trade_by_quantity?: boolean | number | null
use_last_price_if_zero?: boolean | null
reference_provider_active?: boolean | null
}
export interface PInventory {
+54
View File
@@ -0,0 +1,54 @@
export type ProductCashFeeSource = {
cash_fee_buy?: number | null
cash_fee_sell?: number | null
cash_fee_buy_is_percent?: boolean | null
cash_fee_sell_is_percent?: boolean | null
}
/** 0.5 => 0.5% ، 5 => 5% */
export function resolveCashFeePercentRatio(percentValue?: number | null) {
const offset = Number(percentValue ?? 0)
if (!offset || !Number.isFinite(offset)) {
return 0
}
return offset / 100
}
export function resolveProductCashFeeRial(
feeValue: number | null | undefined,
isPercent: boolean | null | undefined,
basePriceRial: number,
) {
const raw = Number(feeValue ?? 0)
if (!raw || !Number.isFinite(raw)) {
return 0
}
if (!isPercent) {
return Math.round(raw)
}
const base = Number(basePriceRial)
if (!base || base <= 0) {
return 0
}
return Math.round(Math.abs(base * resolveCashFeePercentRatio(raw)))
}
export function resolveProductCashFeeBuyRial(product: ProductCashFeeSource, basePriceRial: number) {
return resolveProductCashFeeRial(
product.cash_fee_buy,
product.cash_fee_buy_is_percent,
basePriceRial,
)
}
export function resolveProductCashFeeSellRial(product: ProductCashFeeSource, basePriceRial: number) {
return resolveProductCashFeeRial(
product.cash_fee_sell,
product.cash_fee_sell_is_percent,
basePriceRial,
)
}
+87
View File
@@ -0,0 +1,87 @@
/** دسته‌بندی یکسان محصول — هم‌تراز با khaneghi_user */
export const PRODUCT_CATEGORY_ABSHODE = 'آبشده'
export const LEGACY_GROUP_KATOD = 'کاتد مس'
export const LEGACY_MELTED_GROUP_TYPES = [LEGACY_GROUP_KATOD] as const
export function normalizeProductGroupType(groupType?: string | null) {
return (groupType ?? '').trim()
}
/** محصول آبشده‌ای — قیمت از مظنه اپ (مگر قیمت ثابت / مرجع) */
export function isMeltedTradeGroup(groupType?: string | null) {
const group = normalizeProductGroupType(groupType)
return group === PRODUCT_CATEGORY_ABSHODE
|| LEGACY_MELTED_GROUP_TYPES.includes(group as typeof LEGACY_MELTED_GROUP_TYPES[number])
}
export interface QuantityTradeProductSource {
group_type?: string | null
trade_by_quantity?: boolean | number | null
}
export function usesQuantityTrade(product?: QuantityTradeProductSource | null) {
if (!product) {
return false
}
return product.trade_by_quantity === true || Number(product.trade_by_quantity) === 1
}
export function isMetalCommodityGroup(groupType?: string | null) {
const group = normalizeProductGroupType(groupType)
return group === 'مس' || group === 'نقره'
}
export type MeltedPriceSource = 'app' | 'fixed' | 'reference'
export function resolveMeltedPriceSource(product?: {
reading_method?: string | null
direct_price?: number | null
} | null): MeltedPriceSource {
if (product?.reading_method) {
return 'reference'
}
if (Number(product?.direct_price) === 1) {
return 'fixed'
}
return 'app'
}
/** محصول با پرچم صریح «قیمت اپ» */
export function isAppPriceProduct(product?: {
app_price?: boolean | number | null
reading_method?: string | null
} | null) {
if (!product || product.reading_method) {
return false
}
return product.app_price === true || Number(product.app_price) === 1
}
export function isMeltedProductUsingAppPrice(product?: {
group_type?: string | null
reading_method?: string | null
direct_price?: number | null
trade_by_quantity?: boolean | number | null
price?: number | null
} | null) {
if (!product || resolveMeltedPriceSource(product) !== 'app') {
return false
}
if (isMeltedTradeGroup(product.group_type)) {
return true
}
if (!usesQuantityTrade(product)) {
return false
}
if (Number(product.direct_price) === 1) {
return false
}
return Number(product.price ?? 0) <= 0
}
+201 -36
View File
@@ -1,93 +1,257 @@
import type { Product } from '@/models/products'
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
import { resolveProductCashFeeBuyRial, resolveProductCashFeeSellRial } from '@/utils/productCashFee'
import {
isAppPriceProduct,
isMeltedProductUsingAppPrice,
isMeltedTradeGroup,
isMetalCommodityGroup,
usesQuantityTrade,
} from '@/utils/productCategories'
/**
* قیمت‌گیری ماژولار محصول در حالت فروشگاهی (is_store=true).
*
* منبع حقیقت قیمت و وضعیت نمایش، سرور است: `/customer/product/list` و رویداد SSE
* `list` هر دو قیمت هر محصول (`price`/`price_profit`) و `show` را محاسبه‌شده می‌فرستند.
* این ماژول فقط همان قیمت سرور را می‌خواند و فقط برای آبشده وقتی قیمت سرور صفر است
* به قیمت زندهٔ مرجع (`lastPrice`) fallback می‌کند — هیچ‌جا قیمت سرور بازنویسی نمی‌شود.
* قیمت‌گیری محصول — هم‌تراز با khaneghi_user:
* آبشده / قیمت‌اپ از مظنه زنده؛ قیمت ثابت و مرجع از سرور.
*/
const MELTED_GROUP = 'آبشده'
export function isMeltedProduct(product?: Product | null): boolean {
return product?.group_type === MELTED_GROUP
function productUsesLastPriceIfZero(product: Product): boolean {
return Boolean(product.use_last_price_if_zero)
}
/** قیمت پایهٔ خرید (بدون کارمزد) — قیمت سرور، با fallback زندهٔ آبشده وقتی سرور صفر است */
function hasReferenceReadingMethod(product: Product): boolean {
return Boolean(product.reading_method)
}
function hasProductUnitWeight(product?: Product | null) {
if (!product || isMeltedTradeGroup(product.group_type)) {
return false
}
return Number(product.amount ?? 0) > 0
}
function productPriceAmountFactor(product?: Product | null) {
if (!product || product.reading_method) {
return 1
}
if (isAppPriceProduct(product)) {
const amount = Number(product.amount ?? 0)
return amount > 0 ? amount : 1
}
if (hasProductUnitWeight(product) && Number(product.direct_price ?? 0) === 0) {
const amount = Number(product.amount ?? 0)
return amount > 0 ? amount : 1
}
return 1
}
function getReferenceProductBaseBuyPrice(product: Product): number {
const storedUnit = Number(product.price ?? 0)
const useLast = productUsesLastPriceIfZero(product)
if (!useLast) {
return storedUnit > 0 ? storedUnit : 0
}
return storedUnit
}
export function resolvePriceOffsetRatio(offsetPercent?: number | null) {
const offset = Number(offsetPercent ?? 0)
if (!offset) {
return 0
}
if (Math.abs(offset) > 0 && Math.abs(offset) < 1) {
return offset
}
return offset / 100
}
export function applyPriceOffsetPercent(basePrice: number, offsetPercent?: number | null) {
const ratio = resolvePriceOffsetRatio(offsetPercent)
if (!ratio) {
return basePrice
}
return Math.round(basePrice * (1 + ratio))
}
/** قیمت پایه بدون کارمزد */
export function getProductBaseBuyPrice(product?: Product | null): number {
if (!product) {
return 0
}
const serverPrice = Number(product.price ?? 0)
if (serverPrice > 0) {
return serverPrice
}
if (isMeltedProductUsingAppPrice(product) || isAppPriceProduct(product)) {
const apiPrice = Number(product.price ?? 0)
const useLast = productUsesLastPriceIfZero(product)
if (!useLast) {
const liveBuy = getTradeBuyPrice(lastPrice.value)
if (liveBuy > 0) {
return applyPriceOffsetPercent(liveBuy, product.price_offset_percent)
}
return 0
}
if (apiPrice > 0) {
return apiPrice
}
if (isMeltedProduct(product)) {
const liveBuy = getTradeBuyPrice(lastPrice.value)
if (liveBuy > 0) {
const amount = Number(product.amount ?? 0)
return amount > 0 ? liveBuy * amount : liveBuy
return applyPriceOffsetPercent(liveBuy, product.price_offset_percent)
}
return 0
}
return 0
if (isMeltedTradeGroup(product.group_type)) {
return Number(product.price ?? 0)
}
if (isMetalCommodityGroup(product.group_type)) {
return Number(product.price_profit ?? 0) + Number(product.price ?? 0)
}
if (hasReferenceReadingMethod(product)) {
return getReferenceProductBaseBuyPrice(product)
}
return Number(product.price ?? 0)
}
/** قیمت خرید نمایشی محصول (همان مقداری که در لیست و صفحهٔ محصول نشان داده می‌شود) */
/** قیمت خرید نمایشی */
export function getProductBuyPrice(product?: Product | null): number {
if (!product) {
return 0
}
const base = getProductBaseBuyPrice(product)
if (base <= 0) {
return 0
}
return base + Number(product?.cash_fee_buy ?? 0)
const fee = resolveProductCashFeeBuyRial(product, base)
const withFee = base + fee
if (isAppPriceProduct(product)) {
return withFee * productPriceAmountFactor(product)
}
if (
isMeltedProductUsingAppPrice(product)
|| isMeltedTradeGroup(product.group_type)
|| isMetalCommodityGroup(product.group_type)
) {
return withFee
}
if (usesQuantityTrade(product) && hasProductUnitWeight(product)) {
return withFee * productPriceAmountFactor(product)
}
if (usesQuantityTrade(product)) {
return withFee
}
return withFee * productPriceAmountFactor(product)
}
/** قیمت پایهٔ فروش (بدون کارمزد) — قیمت فروش سرور، با fallback زندهٔ آبشده */
/** قیمت پایه فروش */
export function getProductBaseSellPrice(product?: Product | null): number {
if (!product) {
return 0
}
if (isMeltedProductUsingAppPrice(product) || isAppPriceProduct(product)) {
const apiPrice = Number(product.price ?? 0)
const useLast = productUsesLastPriceIfZero(product)
if (!useLast) {
const liveSell = getTradeSellPrice(lastPrice.value)
if (liveSell > 0) {
return applyPriceOffsetPercent(liveSell, product.price_offset_percent)
}
return 0
}
if (apiPrice > 0) {
return apiPrice
}
const liveSell = getTradeSellPrice(lastPrice.value)
if (liveSell > 0) {
return applyPriceOffsetPercent(liveSell, product.price_offset_percent)
}
return 0
}
if (isMeltedTradeGroup(product.group_type)) {
return Number(product.sellPrice ?? product.price ?? 0)
}
const serverSell = Number(product.sellPrice ?? 0)
if (serverSell > 0) {
return serverSell
}
if (isMeltedProduct(product)) {
const liveSell = getTradeSellPrice(lastPrice.value)
if (liveSell > 0) {
const amount = Number(product.amount ?? 0)
return amount > 0 ? liveSell * amount : liveSell
}
}
return getProductBaseBuyPrice(product)
}
/** قیمت فروش نمایشی محصول */
/** قیمت فروش نمایشی */
export function getProductSellPrice(product?: Product | null): number {
if (!product) {
return 0
}
const base = getProductBaseSellPrice(product)
if (base <= 0) {
return 0
}
return base - Number(product?.cash_fee_sell ?? 0)
if (isAppPriceProduct(product)) {
const withFee = base - resolveProductCashFeeSellRial(product, base)
return withFee * productPriceAmountFactor(product)
}
if (isMeltedProductUsingAppPrice(product) || isMeltedTradeGroup(product.group_type)) {
return base - resolveProductCashFeeSellRial(product, base)
}
if (usesQuantityTrade(product) && hasProductUnitWeight(product)) {
const withFee = base - resolveProductCashFeeSellRial(product, base)
return withFee * productPriceAmountFactor(product)
}
if (usesQuantityTrade(product)) {
return base - resolveProductCashFeeSellRial(product, base)
}
return base - resolveProductCashFeeSellRial(product, base)
}
/** محصول وقتی قیمت پایهٔ معتبری ندارد، غیرفعال (غیرقابل معامله) است */
export function isProductPriceInactive(product?: Product | null): boolean {
if (!product) {
return true
}
if (
hasReferenceReadingMethod(product)
&& !productUsesLastPriceIfZero(product)
&& product.reference_provider_active === false
) {
return true
}
return getProductBaseBuyPrice(product) <= 0
}
/**
* نمایش محصول در فروشگاه — اولویت با `show` سرور.
* فقط برای آبشده‌ای که قیمت سرورش هنوز نرسیده، با قیمت زنده تکمیل می‌کنیم.
* آبشدهٔ قیمت‌اپ با مظنه زنده حتی اگر show هنوز false باشد.
*/
export function isProductVisible(product?: Product | null): boolean {
if (!product || product.active === false) {
@@ -98,6 +262,7 @@ export function isProductVisible(product?: Product | null): boolean {
return true
}
// سازگاری: آبشده‌ای که سرور هنوز قیمت نداده ولی قیمت زنده موجود است
return isMeltedProduct(product) && getProductBaseBuyPrice(product) > 0
return isMeltedProductUsingAppPrice(product) && getProductBaseBuyPrice(product) > 0
}
export { isMeltedTradeGroup, isMeltedProductUsingAppPrice } from '@/utils/productCategories'