fix(trade): use stream closed and trade_open instead of device clock

Changing the phone time can no longer open a product that the server has closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 12:58:19 +03:30
co-authored by Cursor
parent 2d3c590c71
commit bc2acc3eb6
11 changed files with 86 additions and 24 deletions
+1
View File
@@ -10,6 +10,7 @@ export interface Product {
unit: string
active: boolean
show: boolean
trade_open?: boolean
show_in_assets?: boolean
show_on_tv?: boolean
open_sell: boolean
+13
View File
@@ -24,6 +24,19 @@ export function isProductTradeOpen(product: TradeOpenProduct, isBuy: boolean) {
return isBuy ? isProductBuyOpen(product) : isProductSellOpen(product)
}
/** ساعت کاری محصول از استریم (trade_open) — نه ساعت دستگاه */
export function isProductTradingHoursOpen(
product?: { show?: boolean; trade_open?: boolean } | null,
) {
if (!product) return false
if (typeof product.trade_open === 'boolean') return product.trade_open
return product.show === true
}
export function productHoursClosedMessage() {
return 'این محصول در حال حاضر خارج از ساعت کاری است.'
}
/** تب اولیه با توجه به query و وضعیت باز/بسته خرید و فروش */
export function resolveProductTradeIsBuy(product: TradeOpenProduct, routeState?: string | null) {
const wantSell = routeState === 'sell'
+5 -14
View File
@@ -225,20 +225,11 @@ export function isStoreClosedBySchedule(now = new Date()): boolean {
return isOutsideTimeWindow(now, start, end)
}
/** باز/بسته بودن فروشگاه فقط از استریم last.closed — نه ساعت دستگاه کاربر */
export function isStoreClosed(): boolean {
void productTradingHoursVersion.value
const { closePriceEnabled } = getStoreTradingHours()
if (closePriceEnabled) {
if (lastPrice.value?.closed === true) {
return true
}
return isStoreClosedBySchedule()
}
// وقتی close_price خاموش است، صفر بودن قیمت مرجع به معنی بسته بودن فروشگاه نیست.
return isStoreClosedBySchedule()
void lastPriceReady.value
void lastPrice.value
return lastPrice.value?.closed === true
}
export function getStoreClosedMessage(): string {
@@ -306,7 +297,7 @@ export function getPriceDisplayState(): PriceDisplayState {
return serverStatus
}
if (isOutsideGlobalStoreHours()) {
if (p?.closed === true) {
if (!shouldShowHomePriceWhenClosed()) return 'closed'
return hasShowableHomePrice(p) ? 'open' : 'no_quote'
}
+11
View File
@@ -300,6 +300,8 @@ import {
isProductBuyOpen,
isProductSellOpen,
isProductTradeOpen,
isProductTradingHoursOpen,
productHoursClosedMessage,
productTradeClosedMessage,
resolveProductTradeIsBuy,
} from '@/utils/productTradeOpen'
@@ -468,6 +470,7 @@ const priceUnavailable = computed(() => {
const disableTransaction = computed(() =>
loading.value ||
isStoreClosed() ||
!isProductTradingHoursOpen(props.ProductItem) ||
!isProductTradeOpen(props.ProductItem, isBuy.value) ||
form.value?.goldValue === undefined ||
form.value?.goldValue === '' ||
@@ -763,6 +766,10 @@ function okConfirm() {
message.warning(getStoreClosedMessage())
return
}
if (!isProductTradingHoursOpen(props.ProductItem)) {
message.warning(productHoursClosedMessage())
return
}
if (!isProductTradeOpen(props.ProductItem, isBuy.value)) {
message.warning(productTradeClosedMessage(isBuy.value))
return
@@ -782,6 +789,10 @@ async function ok() {
message.warning(getStoreClosedMessage())
return
}
if (!isProductTradingHoursOpen(props.ProductItem)) {
message.warning(productHoursClosedMessage())
return
}
if (!isProductTradeOpen(props.ProductItem, isBuy.value)) {
message.warning(productTradeClosedMessage(isBuy.value))
return
@@ -274,6 +274,8 @@ import { SelectItem } from '@/models/SelectItem'
import Bg_main from '@/components/bg_main.vue'
import { useWalletAssets } from '@/composables/useWalletAssets'
import { useCartStore } from '@/store/cartStore'
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
import { isProductTradingHoursOpen, productHoursClosedMessage } from '@/utils/productTradeOpen'
const typeDeal: SelectItem[] = [
{
@@ -346,6 +348,9 @@ const finalPrice = computed(() => (form.value?.goldValue * (isBuy.value ? props.
const payablePrice = computed(() => finalPrice.value)
const disableTransaction = computed(() => {
if (isStoreClosed() || !isProductTradingHoursOpen(props.ProductItem)) {
return true
}
// If gateway is enabled, skip wallet balance checks
if (isBuy.value && form.value?.gateway) {
return (
@@ -662,6 +667,14 @@ async function initPrice() {
async function okConfirm() {
try {
if (isStoreClosed()) {
message.warning(getStoreClosedMessage())
return
}
if (!isProductTradingHoursOpen(props.ProductItem)) {
message.warning(productHoursClosedMessage())
return
}
console.log('okConfirm called, form values:', form.value)
console.log('disableTransaction:', disableTransaction.value)
await refInput.value?.validateFields()
@@ -703,6 +716,14 @@ async function okConfirm() {
async function ok() {
console.log('ok() function called')
try {
if (isStoreClosed()) {
message.warning(getStoreClosedMessage())
return
}
if (!isProductTradingHoursOpen(props.ProductItem)) {
message.warning(productHoursClosedMessage())
return
}
loading.value = true
infoVisible.value = true
const data = await createOrder({
@@ -65,7 +65,7 @@
<template>
<div
class="product-card relative flex cursor-pointer rounded-xl border border-gray-200 bg-white p-3 transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
:class="!props.ProductItem.show && 'opacity-50 grayscale'"
:class="!isProductTradingHoursOpen(props.ProductItem) && 'opacity-50 grayscale'"
>
<span
v-if="isOutOfStock"
@@ -105,6 +105,7 @@ import { computed } from 'vue'
import type { Product } from '@/models/products'
import { usePrice } from '@/utils/usePrice'
import { getProductBuyPrice } from '@/utils/productPrice'
import { isProductTradingHoursOpen } from '@/utils/productTradeOpen'
import ProductImage from '@/components/ProductImage.vue'
interface Props {
@@ -1,7 +1,7 @@
<template>
<div
class="product-card relative flex h-full flex-col rounded-xl border border-gray-200 bg-white p-3 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg cursor-pointer"
:class="!props.ProductItem.show && 'opacity-50 grayscale'"
:class="!isProductTradingHoursOpen(props.ProductItem) && 'opacity-50 grayscale'"
>
<span
v-if="isOutOfStock"
@@ -37,6 +37,7 @@ import { computed } from 'vue'
import type { Product } from '@/models/products'
import { usePrice } from '@/utils/usePrice'
import { getProductBuyPrice } from '@/utils/productPrice'
import { isProductTradingHoursOpen } from '@/utils/productTradeOpen'
import ProductImage from '@/components/ProductImage.vue'
interface Props {
+2
View File
@@ -80,6 +80,7 @@ import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import ProductItemVertical from '@/views/base/products/components/ProductItemVertical.vue'
import MobileProductsView from '@/views/base/products/mobile/MobileProductsView.vue'
import { isProductVisible } from '@/utils/productPrice'
import { isProductTradingHoursOpen } from '@/utils/productTradeOpen'
import {
productsActiveKey,
productsFirstItems,
@@ -102,6 +103,7 @@ const visibleTypeItems = computed(() => {
})
function clickProduct(item: Product) {
if (!isProductTradingHoursOpen(item)) return
if (isProductVisible(item)) {
router.push({ name: 'productDetail', params: { id: item.id } })
}
@@ -3,9 +3,10 @@ import { useRoute, useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { cloneDeep } from 'lodash'
import { getProductById } from '@/api/products'
import { findProductInCatalog, getProductsCatalog } from '@/composables/useProductsCatalog'
import { findProductInCatalog, getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
import { getLastPrices, stopPriceInterval } from '@/utils/price'
import { getProductBuyPrice } from '@/utils/productPrice'
import { isProductTradingHoursOpen } from '@/utils/productTradeOpen'
import { loading } from '@/utils'
import type { Product } from '@/models/products'
@@ -13,6 +14,7 @@ export interface ProductDetailPageContext {
product: ReturnType<typeof ref<Product | undefined>>
showFilter: ReturnType<typeof ref<boolean>>
isOutOfStock: ReturnType<typeof computed<boolean>>
hoursOpen: ReturnType<typeof computed<boolean>>
handleBuyClick: () => void
goToProduct: (id: number) => void
getProductBuyPrice: typeof getProductBuyPrice
@@ -33,6 +35,7 @@ export function useProductDetailPage() {
})
const isOutOfStock = computed(() => !product.value?.inventory || product.value.inventory === 0)
const hoursOpen = computed(() => isProductTradingHoursOpen(product.value))
watch(productId, (newId, oldId) => {
if (newId && newId !== oldId) {
@@ -41,6 +44,23 @@ export function useProductDetailPage() {
}
})
watch(
() => getProductsCatalogRef().value.map((p) => `${p.id}:${p.trade_open}:${p.show}`).join('|'),
() => {
const current = product.value
if (!current?.id) return
const live = findProductInCatalog(current.id)
if (!live) return
current.show = live.show
if (typeof live.trade_open === 'boolean') {
current.trade_open = live.trade_open
}
if (showFilter.value && !isProductTradingHoursOpen(current)) {
showFilter.value = false
}
},
)
async function loadProduct() {
try {
loading.value = true
@@ -67,7 +87,7 @@ export function useProductDetailPage() {
}
function handleBuyClick() {
if (!product.value?.show) return
if (!isProductTradingHoursOpen(product.value)) return
showFilter.value = true
product.value = cloneDeep(product.value)
}
@@ -88,6 +108,7 @@ export function useProductDetailPage() {
product,
showFilter,
isOutOfStock,
hoursOpen,
handleBuyClick,
goToProduct,
getProductBuyPrice,
@@ -18,14 +18,14 @@
<div class="dproduct-info__price-row">
<span class="dproduct-info__price">{{ usePrice(getProductBuyPrice(product)) }}</span>
<BasicButton
v-if="product.show && product.open_buy && product.inventory"
v-if="hoursOpen && product.open_buy && product.inventory"
type="primary"
size="large"
@click="handleBuyClick"
>
خرید محصول
</BasicButton>
<div v-else-if="!product.show" class="text-gray-500">این محصول در حال حاضر در دسترس نیست</div>
<div v-else-if="!hoursOpen" class="text-gray-500">این محصول در حال حاضر در دسترس نیست</div>
<div v-else-if="!product.open_buy" class="text-gray-500">خرید این محصول در حال حاضر امکان پذیر نیست</div>
<div v-else class="text-gray-500">ناموجود</div>
</div>
@@ -72,7 +72,7 @@ import { loading } from '@/utils'
import { usePrice } from '@/utils/usePrice'
import { useProductDetailPageContext } from '../composables/useProductDetailPage'
const { product, isOutOfStock, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext()
const { product, isOutOfStock, hoursOpen, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext()
</script>
<style scoped>
@@ -17,7 +17,7 @@
</div>
<div class="flex-1">
<BasicButton
v-if="product?.show && product?.open_buy && product?.inventory"
v-if="hoursOpen && product?.open_buy && product?.inventory"
class="w-full py-3"
type="primary"
size="large"
@@ -25,7 +25,7 @@
>
خرید محصول
</BasicButton>
<div v-else-if="!product?.show" class="text-center text-gray-500 py-4">
<div v-else-if="!hoursOpen" class="text-center text-gray-500 py-4">
این محصول در حال حاضر در دسترس نیست
</div>
<div v-else-if="!product?.open_buy" class="text-center text-gray-500 py-4">
@@ -76,7 +76,7 @@ import { Swiper, SwiperSlide } from 'swiper/vue'
import 'swiper/swiper-bundle.css'
import { useProductDetailPageContext } from '../composables/useProductDetailPage'
const { product, isOutOfStock, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext()
const { product, isOutOfStock, hoursOpen, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext()
</script>
<style scoped>