feat: YooKassa webhook, payment timeout, reserved status, BookingConfirmPage

- Add YooKassa webhook handler (POST /api/yookassa/webhook) that updates
  booking to confirmed+paid on payment.succeeded, cancelled on payment.canceled
- Add public status endpoint GET /api/online-bookings/:id for return URL polling
- Add 'reserved' booking status (violet) shown in calendar while awaiting payment
- Add 'website' to BookingSource type
- Payment timeout job (every 2 min) auto-cancels expired unpaid bookings
- Widget setting: "Время ожидания оплаты" (5–60 min, default 15)
- BookingConfirmPage at /booking-confirm/:id — polls status, countdown timer,
  shows success/pending/cancelled state
- Widget bookings now use status='reserved' instead of 'inquiry' when YooKassa
  is configured; set to 'confirmed' by webhook on payment.succeeded
- Migration 070: add 'reserved' to bookings status constraint + payment_expires_at

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 02:55:24 +03:00
parent b0bbb11c87
commit fb7d907b40
13 changed files with 380 additions and 10 deletions

View File

@@ -372,7 +372,7 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
{/* Legend */}
<div className="hidden lg:flex items-center gap-2 text-xs text-slate-500">
{(['confirmed', 'checked_in', 'checked_out', 'inquiry'] as const).map(s => (
{(['confirmed', 'checked_in', 'checked_out', 'reserved', 'inquiry'] as const).map(s => (
<div key={s} className="flex items-center gap-1.5">
<div className={cn('w-2.5 h-2.5 rounded-sm shrink-0', BOOKING_STATUS_COLORS[s].split(' ')[0])} />
<span>{BOOKING_STATUS_LABELS[s]}</span>