diff --git a/backend/src/routes/bookings.ts b/backend/src/routes/bookings.ts index 07e2782..b55efae 100644 --- a/backend/src/routes/bookings.ts +++ b/backend/src/routes/bookings.ts @@ -3,6 +3,7 @@ import { db } from '../db' import { notifyNetupCheckin, notifyNetupCheckout } from './netup' import { getHkSettings } from './housekeeping-settings' import { broadcast } from './ws' +import { upsertGuestFromBooking } from '../services/guestUpsert' import { transporter } from '../email' import { createHold } from '../services/yookassa' import { randomUUID } from 'crypto' @@ -108,6 +109,14 @@ const bookings: FastifyPluginAsync = async (fastify) => { ) const booking = rows[0] + // Auto-upsert guest record in CRM + upsertGuestFromBooking({ + hotelId, + guestName: guest_name, + guestEmail: guest_email, + guestPhone: guest_phone, + }) + // Auto-create YooKassa hold + send deposit email for online bookings with email if (source === 'online' && guest_email) { try { diff --git a/backend/src/routes/publicWidget.ts b/backend/src/routes/publicWidget.ts index d657dc6..b83003d 100644 --- a/backend/src/routes/publicWidget.ts +++ b/backend/src/routes/publicWidget.ts @@ -2,6 +2,7 @@ import { FastifyPluginAsync } from 'fastify' import { db } from '../db' import { getGatewayForModule } from './paymentGateways' import { createCharge } from '../services/yookassa' +import { upsertGuestFromBooking } from '../services/guestUpsert' type SlugParam = { Params: { slug: string } } type SlugIdParam = { Params: { slug: string; bookingId: string } } @@ -456,6 +457,14 @@ const publicWidget: FastifyPluginAsync = async (fastify) => { await db.query('UPDATE online_bookings SET booking_id = $1 WHERE id = $2', [bookingId, onlineBookingId]) + // Auto-create/update guest in CRM + upsertGuestFromBooking({ + hotelId: hotel.id, + guestName, + guestEmail, + guestPhone, + }) + if (paymentMethod === 'yookassa') { // Create YooKassa payment try { diff --git a/backend/src/services/guestUpsert.ts b/backend/src/services/guestUpsert.ts new file mode 100644 index 0000000..3dce73d --- /dev/null +++ b/backend/src/services/guestUpsert.ts @@ -0,0 +1,105 @@ +import { db } from '../db' + +/** + * Parse a full name string (Russian format: "Фамилия Имя [Отчество]") + * into first_name, last_name, middle_name parts. + */ +function parseName(fullName: string): { firstName: string; lastName: string; middleName: string | null } { + const parts = fullName.trim().split(/\s+/) + if (parts.length === 1) { + return { firstName: parts[0], lastName: '', middleName: null } + } + return { + lastName: parts[0], + firstName: parts[1], + middleName: parts[2] ?? null, + } +} + +/** + * Create or update a guest record based on booking data. + * - Matches existing guest by email OR phone (same hotel) + * - If found: updates name/phone/email if they were empty + * - If not found: creates new guest record + * Non-throwing: errors are swallowed so booking creation is never blocked. + */ +export async function upsertGuestFromBooking(params: { + hotelId: string + guestName: string + guestEmail?: string | null + guestPhone?: string | null +}): Promise { + try { + const { hotelId, guestName, guestEmail, guestPhone } = params + + if (!guestName?.trim()) return + + // Build match conditions + const conditions: string[] = [] + const matchParams: unknown[] = [hotelId] + if (guestEmail?.trim()) { + matchParams.push(guestEmail.toLowerCase().trim()) + conditions.push(`LOWER(email) = $${matchParams.length}`) + } + if (guestPhone?.trim()) { + const cleanPhone = guestPhone.replace(/\D/g, '') + if (cleanPhone.length >= 7) { + matchParams.push(cleanPhone) + conditions.push(`REGEXP_REPLACE(phone, '[^0-9]', '', 'g') = $${matchParams.length}`) + } + } + + if (conditions.length > 0) { + // Check if guest already exists + const { rows } = await db.query( + `SELECT id, first_name, last_name, email, phone + FROM guests + WHERE hotel_id = $1 AND (${conditions.join(' OR ')}) + ORDER BY updated_at DESC + LIMIT 1`, + matchParams, + ) + + if (rows[0]) { + // Guest exists — fill in any blank fields + const updates: string[] = [] + const upParams: unknown[] = [] + const g = rows[0] + + if ((!g.email || g.email === '') && guestEmail?.trim()) { + upParams.push(guestEmail.toLowerCase().trim()) + updates.push(`email = $${upParams.length}`) + } + if ((!g.phone || g.phone === '') && guestPhone?.trim()) { + upParams.push(guestPhone.trim()) + updates.push(`phone = $${upParams.length}`) + } + if (updates.length > 0) { + upParams.push(rows[0].id) + await db.query( + `UPDATE guests SET ${updates.join(', ')}, updated_at = NOW() WHERE id = $${upParams.length}`, + upParams, + ) + } + return + } + } + + // No existing guest found — create new + const { firstName, lastName, middleName } = parseName(guestName) + await db.query( + `INSERT INTO guests (hotel_id, first_name, last_name, middle_name, email, phone) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + hotelId, + firstName, + lastName, + middleName, + guestEmail?.toLowerCase().trim() ?? null, + guestPhone?.trim() ?? null, + ], + ) + } catch { + // Non-critical — never block booking creation + } +}