feat: auto-create guest in CRM on booking creation

- Add upsertGuestFromBooking() service — creates or updates guest record
  when a booking is made (matches by email/phone, creates if not found)
- Integrated into bookings.ts (manual PMS bookings) and publicWidget.ts
  (online widget bookings)
- Name parsed as Russian format: "Фамилия Имя [Отчество]"
- Non-throwing: booking creation never fails due to guest upsert errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 11:59:41 +03:00
parent 59307ba6b4
commit 1c19334fb2
3 changed files with 123 additions and 0 deletions

View File

@@ -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<void> {
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
}
}