feat: centralized payment gateways + booking widget API

Backend:
- Migration 065: hotel_payment_gateways table (migrates YooKassa from deposit_settings)
- online_bookings table for widget submissions
- Routes: CRUD /api/hotels/:slug/payment-gateways
- Public widget API: /api/widget/:slug/{config,availability,bookings}
- yookassa.ts: add createCharge() for immediate capture

Frontend:
- PaymentSettingsPage: add 'Онлайн-оплата' section with gateway CRUD and module toggles
- DepositSettingsPage: replace YooKassa fields with link to centralized settings
- BookingWidgetPage: connect to real API, show gateway status, real booking submit
- api.ts: add PaymentGateway types, paymentGateways and widget API methods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 20:37:06 +03:00
parent 340b3ddec9
commit 6859817bdf
9 changed files with 798 additions and 70 deletions

View File

@@ -843,6 +843,33 @@ export const api = {
updateSettings: (slug: string, requirePaymentCheckin: 'none' | 'soft' | 'hard') =>
req<{ requirePaymentCheckin: string }>('PATCH', `/api/hotels/${slug}/payment-settings`, { require_payment_checkin: requirePaymentCheckin }),
},
paymentGateways: {
list: (slug: string) =>
req<PaymentGateway[]>('GET', `/api/hotels/${slug}/payment-gateways`),
create: (slug: string, data: PaymentGatewayPayload) =>
req<PaymentGateway>('POST', `/api/hotels/${slug}/payment-gateways`, data),
update: (slug: string, id: string, data: Partial<PaymentGatewayPayload>) =>
req<PaymentGateway>('PATCH', `/api/hotels/${slug}/payment-gateways/${id}`, data),
remove: (slug: string, id: string) =>
req<{ ok: boolean }>('DELETE', `/api/hotels/${slug}/payment-gateways/${id}`),
},
// Public widget API (no auth)
widget: {
getConfig: (slug: string) =>
fetch(`${BASE}/api/widget/${slug}/config`).then(r => r.json()) as Promise<WidgetConfig>,
getAvailability: (slug: string, checkIn: string, checkOut: string) =>
fetch(`${BASE}/api/widget/${slug}/availability?checkIn=${checkIn}&checkOut=${checkOut}`).then(r => r.json()) as Promise<WidgetRoom[]>,
createBooking: (slug: string, data: WidgetBookingPayload) =>
fetch(`${BASE}/api/widget/${slug}/bookings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}).then(r => r.json()) as Promise<{ bookingId: string; status: string; confirmationUrl: string | null }>,
getBookingStatus: (slug: string, bookingId: string) =>
fetch(`${BASE}/api/widget/${slug}/bookings/${bookingId}/status`).then(r => r.json()),
},
}
// ── Schedule ─────────────────────────────────────────────────────────────────
@@ -1561,6 +1588,64 @@ export interface BookingDeposit {
roomNumber?: string
}
export interface PaymentGateway {
id: string
provider: string
label: string
shopId: string | null
secretKey: string | null
currency: string
isActive: boolean
modules: string[]
createdAt: string
}
export interface PaymentGatewayPayload {
provider?: string
label: string
shopId?: string
secretKey?: string
currency?: string
isActive?: boolean
modules?: string[]
}
export interface WidgetRoom {
id: string
number: string
name: string
type: string
floor: number
maxGuests: number
baseRate: number
amenities: string[]
description: string
photos: string[]
}
export interface WidgetConfig {
hotelId: string
hotelName: string
slug: string
paymentEnabled: boolean
currency: string
rooms: WidgetRoom[]
}
export interface WidgetBookingPayload {
roomId: string
checkIn: string
checkOut: string
guestName: string
guestEmail?: string
guestPhone?: string
adults?: number
children?: number
totalAmount: number
notes?: string
services?: Array<{ name: string; price: number }>
}
function toHotelPayload(h: HotelPayload): Record<string, unknown> {
const out: Record<string, unknown> = {}
if (h.name !== undefined) out.name = h.name