Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
190 lines
5.7 KiB
TypeScript
190 lines
5.7 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
import type {
|
|
CreatePaymentResponseDto,
|
|
PaymentCheckoutResponseDto,
|
|
PaymentReceiptResponseDto,
|
|
PaymentResponseDto,
|
|
} from '@/api/generated/models'
|
|
import type { BookingStatus } from '@/api/generated/models'
|
|
import {
|
|
createServiceError,
|
|
errorResult,
|
|
handleServiceError,
|
|
type ServiceCallOptions,
|
|
type ServiceResult,
|
|
shouldBubbleErrorToParent,
|
|
successResult,
|
|
} from '@/services/errorHandler'
|
|
import { unwrapApiData } from '@/services/apiResponse'
|
|
import { getBookings } from '@/api/generated/bookings/bookings'
|
|
import { getEvents } from '@/api/generated/events/events'
|
|
import { getFinancial } from '@/api/generated/financial/financial'
|
|
import { unwrapApiPayload } from '@/helpers/listResponse'
|
|
import { texts } from '@/texts'
|
|
|
|
// docs/workflows/fa/payment.md — consumer checkout for a `pending_payment`
|
|
// booking. Payment creation is a *second, separate* call from booking
|
|
// creation (owned by saza2's services/bookings.ts) — this file only covers
|
|
// the payment leg: reading the booking/event to render the checkout
|
|
// screen, then POST /bookings/:bookingId/payments.
|
|
|
|
export interface BookingForPayment {
|
|
id: string
|
|
eventId: string
|
|
bookingCode: string
|
|
status: BookingStatus
|
|
expiresAt: string | null
|
|
}
|
|
|
|
export interface EventSummaryForPayment {
|
|
id: string
|
|
slug?: string | null
|
|
title: string
|
|
price: number
|
|
}
|
|
|
|
export type PaymentCheckout = PaymentCheckoutResponseDto
|
|
|
|
export type Payment = PaymentResponseDto
|
|
|
|
// Orval snapshot may lag backend fields returned at runtime for checkout.
|
|
export type CreatePaymentResult = CreatePaymentResponseDto & {
|
|
depositId?: string | null
|
|
depositAmount?: number
|
|
walletBalanceUsed?: number
|
|
}
|
|
|
|
export type PaymentReceipt = PaymentReceiptResponseDto
|
|
|
|
const bookingForPaymentSchema = z.object({
|
|
id: z.string().min(1),
|
|
eventId: z.string().min(1),
|
|
bookingCode: z.string(),
|
|
status: z.enum(['pending_payment', 'confirmed', 'cancelled', 'expired', 'refunded', 'no_show']),
|
|
expiresAt: z.string().nullable(),
|
|
})
|
|
|
|
const paymentReceiptSchema = z.object({
|
|
id: z.string().min(1),
|
|
paymentId: z.string().min(1),
|
|
userId: z.string().min(1),
|
|
bookingId: z.string().min(1),
|
|
eventId: z.string().min(1),
|
|
receiptCode: z.string(),
|
|
snapshot: z.record(z.string(), z.unknown()),
|
|
issuedAt: z.string(),
|
|
})
|
|
|
|
const bookingsApi = getBookings()
|
|
const eventsApi = getEvents()
|
|
const financialApi = getFinancial()
|
|
|
|
export const GET_BOOKING_FOR_PAYMENT = async (
|
|
bookingId: string,
|
|
options?: ServiceCallOptions
|
|
): Promise<ServiceResult<BookingForPayment>> => {
|
|
try {
|
|
const res = await bookingsApi.bookingsControllerFindOne(bookingId)
|
|
const parsed = bookingForPaymentSchema.safeParse(unwrapApiPayload(res.data))
|
|
|
|
if (!parsed.success) throw createServiceError(texts.common.invalidServerResponse)
|
|
|
|
return successResult(parsed.data)
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|
|
|
|
// Booking detail (GET /bookings/:id) intentionally doesn't carry the
|
|
// event's price/title — only `eventId` — so the checkout screen needs a
|
|
// second read against the public event-detail endpoint.
|
|
export const GET_EVENT_SUMMARY = async (eventId: string, options?: ServiceCallOptions): Promise<ServiceResult<EventSummaryForPayment>> => {
|
|
try {
|
|
const res = await eventsApi.eventsControllerFindOne(eventId)
|
|
const payload = unwrapApiPayload(res.data)
|
|
|
|
if (!payload?.id) {
|
|
throw createServiceError(texts.common.invalidServerResponse)
|
|
}
|
|
|
|
return successResult({
|
|
id: payload.id as string,
|
|
slug: typeof payload.slug === 'string' ? payload.slug : null,
|
|
title: payload.title as string,
|
|
price: Number(payload.price ?? 0),
|
|
})
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|
|
|
|
// payment.md "تصمیم ۲": `walletAmount` is a single clamp-friendly number —
|
|
// the backend clamps to min(requested, wallet.balance, totalAmount)
|
|
// regardless, so the frontend just sends what the "use wallet" toggle
|
|
// implies.
|
|
export const CREATE_PAYMENT = async (
|
|
bookingId: string,
|
|
walletAmount: number,
|
|
options?: ServiceCallOptions & { discountCode?: string }
|
|
): Promise<ServiceResult<CreatePaymentResult>> => {
|
|
try {
|
|
const res = await financialApi.paymentsControllerCreate(bookingId, {
|
|
walletAmount,
|
|
discountCode: options?.discountCode?.trim() || undefined,
|
|
})
|
|
const payload = res.data
|
|
|
|
if (!payload.success) {
|
|
throw createServiceError(payload.message || texts.wallet.paymentFailed)
|
|
}
|
|
|
|
const result = unwrapApiData(payload)
|
|
|
|
if (!result?.id) {
|
|
throw createServiceError(texts.common.invalidServerResponse)
|
|
}
|
|
|
|
return successResult(result)
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|
|
|
|
export const GET_PAYMENT_RECEIPT = async (paymentId: string, options?: ServiceCallOptions): Promise<ServiceResult<PaymentReceipt>> => {
|
|
try {
|
|
const res = await financialApi.paymentsControllerReceipt(paymentId)
|
|
const parsed = paymentReceiptSchema.safeParse(unwrapApiPayload(res.data))
|
|
|
|
if (!parsed.success) throw createServiceError(texts.common.invalidServerResponse)
|
|
|
|
return successResult(parsed.data)
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|