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') && (
+
+
+
Авто-подтверждение брони при оплате
+
Статус брони автоматически меняется на «Подтверждена» при успешной оплате через виджет
+
+
+
+ )}