feat: long-stay deposit charge + refunds + auto-confirm widget + white page fix

- Widget white page fix: fallback photo when real rooms have no photos array
- Long-stay deposits: if booking > threshold days and toggle on → full charge instead of hold (bypasses YooKassa 7-day limit)
- DepositSettingsPage: toggle + day threshold input for long-stay full payment
- Refund endpoint: POST /deposit/refund supports full and partial refunds via YooKassa refunds API
- DepositWidget: refund UI for yookassa_charge deposits (full/partial, shows remaining amount)
- partially_refunded status support with badge and "Вернуть ещё" button
- Auto-confirm: webhook now auto-confirms booking status on payment.succeeded for widget bookings
- PaymentSettingsPage: auto-confirm toggle per gateway (shown when booking-widget module active)
- Migration 067: long_stay columns on hotel_deposit_settings, auto_confirm_on_payment on gateways, refunded_amount on deposits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 20:30:26 +03:00
parent bddfa355f2
commit 57531f2022
9 changed files with 368 additions and 53 deletions

View File

@@ -30,6 +30,8 @@ export function DepositSettingsPage() {
const [isEnabled, setIsEnabled] = useState(false)
const [amount, setAmount] = useState('5000')
const [releaseRequiresCheckout, setReleaseRequiresCheckout] = useState(false)
const [longStayFullPayment, setLongStayFullPayment] = useState(false)
const [longStayThresholdDays, setLongStayThresholdDays] = useState('7')
const [shopId, setShopId] = useState('')
const [secretKey, setSecretKey] = useState('')
const [showSecret, setShowSecret] = useState(false)
@@ -53,6 +55,8 @@ export function DepositSettingsPage() {
setIsEnabled(s.isEnabled)
setAmount(String(s.amount))
setReleaseRequiresCheckout(s.releaseRequiresCheckout ?? false)
setLongStayFullPayment(s.longStayFullPayment ?? false)
setLongStayThresholdDays(String(s.longStayThresholdDays ?? 7))
setShopId(s.yookassaShopId ?? '')
setSecretKey(s.yookassaSecretKey ?? '')
setPresets(p)
@@ -72,6 +76,8 @@ export function DepositSettingsPage() {
is_enabled: isEnabled,
amount: parseFloat(amount) || 5000,
release_requires_checkout: releaseRequiresCheckout,
long_stay_full_payment: longStayFullPayment,
long_stay_threshold_days: parseInt(longStayThresholdDays) || 7,
}
if (shopId) payload.yookassa_shop_id = shopId
if (secretKey && secretKey !== '••••••••') payload.yookassa_secret_key = secretKey
@@ -195,6 +201,29 @@ export function DepositSettingsPage() {
<Toggle on={releaseRequiresCheckout} onChange={() => setReleaseRequiresCheckout(v => !v)} />
</div>
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-sm font-medium text-slate-800 dark:text-slate-200">Длинная бронь полная оплата</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
Для броней длиннее указанного кол-ва ночей снимать депозит сразу (не замораживать). Обход 7-дневного лимита ЮКасса
</p>
{longStayFullPayment && (
<div className="flex items-center gap-2 mt-2">
<span className="text-xs text-slate-600 dark:text-slate-300">Порог:</span>
<input
type="number" min="1" max="365"
value={longStayThresholdDays}
onChange={e => setLongStayThresholdDays(e.target.value)}
onFocus={e => e.target.select()}
className="input w-20 text-sm"
/>
<span className="text-xs text-slate-500">ночей</span>
</div>
)}
</div>
<Toggle on={longStayFullPayment} onChange={() => setLongStayFullPayment(v => !v)} />
</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">