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> => { 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> => { 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> => { 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> => { 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) } }