Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ SMTP_PASSWORD=
# Success rate for mock payment (0.0 to 1.0, default 0.95 = 95% success)
# Set to 1.0 to always succeed, 0.0 to always fail (for testing)
PAYMENT_SUCCESS_RATE=0.95
RAZORPAY_KEY_ID=
RAZORPAY_KEY_SECRET=

# ─────────────────────────────────────────────
# FRONTEND
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,8 @@ services:
MONGODB_DB: ticketflow_payments
KAFKA_BOOTSTRAP_SERVERS: kafka:9092
PAYMENT_SUCCESS_RATE: ${PAYMENT_SUCCESS_RATE:-0.95}
RAZORPAY_KEY_ID: ${RAZORPAY_KEY_ID}
RAZORPAY_KEY_SECRET: ${RAZORPAY_KEY_SECRET}
depends_on:
mongodb:
condition: service_healthy
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ services:
MONGODB_DB: ticketflow_payments
KAFKA_BOOTSTRAP_SERVERS: kafka:9092
PAYMENT_SUCCESS_RATE: ${PAYMENT_SUCCESS_RATE:-0.95}
RAZORPAY_KEY_ID: ${RAZORPAY_KEY_ID}
RAZORPAY_KEY_SECRET: ${RAZORPAY_KEY_SECRET}
depends_on:
mongodb:
condition: service_healthy
Expand Down
66 changes: 63 additions & 3 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,35 @@ export interface BookingAccepted {
status: 'PENDING'
}

export interface PaymentOrderRequest {
booking_id: string
user_id: string
amount: number
currency?: string
}

export interface PaymentOrderResponse {
payment_id: string
order_id: string
amount: number
currency: string
razorpay_key_id: string
}

export interface PaymentVerifyRequest {
payment_id: string
razorpay_order_id: string
razorpay_payment_id: string
signature: string
}

export interface PaymentVerifyResponse {
id: string
status: 'PENDING' | 'SUCCESS' | 'FAILED'
providerRef?: string
updatedAt: string
}

// Auth API
export const authApi = {
register: async (data: { name: string; email: string; password: string }) => {
Expand Down Expand Up @@ -133,12 +162,33 @@ export const bookingsApi = {
},
}

export const paymentsApi = {
createOrder: async (data: PaymentOrderRequest): Promise<PaymentOrderResponse> => {
const res = await api.post<PaymentOrderResponse>('/payments/create-order', data)
return res.data
},
verify: async (data: PaymentVerifyRequest): Promise<PaymentVerifyResponse> => {
const res = await api.post<PaymentVerifyResponse>('/payments/verify', data)
return res.data
},
}

const POLL_INTERVAL_MS = 1500
const POLL_TIMEOUT_MS = 30_000
const POLL_TIMEOUT_MS = 45_000

export class BookingStatusTimeoutError extends Error {
bookingId: string

constructor(bookingId: string) {
super('Booking is still processing. Check "My Bookings" for the final status.')
this.name = 'BookingStatusTimeoutError'
this.bookingId = bookingId
}
}

/**
* Polls GET /bookings/:id every 1.5s until the booking leaves PENDING state,
* or until the 30-second timeout is reached.
* or until the timeout is reached.
*/
export async function pollBookingStatus(bookingId: string): Promise<Booking> {
const deadline = Date.now() + POLL_TIMEOUT_MS
Expand All @@ -151,5 +201,15 @@ export async function pollBookingStatus(bookingId: string): Promise<Booking> {
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
}

throw new Error('Booking confirmation timed out. Check "My Bookings" for the final status.')
// One last read avoids false timeout errors if status changed right at the boundary.
try {
const latest = await bookingsApi.getById(bookingId)
if (latest.status !== 'PENDING') {
return latest
}
} catch {
// Ignore final-read errors and treat this as an in-progress timeout.
}

throw new BookingStatusTimeoutError(bookingId)
}
4 changes: 2 additions & 2 deletions frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export function formatDateTime(dateString: string): string {
}

export function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-US', {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'USD',
currency: 'INR',
}).format(amount)
}
10 changes: 9 additions & 1 deletion frontend/src/pages/BookingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import gsap from 'gsap'
import { bookingsApi, Booking, inventoryApi } from '@/lib/api'
import { formatCurrency, formatDateTime } from '@/lib/utils'
import { Ticket } from 'lucide-react'
import { Link } from 'react-router-dom'
import { Link, useLocation } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { TicketModal } from '@/components/TicketModal'

Expand Down Expand Up @@ -63,6 +63,8 @@ async function buildSeatLabelsByBooking(bookings: Booking[]): Promise<Record<str
}

export function BookingsPage() {
const location = useLocation()
const notice = (location.state as { notice?: string } | null)?.notice
const [bookings, setBookings] = useState<Booking[]>([])
const [seatLabelsByBooking, setSeatLabelsByBooking] = useState<Record<string, string[]>>({})
const [loading, setLoading] = useState(true)
Expand Down Expand Up @@ -177,6 +179,12 @@ export function BookingsPage() {
)}
</div>

{notice && (
<div className="mb-6 rounded-xl border border-amber-500/25 bg-amber-500/10 px-4 py-3 text-sm text-amber-300 font-sans-dm">
{notice}
</div>
)}

{/* Loading state */}
{loading && (
<div className="flex items-center gap-3 py-12 text-sm text-muted-foreground font-sans-dm">
Expand Down
Loading