Add booking checkout and webhook flow
This commit is contained in:
108
src/app/bookings/[bookingId]/checkout/page.tsx
Normal file
108
src/app/bookings/[bookingId]/checkout/page.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user