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

@@ -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<SlugIdParam & { Body: { label?: string; shopId?: string; secretKey?: string; currency?: string; isActive?: boolean; modules?: string[] } }>(
fastify.patch<SlugIdParam & { Body: { label?: string; shopId?: string; secretKey?: string; currency?: string; isActive?: boolean; modules?: string[]; autoConfirmOnPayment?: boolean } }>(
'/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 }
}
)