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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user