/** * @page Service Booking Success & Payment Confirmation (`app/services/success/page.tsx`) * @purpose Storefront-chrome confirmation with booking + deposit payment summary. */ 'use client'; import { Suspense, useEffect, useMemo, useState, type ReactNode } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { toast } from 'sonner'; import { formatCurrency } from '@/lib/utils'; import { serviceBookingService } from '@/services/api/serviceBooking'; import { setAccessToken } from '@/services/api/client'; import { useAuthStore } from '@/store/authStore'; import { parseMetadata } from '@/lib/layoutConfig'; import { findServiceSection, isServiceSectionVisible, normalizeServiceLayout, } from '@/lib/servicePageLayout'; import { CheckCircle2, Clock, Home, Loader2, CreditCard, ShieldCheck, Smartphone, Wrench, ArrowRight, } from 'lucide-react'; import { AnnouncementBar } from '@/layout/AnnouncementBar'; import { Header } from '@/layout/Header'; import { Navbar } from '@/layout/Navbar'; import { Footer } from '@/layout/Footer'; import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer'; import { FloatingButtons } from '@/components/FloatingButtons'; import { ClientModalsContainer } from '@/components/ClientModalsContainer'; function PageShell({ children }: { children: React.ReactNode }) { return (
{children}
); } function unwrapJob(raw: any): any { if (!raw) return null; if (raw.job && typeof raw.job === 'object') return { ...raw, ...raw.job }; return raw; } function firstPayment(job: any): any | null { if (!job) return null; const list = Array.isArray(job.payments) ? job.payments : Array.isArray(job.payment_records) ? job.payment_records : job.payment ? [job.payment] : []; return list[0] || null; } function alreadyVerifiedMessage(message?: string) { const text = String(message || '').toLowerCase(); return ( text.includes('already verified') || text.includes('already captured') || text.includes('already processed') ); } function BookingSuccessContent() { const router = useRouter(); const searchParams = useSearchParams(); const { accessToken, authReady, bootstrapAuth } = useAuthStore(); const jobNoParam = searchParams.get('job_no') || 'SRV-BOOKING'; const jobId = searchParams.get('job_id') || ''; const statusParam = searchParams.get('status') || ''; const qPaymentId = searchParams.get('payment_id') || ''; const qRzpPaymentId = searchParams.get('rzp_payment_id') || ''; const qRzpOrderId = searchParams.get('rzp_order_id') || ''; const qRzpSignature = searchParams.get('rzp_signature') || ''; const qAmount = Number(searchParams.get('amount') || 0); const qMethod = searchParams.get('method') || ''; const needsVerify = searchParams.get('verify') === '1'; const [jobDetails, setJobDetails] = useState(null); const [layoutCMS, setLayoutCMS] = useState([]); const [verifyState, setVerifyState] = useState<'idle' | 'verifying' | 'done'>( needsVerify ? 'verifying' : 'idle' ); useEffect(() => { void bootstrapAuth(); }, [bootstrapAuth]); useEffect(() => { if (accessToken) setAccessToken(accessToken); }, [accessToken]); useEffect(() => { toast.dismiss(); serviceBookingService.rememberLocalJob({ job_id: jobId || undefined, job_no: jobNoParam !== 'SRV-BOOKING' ? jobNoParam : undefined, }); }, [jobId, jobNoParam]); useEffect(() => { if (!needsVerify) { setVerifyState('done'); if (jobId) void loadJobDetails(jobId); return; } if (!authReady) return; let cancelled = false; const finish = (verified: boolean) => { if (cancelled) return; setVerifyState('done'); const next = new URLSearchParams(searchParams.toString()); next.delete('verify'); next.delete('rzp_signature'); if (verified) next.set('status', 'PAID'); router.replace(`/services/success?${next.toString()}`); if (jobId) void loadJobDetails(jobId); }; const runVerify = async () => { const token = useAuthStore.getState().accessToken; if (token) setAccessToken(token); if (!qPaymentId || !qRzpOrderId || !qRzpPaymentId || !qRzpSignature) { finish(Boolean(qRzpPaymentId)); return; } try { let timer = 0; const verifyPromise = serviceBookingService.verifyServicePayment({ payment_id: qPaymentId, razorpay_order_id: qRzpOrderId, razorpay_payment_id: qRzpPaymentId, razorpay_signature: qRzpSignature, }); const timeoutPromise = new Promise((_, reject) => { timer = window.setTimeout(() => reject(new Error('timeout')), 10000); }); try { await Promise.race([verifyPromise, timeoutPromise]); } finally { window.clearTimeout(timer); } finish(true); } catch (err: any) { if (alreadyVerifiedMessage(err?.message) || err?.message === 'timeout') { finish(true); return; } toast.error(err?.message || 'Payment confirmation is taking longer than expected.'); finish(Boolean(qRzpPaymentId)); } }; void runVerify(); return () => { cancelled = true; }; }, [authReady, needsVerify, jobId]); useEffect(() => { const loadLayout = async () => { try { const cms = await serviceBookingService.fetchServicePageLayout(); setLayoutCMS(normalizeServiceLayout(cms)); } catch { /* defaults */ } }; void loadLayout(); }, []); const loadJobDetails = async (id: string) => { try { const data = await serviceBookingService.fetchServiceJob(id); setJobDetails(data); const item = unwrapJob(data); serviceBookingService.rememberLocalJob({ job_id: item?.job_id || id, job_no: item?.job_no || jobNoParam, }); } catch { /* query-param fallback */ } }; const job = unwrapJob(jobDetails); const payment = firstPayment(job); const isPaid = statusParam === 'PAID' || Boolean(qRzpPaymentId) || String(payment?.status || payment?.payment_status || '').toUpperCase() === 'CAPTURED'; const isPending = statusParam === 'PENDING' && !isPaid; const jobNo = job?.job_no || jobNoParam; const status = job?.status || (isPending ? 'PENDING' : 'BOOKED'); const deviceLabel = [job?.device_brand, job?.device_model].filter(Boolean).join(' '); const serviceName = job?.service_name || job?.custom_service_name || job?.repair_category || ''; const variantName = job?.variant_name || job?.repair_variant_name || payment?.variant_name || ''; const basePrice = Number( job?.base_price ?? job?.quoted_price ?? payment?.base_amount ?? 0 ); const paidFromPayment = Number(payment?.amount ?? payment?.paid_amount ?? 0); const paidFromQuery = qAmount > 1000 ? qAmount / 100 : qAmount; const paidAmount = paidFromPayment > 0 ? paidFromPayment : paidFromQuery > 0 ? paidFromQuery : basePrice > 0 ? basePrice * 0.2 : 0; const doorstepFee = job?.fulfillment_type === 'DOORSTEP_PICKUP' ? 250 : 0; const totalQuote = basePrice + doorstepFee; const remaining = Math.max(totalQuote - paidAmount, 0); const paymentMethod = payment?.provider || payment?.payment_method || qMethod || (isPending ? '—' : 'Razorpay'); const paymentType = payment?.payment_type || (isPending ? 'ADVANCE' : 'ADVANCE'); const paymentStatus = payment?.status || payment?.payment_status || (isPaid ? 'CAPTURED' : isPending ? 'PENDING' : 'CAPTURED'); const gatewayPaymentId = payment?.razorpay_payment_id || payment?.gateway_payment_id || payment?.provider_payment_id || qRzpPaymentId || ''; const internalPaymentId = payment?.payment_id || qPaymentId || ''; const paidAt = payment?.paid_at || payment?.created_at || payment?.verified_at; const appointmentStart = job?.appointment?.scheduled_start || job?.scheduled_start; const howItWorksCMS = findServiceSection(layoutCMS, 'service_how_it_works'); const howMeta = parseMetadata(howItWorksCMS); const showHow = isServiceSectionVisible(layoutCMS, 'service_how_it_works'); const nextSteps = howMeta.steps || [ { number: '1', title: 'Technician Assignment', desc: 'Our system matches a certified repair specialist for your device model.', }, { number: '2', title: 'On-Site Service', desc: 'Technician visits your location at the requested time slot to fix your device.', }, { number: '3', title: 'Inspect & Settle', desc: 'Verify your device after repair, pay remaining balance, and get warranty cover.', }, ]; const nextTitle = howItWorksCMS?.title || 'What happens next?'; const nextSubtitle = howItWorksCMS?.subtitle || 'Your repair journey continues with these steps'; const infoRows = useMemo(() => { const rows: { label: string; value: ReactNode }[] = [ { label: 'Booking reference', value: {jobNo} }, { label: 'Job status', value: status }, ]; if (deviceLabel) rows.push({ label: 'Device', value: deviceLabel }); if (serviceName) rows.push({ label: 'Service', value: serviceName }); if (variantName) rows.push({ label: 'Price variant', value: variantName }); if (appointmentStart) { rows.push({ label: 'Scheduled slot', value: new Date(appointmentStart).toLocaleString(), }); } return rows; }, [jobNo, status, deviceLabel, serviceName, variantName, appointmentStart]); if (verifyState === 'verifying' || (needsVerify && !authReady)) { return (

Verifying payment

Please wait a moment. Do not refresh this page.

); } return (
Repair booking

{isPending ? 'Payment confirmation' : 'Payment verified'}

Reference:{' '} {jobNo}
{isPending ? ( ) : ( )}

{isPending ? 'Booking received' : 'Payment verified'}

Service order{' '} {jobNo}{' '} {isPending ? 'is saved. Complete the advance payment to lock your technician slot.' : 'is confirmed. Your advance payment is complete and our team is preparing your repair schedule.'}

Booking details
{infoRows.map((row) => (
{row.label}
{row.value}
))}
Payment information
Payment type
{paymentType}
Method
{paymentMethod}
Payment status
{['CAPTURED', 'PAID', 'SUCCESS', 'COMPLETED'].includes(String(paymentStatus).toUpperCase()) ? 'PAID' : String(paymentStatus).replace(/_/g, ' ')}
{basePrice > 0 && (
Base service quote
{formatCurrency(basePrice)}
)}
{isPending ? 'Advance due (20%)' : 'Advance paid (20%)'}
{paidAmount > 0 ? formatCurrency(paidAmount) : '—'}
{basePrice > 0 && (
Balance due at pickup
{formatCurrency(remaining)}
)} {internalPaymentId ? (
Payment ID
{internalPaymentId}
) : null} {gatewayPaymentId ? (
Razorpay payment ID
{gatewayPaymentId}
) : null} {paidAt ? (
Paid at
{new Date(paidAt).toLocaleString()}
) : null}
Remaining balance is collected after the repair is completed and verified. Warranty applies to the selected variant.
{showHow && (
Next steps

{nextTitle}

{nextSubtitle ? (

{nextSubtitle}

) : null}
= 3 ? 'md:grid-cols-3' : nextSteps.length === 2 ? 'md:grid-cols-2' : '' }`} > {nextSteps.map((step: any, idx: number) => (
{step.number || idx + 1}
{step.title}
{step.desc}
))}
)}
Track repair status Book another repair Return to storefront
); } export default function ServiceBookingSuccessPage() { return ( Verifying payment } > ); }