feat: persist rate overrides to DB when applying prices in AvailabilityPage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 16:14:45 +03:00
parent 8312d2372e
commit e5daf91ed4
5 changed files with 194 additions and 34 deletions

View File

@@ -26,6 +26,7 @@ import rentalRoutes from './routes/rental'
import categoriesRoutes from './routes/categories'
import tariffsRoutes from './routes/tariffs'
import ratePeriodsRoutes from './routes/rate-periods'
import rateOverridesRoutes from './routes/rate-overrides'
import uploadRoutes from './routes/upload'
export async function buildApp() {
@@ -97,6 +98,7 @@ export async function buildApp() {
await fastify.register(categoriesRoutes)
await fastify.register(tariffsRoutes)
await fastify.register(ratePeriodsRoutes)
await fastify.register(rateOverridesRoutes)
await fastify.register(uploadRoutes)
return fastify

View File

@@ -0,0 +1,94 @@
import { FastifyPluginAsync } from 'fastify'
import { db } from '../db'
type SlugParam = { Params: { slug: string } }
const rateOverrides: FastifyPluginAsync = async (fastify) => {
const getHotelId = async (slug: string): Promise<string | null> => {
const { rows } = await db.query('SELECT id FROM hotels WHERE slug = $1', [slug])
return rows[0]?.id ?? null
}
const canAccess = (userSlug: string | null, role: string, slug: string) =>
role === 'super_admin' || userSlug === slug
// GET /api/hotels/:slug/rate-overrides
fastify.get<SlugParam>(
'/api/hotels/:slug/rate-overrides',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
const { slug } = request.params
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
return reply.code(403).send({ error: 'Forbidden' })
const hotelId = await getHotelId(slug)
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
const { rows } = await db.query(
`SELECT category_id, to_char(date, 'YYYY-MM-DD') AS date,
price, extra_person, min_nights, channel_prices, closed
FROM rate_overrides
WHERE hotel_id = $1
ORDER BY date, category_id`,
[hotelId],
)
return rows
},
)
// POST /api/hotels/:slug/rate-overrides/bulk — upsert many cells at once
fastify.post<SlugParam & { Body: {
overrides: Array<{
category_id: string; date: string; price: number
extra_person?: number; min_nights?: number
channel_prices?: Record<string, number>; closed?: boolean
}>
} }>(
'/api/hotels/:slug/rate-overrides/bulk',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
const { slug } = request.params
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
return reply.code(403).send({ error: 'Forbidden' })
const hotelId = await getHotelId(slug)
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
const { overrides } = request.body
if (!overrides?.length) return { count: 0 }
// Build parameterized bulk upsert:
// $1 = hotelId, then per row: category_id, date, price, extra_person, min_nights, channel_prices, closed
const vals: unknown[] = [hotelId]
const rowPlaceholders: string[] = []
let idx = 2
for (const o of overrides) {
rowPlaceholders.push(`($1, $${idx}, $${idx+1}, $${idx+2}, $${idx+3}, $${idx+4}, $${idx+5}, $${idx+6})`)
vals.push(
o.category_id,
o.date,
o.price,
o.extra_person ?? 0,
o.min_nights ?? 1,
JSON.stringify(o.channel_prices ?? {}),
o.closed ?? false,
)
idx += 7
}
await db.query(
`INSERT INTO rate_overrides
(hotel_id, category_id, date, price, extra_person, min_nights, channel_prices, closed)
VALUES ${rowPlaceholders.join(', ')}
ON CONFLICT (hotel_id, category_id, date) DO UPDATE SET
price = EXCLUDED.price,
extra_person = EXCLUDED.extra_person,
min_nights = EXCLUDED.min_nights,
channel_prices = EXCLUDED.channel_prices,
closed = EXCLUDED.closed,
updated_at = NOW()`,
vals,
)
return { count: overrides.length }
},
)
}
export default rateOverrides