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:
@@ -3,7 +3,8 @@ import { broadcast } from './routes/ws'
|
||||
import { createNotification } from './routes/notifications'
|
||||
import { getHkSettings } from './routes/housekeeping-settings'
|
||||
|
||||
const JOB_INTERVAL_MS = 60 * 60 * 1000 // every 1 hour
|
||||
const JOB_INTERVAL_MS = 60 * 60 * 1000 // every 1 hour
|
||||
const PAYMENT_EXPIRY_INTERVAL = 2 * 60 * 1000 // every 2 minutes
|
||||
|
||||
async function runAutoJobs(): Promise<void> {
|
||||
try {
|
||||
@@ -14,6 +15,42 @@ async function runAutoJobs(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function runPaymentExpiryJob(): Promise<void> {
|
||||
try {
|
||||
// Find online bookings whose payment window has expired and are still pending
|
||||
const { rows: expired } = await db.query<{
|
||||
id: string; booking_id: string | null; slug: string
|
||||
}>(
|
||||
`SELECT ob.id, ob.booking_id, h.slug
|
||||
FROM online_bookings ob
|
||||
JOIN hotels h ON h.id = ob.hotel_id
|
||||
WHERE ob.payment_expires_at IS NOT NULL
|
||||
AND ob.payment_expires_at < NOW()
|
||||
AND ob.status = 'pending'
|
||||
AND ob.payment_method = 'yookassa'`,
|
||||
)
|
||||
|
||||
for (const ob of expired) {
|
||||
await db.query(
|
||||
`UPDATE online_bookings SET status = 'cancelled' WHERE id = $1`,
|
||||
[ob.id],
|
||||
)
|
||||
if (ob.booking_id) {
|
||||
const { rows: bRows } = await db.query(
|
||||
`UPDATE bookings SET status = 'cancelled', updated_at = NOW()
|
||||
WHERE id = $1 AND status = 'reserved' RETURNING *`,
|
||||
[ob.booking_id],
|
||||
)
|
||||
if (bRows[0]) {
|
||||
broadcast(ob.slug, { type: 'booking:updated', booking: bRows[0] })
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[jobs] payment expiry error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function runAutoCancelNoShows(): Promise<void> {
|
||||
// Find hotels where auto_cancel_noshow_enabled = true
|
||||
// Also fetch check_in_time to calculate from the correct arrival time
|
||||
@@ -160,9 +197,14 @@ export function startJobs(): void {
|
||||
// Small delay to let DB migrations finish on startup
|
||||
setTimeout(() => {
|
||||
runAutoJobs().catch(console.error)
|
||||
runPaymentExpiryJob().catch(console.error)
|
||||
}, 15_000)
|
||||
|
||||
setInterval(() => {
|
||||
runAutoJobs().catch(console.error)
|
||||
}, JOB_INTERVAL_MS)
|
||||
|
||||
setInterval(() => {
|
||||
runPaymentExpiryJob().catch(console.error)
|
||||
}, PAYMENT_EXPIRY_INTERVAL)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user