Add e-payment UI, checkout payment result, and legacy migrate domain lookup.
Wire Mellat settings in business dashboard, customer bank return page with payment details, and super-admin migrate modal domain confirmation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
a11f9643bb
commit
cc94b929e0
@@ -26,6 +26,7 @@ import { CheckoutDeliveryStep } from './pages/checkout/CheckoutDeliveryStep'
|
||||
import { CheckoutPaymentStep } from './pages/checkout/CheckoutPaymentStep'
|
||||
import { CheckoutSuccessStep } from './pages/checkout/CheckoutSuccessStep'
|
||||
import { CheckoutFailedStep } from './pages/checkout/CheckoutFailedStep'
|
||||
import { CheckoutResultStep } from './pages/checkout/CheckoutResultStep'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -44,6 +45,7 @@ function App() {
|
||||
<Route path="cart" element={<CheckoutCartStep />} />
|
||||
<Route path="delivery" element={<CheckoutDeliveryStep />} />
|
||||
<Route path="payment" element={<CheckoutPaymentStep />} />
|
||||
<Route path="result" element={<CheckoutResultStep />} />
|
||||
<Route path="success" element={<CheckoutSuccessStep />} />
|
||||
<Route path="failed" element={<CheckoutFailedStep />} />
|
||||
<Route index element={<CheckoutCartStep />} />
|
||||
|
||||
@@ -8,6 +8,7 @@ export const customerRouteTitleRules: RouteTitleRule[] = [
|
||||
{ match: '/checkout/cart', labels: ['Checkout', 'Shopping Cart'] },
|
||||
{ match: '/checkout/delivery', labels: ['Checkout', 'Delivery'] },
|
||||
{ match: '/checkout/payment', labels: ['Checkout', 'Payment'] },
|
||||
{ match: '/checkout/result', labels: ['Checkout', 'Payment result'] },
|
||||
{ match: '/checkout/success', labels: ['Checkout', 'Success'] },
|
||||
{ match: '/checkout/failed', labels: ['Checkout', 'Failed'] },
|
||||
{ match: '/checkout', labels: ['Checkout'] },
|
||||
|
||||
@@ -1,12 +1,40 @@
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import orderFailedImg from '../../assets/order-failed.png'
|
||||
import type { Order } from '../../services/orderService'
|
||||
import { formatIrtPrice } from '../../utils/irtPrice'
|
||||
import {
|
||||
gatewayLabelFa,
|
||||
transactionStatusLabelFa,
|
||||
type CheckoutPaymentInfo,
|
||||
} from '../../utils/checkoutPayment'
|
||||
import styles from './CheckoutSteps.module.css'
|
||||
|
||||
export function CheckoutFailedStep() {
|
||||
type FailedLocationState = {
|
||||
message?: string
|
||||
payment?: CheckoutPaymentInfo
|
||||
order?: Order
|
||||
}
|
||||
|
||||
type CheckoutFailedStepProps = {
|
||||
message?: string
|
||||
payment?: CheckoutPaymentInfo | null
|
||||
order?: Order | null
|
||||
}
|
||||
|
||||
export function CheckoutFailedStep({
|
||||
message: messageProp,
|
||||
payment: paymentProp,
|
||||
order: orderProp,
|
||||
}: CheckoutFailedStepProps = {}) {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const navState = (location.state as FailedLocationState | null) ?? null
|
||||
const payment = paymentProp ?? navState?.payment ?? null
|
||||
const order = orderProp ?? navState?.order ?? null
|
||||
const message =
|
||||
(location.state as { message?: string } | null)?.message ||
|
||||
messageProp ||
|
||||
navState?.message ||
|
||||
payment?.message ||
|
||||
'ثبت سفارش ممکن نشد. لطفاً دوباره تلاش کنید.'
|
||||
|
||||
return (
|
||||
@@ -20,10 +48,57 @@ export function CheckoutFailedStep() {
|
||||
</div>
|
||||
|
||||
<div className={styles.successHeader}>
|
||||
<h1 className={styles.stepTitle}>سفارش ثبت نشد</h1>
|
||||
<h1 className={styles.stepTitle}>
|
||||
{payment ? 'پرداخت ناموفق' : 'سفارش ثبت نشد'}
|
||||
</h1>
|
||||
<p className={styles.stepDesc}>{message}</p>
|
||||
</div>
|
||||
|
||||
{(order || payment) && (
|
||||
<dl className={styles.successMeta}>
|
||||
{order ? (
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>شماره سفارش</dt>
|
||||
<dd className={styles.successMetaValue}>{order.orderNumber}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{payment?.amount != null ? (
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>مبلغ</dt>
|
||||
<dd className={styles.successMetaValue}>
|
||||
{formatIrtPrice(payment.amount)}
|
||||
</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{payment ? (
|
||||
<>
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>وضعیت پرداخت</dt>
|
||||
<dd>{transactionStatusLabelFa('failed')}</dd>
|
||||
</div>
|
||||
{payment.gatewayType ? (
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>درگاه</dt>
|
||||
<dd>{gatewayLabelFa(payment.gatewayType)}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{payment.trackId ? (
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>کد پیگیری</dt>
|
||||
<dd className={styles.successMetaValue}>{payment.trackId}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{payment.transactionId ? (
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>شناسه تراکنش</dt>
|
||||
<dd className={styles.successMetaValue}>{payment.transactionId}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</dl>
|
||||
)}
|
||||
|
||||
<div className={styles.successActions}>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -4,14 +4,20 @@ import { useAuth } from '../../context/AuthContext'
|
||||
import { CheckoutStepper, type CheckoutStepId } from '../../components/checkout/CheckoutStepper'
|
||||
import { loginPathWithRedirect, safeRedirectPath } from '../../lib/redirect'
|
||||
|
||||
/** Cart and checkout login are public. Delivery, payment, and success require auth. */
|
||||
/** Cart and checkout login are public. Delivery, payment, and result require auth. */
|
||||
const PUBLIC_STEPS = new Set<CheckoutStepId>(['cart', 'login'])
|
||||
|
||||
function stepFromPath(pathname: string): CheckoutStepId {
|
||||
if (pathname.endsWith('/login')) return 'login'
|
||||
if (pathname.endsWith('/delivery')) return 'delivery'
|
||||
if (pathname.endsWith('/payment')) return 'payment'
|
||||
if (pathname.endsWith('/success') || pathname.endsWith('/failed')) return 'payment'
|
||||
if (
|
||||
pathname.endsWith('/success') ||
|
||||
pathname.endsWith('/failed') ||
|
||||
pathname.endsWith('/result')
|
||||
) {
|
||||
return 'payment'
|
||||
}
|
||||
return 'cart'
|
||||
}
|
||||
|
||||
@@ -26,11 +32,12 @@ export function CheckoutFlow() {
|
||||
}
|
||||
|
||||
if (!isAuthenticated && !PUBLIC_STEPS.has(step)) {
|
||||
const returnTo = `${location.pathname}${location.search}`
|
||||
return (
|
||||
<Navigate
|
||||
to={loginPathWithRedirect(location.pathname, '/checkout/login')}
|
||||
to={loginPathWithRedirect(returnTo, '/checkout/login')}
|
||||
replace
|
||||
state={{ from: location.pathname }}
|
||||
state={{ from: returnTo }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -43,11 +50,14 @@ export function CheckoutFlow() {
|
||||
return <Navigate to={destination} replace />
|
||||
}
|
||||
|
||||
const hideStepper =
|
||||
location.pathname === '/checkout/success' ||
|
||||
location.pathname === '/checkout/failed' ||
|
||||
location.pathname === '/checkout/result'
|
||||
|
||||
return (
|
||||
<>
|
||||
{step !== 'login' &&
|
||||
location.pathname !== '/checkout/success' &&
|
||||
location.pathname !== '/checkout/failed' && (
|
||||
{step !== 'login' && !hideStepper && (
|
||||
<CheckoutStepper current={step} isAuthenticated={isAuthenticated} />
|
||||
)}
|
||||
<Outlet />
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ApiError, isAbortError } from '../../lib/api'
|
||||
import {
|
||||
checkoutCart,
|
||||
getCart,
|
||||
submitPaymentRedirect,
|
||||
type CheckoutPaymentType,
|
||||
type OnlineGatewayType,
|
||||
} from '../../services/cartService'
|
||||
@@ -16,9 +17,9 @@ import { CheckoutActionBar } from '../../components/checkout/CheckoutActionBar'
|
||||
import { CheckoutBackButton } from '../../components/checkout/CheckoutBackButton'
|
||||
import styles from './CheckoutSteps.module.css'
|
||||
|
||||
const ONLINE_GATEWAYS: { id: OnlineGatewayType; label: string }[] = [
|
||||
{ id: 'mellat_behpardakht', label: 'درگاه پرداخت بانک ملت - بهپرداخت' },
|
||||
{ id: 'saman_kish', label: 'درگاه پرداخت بانک سامان - سامان کیش' },
|
||||
const ONLINE_GATEWAYS: { id: OnlineGatewayType; label: string; ready: boolean }[] = [
|
||||
{ id: 'mellat', label: 'درگاه پرداخت بانک ملت - بهپرداخت', ready: true },
|
||||
{ id: 'sep', label: 'درگاه پرداخت بانک سامان - سامان کیش', ready: false },
|
||||
]
|
||||
|
||||
export function CheckoutPaymentStep() {
|
||||
@@ -31,7 +32,7 @@ export function CheckoutPaymentStep() {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [paymentType, setPaymentType] = useState<CheckoutPaymentType>('e_payment_gate')
|
||||
const [gatewayType, setGatewayType] = useState<OnlineGatewayType>('mellat_behpardakht')
|
||||
const [gatewayType, setGatewayType] = useState<OnlineGatewayType>('mellat')
|
||||
const [transferAccount, setTransferAccount] = useState('')
|
||||
const [transferRefNumber, setTransferRefNumber] = useState('')
|
||||
const [discountApplied, setDiscountApplied] = useState(false)
|
||||
@@ -100,7 +101,14 @@ export function CheckoutPaymentStep() {
|
||||
return
|
||||
}
|
||||
|
||||
// Only bank transfer requires extra fields before creating the order
|
||||
if (paymentType === 'e_payment_gate') {
|
||||
const gateway = ONLINE_GATEWAYS.find((item) => item.id === gatewayType)
|
||||
if (!gateway?.ready) {
|
||||
setError('این درگاه هنوز فعال نیست. لطفاً ملت را انتخاب کنید.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (paymentType === 'transfer') {
|
||||
if (!transferAccount.trim() || !transferRefNumber.trim()) {
|
||||
setError('لطفاً شماره حساب/کارت و شماره مرجع را وارد کنید.')
|
||||
@@ -119,9 +127,12 @@ export function CheckoutPaymentStep() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Both types persist via existing backend TransactionType:
|
||||
// e_payment_gate (+ gatewayType) | transfer (+ account/ref)
|
||||
const payment = buildPaymentPayload()
|
||||
const returnUrl =
|
||||
paymentType === 'e_payment_gate'
|
||||
? `${window.location.origin}/checkout/result`
|
||||
: undefined
|
||||
|
||||
const payload =
|
||||
deliveryMode === 'pickup'
|
||||
? {
|
||||
@@ -132,17 +143,25 @@ export function CheckoutPaymentStep() {
|
||||
postalCode: '0000000000',
|
||||
},
|
||||
customerNotes: notes.join(' · ') || undefined,
|
||||
returnUrl,
|
||||
payment,
|
||||
}
|
||||
: {
|
||||
addressId: selectedAddressId!,
|
||||
customerNotes: notes.join(' · ') || undefined,
|
||||
returnUrl,
|
||||
payment,
|
||||
}
|
||||
|
||||
const result = await checkoutCart(payload)
|
||||
setPlacedOrder(result.order)
|
||||
// Pass order in navigation state so success page does not race context updates
|
||||
|
||||
if (result.payment?.redirect) {
|
||||
showToast('در حال انتقال به درگاه پرداخت...', 'info')
|
||||
submitPaymentRedirect(result.payment.redirect)
|
||||
return
|
||||
}
|
||||
|
||||
navigate('/checkout/success', { replace: true, state: { order: result.order } })
|
||||
showToast('سفارش با موفقیت ثبت شد.', 'success')
|
||||
} catch (err) {
|
||||
@@ -243,12 +262,15 @@ export function CheckoutPaymentStep() {
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={() => setGatewayType(gateway.id)}
|
||||
disabled={submitting}
|
||||
disabled={submitting || !gateway.ready}
|
||||
>
|
||||
<span className={styles.paymentSubRadio}>
|
||||
{active ? <span className={styles.paymentSubRadioInner} /> : null}
|
||||
</span>
|
||||
<span>{gateway.label}</span>
|
||||
<span>
|
||||
{gateway.label}
|
||||
{!gateway.ready ? ' (بهزودی)' : ''}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -321,7 +343,15 @@ export function CheckoutPaymentStep() {
|
||||
</div>
|
||||
|
||||
<CheckoutActionBar
|
||||
primaryLabel={submitting ? 'در حال ثبت سفارش...' : 'پرداخت و ثبت سفارش'}
|
||||
primaryLabel={
|
||||
submitting
|
||||
? paymentType === 'e_payment_gate'
|
||||
? 'در حال انتقال به درگاه...'
|
||||
: 'در حال ثبت سفارش...'
|
||||
: paymentType === 'e_payment_gate'
|
||||
? 'پرداخت آنلاین'
|
||||
: 'پرداخت و ثبت سفارش'
|
||||
}
|
||||
onPrimary={() => void handlePlaceOrder()}
|
||||
primaryDisabled={submitting}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Navigate, useSearchParams } from 'react-router-dom'
|
||||
import { useCheckout } from '../../context/CheckoutContext'
|
||||
import { ApiError, isAbortError } from '../../lib/api'
|
||||
import { getOrder, type Order } from '../../services/orderService'
|
||||
import {
|
||||
buildPaymentInfoFromOrder,
|
||||
type CheckoutPaymentInfo,
|
||||
type PaymentResultStatus,
|
||||
} from '../../utils/checkoutPayment'
|
||||
import { CheckoutFailedStep } from './CheckoutFailedStep'
|
||||
import { CheckoutSuccessStep } from './CheckoutSuccessStep'
|
||||
import styles from './CheckoutSteps.module.css'
|
||||
|
||||
function readStatus(raw: string | null): PaymentResultStatus | null {
|
||||
if (raw === 'success' || raw === 'failed') return raw
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Bank return URL target (`returnUrl`).
|
||||
* Query: status, orderId, transactionId, trackId, message
|
||||
*/
|
||||
export function CheckoutResultStep() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const { setPlacedOrder } = useCheckout()
|
||||
const [order, setOrder] = useState<Order | null>(null)
|
||||
const [payment, setPayment] = useState<CheckoutPaymentInfo | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const status = readStatus(searchParams.get('status'))
|
||||
const orderId = searchParams.get('orderId')?.trim() || ''
|
||||
const transactionId = searchParams.get('transactionId')?.trim() || undefined
|
||||
const trackId = searchParams.get('trackId')?.trim() || undefined
|
||||
const message = searchParams.get('message')?.trim() || undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (!status || !orderId) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const data = await getOrder(orderId, controller.signal)
|
||||
if (controller.signal.aborted) return
|
||||
setOrder(data.order)
|
||||
setPlacedOrder(data.order)
|
||||
setPayment(
|
||||
buildPaymentInfoFromOrder(data.order, {
|
||||
status,
|
||||
transactionId,
|
||||
trackId,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) return
|
||||
// Still show result shell with query payment data if order fetch fails
|
||||
setPayment({
|
||||
status: status!,
|
||||
transactionId,
|
||||
trackId,
|
||||
message:
|
||||
message ||
|
||||
(err instanceof ApiError
|
||||
? err.message
|
||||
: 'بارگذاری جزئیات سفارش ممکن نشد.'),
|
||||
})
|
||||
setError(err instanceof ApiError ? err.message : 'بارگذاری سفارش ممکن نشد.')
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void load()
|
||||
return () => controller.abort()
|
||||
}, [status, orderId, transactionId, trackId, message, setPlacedOrder])
|
||||
|
||||
if (!status) {
|
||||
return <Navigate to="/checkout/cart" replace />
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className={styles.loading}>در حال دریافت نتیجه پرداخت...</div>
|
||||
}
|
||||
|
||||
if (status === 'success') {
|
||||
if (!order) {
|
||||
return (
|
||||
<CheckoutFailedStep
|
||||
message={error || message || 'پرداخت تأیید شد اما سفارش یافت نشد.'}
|
||||
payment={payment}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <CheckoutSuccessStep order={order} payment={payment} />
|
||||
}
|
||||
|
||||
return (
|
||||
<CheckoutFailedStep
|
||||
message={
|
||||
payment?.message ||
|
||||
message ||
|
||||
error ||
|
||||
'پرداخت انجام نشد. در صورت کسر وجه، مبلغ بهزودی به حساب شما بازمیگردد.'
|
||||
}
|
||||
payment={payment}
|
||||
order={order}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -4,14 +4,35 @@ import { useCheckout } from '../../context/CheckoutContext'
|
||||
import type { Order } from '../../services/orderService'
|
||||
import { getWebsiteUrl } from '../../services/websiteService'
|
||||
import { formatIrtPrice } from '../../utils/irtPrice'
|
||||
import {
|
||||
gatewayLabelFa,
|
||||
transactionStatusLabelFa,
|
||||
type CheckoutPaymentInfo,
|
||||
} from '../../utils/checkoutPayment'
|
||||
import orderSucceedImg from '../../assets/order-succeed.png'
|
||||
import styles from './CheckoutSteps.module.css'
|
||||
|
||||
export function CheckoutSuccessStep() {
|
||||
type SuccessLocationState = {
|
||||
order?: Order
|
||||
payment?: CheckoutPaymentInfo
|
||||
}
|
||||
|
||||
type CheckoutSuccessStepProps = {
|
||||
/** When rendered from bank result page */
|
||||
order?: Order
|
||||
payment?: CheckoutPaymentInfo | null
|
||||
}
|
||||
|
||||
export function CheckoutSuccessStep({
|
||||
order: orderProp,
|
||||
payment: paymentProp,
|
||||
}: CheckoutSuccessStepProps = {}) {
|
||||
const location = useLocation()
|
||||
const { placedOrder, setPlacedOrder, resetCheckout } = useCheckout()
|
||||
const orderFromNav = (location.state as { order?: Order } | null)?.order
|
||||
const order = placedOrder ?? orderFromNav ?? null
|
||||
const navState = (location.state as SuccessLocationState | null) ?? null
|
||||
const orderFromNav = navState?.order
|
||||
const payment = paymentProp ?? navState?.payment ?? null
|
||||
const order = orderProp ?? placedOrder ?? orderFromNav ?? null
|
||||
const websiteUrl = getWebsiteUrl()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,8 +56,14 @@ export function CheckoutSuccessStep() {
|
||||
</div>
|
||||
|
||||
<div className={styles.successHeader}>
|
||||
<h1 className={styles.stepTitle}>سفارش ثبت شد</h1>
|
||||
<p className={styles.stepDesc}>از خرید شما سپاسگزاریم — سفارش دریافت شد.</p>
|
||||
<h1 className={styles.stepTitle}>
|
||||
{payment ? 'پرداخت موفق' : 'سفارش ثبت شد'}
|
||||
</h1>
|
||||
<p className={styles.stepDesc}>
|
||||
{payment
|
||||
? 'پرداخت شما تأیید شد و سفارش ثبت گردید.'
|
||||
: 'از خرید شما سپاسگزاریم — سفارش دریافت شد.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<dl className={styles.successMeta}>
|
||||
@@ -46,17 +73,50 @@ export function CheckoutSuccessStep() {
|
||||
</div>
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>مبلغ</dt>
|
||||
<dd className={styles.successMetaValue}>{formatIrtPrice(order.total)}</dd>
|
||||
</div>
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>وضعیت</dt>
|
||||
<dd>در انتظار تأیید</dd>
|
||||
<dd className={styles.successMetaValue}>
|
||||
{formatIrtPrice(payment?.amount ?? order.total)}
|
||||
</dd>
|
||||
</div>
|
||||
{payment ? (
|
||||
<>
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>وضعیت پرداخت</dt>
|
||||
<dd>{transactionStatusLabelFa('completed')}</dd>
|
||||
</div>
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>درگاه</dt>
|
||||
<dd>{gatewayLabelFa(payment.gatewayType)}</dd>
|
||||
</div>
|
||||
{payment.trackId ? (
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>کد پیگیری</dt>
|
||||
<dd className={styles.successMetaValue}>{payment.trackId}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{payment.transactionId ? (
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>شناسه تراکنش</dt>
|
||||
<dd className={styles.successMetaValue}>{payment.transactionId}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{payment.gatewayRef ? (
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>شناسه مرجع درگاه</dt>
|
||||
<dd className={styles.successMetaValue}>{payment.gatewayRef}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.successMetaRow}>
|
||||
<dt>وضعیت</dt>
|
||||
<dd>در انتظار تأیید</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
|
||||
<div className={styles.successActions}>
|
||||
<Link to="/" className={styles.successDarkBtn} onClick={() => resetCheckout()}>
|
||||
رفتن به داشبورد مدیریت
|
||||
<Link to="/orders" className={styles.successDarkBtn} onClick={() => resetCheckout()}>
|
||||
مشاهده سفارشها
|
||||
</Link>
|
||||
<a href={websiteUrl} className={styles.successNeutralBtn} onClick={() => resetCheckout()}>
|
||||
بازگشت به وبسایت
|
||||
|
||||
@@ -46,7 +46,8 @@ export interface ShippingAddressInput {
|
||||
|
||||
export type CheckoutPaymentType = 'e_payment_gate' | 'transfer'
|
||||
|
||||
export type OnlineGatewayType = 'mellat_behpardakht' | 'saman_kish'
|
||||
/** Matches backend PaymentGatewayId (mellat first; others later). */
|
||||
export type OnlineGatewayType = 'mellat' | 'sep' | 'snappay' | 'digipay' | 'zarinpal'
|
||||
|
||||
export interface CheckoutPaymentInput {
|
||||
type: CheckoutPaymentType
|
||||
@@ -55,13 +56,33 @@ export interface CheckoutPaymentInput {
|
||||
transferRefNumber?: string
|
||||
}
|
||||
|
||||
export interface PaymentRedirect {
|
||||
method: 'GET' | 'POST'
|
||||
url: string
|
||||
fields?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface CheckoutPaymentResult {
|
||||
gatewayType: string
|
||||
transactionId: string
|
||||
redirect: PaymentRedirect
|
||||
}
|
||||
|
||||
export interface CheckoutInput {
|
||||
addressId?: string
|
||||
shippingAddress?: ShippingAddressInput
|
||||
customerNotes?: string
|
||||
/** Absolute URL for bank callback return (required for e_payment_gate). */
|
||||
returnUrl?: string
|
||||
payment: CheckoutPaymentInput
|
||||
}
|
||||
|
||||
export interface CheckoutResponse {
|
||||
message: string
|
||||
order: Order
|
||||
payment?: CheckoutPaymentResult
|
||||
}
|
||||
|
||||
function cartPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
@@ -98,9 +119,34 @@ export async function removeCartItem(itemId: string) {
|
||||
}
|
||||
|
||||
export async function checkoutCart(input: CheckoutInput) {
|
||||
return apiRequest<{ message: string; order: Order }>(cartPath('/checkout'), {
|
||||
return apiRequest<CheckoutResponse>(cartPath('/checkout'), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: input,
|
||||
})
|
||||
}
|
||||
|
||||
/** Submit a gateway redirect (POST form or GET navigation). */
|
||||
export function submitPaymentRedirect(redirect: PaymentRedirect) {
|
||||
if (redirect.method === 'GET') {
|
||||
window.location.assign(redirect.url)
|
||||
return
|
||||
}
|
||||
|
||||
const form = document.createElement('form')
|
||||
form.method = 'POST'
|
||||
form.action = redirect.url
|
||||
form.style.display = 'none'
|
||||
form.acceptCharset = 'UTF-8'
|
||||
|
||||
for (const [name, value] of Object.entries(redirect.fields ?? {})) {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'hidden'
|
||||
input.name = name
|
||||
input.value = value
|
||||
form.appendChild(input)
|
||||
}
|
||||
|
||||
document.body.appendChild(form)
|
||||
form.submit()
|
||||
}
|
||||
|
||||
@@ -38,6 +38,22 @@ export interface OrderCustomer {
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export interface OrderTransaction {
|
||||
id: string
|
||||
type: string
|
||||
status: string
|
||||
amount: number
|
||||
posType: string | null
|
||||
gatewayType: string | null
|
||||
gatewayRef: string | null
|
||||
gatewayTrackId: string | null
|
||||
transferAccount: string | null
|
||||
transferRefNumber: string | null
|
||||
notes: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string
|
||||
businessId: string
|
||||
@@ -61,6 +77,7 @@ export interface Order {
|
||||
updatedAt: string
|
||||
customer: OrderCustomer
|
||||
items: OrderItem[]
|
||||
transactions?: OrderTransaction[]
|
||||
}
|
||||
|
||||
export interface OrdersListResponse {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { Order, OrderTransaction } from '../services/orderService'
|
||||
|
||||
export type PaymentResultStatus = 'success' | 'failed'
|
||||
|
||||
export interface CheckoutPaymentInfo {
|
||||
status: PaymentResultStatus
|
||||
transactionId?: string
|
||||
trackId?: string
|
||||
gatewayType?: string
|
||||
gatewayRef?: string
|
||||
amount?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export function gatewayLabelFa(gatewayType: string | null | undefined): string {
|
||||
switch (gatewayType) {
|
||||
case 'mellat':
|
||||
return 'بانک ملت (بهپرداخت)'
|
||||
case 'sep':
|
||||
return 'سپ (سامان کیش)'
|
||||
case 'snappay':
|
||||
return 'اسنپپی'
|
||||
case 'digipay':
|
||||
return 'دیجیپی'
|
||||
case 'zarinpal':
|
||||
return 'زرینپال'
|
||||
default:
|
||||
return gatewayType?.trim() || 'درگاه آنلاین'
|
||||
}
|
||||
}
|
||||
|
||||
export function transactionStatusLabelFa(status: string | null | undefined): string {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'پرداخت موفق'
|
||||
case 'pending':
|
||||
return 'در انتظار پرداخت'
|
||||
case 'failed':
|
||||
return 'پرداخت ناموفق'
|
||||
case 'refunded':
|
||||
return 'بازگشت وجه'
|
||||
default:
|
||||
return status || '—'
|
||||
}
|
||||
}
|
||||
|
||||
export function pickOrderPaymentTransaction(
|
||||
order: Order | null | undefined,
|
||||
transactionId?: string,
|
||||
): OrderTransaction | null {
|
||||
const rows = order?.transactions ?? []
|
||||
if (!rows.length) return null
|
||||
if (transactionId) {
|
||||
return rows.find((row) => row.id === transactionId) ?? null
|
||||
}
|
||||
return (
|
||||
rows.find((row) => row.type === 'e_payment_gate') ??
|
||||
rows[0] ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
export function buildPaymentInfoFromOrder(
|
||||
order: Order,
|
||||
opts: {
|
||||
status: PaymentResultStatus
|
||||
transactionId?: string
|
||||
trackId?: string
|
||||
message?: string
|
||||
},
|
||||
): CheckoutPaymentInfo {
|
||||
const tx = pickOrderPaymentTransaction(order, opts.transactionId)
|
||||
return {
|
||||
status: opts.status,
|
||||
transactionId: tx?.id ?? opts.transactionId,
|
||||
trackId: tx?.gatewayTrackId ?? opts.trackId,
|
||||
gatewayType: tx?.gatewayType ?? undefined,
|
||||
gatewayRef: tx?.gatewayRef ?? undefined,
|
||||
amount: tx?.amount ?? order.total,
|
||||
message: opts.message,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user