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:
12
backend/migrations/022_rate_overrides.sql
Normal file
12
backend/migrations/022_rate_overrides.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE IF NOT EXISTS rate_overrides (
|
||||
hotel_id UUID NOT NULL REFERENCES hotels(id) ON DELETE CASCADE,
|
||||
category_id TEXT NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
price INTEGER NOT NULL,
|
||||
extra_person INTEGER NOT NULL DEFAULT 0,
|
||||
min_nights INTEGER NOT NULL DEFAULT 1,
|
||||
channel_prices JSONB NOT NULL DEFAULT '{}',
|
||||
closed BOOLEAN NOT NULL DEFAULT false,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (hotel_id, category_id, date)
|
||||
);
|
||||
@@ -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
|
||||
|
||||
94
backend/src/routes/rate-overrides.ts
Normal file
94
backend/src/routes/rate-overrides.ts
Normal 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
|
||||
Reference in New Issue
Block a user