From c8d2d87db44ab3dca17454efad75b8174e43b8b5 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Wed, 8 Apr 2026 03:02:18 +0300 Subject: [PATCH] fix: BookingConfirmPage date format + guest confirmation email on payment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/src/email.ts | 64 +++++++++++++++++++++++++++ backend/src/routes/yookassaWebhook.ts | 18 +++++++- src/pages/BookingConfirmPage.tsx | 2 +- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/backend/src/email.ts b/backend/src/email.ts index 17a1839..481612d 100644 --- a/backend/src/email.ts +++ b/backend/src/email.ts @@ -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 { + 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)', + '✅', + 'Бронирование подтверждено', + `

Бронирование подтверждено!

+

Привет, ${firstName}! Ваш платёж получен и бронирование в ${hotelName} подтверждено.

+ + + + + + + + + + + + + + + + + +
Гость${guestName}
Заезд${fmtDate(checkIn)}
Выезд${fmtDate(checkOut)}
Сумма оплачена${amount} ₽
+
+ + Посмотреть бронирование + +
+

+ Ждём вас в ${hotelName}! Если есть вопросы — свяжитесь с нами напрямую. +

`, + ) + + 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' }, + }) +} diff --git a/backend/src/routes/yookassaWebhook.ts b/backend/src/routes/yookassaWebhook.ts index f2b79cf..25f5ab5 100644 --- a/backend/src/routes/yookassaWebhook.ts +++ b/backend/src/routes/yookassaWebhook.ts @@ -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`, diff --git a/src/pages/BookingConfirmPage.tsx b/src/pages/BookingConfirmPage.tsx index 76ea6eb..27bbb13 100644 --- a/src/pages/BookingConfirmPage.tsx +++ b/src/pages/BookingConfirmPage.tsx @@ -19,7 +19,7 @@ interface OnlineBookingStatus { } function formatDate(d: string) { - const [y, m, day] = d.split('-') + const [y, m, day] = d.slice(0, 10).split('-') return `${day}.${m}.${y}` }