diff --git a/backend/migrations/067_long_stay_refund.sql b/backend/migrations/067_long_stay_refund.sql new file mode 100644 index 0000000..60152ba --- /dev/null +++ b/backend/migrations/067_long_stay_refund.sql @@ -0,0 +1,12 @@ +-- Long-stay: charge full deposit amount instead of hold (bypass 7-day YooKassa limit) +ALTER TABLE hotel_deposit_settings + ADD COLUMN IF NOT EXISTS long_stay_full_payment BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS long_stay_threshold_days INT NOT NULL DEFAULT 7; + +-- Auto-confirm booking on successful online payment (per payment gateway) +ALTER TABLE hotel_payment_gateways + ADD COLUMN IF NOT EXISTS auto_confirm_on_payment BOOLEAN NOT NULL DEFAULT true; + +-- Track refunded amount for captured (charged) deposits +ALTER TABLE booking_deposits + ADD COLUMN IF NOT EXISTS refunded_amount NUMERIC(12,2); diff --git a/backend/src/routes/deposit.ts b/backend/src/routes/deposit.ts index 6773614..4f48a05 100644 --- a/backend/src/routes/deposit.ts +++ b/backend/src/routes/deposit.ts @@ -1,6 +1,6 @@ import { FastifyPluginAsync } from 'fastify' import { db } from '../db' -import { createHold, capturePayment, cancelPayment } from '../services/yookassa' +import { createHold, createCharge, capturePayment, cancelPayment, createRefund } from '../services/yookassa' import { transporter } from '../email' type SlugParam = { Params: { slug: string } } @@ -37,7 +37,7 @@ const deposit: FastifyPluginAsync = async (fastify) => { [hotelId], ) if (!rows[0]) { - return { hotelId, isEnabled: false, amount: 5000, yookassaShopId: null, yookassaSecretKey: null, releaseRequiresCheckout: false } + return { hotelId, isEnabled: false, amount: 5000, yookassaShopId: null, yookassaSecretKey: null, releaseRequiresCheckout: false, longStayFullPayment: false, longStayThresholdDays: 7 } } // Mask secret key const row = rows[0] @@ -54,6 +54,7 @@ const deposit: FastifyPluginAsync = async (fastify) => { is_enabled?: boolean; amount?: number yookassa_shop_id?: string; yookassa_secret_key?: string release_requires_checkout?: boolean + long_stay_full_payment?: boolean; long_stay_threshold_days?: number } }>( '/api/hotels/:slug/deposit/settings', { onRequest: [fastify.authenticate] }, @@ -65,21 +66,24 @@ const deposit: FastifyPluginAsync = async (fastify) => { const hotelId = await getHotelId(slug) if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) - const { is_enabled, amount, yookassa_shop_id, yookassa_secret_key, release_requires_checkout } = request.body + const { is_enabled, amount, yookassa_shop_id, yookassa_secret_key, release_requires_checkout, + long_stay_full_payment, long_stay_threshold_days } = request.body - // Upsert settings const { rows } = await db.query( - `INSERT INTO hotel_deposit_settings (hotel_id, is_enabled, amount, yookassa_shop_id, yookassa_secret_key, release_requires_checkout, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, NOW()) + `INSERT INTO hotel_deposit_settings (hotel_id, is_enabled, amount, yookassa_shop_id, yookassa_secret_key, release_requires_checkout, long_stay_full_payment, long_stay_threshold_days, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW()) ON CONFLICT (hotel_id) DO UPDATE SET is_enabled = COALESCE($2, hotel_deposit_settings.is_enabled), amount = COALESCE($3, hotel_deposit_settings.amount), yookassa_shop_id = COALESCE($4, hotel_deposit_settings.yookassa_shop_id), yookassa_secret_key = CASE WHEN $5 IS NOT NULL AND $5 != '••••••••' THEN $5 ELSE hotel_deposit_settings.yookassa_secret_key END, release_requires_checkout = COALESCE($6, hotel_deposit_settings.release_requires_checkout), + long_stay_full_payment = COALESCE($7, hotel_deposit_settings.long_stay_full_payment), + long_stay_threshold_days = COALESCE($8, hotel_deposit_settings.long_stay_threshold_days), updated_at = NOW() RETURNING *`, - [hotelId, is_enabled ?? false, amount ? amount.toFixed(2) : '5000.00', yookassa_shop_id ?? null, yookassa_secret_key ?? null, release_requires_checkout ?? null], + [hotelId, is_enabled ?? false, amount ? amount.toFixed(2) : '5000.00', yookassa_shop_id ?? null, yookassa_secret_key ?? null, + release_requires_checkout ?? null, long_stay_full_payment ?? null, long_stay_threshold_days ?? null], ) return rows[0] }, @@ -166,27 +170,43 @@ const deposit: FastifyPluginAsync = async (fastify) => { return reply.code(400).send({ error: 'YooKassa credentials not configured' }) } - // Get booking for description + // Get booking dates + guest name const { rows: bRows } = await db.query( - 'SELECT id, guest_name FROM bookings WHERE id = $1 AND hotel_id = $2', + 'SELECT id, guest_name, check_in, check_out FROM bookings WHERE id = $1 AND hotel_id = $2', [bookingId, hotelId], ) if (!bRows[0]) return reply.code(404).send({ error: 'Booking not found' }) - const payment = await createHold({ - shopId: settings.yookassa_shop_id, - secretKey: settings.yookassa_secret_key, - amount: Number(settings.amount), - description: `Депозит за бронирование — ${bRows[0].guest_name}`, - returnUrl: `${appUrl()}/${slug}/bookings`, - }) + const nights = Math.ceil( + (new Date(bRows[0].check_out).getTime() - new Date(bRows[0].check_in).getTime()) / 86400000, + ) + const threshold = settings.long_stay_threshold_days ?? 7 + const useCharge = settings.long_stay_full_payment && nights > threshold + + const payment = useCharge + ? await createCharge({ + shopId: settings.yookassa_shop_id, + secretKey: settings.yookassa_secret_key, + amount: Number(settings.amount), + description: `Депозит за бронирование — ${bRows[0].guest_name}`, + returnUrl: `${appUrl()}/${slug}/bookings`, + }) + : await createHold({ + shopId: settings.yookassa_shop_id, + secretKey: settings.yookassa_secret_key, + amount: Number(settings.amount), + description: `Депозит за бронирование — ${bRows[0].guest_name}`, + returnUrl: `${appUrl()}/${slug}/bookings`, + }) + + const paymentMethod = useCharge ? 'yookassa_charge' : 'yookassa_hold' const { rows } = await db.query( `INSERT INTO booking_deposits (hotel_id, booking_id, amount, status, payment_method, yookassa_payment_id, yookassa_confirmation_url) - VALUES ($1, $2, $3, 'hold_created', 'yookassa_hold', $4, $5) + VALUES ($1, $2, $3, 'hold_created', $4, $5, $6) RETURNING *`, - [hotelId, bookingId, settings.amount, payment.id, + [hotelId, bookingId, settings.amount, paymentMethod, payment.id, payment.confirmation?.confirmation_url ?? null], ) return reply.code(201).send({ @@ -373,6 +393,67 @@ ${refund > 0 && (!items || items.length === 0) ? `

Ос }, ) + // ── POST /api/hotels/:slug/bookings/:bookingId/deposit/refund ──────────── + // Partial or full refund of a captured (yookassa_charge) deposit + fastify.post( + '/api/hotels/:slug/bookings/:bookingId/deposit/refund', + { onRequest: [fastify.authenticate] }, + async (request, reply) => { + const { slug, bookingId } = request.params + if (!canAccess(request.user.hotelSlug, request.user.role, slug) || !isManager(request.user.role)) { + return reply.code(403).send({ error: 'Forbidden' }) + } + const hotelId = await getHotelId(slug) + if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) + + const { amount: reqAmount, reason } = request.body + + const { rows: depRows } = await db.query( + `SELECT * FROM booking_deposits + WHERE booking_id = $1 AND hotel_id = $2 + AND payment_method = 'yookassa_charge' AND status = 'captured' + ORDER BY created_at DESC LIMIT 1`, + [bookingId, hotelId], + ) + if (!depRows[0]) return reply.code(404).send({ error: 'No refundable deposit found' }) + const dep = depRows[0] + + const { rows: settingsRows } = await db.query( + 'SELECT * FROM hotel_deposit_settings WHERE hotel_id = $1', [hotelId], + ) + const settings = settingsRows[0] + if (!settings?.yookassa_shop_id || !settings?.yookassa_secret_key) { + return reply.code(400).send({ error: 'YooKassa credentials not configured' }) + } + + const depositAmount = Number(dep.amount) + const alreadyRefunded = Number(dep.refunded_amount ?? 0) + const remaining = depositAmount - alreadyRefunded + const refundAmount = reqAmount !== undefined ? Math.min(reqAmount, remaining) : remaining + + if (refundAmount <= 0) return reply.code(400).send({ error: 'Nothing to refund' }) + + await createRefund({ + shopId: settings.yookassa_shop_id, + secretKey: settings.yookassa_secret_key, + paymentId: dep.yookassa_payment_id, + amount: refundAmount, + }) + + const newRefunded = alreadyRefunded + refundAmount + const isFullRefund = newRefunded >= depositAmount - 0.01 + const newStatus = isFullRefund ? 'refunded' : 'partially_refunded' + + const { rows } = await db.query( + `UPDATE booking_deposits SET + status = $1, refunded_amount = $2, retention_reason = $3, released_at = COALESCE(released_at, NOW()) + WHERE id = $4 RETURNING *`, + [newStatus, newRefunded.toFixed(2), reason ?? null, dep.id], + ) + return rows[0] + }, + ) + // ── GET /api/pay/:slug — public, no auth ───────────────────────────────── fastify.get<{ Params: { slug: string } }>( '/api/pay/:slug', @@ -632,22 +713,54 @@ ${refund > 0 && (!items || items.length === 0) ? `

Ос canceled: 'cancelled', } const newStatus = statusMap[object.status] - if (!newStatus) return { ok: true } - const card = object.payment_method?.card - if (card?.last4) { - await db.query( - `UPDATE booking_deposits - SET status = $1, card_last4 = $2, card_brand = $3 - WHERE yookassa_payment_id = $4`, - [newStatus, card.last4, card.card_type ?? null, object.id], - ) - } else { - await db.query( - `UPDATE booking_deposits SET status = $1 WHERE yookassa_payment_id = $2`, - [newStatus, object.id], - ) + if (newStatus) { + const card = object.payment_method?.card + if (card?.last4) { + await db.query( + `UPDATE booking_deposits + SET status = $1, card_last4 = $2, card_brand = $3 + WHERE yookassa_payment_id = $4`, + [newStatus, card.last4, card.card_type ?? null, object.id], + ) + } else { + await db.query( + `UPDATE booking_deposits SET status = $1 WHERE yookassa_payment_id = $2`, + [newStatus, object.id], + ) + } } + + // Auto-confirm booking on successful online widget payment + if (object.status === 'succeeded') { + const { rows: obRows } = await db.query( + `SELECT ob.booking_id, ob.hotel_id + FROM online_bookings ob + WHERE ob.yookassa_payment_id = $1`, + [object.id], + ) + if (obRows[0]?.booking_id) { + await db.query( + `UPDATE online_bookings SET yookassa_status = 'succeeded' WHERE yookassa_payment_id = $1`, + [object.id], + ) + // Check gateway auto_confirm_on_payment + const { rows: gwRows } = await db.query( + `SELECT auto_confirm_on_payment FROM hotel_payment_gateways + WHERE hotel_id = $1 AND is_active = true + ORDER BY created_at LIMIT 1`, + [obRows[0].hotel_id], + ) + const autoConfirm = gwRows[0]?.auto_confirm_on_payment !== false // default true + if (autoConfirm) { + await db.query( + `UPDATE bookings SET status = 'confirmed' WHERE id = $1 AND status = 'inquiry'`, + [obRows[0].booking_id], + ) + } + } + } + return { ok: true } }, ) diff --git a/backend/src/routes/paymentGateways.ts b/backend/src/routes/paymentGateways.ts index 8729bbd..4e95c4b 100644 --- a/backend/src/routes/paymentGateways.ts +++ b/backend/src/routes/paymentGateways.ts @@ -36,6 +36,7 @@ const paymentGateways: FastifyPluginAsync = async (fastify) => { shopId: r.shop_id, secretKey: r.secret_key, currency: r.currency, isActive: r.is_active, modules: r.modules ?? MODULES, createdAt: r.created_at, + autoConfirmOnPayment: r.auto_confirm_on_payment ?? true, })) }) @@ -55,35 +56,39 @@ const paymentGateways: FastifyPluginAsync = async (fastify) => { const r = rows[0] return { id: r.id, provider: r.provider, label: r.label, shopId: r.shop_id, secretKey: r.secret_key ? '••••••••' : null, - currency: r.currency, isActive: r.is_active, modules: r.modules } + currency: r.currency, isActive: r.is_active, modules: r.modules, + autoConfirmOnPayment: r.auto_confirm_on_payment ?? true } } ) // ── PATCH /api/hotels/:slug/payment-gateways/:id ─────────────────────────── - fastify.patch( + fastify.patch( '/api/hotels/:slug/payment-gateways/:id', { onRequest: [fastify.authenticate] }, async (req, reply) => { const { slug, id } = req.params if (!canAccess(req.user.hotelSlug, req.user.role, slug)) return reply.code(403).send({ error: 'Forbidden' }) const hotelId = await getHotelId(slug) if (!hotelId) return reply.code(404).send({ error: 'Not found' }) - const { label, shopId, secretKey, currency, isActive, modules } = req.body + const { label, shopId, secretKey, currency, isActive, modules, autoConfirmOnPayment } = req.body const { rows } = await db.query( `UPDATE hotel_payment_gateways SET - label = COALESCE($1, label), - shop_id = COALESCE($2, shop_id), - secret_key = CASE WHEN $3 IS NOT NULL AND $3 != '••••••••' THEN $3 ELSE secret_key END, - currency = COALESCE($4, currency), - is_active = COALESCE($5, is_active), - modules = COALESCE($6::jsonb, modules) - WHERE id = $7 AND hotel_id = $8 RETURNING *`, + label = COALESCE($1, label), + shop_id = COALESCE($2, shop_id), + secret_key = CASE WHEN $3 IS NOT NULL AND $3 != '••••••••' THEN $3 ELSE secret_key END, + currency = COALESCE($4, currency), + is_active = COALESCE($5, is_active), + modules = COALESCE($6::jsonb, modules), + auto_confirm_on_payment = COALESCE($7, auto_confirm_on_payment) + WHERE id = $8 AND hotel_id = $9 RETURNING *`, [label ?? null, shopId ?? null, secretKey ?? null, currency ?? null, - isActive ?? null, modules ? JSON.stringify(modules) : null, id, hotelId], + isActive ?? null, modules ? JSON.stringify(modules) : null, + autoConfirmOnPayment ?? null, id, hotelId], ) if (!rows[0]) return reply.code(404).send({ error: 'Not found' }) const r = rows[0] return { id: r.id, provider: r.provider, label: r.label, shopId: r.shop_id, secretKey: r.secret_key ? '••••••••' : null, - currency: r.currency, isActive: r.is_active, modules: r.modules } + currency: r.currency, isActive: r.is_active, modules: r.modules, + autoConfirmOnPayment: r.auto_confirm_on_payment ?? true } } ) diff --git a/backend/src/services/yookassa.ts b/backend/src/services/yookassa.ts index 73cbe34..7e805ca 100644 --- a/backend/src/services/yookassa.ts +++ b/backend/src/services/yookassa.ts @@ -91,6 +91,32 @@ export async function capturePayment(params: { } } +// Full or partial refund of a captured (charged) payment +export async function createRefund(params: { + shopId: string + secretKey: string + paymentId: string + amount: number +}): Promise { + const auth = Buffer.from(`${params.shopId}:${params.secretKey}`).toString('base64') + const res = await fetch('https://api.yookassa.ru/v3/refunds', { + method: 'POST', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + 'Idempotence-Key': randomUUID(), + }, + body: JSON.stringify({ + payment_id: params.paymentId, + amount: { value: params.amount.toFixed(2), currency: 'RUB' }, + }), + }) + if (!res.ok) { + const err = await res.text() + throw new Error(`YooKassa refund error ${res.status}: ${err}`) + } +} + export async function cancelPayment(params: { shopId: string secretKey: string diff --git a/src/components/bookings/BookingDetailPanel.tsx b/src/components/bookings/BookingDetailPanel.tsx index b512bbf..091479f 100644 --- a/src/components/bookings/BookingDetailPanel.tsx +++ b/src/components/bookings/BookingDetailPanel.tsx @@ -72,6 +72,10 @@ function DepositWidget({ slug, bookingId, bookingStatus, onDepositChange }: { sl const [releaseComment, setReleaseComment] = useState('') const [yookassaMsg, setYookassaMsg] = useState(null) const [releaseError, setReleaseError] = useState(null) + const [showRefundForm, setShowRefundForm] = useState(false) + const [refundAmount, setRefundAmount] = useState('') + const [refundComment, setRefundComment] = useState('') + const [refunding, setRefunding] = useState(false) const [presets, setPresets] = useState([]) const [minibarItems, setMinibarItems] = useState>([]) const [minibarTotal, setMinibarTotal] = useState(0) @@ -164,6 +168,22 @@ function DepositWidget({ slug, bookingId, bookingStatus, onDepositChange }: { sl } } + const handleRefund = async (full: boolean) => { + setRefunding(true) + try { + const amt = full ? undefined : (parseFloat(refundAmount) || undefined) + const dep = await api.deposits.refund(slug, bookingId, amt, refundComment || undefined) + setDeposit(dep) + setShowRefundForm(false) + setRefundAmount('') + setRefundComment('') + } catch { + setReleaseError('Не удалось выполнить возврат') + } finally { + setRefunding(false) + } + } + const addReleaseItem = (name: string, amount: number) => { setReleaseItems(prev => [...prev, { id: `item-${Date.now()}`, name, amount: String(amount) }]) } @@ -491,11 +511,93 @@ function DepositWidget({ slug, bookingId, bookingStatus, onDepositChange }: { sl )} - {/* Final statuses */} + {/* captured (hold → taken or charge → taken) */} {deposit?.status === 'captured' && ( - - Списано {formatCurrency(deposit.capturedAmount ?? 0)} - +

+ + {deposit.paymentMethod === 'yookassa_charge' ? '✓ Оплачен' : 'Списано'} {formatCurrency(Number(deposit.amount))} + + {/* Refund UI for charge-based deposits */} + {deposit.paymentMethod === 'yookassa_charge' && !showRefundForm && !releaseBlocked && ( + + )} + {deposit.paymentMethod === 'yookassa_charge' && !showRefundForm && releaseBlocked && ( +

+ Возврат заблокирован до выезда +

+ )} + {showRefundForm && deposit.paymentMethod === 'yookassa_charge' && ( +
+

Возврат депозита

+
+ +
+ setRefundAmount(e.target.value)} + onFocus={e => e.target.select()} + placeholder="Полный возврат" + className="input text-sm w-40" + /> + +
+
+
+ setRefundComment(e.target.value)} + placeholder="Комментарий (необязательно)" + className="input text-sm w-full" + /> +
+ {releaseError &&

{releaseError}

} +
+ + +
+
+ )} + {deposit.refundedAmount && Number(deposit.refundedAmount) > 0 && ( +

+ Возвращено: {formatCurrency(Number(deposit.refundedAmount))} +

+ )} +
+ )} + {deposit?.status === 'partially_refunded' && ( +
+ + Частичный возврат + +

+ Возвращено: {formatCurrency(Number(deposit.refundedAmount ?? 0))} из {formatCurrency(Number(deposit.amount))} +

+ {!showRefundForm && !releaseBlocked && ( + + )} +
)} {deposit?.status === 'refunded' && ( diff --git a/src/lib/api.ts b/src/lib/api.ts index faf4192..d80f122 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -805,6 +805,9 @@ export const api = { cancel: (slug: string, bookingId: string) => req('DELETE', `/api/hotels/${slug}/bookings/${bookingId}/deposit`), + refund: (slug: string, bookingId: string, amount?: number, reason?: string) => + req('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/refund`, { amount, reason }), + getMinibarForBooking: (slug: string, bookingId: string) => req<{ items: Array<{ itemName: string; quantity: number; recordedPrice: number; currentPrice: number; lineTotal: number }>; total: number }>( 'GET', `/api/hotels/${slug}/bookings/${bookingId}/deposit/minibar`, @@ -1555,6 +1558,8 @@ export interface DepositSettings { yookassaShopId: string | null yookassaSecretKey: string | null releaseRequiresCheckout: boolean + longStayFullPayment: boolean + longStayThresholdDays: number updatedAt?: string } @@ -1564,6 +1569,8 @@ export interface DepositSettingsPayload { yookassa_shop_id?: string yookassa_secret_key?: string release_requires_checkout?: boolean + long_stay_full_payment?: boolean + long_stay_threshold_days?: number } export interface BookingDeposit { @@ -1576,6 +1583,7 @@ export interface BookingDeposit { yookassaPaymentId: string | null yookassaConfirmationUrl: string | null capturedAmount: number | null + refundedAmount: number | null retentionReason: string | null guestEmailSent: boolean cardLast4: string | null @@ -1600,6 +1608,7 @@ export interface PaymentGateway { currency: string isActive: boolean modules: string[] + autoConfirmOnPayment: boolean createdAt: string } @@ -1611,6 +1620,7 @@ export interface PaymentGatewayPayload { currency?: string isActive?: boolean modules?: string[] + autoConfirmOnPayment?: boolean } export interface WidgetRoom { diff --git a/src/pages/BookingWidgetPage.tsx b/src/pages/BookingWidgetPage.tsx index ccb8adb..a64425a 100644 --- a/src/pages/BookingWidgetPage.tsx +++ b/src/pages/BookingWidgetPage.tsx @@ -719,7 +719,7 @@ function WidgetPreview({ settings, slug, realRooms, paymentEnabled }: { const isExpanded = expandedRoom === room.id const isSelected = selected === room.id const curPhoto = photoIndex[room.id] ?? 0 - const photo = room.photos[curPhoto] + const photo = room.photos[curPhoto] ?? { bg: 'from-slate-200 to-slate-300', emoji: '🛏️', label: 'Фото' } return (
setReleaseRequiresCheckout(v => !v)} />
+
+
+

Длинная бронь — полная оплата

+

+ Для броней длиннее указанного кол-ва ночей снимать депозит сразу (не замораживать). Обход 7-дневного лимита ЮКасса +

+ {longStayFullPayment && ( +
+ Порог: + setLongStayThresholdDays(e.target.value)} + onFocus={e => e.target.select()} + className="input w-20 text-sm" + /> + ночей +
+ )} +
+ setLongStayFullPayment(v => !v)} /> +
+

ЮКасса настраивается в одном месте

diff --git a/src/pages/PaymentSettingsPage.tsx b/src/pages/PaymentSettingsPage.tsx index 488cb88..117028a 100644 --- a/src/pages/PaymentSettingsPage.tsx +++ b/src/pages/PaymentSettingsPage.tsx @@ -55,9 +55,10 @@ export function PaymentSettingsPage() { const [gwShopId, setGwShopId] = useState('') const [gwSecretKey, setGwSecretKey] = useState('') const [gwCurrency, setGwCurrency] = useState('RUB') - const [gwModules, setGwModules] = useState(['deposit', 'booking-widget', 'room-service']) - const [gwSaving, setGwSaving] = useState(false) - const [gwSecretVisible, setGwSecretVisible] = useState(false) + const [gwModules, setGwModules] = useState(['deposit', 'booking-widget', 'room-service']) + const [gwAutoConfirm, setGwAutoConfirm] = useState(true) + const [gwSaving, setGwSaving] = useState(false) + const [gwSecretVisible, setGwSecretVisible] = useState(false) useEffect(() => { if (!slug) return @@ -137,9 +138,11 @@ export function PaymentSettingsPage() { if (gw) { setGwEditId(gw.id); setGwLabel(gw.label); setGwShopId(gw.shopId ?? '') setGwSecretKey(''); setGwCurrency(gw.currency); setGwModules(gw.modules ?? ['deposit','booking-widget','room-service']) + setGwAutoConfirm(gw.autoConfirmOnPayment ?? true) } else { setGwEditId(null); setGwLabel('ЮКасса'); setGwShopId(''); setGwSecretKey('') setGwCurrency('RUB'); setGwModules(['deposit','booking-widget','room-service']) + setGwAutoConfirm(true) } setGwFormOpen(true) setGwSecretVisible(false) @@ -149,7 +152,7 @@ export function PaymentSettingsPage() { if (!gwShopId.trim()) return setGwSaving(true) try { - const data = { label: gwLabel, shopId: gwShopId.trim(), secretKey: gwSecretKey.trim() || undefined, currency: gwCurrency, modules: gwModules } + const data = { label: gwLabel, shopId: gwShopId.trim(), secretKey: gwSecretKey.trim() || undefined, currency: gwCurrency, modules: gwModules, autoConfirmOnPayment: gwAutoConfirm } if (gwEditId) { const updated = await api.paymentGateways.update(slug, gwEditId, data) setGateways(p => p.map(g => g.id === gwEditId ? updated : g)) @@ -429,6 +432,21 @@ export function PaymentSettingsPage() { ))}

+ {gwModules.includes('booking-widget') && ( +
+
+

Авто-подтверждение брони при оплате

+

Статус брони автоматически меняется на «Подтверждена» при успешной оплате через виджет

+
+ +
+ )}