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

@@ -182,59 +182,17 @@ export function DepositSettingsPage() {
/>
</div>
<div className="space-y-3">
<div>
<p className="font-medium text-slate-800 dark:text-slate-200 text-sm">ЮКасса (опционально)</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
Укажите реквизиты ЮКасса для создания холда (предавторизации) оплаты.
</p>
</div>
<div>
<label className="form-label">ID магазина (shopId)</label>
<input value={shopId} onChange={e => setShopId(e.target.value)} placeholder="123456" className="input w-full" />
</div>
<div>
<label className="form-label">Секретный ключ</label>
<div className="relative">
<input
type={showSecret ? 'text' : 'password'}
value={secretKey} onChange={e => setSecretKey(e.target.value)}
placeholder={settings?.yookassaSecretKey ? '••••••••' : 'live_xxxx...'}
className="input w-full pr-10"
/>
<button type="button" onClick={() => setShowSecret(s => !s)} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
{showSecret ? <EyeOff size={15} /> : <Eye size={15} />}
</button>
</div>
{settings?.yookassaSecretKey && (
<p className="text-xs text-slate-400 mt-1">Ключ уже сохранён. Оставьте поле пустым, чтобы не менять.</p>
)}
</div>
<div className="rounded-lg bg-violet-50 dark:bg-violet-900/10 border border-violet-200 dark:border-violet-700 p-3 space-y-1.5">
<p className="text-xs font-semibold text-violet-800 dark:text-violet-300">ЮКасса настраивается в одном месте</p>
<p className="text-xs text-violet-700 dark:text-violet-400">
Реквизиты ЮКасса (Shop ID и секретный ключ) теперь управляются централизованно.
Перейдите в <a href="/settings/payments" className="underline font-medium">Настройки оплаты Онлайн-оплата</a> и добавьте шлюз один раз он будет работать для депозита, онлайн-бронирования и Room Service.
</p>
</div>
</div>
{shopId && (
<div className="border-t border-slate-100 dark:border-slate-700 pt-4">
<div className="rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-3 space-y-1.5">
<p className="text-xs font-semibold text-blue-800 dark:text-blue-300 flex items-center gap-1.5">
<Info size={13} /> Настройте webhook в ЮКасса
</p>
<p className="text-xs text-blue-700 dark:text-blue-400">
Чтобы статус депозита обновлялся автоматически, добавьте URL в личном кабинете ЮКасса (Настройки HTTP-уведомления):
</p>
<code className="block text-xs font-mono bg-blue-100 dark:bg-blue-900/40 text-blue-900 dark:text-blue-200 rounded px-2 py-1 break-all select-all">
https://api.hotelsync.ru/api/webhooks/yookassa
</code>
<p className="text-xs text-blue-600 dark:text-blue-400">
Подписки: <strong>payment.waiting_for_capture</strong>, <strong>payment.succeeded</strong>, <strong>payment.canceled</strong>
</p>
</div>
</div>
)}
{shopId && (
<div className="border-t border-slate-100 dark:border-slate-700 pt-4">
<p className="font-medium text-slate-800 dark:text-slate-200 text-sm mb-1">QR-код для гостей</p>
<div className="border-t border-slate-100 dark:border-slate-700 pt-4">
<p className="font-medium text-slate-800 dark:text-slate-200 text-sm mb-1">QR-код для гостей</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-4">
Распечатайте и разместите на стойке ресепшена.
</p>
@@ -259,7 +217,6 @@ export function DepositSettingsPage() {
}
`}</style>
</div>
)}
</>
)}
</div>