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:
@@ -337,32 +337,44 @@ const publicWidget: FastifyPluginAsync = async (fastify) => {
|
||||
const gateway = await getGatewayForModule(hotel.id, 'booking-widget')
|
||||
const paymentMethod = (gateway?.shop_id && gateway?.secret_key) ? 'yookassa' : 'none'
|
||||
|
||||
// Read payment timeout setting (in minutes, default 15)
|
||||
const { rows: tRows } = await db.query(
|
||||
`SELECT value FROM hotel_settings WHERE hotel_id = $1 AND key = 'widget_payment_timeout'`,
|
||||
[hotel.id],
|
||||
)
|
||||
const paymentTimeoutMin: number = tRows[0]?.value ? Number(tRows[0].value) || 15 : 15
|
||||
const paymentExpiresAt = paymentMethod === 'yookassa'
|
||||
? new Date(Date.now() + paymentTimeoutMin * 60_000).toISOString()
|
||||
: null
|
||||
|
||||
// Create online_booking record
|
||||
const { rows } = await db.query(
|
||||
`INSERT INTO online_bookings
|
||||
(hotel_id, room_id, guest_name, guest_email, guest_phone, check_in, check_out,
|
||||
adults, children, total_amount, notes, services, payment_method)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING id`,
|
||||
adults, children, total_amount, notes, services, payment_method, payment_expires_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id`,
|
||||
[hotel.id, roomId, guestName, guestEmail ?? null, guestPhone ?? null,
|
||||
checkIn, checkOut, adults, children, totalAmount.toFixed(2), notes ?? null,
|
||||
JSON.stringify(services ?? []), paymentMethod],
|
||||
JSON.stringify(services ?? []), paymentMethod, paymentExpiresAt],
|
||||
)
|
||||
const onlineBookingId = rows[0].id
|
||||
|
||||
// Also create draft booking in main bookings table
|
||||
// Create booking in main table
|
||||
// status='reserved' while awaiting payment → 'confirmed' after webhook; 'confirmed' directly if no payment
|
||||
const bookingStatus = paymentMethod === 'yookassa' ? 'reserved' : 'confirmed'
|
||||
const { rows: bRows } = await db.query(
|
||||
`INSERT INTO bookings
|
||||
(hotel_id, room_id, guest_name, guest_email, guest_phone, check_in, check_out,
|
||||
adults, children, total_amount, source, status, notes)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'website','inquiry',$11) RETURNING id`,
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'website',$11,$12) RETURNING id`,
|
||||
[hotel.id, roomId, guestName, guestEmail ?? null, guestPhone ?? null,
|
||||
checkIn, checkOut, adults, children, totalAmount.toFixed(2), notes ?? null],
|
||||
checkIn, checkOut, adults, children, totalAmount.toFixed(2), bookingStatus, notes ?? null],
|
||||
)
|
||||
const bookingId = bRows[0].id
|
||||
|
||||
// Update online_booking with the main booking id
|
||||
// Link online_booking → booking
|
||||
await db.query('UPDATE online_bookings SET booking_id = $1 WHERE id = $2',
|
||||
[bookingId, onlineBookingId]).catch(() => {}) // column may not exist yet, ignore
|
||||
[bookingId, onlineBookingId])
|
||||
|
||||
if (paymentMethod === 'yookassa') {
|
||||
// Create YooKassa payment
|
||||
|
||||
Reference in New Issue
Block a user