From bfc4bec4cd1ed8d338b071384463a2ef28ffc338 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 22 Jun 2026 21:10:25 +0330 Subject: [PATCH] price handling --- src/models/products.ts | 1 + src/utils/inputRules.ts | 15 +- src/utils/productAssets.ts | 2 +- src/utils/productDealMethod.ts | 230 ++++++++++++++++-- src/utils/productPrice.ts | 28 +++ src/utils/usePrice.ts | 15 +- .../base/products/components/DealProduct.vue | 137 +++++++++-- .../base/products/components/ProductItem.vue | 5 +- .../desktop/components/DesktopProductCard.vue | 5 +- 9 files changed, 388 insertions(+), 50 deletions(-) diff --git a/src/models/products.ts b/src/models/products.ts index fcedf49..c74ffef 100644 --- a/src/models/products.ts +++ b/src/models/products.ts @@ -26,6 +26,7 @@ export interface Product { direct_price: number | undefined price_offset_percent?: number | null use_last_price_if_zero?: boolean + reference_provider_active?: boolean trade_by_quantity?: boolean reading_method: any created_at: any diff --git a/src/utils/inputRules.ts b/src/utils/inputRules.ts index 01289d7..3a8b9c6 100644 --- a/src/utils/inputRules.ts +++ b/src/utils/inputRules.ts @@ -151,6 +151,7 @@ let amountP =0 let isBuyP=true let NumP=0 let groupTypeP: string | undefined +let meltedBalanceP = 0 const checkNumber = async (_rule: Rule, value: number) => { @@ -165,12 +166,12 @@ if (isBuyP){ if (!availableCash && isCashLikeDealMethod(methodP)){ return Promise.reject(getInsufficientCashWalletMessage(groupTypeP)) } - if (!walletCash?.walletInfo?.gold && isMainMetalWalletDealMethod(methodP)){ + if (!meltedBalanceP && isMainMetalWalletDealMethod(methodP)){ return Promise.reject(getInsufficientGoldWalletMessage(groupTypeP, methodP)) } if (priceP * value > availableCash && isCashLikeDealMethod(methodP)) return Promise.reject(getInsufficientCashWalletMessage(groupTypeP)) - if (amountP * value > walletCash?.walletInfo?.gold && isMainMetalWalletDealMethod(methodP)) + if (amountP * value > meltedBalanceP && isMainMetalWalletDealMethod(methodP)) return Promise.reject(getInsufficientGoldWalletMessage(groupTypeP, methodP)) else return Promise.resolve(); @@ -182,12 +183,20 @@ if (isBuyP){ } } -export const justNumber2 = (price: number, method?: string , amount?:number ,isBuy?:boolean, groupType?: string) => { +export const justNumber2 = ( + price: number, + method?: string, + amount?: number, + isBuy?: boolean, + groupType?: string, + meltedBalance?: number, +) => { priceP = price methodP = method amountP=amount isBuyP=isBuy groupTypeP = groupType + meltedBalanceP = meltedBalance ?? 0 return { required: true, validator: checkNumber, diff --git a/src/utils/productAssets.ts b/src/utils/productAssets.ts index a053858..fc88b56 100644 --- a/src/utils/productAssets.ts +++ b/src/utils/productAssets.ts @@ -391,7 +391,7 @@ export function formatMetalAssetsInfoText(metal: WalletMetalTotal): string { } function formatRialNumber(value: number): string { - return usePriceWithoutType(Math.round(value)) + return usePriceWithoutType(value) } /** متن توضیح مودال دارایی ریالی */ diff --git a/src/utils/productDealMethod.ts b/src/utils/productDealMethod.ts index 9e2c27f..6bbf7ea 100644 --- a/src/utils/productDealMethod.ts +++ b/src/utils/productDealMethod.ts @@ -1,4 +1,6 @@ import type { SelectItem } from '@/models/SelectItem' +import type { Product } from '@/models/products' +import type { WalletInventoryCard } from '@/models/walletAssets' import { isBoursecuProject } from '@/utils/config' import { useSettingsStore } from '@/store/settingsStore' import { @@ -8,7 +10,9 @@ import { LEGACY_GROUP_MAFTOL, normalizeProductGroupType, PRODUCT_CATEGORY_ABSHODE, + usesQuantityTrade, } from '@/utils/productCategories' +import { formatInventoryCardValue } from '@/utils/productAssets' /** @deprecated از isMeltedTradeGroup استفاده کنید */ export const BOURSECU_GROUP_ABSHODE = PRODUCT_CATEGORY_ABSHODE @@ -183,29 +187,190 @@ export function getProductQuantityUnit(product?: ProductUnitSource | null) { return getWeightUnit() } -function getDefaultTypeDealOptions(): SelectItem[] { - const settingsStore = useSettingsStore() - return [ - { value: 'نقدی', key: 'cash' }, - { value: settingsStore.metalTypeLabel, key: 'gold' }, - ] +export const MELTED_DEAL_METHOD_PREFIX = 'gold:' + +export interface ProductDealMethodContext { + tradedProduct?: Product | null + productMetalId?: number | null + inventoryCards?: WalletInventoryCard[] + catalog?: Product[] } -export function getProductDealMethodOptions(groupType?: string | null, isBuy = true): SelectItem[] { +/** سلکت نحوه معامله فقط وقتی محصول مورد معامله وزن دارد */ +export function shouldShowProductDealMethodSelector(product?: Product | null) { + return hasProductUnitWeight(product) +} + +export function shouldShowProductDealMethodSelect( + product?: Product | null, + options?: SelectItem[] | null, +) { + return shouldShowProductDealMethodSelector(product) && (options?.length ?? 0) > 1 +} + +export function buildMeltedDealMethodKey(productId: number) { + return `${MELTED_DEAL_METHOD_PREFIX}${productId}` +} + +export function parseMeltedDealMethodProductId(method?: string | null): number | null { + if (!method?.startsWith(MELTED_DEAL_METHOD_PREFIX)) { + return null + } + + const productId = Number(method.slice(MELTED_DEAL_METHOD_PREFIX.length)) + return productId > 0 ? productId : null +} + +export function isMeltedProductDealMethod(method?: string | null) { + return parseMeltedDealMethodProductId(method) != null +} + +function isQuantityTradeCatalogProduct( + product?: Product | null, + metalId?: number | null, + excludeProductId?: number | null, +) { + if (!product || !usesQuantityTrade(product)) { + return false + } + + if (excludeProductId && Number(product.id) === Number(excludeProductId)) { + return false + } + + return Number(product.metal_id) === Number(metalId ?? 0) +} + +function findInventoryCardForProduct( + cards: WalletInventoryCard[], + productId?: number | null, +) { + if (!productId) { + return undefined + } + + return cards.find((card) => Number(card.product_id) === Number(productId)) +} + +export function getQuantityTradePaymentDealOptions( + context?: ProductDealMethodContext | null, + isBuy = true, +): SelectItem[] { + const metalId = Number(context?.productMetalId ?? 0) + const catalog = context?.catalog ?? [] + const cards = context?.inventoryCards ?? [] + const tradedProductId = context?.tradedProduct?.id ?? null + + if (!metalId || !catalog.length) { + return [] + } + + const matchingProducts = catalog.filter((product) => + isQuantityTradeCatalogProduct(product, metalId, tradedProductId) + ) + + if (isBuy) { + const options: SelectItem[] = [] + + for (const product of matchingProducts) { + const card = findInventoryCardForProduct(cards, product.id) + if (!card || Number(card.quantity ?? 0) <= 0) { + continue + } + + options.push({ + key: buildMeltedDealMethodKey(product.id), + value: `${product.title} — ${formatInventoryCardValue(card)}`, + }) + } + + return options + } + + return matchingProducts.map((product) => { + const card = findInventoryCardForProduct(cards, product.id) + const inventorySuffix = card ? ` — ${formatInventoryCardValue(card)}` : '' + + return { + key: buildMeltedDealMethodKey(product.id), + value: `دریافت ${product.title}${inventorySuffix}`, + } + }) +} + +/** @deprecated از getQuantityTradePaymentDealOptions استفاده کنید */ +export function getMeltedPaymentDealOptions( + context?: ProductDealMethodContext | null, + isBuy = true, +) { + return getQuantityTradePaymentDealOptions(context, isBuy) +} + +export function findMeltedDealMethodCard( + method?: string | null, + cards: WalletInventoryCard[] = [], +) { + return findInventoryCardForProduct(cards, parseMeltedDealMethodProductId(method)) +} + +export function findProductDealMethodProduct( + method?: string | null, + catalog: Product[] = [], +) { + const productId = parseMeltedDealMethodProductId(method) + if (!productId) { + return null + } + + return catalog.find((product) => Number(product.id) === productId) ?? null +} + +export function resolveProductTradeBuyUnitPrice(product?: Product | null) { + if (!product) { + return 0 + } + + const basePrice = (product.price ?? 0) + (product.cash_fee_buy ?? 0) + return product.direct_price === 0 + ? basePrice * (product.amount ?? 0) + : basePrice +} + +export function resolveProductTradeSellUnitPrice(product?: Product | null) { + if (!product) { + return 0 + } + + const basePrice = (product.price ?? 0) - (product.cash_fee_sell ?? 0) + return product.direct_price === 0 + ? basePrice * (product.amount ?? 0) + : basePrice +} + +function getDefaultTypeDealOptions(context?: ProductDealMethodContext | null, isBuy = true): SelectItem[] { + const cashOption: SelectItem = { value: 'نقدی', key: 'cash' } + + if (!shouldShowProductDealMethodSelector(context?.tradedProduct ?? null)) { + return [cashOption] + } + + const quantityOptions = getQuantityTradePaymentDealOptions(context, isBuy) + if (quantityOptions.length) { + return [cashOption, ...quantityOptions] + } + + return [cashOption] +} + +export function getProductDealMethodOptions( + groupType?: string | null, + isBuy = true, + context?: ProductDealMethodContext | null, +): SelectItem[] { const settingsStore = useSettingsStore() if (!isBoursecuProject()) { - if (isBuy) { - return [ - { value: 'نقدی', key: 'cash' }, - { value: settingsStore.metalTypeLabel, key: 'gold' }, - ] - } - - return [ - { value: 'نقدی', key: 'cash' }, - { value: `دریافت ${settingsStore.metalTypeLabel}`, key: 'gold' }, - ] + return getDefaultTypeDealOptions(context, isBuy) } if (isMaftolMesGroup(groupType)) { @@ -230,11 +395,15 @@ export function getProductDealMethodOptions(groupType?: string | null, isBuy = t return [{ value: settingsStore.meltedGoldLabel, key: 'gold' }] } - return getDefaultTypeDealOptions() + return getDefaultTypeDealOptions(context, isBuy) } -export function getDefaultProductDealMethod(groupType?: string | null, isBuy = true) { - return getProductDealMethodOptions(groupType, isBuy)[0]?.key ?? 'cash' +export function getDefaultProductDealMethod( + groupType?: string | null, + isBuy = true, + context?: ProductDealMethodContext | null, +) { + return getProductDealMethodOptions(groupType, isBuy, context)[0]?.key ?? 'cash' } export function getProductDealMethodPlaceholder(isBuy = true) { @@ -265,7 +434,24 @@ export function isCashLikeDealMethod(method?: string | null) { } export function isMainMetalWalletDealMethod(method?: string | null) { - return method === 'gold' || method === 'katod' + return method === 'gold' || method === 'katod' || isMeltedProductDealMethod(method) +} + +export function getMeltedDealMethodBalance( + method?: string | null, + cards: WalletInventoryCard[] = [], + mainWalletGold?: number | null, +) { + const card = findMeltedDealMethodCard(method, cards) + if (card) { + return Number(card.quantity ?? 0) + } + + if (method === 'gold' || method === 'katod') { + return Number(mainWalletGold ?? 0) + } + + return 0 } /** برچسب کیف فلز اصلی (main wallet gold) — از setting metal_type */ diff --git a/src/utils/productPrice.ts b/src/utils/productPrice.ts index 0422309..2d946c6 100644 --- a/src/utils/productPrice.ts +++ b/src/utils/productPrice.ts @@ -7,6 +7,22 @@ function productUsesLastPriceIfZero(product: Product): boolean { return Boolean(product.use_last_price_if_zero) } +function hasReferenceReadingMethod(product: Product): boolean { + return Boolean(product.reading_method) +} + +/** قیمت پایه محصولات با reading_method — بدون fallback وقتی تیک «قیمت صفر از مرجع» خاموش است */ +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) { @@ -74,6 +90,10 @@ export function getProductBaseBuyPrice(product: Product): number { return Number(product.price_profit ?? 0) + Number(product.price ?? 0) } + if (hasReferenceReadingMethod(product)) { + return getReferenceProductBaseBuyPrice(product) + } + return Number(product.price ?? 0) } @@ -148,6 +168,14 @@ export function getProductSellPrice(product: Product): number { } export function isProductPriceInactive(product: Product): boolean { + if ( + hasReferenceReadingMethod(product) + && !productUsesLastPriceIfZero(product) + && product.reference_provider_active === false + ) { + return true + } + return getProductBaseBuyPrice(product) <= 0 } diff --git a/src/utils/usePrice.ts b/src/utils/usePrice.ts index f4eef89..f374c49 100644 --- a/src/utils/usePrice.ts +++ b/src/utils/usePrice.ts @@ -6,18 +6,27 @@ function formatGroupedPrice(price: string | number | null | undefined, digit: bo return value } +/** مبلغ ریالی — بدون اعشار، گرد به بالا */ +export function ceilRial(price?: number | string | null): number { + const num = Number(price ?? 0) + if (!Number.isFinite(num)) { + return 0 + } + + return Math.ceil(num) +} + export function formatNumber(value: string | number | null | undefined, grouped = false): string { return formatGroupedPrice(value, grouped) } export function usePrice(price: number, digit = true, unit = 'ریال') { - price = price ?? 0 - const formatted = formatGroupedPrice(price, digit) + const formatted = formatGroupedPrice(ceilRial(price), digit) return `${formatted} ${unit}` } export function usePriceWithoutType(price?: number, digit = true) { - return formatGroupedPrice(price ?? '', digit) + return formatGroupedPrice(ceilRial(price), digit) } export function useTowDecimal(num: string | number): string | number { diff --git a/src/views/base/products/components/DealProduct.vue b/src/views/base/products/components/DealProduct.vue index f6275fc..3aaf49b 100644 --- a/src/views/base/products/components/DealProduct.vue +++ b/src/views/base/products/components/DealProduct.vue @@ -22,10 +22,16 @@
-
- -
+
+ +
{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}
@@ -33,6 +39,16 @@
ریال
+
+
قیمت {{ selectedMeltedPaymentProduct.title }}:
+
+ {{ usePriceWithoutType(selectedMeltedTradePrice) }} +
+
ریال
+
@@ -48,7 +64,7 @@
- + + :rules="[justNumber2(props?.ProductItem?.price , form.method , props?.ProductItem?.amount, true, props?.ProductItem?.group_type, selectedMeltedPaymentBalance)]"> -
+
{{ buyMetalWalletBalanceText }}
@@ -272,6 +288,7 @@ import { Product } from '@/models/products' import BasicSelect from '@/components/BasicPacks/BasicSelect.vue' import Bg_main from '@/components/bg_main.vue' import { useFundUserStore } from '@/store/fundStore' +import { getProductsCatalogRef } from '@/composables/useProductsCatalog' import { getDefaultProductDealMethod, getProductDealMethodOptions, @@ -282,6 +299,12 @@ import { formatMainMetalReceiveText, calcSellReceivedMetalWeight, isMainMetalWalletDealMethod, + findMeltedDealMethodCard, + findProductDealMethodProduct, + getMeltedDealMethodBalance, + shouldShowProductDealMethodSelect, + resolveProductTradeBuyUnitPrice, + resolveProductTradeSellUnitPrice, formatProductWeightAmount, getProductQuantityUnit, getProductTotalWeightLabel, @@ -296,10 +319,10 @@ import { productMinOrderCash } from '@/utils/productOrderLimit' import { useWalletAssets } from '@/composables/useWalletAssets' -import { formatProductWalletBalanceText, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets' +import { formatProductWalletBalanceText, formatInventoryCardValue, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets' import { isStoreClosed, getStoreClosedMessage } from '@/utils/storeHours' -const typeDeal = computed(() => getProductDealMethodOptions(props?.ProductItem?.group_type, isBuy.value)) +const catalogProducts = getProductsCatalogRef() const isMetalReceiveDeal = computed(() => !isBuy.value && isMainMetalWalletDealMethod(form.value.method) @@ -347,12 +370,6 @@ const productWalletBalanceText = computed(() => ) ) -const buyMetalWalletBalanceText = computed(() => - form.value.method === 'katod' - ? formatMainMetalWalletBalanceText(walletStore?.walletInfo?.gold ?? 0) - : goldWalletBalanceText.value -) - const infoVisible = ref(false) const walletStore = useWalletStore() const { assetsSummary, loadWalletAssets } = useWalletAssets() @@ -566,18 +583,98 @@ const form = ref({ goldVazn:undefined }) +const inventoryCards = computed(() => assetsSummary.value?.inventory_cards ?? []) + +const dealMethodContext = computed(() => ({ + tradedProduct: props?.ProductItem ?? null, + productMetalId: props?.ProductItem?.metal_id, + inventoryCards: inventoryCards.value, + catalog: catalogProducts.value, +})) + +const typeDeal = computed(() => + getProductDealMethodOptions(props?.ProductItem?.group_type, isBuy.value, dealMethodContext.value) +) + +const showDealMethodSelect = computed(() => + shouldShowProductDealMethodSelect(props?.ProductItem, typeDeal.value) +) + +const selectedMeltedPaymentCard = computed(() => + findMeltedDealMethodCard(form.value.method, inventoryCards.value) +) + +const selectedMeltedPaymentProduct = computed(() => + findProductDealMethodProduct(form.value.method, catalogProducts.value) +) + +const selectedMeltedTradePrice = computed(() => { + const product = selectedMeltedPaymentProduct.value + if (!product) { + return 0 + } + + return isBuy.value + ? resolveProductTradeBuyUnitPrice(product) + : resolveProductTradeSellUnitPrice(product) +}) + +const selectedMeltedPaymentBalance = computed(() => + getMeltedDealMethodBalance( + form.value.method, + inventoryCards.value, + walletStore?.walletInfo?.gold + ) +) + +const buyMetalWalletBalanceText = computed(() => { + const card = selectedMeltedPaymentCard.value + const product = selectedMeltedPaymentProduct.value + if (card) { + return `موجودی ${card.title}: ${formatInventoryCardValue(card)}` + } + if (product) { + return `موجودی ${product.title}: ۰ ${getProductQuantityUnit(product)}` + } + + if (form.value.method === 'katod') { + return formatMainMetalWalletBalanceText(walletStore?.walletInfo?.gold ?? 0) + } + + return goldWalletBalanceText.value +}) + function resetDealMethod() { - form.value.method = getDefaultProductDealMethod(props?.ProductItem?.group_type, isBuy.value) + form.value.method = getDefaultProductDealMethod( + props?.ProductItem?.group_type, + isBuy.value, + dealMethodContext.value, + ) } watch( - () => [props?.ProductItem?.id, props?.ProductItem?.group_type], + () => [props?.ProductItem?.id, props?.ProductItem?.group_type, props?.ProductItem?.amount, inventoryCards.value, catalogProducts.value], () => { resetDealMethod() }, { immediate: true } ) +watch(showDealMethodSelect, (visible) => { + if (!visible) { + form.value.method = 'cash' + } +}) + +watch( + () => form.value.method, + (method) => { + if (!typeDeal.value.some((item) => item.key === method)) { + resetDealMethod() + } + } +) + // تابع تبدیل اعداد فارسی به انگلیسی const persianToEnglish = (str: string | number | undefined): string => { if (!str) return '' @@ -831,7 +928,11 @@ async function ok() { goldValue: undefined, goldCash: undefined, percentWallet: undefined, - method: getDefaultProductDealMethod(props?.ProductItem?.group_type, isBuy.value) + method: getDefaultProductDealMethod( + props?.ProductItem?.group_type, + isBuy.value, + dealMethodContext.value, + ) } } catch (e) { // console.log(e) diff --git a/src/views/base/products/components/ProductItem.vue b/src/views/base/products/components/ProductItem.vue index 941895a..09e84a5 100644 --- a/src/views/base/products/components/ProductItem.vue +++ b/src/views/base/products/components/ProductItem.vue @@ -1,6 +1,6 @@