feat(trade): wait on colleague order until status is final

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-02 15:55:08 +03:30
co-authored by Cursor
parent 164bb267f1
commit 0d19412c96
6 changed files with 222 additions and 15 deletions
+11
View File
@@ -104,6 +104,7 @@
</div>
<GoldDayCalculatorModal />
<GoldLoading v-model:show="colleagueOrderWaiting" />
</div>
</template>
@@ -129,6 +130,8 @@ import EnamadBadge from '@/components/EnamadBadge.vue'
import { usePwaInstall } from '@/composables/usePwaInstall'
import PwaInstallGuideModal from '@/components/pwa/PwaInstallGuideModal.vue'
import GoldDayCalculatorModal from '@/components/GoldDayCalculatorModal.vue'
import GoldLoading from '@/components/GoldLoading.vue'
import { colleagueOrderWaiting, resumeColleagueOrderIfPending } from '@/composables/useColleagueOrderWait'
import { useUserStore } from '@/store/userStore'
import { useSettingsStore } from '@/store/settingsStore'
import { connectChatIfExists, disconnectChat, isSettingFlagOn, unreadSupportCount, useChatService } from '@/utils/chat'
@@ -272,6 +275,14 @@ watch(
{ immediate: true },
)
watch(
() => getToken(),
(token) => {
if (token) void resumeColleagueOrderIfPending()
},
{ immediate: true },
)
watch(latestIncomingNotification, (payload) => {
if (!payload) return
if (payload.at === lastSupportToastAt) return
+14 -3
View File
@@ -7,11 +7,22 @@ import {useUserStore} from "@/store/userStore";
const userStore = useUserStore()
export const createOrder = async (obj: Order): Promise<any> => {
return await useAxios.post<Order>(`/customer${!userStore.isSingle ? '' : '/single'}/order`, obj)
function customerOrderPrefix() {
return `/customer${!userStore.isSingle ? '' : '/single'}`
}
export const createOrder = async (obj: Order): Promise<any> => {
return await useAxios.post<Order>(`${customerOrderPrefix()}/order`, obj)
}
export const getPendingColleagueOrder = async (): Promise<any> => {
return await useAxios.get(`${customerOrderPrefix()}/order/pending`)
}
export const getColleagueOrderStatus = async (id: number): Promise<any> => {
return await useAxios.get(`${customerOrderPrefix()}/order/${id}/status`)
}
export const getOrders = async (tbl: Ref<UnwrapRef<TableInput>>): Promise<TableOutput<Transaction>> => {
return await useAxios.post<TableOutput<Order>>(`/customer${!userStore.isSingle ? '' : '/single'}/order/table`, tbl)
return await useAxios.post<TableOutput<Order>>(`${customerOrderPrefix()}/order/table`, tbl)
}
+176
View File
@@ -0,0 +1,176 @@
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import router from '@/router'
import { getColleagueOrderStatus, getPendingColleagueOrder } from '@/api/order'
import { useUserStore } from '@/store/userStore'
import { useWalletStore } from '@/store/walletStore'
import { useFundUserStore } from '@/store/fundStore'
import { getToken } from '@/utils'
export const colleagueOrderWaiting = ref(false)
const CLIENT_POLL_MS = 5000
const STREAM_EVENT = 'colleague-order'
type WaitResult = 'success' | 'failed' | 'none'
let inflight: Promise<WaitResult> | null = null
export function emitColleagueOrderFromStream(payload: unknown) {
window.dispatchEvent(new CustomEvent(STREAM_EVENT, { detail: payload }))
}
export function isColleagueOrderPending(res: unknown): boolean {
if (!res || typeof res !== 'object') return false
const row = res as Record<string, any>
if (row.pending === true) return true
if (Array.isArray(row) && row[0]?.pending === true) return true
const sts = Number(
row[0]?.sts ?? row.sts ?? row[1]?.order?.sts ?? row.order?.sts ?? NaN
)
return sts === 4
}
export function colleagueOrderIdFromResponse(res: unknown): number | null {
if (!res || typeof res !== 'object') return null
const row = res as Record<string, any>
const id = Number(
row[0]?.order_id ?? row.order_id ?? row[1]?.order?.id ?? row.order?.id ?? NaN
)
return id > 0 ? id : null
}
async function finishSuccess() {
try {
await useUserStore().getMyInfo()
} catch {
/* ignore */
}
try {
await useWalletStore().getMyWallet()
} catch {
/* ignore */
}
try {
await useFundUserStore().getMyFund()
} catch {
/* ignore */
}
try {
const { resetTransactionsPageData } = await import(
'@/views/base/transactions/composables/useTransactionsPageData'
)
resetTransactionsPageData()
} catch {
/* optional */
}
message.success('سفارش شما با موفقیت انجام شد')
if (router.currentRoute.value.name !== 'transactions') {
await router.push({ name: 'transactions' })
}
}
function stsFromPayload(payload: Record<string, any> | null | undefined): number | null {
if (!payload) return null
const sts = Number(payload.sts ?? payload.order?.sts)
return Number.isFinite(sts) ? sts : null
}
async function readStatus(orderId: number | null): Promise<number | null> {
try {
if (orderId && orderId > 0) {
const row = (await getColleagueOrderStatus(orderId)) as Record<string, any>
return stsFromPayload(row)
}
const row = (await getPendingColleagueOrder()) as Record<string, any>
if (row?.pending === false && row?.sts == null) {
return null
}
return stsFromPayload(row)
} catch {
return null
}
}
export async function waitForColleagueOrder(orderId?: number | null): Promise<WaitResult> {
if (inflight) return inflight
inflight = (async () => {
colleagueOrderWaiting.value = true
let settled: WaitResult | null = null
const finish = (result: WaitResult) => {
if (settled) return
settled = result
}
const onStream = (event: Event) => {
const detail = (event as CustomEvent).detail as Record<string, any> | undefined
if (orderId && Number(detail?.order_id) > 0 && Number(detail.order_id) !== orderId) {
return
}
const sts = stsFromPayload(detail)
if (sts === 2) finish('success')
if (sts === 3) finish('failed')
}
window.addEventListener(STREAM_EVENT, onStream)
try {
while (!settled) {
const sts = await readStatus(orderId ?? null)
if (sts === 2) {
finish('success')
break
}
if (sts === 3) {
finish('failed')
break
}
await new Promise<void>((resolve) => {
const timer = window.setTimeout(resolve, CLIENT_POLL_MS)
const once = () => {
window.clearTimeout(timer)
resolve()
}
window.addEventListener(STREAM_EVENT, once, { once: true })
})
}
} finally {
window.removeEventListener(STREAM_EVENT, onStream)
colleagueOrderWaiting.value = false
}
if (settled === 'success') {
await finishSuccess()
return 'success'
}
message.error('سفارش توسط همکار رد شد')
return 'failed'
})().finally(() => {
inflight = null
})
return inflight
}
export async function afterCreateOrder(res: unknown): Promise<WaitResult> {
if (isColleagueOrderPending(res)) {
return waitForColleagueOrder(colleagueOrderIdFromResponse(res))
}
await finishSuccess()
return 'success'
}
export async function resumeColleagueOrderIfPending(): Promise<WaitResult> {
if (!getToken()) return 'none'
try {
const row = (await getPendingColleagueOrder()) as Record<string, any>
if (!row?.pending) return 'none'
const id = Number(row.order_id ?? row.order?.id ?? 0)
return waitForColleagueOrder(id > 0 ? id : null)
} catch {
return 'none'
}
}
+10
View File
@@ -560,6 +560,16 @@ function bindStreamEvents(source: AuthPriceStream) {
}
})
source.addEventListener('colleague_order', (event) => {
try {
void import('@/composables/useColleagueOrderWait').then(({ emitColleagueOrderFromStream }) => {
emitColleagueOrderFromStream(JSON.parse(event.data))
})
} catch (error) {
console.warn('Colleague order SSE parse failed', error)
}
})
source.addEventListener('ping', () => {
touchStreamActivity()
})
+6 -6
View File
@@ -251,6 +251,7 @@ import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import router from '@/router'
import { useWalletStore } from '@/store/walletStore'
import { createOrder } from '@/api/order'
import { afterCreateOrder } from '@/composables/useColleagueOrderWait'
import { message } from 'ant-design-vue'
import type { Price } from '@/models/home'
import { getLastPrice } from '@/api/home'
@@ -790,7 +791,7 @@ async function ok() {
loading.value = true
await nextTick()
infoVisible.value = true
await createOrder({
const res = await createOrder({
direction: isBuy.value ? 1 : 2,
price: Math.round(unitPrice.value),
amount: normalizeOrderGoldAmount(orderGoldAmount.value),
@@ -798,17 +799,16 @@ async function ok() {
target_cash: calcTradeTargetCash(form.value.goldCash, inputLock.value),
abshode_id: props.ProductItem?.id,
})
await walletStore.getMyWallet()
await loadWalletAssets(true)
await router.push({ name: 'transactions' })
const result = await afterCreateOrder(res)
if (result !== 'success') {
return
}
form.value = {
goldValue: undefined,
goldCash: undefined,
percentWallet: 0,
}
inputLock.value = null
message.success('سفارش شما با موفقیت انجام شد')
await fundStore.getMyFund()
} catch (e) {
// console.log(e)
// await initPrice()
@@ -252,6 +252,7 @@ import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import router from '@/router'
import { useWalletStore } from '@/store/walletStore'
import { createOrder } from '@/api/order'
import { afterCreateOrder } from '@/composables/useColleagueOrderWait'
import { FormInstance, message } from 'ant-design-vue'
import type { Price } from '@/models/home'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
@@ -734,9 +735,10 @@ async function ok() {
window.location.href = returnUrl
return // Stop execution here
}
// If no gateway, continue with normal flow
await walletStore.getMyWallet()
await router.push({ name: 'transactions' })
const result = await afterCreateOrder(data)
if (result !== 'success') {
return
}
form.value = {
goldValue: undefined,
goldCash: undefined,
@@ -744,9 +746,6 @@ async function ok() {
method: 'cash',
gateway: false
}
// refreshTable()
message.success('سفارش شما با موفقیت انجام شد')
await loadWalletAssets(true)
} catch (e) {
// console.log(e)
// await initPrice()