From 362040a649143c57cdb926df2cc290cc5f4d682e Mon Sep 17 00:00:00 2001 From: adonis Date: Thu, 16 Jul 2026 15:21:53 +0330 Subject: [PATCH] update --- .../BaseDatePicker/src/BaseDatePicker.vue | 2 +- src/models/products.ts | 9 + src/utils/productCashFee.ts | 54 ++++ src/utils/productCategories.ts | 87 +++++++ src/utils/productPrice.ts | 237 +++++++++++++++--- 5 files changed, 352 insertions(+), 37 deletions(-) create mode 100644 src/utils/productCashFee.ts create mode 100644 src/utils/productCategories.ts diff --git a/src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue b/src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue index ff8db80..fff3378 100644 --- a/src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue +++ b/src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue @@ -26,7 +26,7 @@ const _attrs = computed(() => { const props = withDefaults(defineProps(), { value: '', - format: 'YYYY/MM/DD' + format: 'YYYY-MM-DD' }) const emit = defineEmits<{ diff --git a/src/models/products.ts b/src/models/products.ts index dd237fc..01bf583 100644 --- a/src/models/products.ts +++ b/src/models/products.ts @@ -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 { diff --git a/src/utils/productCashFee.ts b/src/utils/productCashFee.ts new file mode 100644 index 0000000..ea39973 --- /dev/null +++ b/src/utils/productCashFee.ts @@ -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, + ) +} diff --git a/src/utils/productCategories.ts b/src/utils/productCategories.ts new file mode 100644 index 0000000..1c9a15b --- /dev/null +++ b/src/utils/productCategories.ts @@ -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 +} diff --git a/src/utils/productPrice.ts b/src/utils/productPrice.ts index 3d6b7e2..e0faed5 100644 --- a/src/utils/productPrice.ts +++ b/src/utils/productPrice.ts @@ -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'