feat: deposit release lock + billing fixes + centralized payment gateway
- Add releaseRequiresCheckout toggle in DepositSettingsPage (UI + payload) - Migration 066: release_requires_checkout column on hotel_deposit_settings - Migration 065: hotel_payment_gateways table for centralized YooKassa config - Block deposit release buttons when releaseRequiresCheckout && not checked_out - Fix billing calculation: use Number() to prevent PostgreSQL NUMERIC string concat - Fix minibar billing: show current price (JOIN minibar_items.price) not recorded price - Auto-poll deposit status every 5s while hold_created; clear QR message on confirmation - Add billing/folio tab in BookingDetailPanel (room charges + minibar + payments + balance) - PaymentSettingsPage: new "Онлайн-оплата (ЮКасса)" section with gateway CRUD - DepositSettingsPage: replace YooKassa fields with link to centralized payment settings - BookingWidgetPage: connect to real API, gateway status banner, async booking submission - New backend routes: paymentGateways, publicWidget - YooKassa service: add createCharge() for immediate payment (online bookings) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,13 +37,14 @@ const deposit: FastifyPluginAsync = async (fastify) => {
|
||||
[hotelId],
|
||||
)
|
||||
if (!rows[0]) {
|
||||
return { hotelId, isEnabled: false, amount: 5000, yookassaShopId: null, yookassaSecretKey: null }
|
||||
return { hotelId, isEnabled: false, amount: 5000, yookassaShopId: null, yookassaSecretKey: null, releaseRequiresCheckout: false }
|
||||
}
|
||||
// Mask secret key
|
||||
const row = rows[0]
|
||||
return {
|
||||
...row,
|
||||
yookassa_secret_key: row.yookassa_secret_key ? '••••••••' : null,
|
||||
release_requires_checkout: row.release_requires_checkout ?? false,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -52,6 +53,7 @@ const deposit: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.patch<SlugParam & { Body: {
|
||||
is_enabled?: boolean; amount?: number
|
||||
yookassa_shop_id?: string; yookassa_secret_key?: string
|
||||
release_requires_checkout?: boolean
|
||||
} }>(
|
||||
'/api/hotels/:slug/deposit/settings',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
@@ -63,20 +65,21 @@ 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 } = request.body
|
||||
const { is_enabled, amount, yookassa_shop_id, yookassa_secret_key, release_requires_checkout } = 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, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
`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())
|
||||
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),
|
||||
updated_at = NOW()
|
||||
RETURNING *`,
|
||||
[hotelId, is_enabled ?? false, amount ? amount.toFixed(2) : '5000.00', yookassa_shop_id ?? null, yookassa_secret_key ?? null],
|
||||
[hotelId, is_enabled ?? false, amount ? amount.toFixed(2) : '5000.00', yookassa_shop_id ?? null, yookassa_secret_key ?? null, release_requires_checkout ?? null],
|
||||
)
|
||||
return rows[0]
|
||||
},
|
||||
|
||||
@@ -247,6 +247,7 @@ const minibar: FastifyPluginAsync = async (fastify) => {
|
||||
mi.name AS item_name,
|
||||
mc.quantity,
|
||||
mc.price_per_unit,
|
||||
mi.price AS current_price,
|
||||
(mc.quantity * mc.price_per_unit) AS total,
|
||||
mc.recorded_at,
|
||||
u.name AS recorded_by_name
|
||||
|
||||
Reference in New Issue
Block a user