109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
import type { Metadata } from 'next';
|
|
import Link from 'next/link';
|
|
import { notFound } from 'next/navigation';
|
|
import { Section } from '@/components/Section';
|
|
import { formatPoundsFromCents } from '@/lib/booking';
|
|
import { getBookingCheckoutContext } from '@/lib/payments';
|
|
import { site } from '@/lib/site';
|
|
|
|
type CheckoutPageProps = {
|
|
params: Promise<{
|
|
bookingId: string;
|
|
}>;
|
|
};
|
|
|
|
export async function generateMetadata({ params }: { params: Promise<{ bookingId: string }> }): Promise<Metadata> {
|
|
const { bookingId } = await params;
|
|
return {
|
|
title: `Checkout ${bookingId} | ${site.name}`,
|
|
description: 'Checkout handoff page for the booking flow.',
|
|
};
|
|
}
|
|
|
|
export default async function BookingCheckoutPage({ params }: CheckoutPageProps) {
|
|
const { bookingId } = await params;
|
|
const booking = await getBookingCheckoutContext(bookingId);
|
|
|
|
if (!booking) {
|
|
notFound();
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<section className="page-hero">
|
|
<p className="brand-kicker">Checkout</p>
|
|
<h2>Finish payment for {booking.property.title}</h2>
|
|
<p>
|
|
Review the quote, then continue to Stripe if the session is configured or use the local simulation path in
|
|
development.
|
|
</p>
|
|
</section>
|
|
|
|
<div className="page-layout">
|
|
<Section
|
|
eyebrow="Quote"
|
|
title="Booking summary"
|
|
description="This page is the last step before the Stripe session or the local dev fallback."
|
|
>
|
|
<div className="admin-summary-grid">
|
|
<article className="admin-card">
|
|
<p className="footer-label">Stay</p>
|
|
<h3>{booking.property.title}</h3>
|
|
<p className="mb-0">
|
|
{booking.arrivalDate.toISOString().slice(0, 10)} to {booking.departureDate.toISOString().slice(0, 10)}
|
|
</p>
|
|
</article>
|
|
<article className="admin-card">
|
|
<p className="footer-label">Total</p>
|
|
<h3>{formatPoundsFromCents(booking.totalCents)}</h3>
|
|
<p className="mb-0">Current payment state: {booking.payment?.status ?? 'REQUIRES_PAYMENT'}</p>
|
|
</article>
|
|
</div>
|
|
|
|
<article className="content-card" style={{ marginTop: '1rem' }}>
|
|
<h3>What happens next</h3>
|
|
<ul>
|
|
<li>Stripe Checkout collects payment when keys are configured.</li>
|
|
<li>The webhook finalises the payment and booking state.</li>
|
|
<li>Email notifications are composed from the payment outcome.</li>
|
|
</ul>
|
|
</article>
|
|
|
|
<article className="content-card" style={{ marginTop: '1rem' }}>
|
|
<h3>Development fallback</h3>
|
|
<p>
|
|
If Stripe is not configured in this environment, open the booking status page and use the simulation
|
|
button to trigger the same webhook finalisation path.
|
|
</p>
|
|
<Link className="btn btn-dark" href={`/bookings/${booking.id}`}>
|
|
Open booking status
|
|
</Link>
|
|
</article>
|
|
</Section>
|
|
|
|
<aside className="content-sidebar">
|
|
<article className="content-card">
|
|
<p className="footer-label">Guest</p>
|
|
<p className="mb-0">
|
|
{booking.firstName} {booking.lastName}
|
|
<br />
|
|
{booking.email}
|
|
</p>
|
|
</article>
|
|
<article className="content-card">
|
|
<h3>Navigation</h3>
|
|
<ul className="link-list">
|
|
<li>
|
|
<Link href="/">Back to home</Link>
|
|
</li>
|
|
<li>
|
|
<Link href="/admin">Open admin console</Link>
|
|
</li>
|
|
</ul>
|
|
</article>
|
|
</aside>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|