fix: BookingConfirmPage date format + guest confirmation email on payment
- Fix date display: slice ISO timestamp to first 10 chars before splitting - Add sendBookingConfirmedEmail() to email.ts — sends green styled HTML email with booking details (dates, amount) on payment.succeeded webhook - Webhook now sends confirmation email to guest email if present Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -125,3 +125,67 @@ export async function sendConfirmationEmail(to: string, name: string, token: str
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function fmtDate(d: string): string {
|
||||
const [y, m, day] = d.slice(0, 10).split('-')
|
||||
return `${day}.${m}.${y}`
|
||||
}
|
||||
|
||||
export async function sendBookingConfirmedEmail(params: {
|
||||
to: string
|
||||
guestName: string
|
||||
hotelName: string
|
||||
checkIn: string
|
||||
checkOut: string
|
||||
totalAmount: number | string
|
||||
bookingConfirmUrl: string
|
||||
}): Promise<void> {
|
||||
const { to, guestName, hotelName, checkIn, checkOut, totalAmount, bookingConfirmUrl } = params
|
||||
const amount = new Intl.NumberFormat('ru-RU').format(Number(totalAmount))
|
||||
const firstName = guestName.split(' ')[0] ?? guestName
|
||||
|
||||
const html = baseHtml(
|
||||
'linear-gradient(135deg,#059669,#10b981)',
|
||||
'✅',
|
||||
'Бронирование подтверждено',
|
||||
`<h2 style="color:#1e293b;font-size:20px;margin:0 0 12px;font-weight:600;">Бронирование подтверждено!</h2>
|
||||
<p style="color:#334155;line-height:1.7;margin:0 0 20px;font-size:15px;">Привет, ${firstName}! Ваш платёж получен и бронирование в <strong>${hotelName}</strong> подтверждено.</p>
|
||||
<table style="width:100%;border-collapse:collapse;margin:0 0 24px;background:#f8fafc;border-radius:10px;overflow:hidden;">
|
||||
<tr>
|
||||
<td style="padding:12px 16px;color:#64748b;font-size:14px;border-bottom:1px solid #e2e8f0;">Гость</td>
|
||||
<td style="padding:12px 16px;color:#1e293b;font-size:14px;font-weight:600;border-bottom:1px solid #e2e8f0;text-align:right;">${guestName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px 16px;color:#64748b;font-size:14px;border-bottom:1px solid #e2e8f0;">Заезд</td>
|
||||
<td style="padding:12px 16px;color:#1e293b;font-size:14px;font-weight:600;border-bottom:1px solid #e2e8f0;text-align:right;">${fmtDate(checkIn)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px 16px;color:#64748b;font-size:14px;border-bottom:1px solid #e2e8f0;">Выезд</td>
|
||||
<td style="padding:12px 16px;color:#1e293b;font-size:14px;font-weight:600;border-bottom:1px solid #e2e8f0;text-align:right;">${fmtDate(checkOut)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px 16px;color:#64748b;font-size:14px;">Сумма оплачена</td>
|
||||
<td style="padding:12px 16px;color:#059669;font-size:14px;font-weight:700;text-align:right;">${amount} ₽</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="text-align:center;margin:0 0 24px;">
|
||||
<a href="${bookingConfirmUrl}" style="display:inline-block;background:#059669;color:#ffffff;text-decoration:none;padding:14px 36px;border-radius:10px;font-weight:600;font-size:16px;">
|
||||
Посмотреть бронирование
|
||||
</a>
|
||||
</div>
|
||||
<p style="color:#475569;font-size:13px;margin:0;line-height:1.6;text-align:center;">
|
||||
Ждём вас в ${hotelName}! Если есть вопросы — свяжитесь с нами напрямую.
|
||||
</p>`,
|
||||
)
|
||||
|
||||
const text = `Бронирование подтверждено — ${hotelName}\n\nПривет, ${firstName}!\n\nВаш платёж получен и бронирование подтверждено.\n\nГость: ${guestName}\nЗаезд: ${fmtDate(checkIn)}\nВыезд: ${fmtDate(checkOut)}\nОплачено: ${amount} ₽\n\nДо встречи!\n© 2026 HotelSync`
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${hotelName}" <${fromAddr()}>`,
|
||||
to,
|
||||
subject: `Бронирование подтверждено — ${hotelName}`,
|
||||
html,
|
||||
text,
|
||||
headers: { 'Content-Language': 'ru' },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FastifyPluginAsync } from 'fastify'
|
||||
import { db } from '../db'
|
||||
import { broadcast } from './ws'
|
||||
import { sendBookingConfirmedEmail } from '../email'
|
||||
|
||||
const yookassaWebhookRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
// POST /api/yookassa/webhook
|
||||
@@ -19,8 +20,11 @@ const yookassaWebhookRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
const { rows } = await db.query<{
|
||||
id: string; hotel_id: string; booking_id: string | null
|
||||
total_amount: string; slug: string
|
||||
guest_name: string; guest_email: string | null
|
||||
check_in: string; check_out: string; hotel_name: string
|
||||
}>(
|
||||
`SELECT ob.id, ob.hotel_id, ob.booking_id, ob.total_amount, h.slug
|
||||
`SELECT ob.id, ob.hotel_id, ob.booking_id, ob.total_amount, h.slug,
|
||||
ob.guest_name, ob.guest_email, ob.check_in, ob.check_out, h.name AS hotel_name
|
||||
FROM online_bookings ob
|
||||
JOIN hotels h ON h.id = ob.hotel_id
|
||||
WHERE ob.yookassa_payment_id = $1
|
||||
@@ -50,6 +54,18 @@ const yookassaWebhookRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
broadcast(ob.slug, { type: 'booking:updated', booking: bRows[0] })
|
||||
}
|
||||
}
|
||||
// Send confirmation email to guest
|
||||
if (ob.guest_email) {
|
||||
sendBookingConfirmedEmail({
|
||||
to: ob.guest_email,
|
||||
guestName: ob.guest_name,
|
||||
hotelName: ob.hotel_name,
|
||||
checkIn: ob.check_in,
|
||||
checkOut: ob.check_out,
|
||||
totalAmount: ob.total_amount,
|
||||
bookingConfirmUrl: `https://app.hotelsync.ru/booking-confirm/${ob.id}`,
|
||||
}).catch(() => {}) // don't fail webhook on email error
|
||||
}
|
||||
} else if (event === 'payment.canceled') {
|
||||
await db.query(
|
||||
`UPDATE online_bookings SET status = 'cancelled', yookassa_status = 'canceled' WHERE id = $1`,
|
||||
|
||||
Reference in New Issue
Block a user