feat: blacklist, guest lookup, bank transfer payment page
- Add is_blacklisted column to guests (migration 068) - POST /widget/bookings: block blacklisted guests (403, generic message) - GET /widget/:slug/guests/lookup: find existing guest by email/phone for auto-fill - Widget form: debounce guest lookup on email/phone input, show "Заполнить" suggestion - Widget payment screen: replace fake card form with proper YooKassa redirect screen or bank transfer details page - Add bank transfer details field to widget settings (saved as widget_bank_details) - GuestsPage: blacklist toggle button in edit modal + 🚫 ЧС badge in table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -42,12 +42,32 @@ const publicWidget: FastifyPluginAsync = async (fastify) => {
|
||||
// Check if YooKassa gateway is configured for booking-widget
|
||||
const gateway = await getGatewayForModule(hotel.id, 'booking-widget')
|
||||
|
||||
// Load saved widget settings
|
||||
const { rows: wsRows } = await db.query(
|
||||
`SELECT key, value FROM hotel_settings WHERE hotel_id = $1 AND key LIKE 'widget_%'`,
|
||||
[hotel.id],
|
||||
)
|
||||
const ws: Record<string, unknown> = {}
|
||||
for (const row of wsRows) ws[row.key] = row.value
|
||||
|
||||
return {
|
||||
hotelId: hotel.id,
|
||||
hotelName: hotel.name,
|
||||
slug: req.params.slug,
|
||||
paymentEnabled: !!(gateway?.shop_id && gateway?.secret_key),
|
||||
currency: gateway?.currency ?? 'RUB',
|
||||
widgetSettings: {
|
||||
primaryColor: (ws.widget_color as string) ?? '#4F46E5',
|
||||
language: (ws.widget_lang as string) ?? 'ru',
|
||||
roomDisplayMode: (ws.widget_room_mode as string) ?? 'rooms',
|
||||
minNights: (ws.widget_min_nights as number) ?? 1,
|
||||
showRental: (ws.widget_show_rental as boolean) ?? false,
|
||||
showPromo: (ws.widget_show_promo as boolean) ?? true,
|
||||
allowExtraBeds: (ws.widget_extra_beds as boolean) ?? true,
|
||||
allowChildren: (ws.widget_children as boolean) ?? true,
|
||||
hotelName: (ws.widget_hotel_name as string) ?? hotel.name,
|
||||
bankDetails: (ws.widget_bank_details as string) ?? '',
|
||||
},
|
||||
rooms: rooms.map(r => ({
|
||||
id: r.id,
|
||||
number: r.number,
|
||||
@@ -75,6 +95,43 @@ const publicWidget: FastifyPluginAsync = async (fastify) => {
|
||||
}
|
||||
})
|
||||
|
||||
// ── GET /api/widget/:slug/guests/lookup ───────────────────────────────────
|
||||
// Lookup existing guest by email or phone (for auto-fill, no auth)
|
||||
fastify.get<SlugParam & { Querystring: { email?: string; phone?: string } }>(
|
||||
'/api/widget/:slug/guests/lookup', async (req, reply) => {
|
||||
const hotel = await getHotelId(req.params.slug)
|
||||
if (!hotel) return reply.code(404).send({ error: 'Hotel not found' })
|
||||
|
||||
const { email, phone } = req.query
|
||||
if (!email && !phone) return reply.code(400).send({ error: 'email or phone required' })
|
||||
|
||||
const conditions: string[] = []
|
||||
const params: unknown[] = [hotel.id]
|
||||
|
||||
if (email) {
|
||||
params.push(email.toLowerCase().trim())
|
||||
conditions.push(`LOWER(g.email) = $${params.length}`)
|
||||
}
|
||||
if (phone) {
|
||||
const cleanPhone = phone.replace(/\D/g, '')
|
||||
params.push(cleanPhone)
|
||||
conditions.push(`REGEXP_REPLACE(g.phone, '\\D', '', 'g') = $${params.length}`)
|
||||
}
|
||||
|
||||
const { rows } = await db.query(
|
||||
`SELECT g.first_name, g.last_name, g.middle_name, g.email, g.phone, g.is_blacklisted
|
||||
FROM guests g
|
||||
WHERE g.hotel_id = $1 AND (${conditions.join(' OR ')})
|
||||
ORDER BY g.updated_at DESC
|
||||
LIMIT 1`,
|
||||
params,
|
||||
)
|
||||
|
||||
if (!rows[0]) return reply.code(404).send({ error: 'Guest not found' })
|
||||
return rows[0]
|
||||
},
|
||||
)
|
||||
|
||||
// ── GET /api/widget/:slug/availability ────────────────────────────────────
|
||||
// Returns available rooms for given dates
|
||||
fastify.get<SlugParam & { Querystring: { checkIn: string; checkOut: string } }>(
|
||||
@@ -221,6 +278,28 @@ const publicWidget: FastifyPluginAsync = async (fastify) => {
|
||||
return reply.code(409).send({ error: 'Room not available for selected dates' })
|
||||
}
|
||||
|
||||
// Blacklist check
|
||||
if (guestEmail || guestPhone) {
|
||||
const blConditions: string[] = []
|
||||
const blParams: unknown[] = [hotel.id]
|
||||
if (guestEmail) {
|
||||
blParams.push(guestEmail.toLowerCase().trim())
|
||||
blConditions.push(`LOWER(email) = $${blParams.length}`)
|
||||
}
|
||||
if (guestPhone) {
|
||||
const cleanPhone = guestPhone.replace(/\D/g, '')
|
||||
blParams.push(cleanPhone)
|
||||
blConditions.push(`REGEXP_REPLACE(phone, '\\D', '', 'g') = $${blParams.length}`)
|
||||
}
|
||||
const { rows: blRows } = await db.query(
|
||||
`SELECT id FROM guests WHERE hotel_id = $1 AND is_blacklisted = TRUE AND (${blConditions.join(' OR ')}) LIMIT 1`,
|
||||
blParams,
|
||||
)
|
||||
if (blRows.length > 0) {
|
||||
return reply.code(403).send({ error: 'Невозможно завершить бронирование. Попробуйте позже или свяжитесь с отелем.' })
|
||||
}
|
||||
}
|
||||
|
||||
// Check if gateway configured
|
||||
const gateway = await getGatewayForModule(hotel.id, 'booking-widget')
|
||||
const paymentMethod = (gateway?.shop_id && gateway?.secret_key) ? 'yookassa' : 'none'
|
||||
|
||||
Reference in New Issue
Block a user