feat: align fee summary, special pricing and support chat with modular

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 17:57:22 +03:30
co-authored by Cursor
parent e9c5830d4e
commit e01192fb5e
11 changed files with 292 additions and 49 deletions
+3 -2
View File
@@ -3,7 +3,7 @@ import { useWebSocket } from '@vueuse/core'
import { useUserStore } from '@/store/userStore'
import { useSettingsStore } from '@/store/settingsStore'
import { startChat } from '@/api/chat'
import { createUuid } from '@/utils/index'
import { createUuid, getToken } from '@/utils/index'
/** true/1/yes/activate */
export function isSettingFlagOn(value: unknown): boolean {
@@ -11,8 +11,9 @@ export function isSettingFlagOn(value: unknown): boolean {
return v === 'true' || v === '1' || v === 'yes' || v === 'activate'
}
/** نمایش چت برای کاربر نهایی: سوییچ دیپلوی + سوییچ مشتری */
/** نمایش چت برای کاربر نهایی: لاگین + سوییچ دیپلوی + سوییچ مشتری */
export function isUserSupportChatEnabled(): boolean {
if (!getToken()) return false
const settings = useSettingsStore()
return (
isSettingFlagOn(settings.getSettingByKey('show_to_users_chat')) &&
+2 -1
View File
@@ -7,6 +7,7 @@ import {
priceWssEnabled,
shouldUsePricePolling,
} from '@/utils/priceSocketState'
import { applyBoardSpecialToLastPayload } from '@/utils/specialPricing'
const LAST_INTERVAL_MS = 10000
@@ -161,7 +162,7 @@ export async function getLastPrices(): Promise<boolean> {
}
export function applyLastPricePayload(dataLast: Price) {
const normalized = normalizePriceResponse(dataLast)
const normalized = normalizePriceResponse(applyBoardSpecialToLastPayload(dataLast as any) as Price)
const nextBuy = getTradeBuyPrice(normalized)
const prevBuy = getTradeBuyPrice(lastPrice.value)
+13 -1
View File
@@ -9,6 +9,10 @@ import {
priceStreamLifecycle,
priceWssEnabled,
} from '@/utils/priceSocketState'
import {
applyBoardSpecialToLastPayload,
setSpecialPricingPayload,
} from '@/utils/specialPricing'
export {
priceSocketConnected,
@@ -326,7 +330,7 @@ function handleLastPayload(payload: unknown) {
return
}
const last = payload as Price
const last = applyBoardSpecialToLastPayload(payload as Price)
if (last.price_last == null && last.price == null && last.buy == null) {
return
}
@@ -413,6 +417,14 @@ function bindStreamEvents(source: EventSource) {
}
})
source.addEventListener('special-pricing', (event) => {
try {
setSpecialPricingPayload(JSON.parse(event.data))
} catch (error) {
console.warn('Special-pricing SSE parse failed', error)
}
})
source.addEventListener('ping', () => {
touchStreamActivity()
})
+29 -3
View File
@@ -5,7 +5,7 @@ export type ProductCashFeeSource = {
cash_fee_sell_is_percent?: boolean | null
}
/** 0.5 => 0.5% ، 5 => 5% */
/** 0.5 => 0.5% ، 5 => 5% — همیشه عدد درصد */
export function resolveCashFeePercentRatio(percentValue?: number | null) {
const offset = Number(percentValue ?? 0)
if (!offset || !Number.isFinite(offset)) {
@@ -15,6 +15,11 @@ export function resolveCashFeePercentRatio(percentValue?: number | null) {
return offset / 100
}
/** @deprecated از resolveCashFeePercentRatio استفاده کنید */
export function resolveFeePercentRatio(percentValue?: number | null) {
return resolveCashFeePercentRatio(percentValue)
}
export function resolveProductCashFeeRial(
feeValue: number | null | undefined,
isPercent: boolean | null | undefined,
@@ -38,17 +43,38 @@ export function resolveProductCashFeeRial(
}
export function resolveProductCashFeeBuyRial(product: ProductCashFeeSource, basePriceRial: number) {
return resolveProductCashFeeRial(
const baseFee = resolveProductCashFeeRial(
product.cash_fee_buy,
product.cash_fee_buy_is_percent,
basePriceRial,
)
const special = Number((product as any).special_profit_buy ?? 0)
return baseFee + (Number.isFinite(special) ? Math.round(special) : 0)
}
export function resolveProductCashFeeSellRial(product: ProductCashFeeSource, basePriceRial: number) {
return resolveProductCashFeeRial(
const baseFee = resolveProductCashFeeRial(
product.cash_fee_sell,
product.cash_fee_sell_is_percent,
basePriceRial,
)
const special = Number((product as any).special_profit_sell ?? 0)
return baseFee + (Number.isFinite(special) ? Math.round(special) : 0)
}
export function formatProductCashFeeLabel(
feeValue: number | null | undefined,
isPercent?: boolean | null,
) {
const raw = Number(feeValue ?? 0)
if (!raw) {
return '۰'
}
if (isPercent) {
const text = Number.isInteger(raw) ? String(raw) : String(parseFloat(raw.toFixed(4)))
return `${text}%`
}
return `${raw} ریال`
}
+7 -3
View File
@@ -1,5 +1,5 @@
import { ref } from 'vue'
import { getSettings } from '@/api/configuration'
import { useSettingsStore } from '@/store/settingsStore'
export const serviceFeePercentage = ref<number>(0.01)
@@ -68,8 +68,12 @@ function feeRate() {
export async function loadServiceFeeFromSettings() {
try {
const settings = await getSettings()
if (settings && Array.isArray(settings)) {
const settingsStore = useSettingsStore()
if (!settingsStore.loaded) {
await settingsStore.fetchSettings()
}
const settings = settingsStore.settings
if (settings?.length) {
const feeSetting = settings.find(
(s) => s?.key === 'fee_service_charge_percentage' && s?.is_active === true
)
+80
View File
@@ -0,0 +1,80 @@
import { ref } from 'vue'
export type SpecialPricingRule = {
product_id?: number
package_id: number
title: string
profit_buy: number
profit_sell: number
show_product: boolean
}
export type SpecialPricingPayload = {
enabled?: boolean
products?: SpecialPricingRule[]
board?: SpecialPricingRule | null
}
const state = ref<{
enabled: boolean
byProduct: Record<number, SpecialPricingRule>
board: SpecialPricingRule | null
}>({
enabled: false,
byProduct: {},
board: null,
})
export function setSpecialPricingPayload(payload?: SpecialPricingPayload | null) {
if (!payload || !payload.enabled) {
state.value = { enabled: false, byProduct: {}, board: null }
return
}
const byProduct: Record<number, SpecialPricingRule> = {}
for (const row of payload.products ?? []) {
const pid = Number(row.product_id ?? 0)
if (pid > 0) byProduct[pid] = row
}
state.value = {
enabled: true,
byProduct,
board: payload.board ?? null,
}
}
export function getSpecialPricingState() {
return state
}
/** اگر سرور قبلاً special_pricing نزده، اختلاف تابلو را روی last اعمال کن */
export function applyBoardSpecialToLastPayload<T extends Record<string, any>>(payload: T): T {
if (!payload || typeof payload !== 'object') return payload
if (payload.special_pricing) return payload
const board = state.value.board
if (!state.value.enabled || !board) return payload
const buyExtra = Number(board.profit_buy ?? 0)
const sellExtra = Number(board.profit_sell ?? 0)
if (buyExtra <= 0 && sellExtra <= 0) return payload
const next = { ...payload } as T & Record<string, any>
if (Number(next.buy ?? 0) > 0 && buyExtra > 0) {
next.buy = Number(next.buy) + buyExtra
}
if (Number(next.sell ?? 0) > 0 && sellExtra > 0) {
next.sell = Math.max(0, Number(next.sell) - sellExtra)
}
if (Number(next.price_last ?? 0) > 0 && buyExtra > 0) {
next.price_last = Number(next.price_last) + buyExtra
}
next.special_pricing = {
package_id: board.package_id,
title: board.title,
profit_buy: buyExtra,
profit_sell: sellExtra,
}
return next
}
+2 -5
View File
@@ -1,5 +1,4 @@
import { ref } from 'vue'
import { getSettings } from '@/api/configuration'
import type { DepositGuideVars } from '@/utils/depositGuideText'
import {
DEFAULT_DEPOSIT_GUIDE_TEXT,
@@ -366,10 +365,8 @@ export async function loadSupportMessengerFromSettings(forceSettingsRefresh = fa
await settingsStore.fetchSettings()
}
const settings = await getSettings()
if (!settings || !Array.isArray(settings)) return
settingsStore.mergeSettings(settings)
const settings = settingsStore.settings
if (!settings?.length) return
supportMessengerSettings.value = {
messengers: resolveSupportMessengersFromSettings(settings),
-8
View File
@@ -1,4 +1,3 @@
import { getSettings } from '@/api/configuration'
import type { AppSetting } from '@/store/settingsStore'
import { useSettingsStore } from '@/store/settingsStore'
@@ -90,13 +89,6 @@ export async function fetchTrackingSettings(force = false): Promise<AppSetting[]
console.log(error)
}
try {
const freshSettings = normalizeSettingsResponse(await getSettings())
if (freshSettings.length > 0) settingsStore.mergeSettings(freshSettings)
} catch (error) {
console.log(error)
}
trackingSettingsCache = [...settingsStore.settings]
return trackingSettingsCache
})()