/**
* @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 (
);
}
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
}
>
);
}