192 lines
8.3 KiB
JavaScript
192 lines
8.3 KiB
JavaScript
const config = require('../../config');
|
||
const { getRoomBasePriceByType } = config;
|
||
const seasonalModule = require('../seasonalPrices');
|
||
|
||
let db;
|
||
|
||
function init(database) {
|
||
db = database;
|
||
}
|
||
|
||
function getEffectivePrice(roomType, date, callback) {
|
||
seasonalModule.getSeasonalPrice(roomType, date, (err, seasonalPrice) => {
|
||
if (err || !seasonalPrice) {
|
||
const basePrice = getRoomBasePriceByType(roomType);
|
||
return callback(null, basePrice);
|
||
}
|
||
callback(null, seasonalPrice);
|
||
});
|
||
}
|
||
|
||
function calculateNightPrices(roomType, checkin, checkout, callback) {
|
||
const nights = config.calculateNights(checkin, checkout);
|
||
if (nights <= 0) return callback(new Error('Invalid dates'));
|
||
|
||
const dates = [];
|
||
for (let i = 0; i < nights; i++) {
|
||
const d = new Date(checkin);
|
||
d.setDate(d.getDate() + i);
|
||
dates.push(d.toISOString().split('T')[0]);
|
||
}
|
||
|
||
let total = 0;
|
||
let processed = 0;
|
||
let finished = false;
|
||
|
||
dates.forEach(date => {
|
||
getEffectivePrice(roomType, date, (err, price) => {
|
||
if (finished) return;
|
||
if (err) {
|
||
finished = true;
|
||
return callback(err, null);
|
||
}
|
||
total += price;
|
||
processed++;
|
||
if (processed === dates.length) {
|
||
finished = true;
|
||
callback(null, total);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function validatePromocode(promocode, callback) {
|
||
if (!promocode) return callback(null, null);
|
||
const now = new Date();
|
||
db.get(`SELECT * FROM promocodes WHERE code = ? AND is_active = 1`, [promocode], (err, row) => {
|
||
if (err || !row) return callback(null, null);
|
||
if (row.valid_from && new Date(row.valid_from) > now) return callback(null, null);
|
||
if (row.valid_to && new Date(row.valid_to) < now) return callback(null, null);
|
||
if (row.valid_days) {
|
||
const createdDate = new Date(row.created_at);
|
||
const expireDate = new Date(createdDate.getTime() + row.valid_days * 24 * 60 * 60 * 1000);
|
||
if (expireDate < new Date()) return callback(null, null);
|
||
}
|
||
callback(null, row);
|
||
});
|
||
}
|
||
|
||
function checkAvailability(roomId, roomType, checkin, checkout, requestedCount, callback) {
|
||
let sql;
|
||
let params;
|
||
|
||
if (roomId) {
|
||
sql = `SELECT r.rooms_count, COALESCE(
|
||
(SELECT COUNT(b.id) FROM bookings b
|
||
WHERE b.room_id = r.id AND b.status IN ('новая','оплачена','зарезервирована','заселена')
|
||
AND b.checkin_date < ? AND b.checkout_date > ?), 0) as booked
|
||
FROM rooms r WHERE r.id = ? AND r.is_active = 1`;
|
||
params = [checkout, checkin, roomId];
|
||
} else {
|
||
sql = `SELECT COALESCE(SUM(r.rooms_count), 0) as rooms_count,
|
||
(SELECT COUNT(b.id) FROM bookings b
|
||
WHERE b.room_type = ? AND b.status IN ('новая','оплачена','зарезервирована','заселена')
|
||
AND b.checkin_date < ? AND b.checkout_date > ?) as booked
|
||
FROM rooms r WHERE r.type = ? AND r.is_active = 1`;
|
||
params = [roomType, checkout, checkin, roomType];
|
||
}
|
||
|
||
db.get(sql, params, (err, row) => {
|
||
if (err) return callback(err, null);
|
||
if (!row) return callback(null, { available: 0, total: 0 });
|
||
const available = Math.max(0, row.rooms_count - row.booked);
|
||
callback(null, { available, total: row.rooms_count, booked: row.booked });
|
||
});
|
||
}
|
||
|
||
function createBooking(req, res) {
|
||
const { name, phone, adults, children, checkin, checkout, wishes, room, room_id, promocode } = req.body;
|
||
if (!name || !phone || !room || !adults || !checkin || !checkout) {
|
||
return res.status(400).json({ error: 'Missing required fields' });
|
||
}
|
||
|
||
const roomType = room;
|
||
const roomIdValue = room_id ? parseInt(room_id) : null;
|
||
|
||
checkAvailability(roomIdValue, roomType, checkin, checkout, 1, (err, avail) => {
|
||
if (err) return res.status(500).json({ error: 'Database error' });
|
||
if (avail.available < 1) {
|
||
return res.status(409).json({
|
||
error: `На выбранные даты нет свободных номеров типа "${roomType}". Доступно: ${avail.available} из ${avail.total}`,
|
||
available: avail.available
|
||
});
|
||
}
|
||
|
||
calculateNightPrices(roomType, checkin, checkout, (err, baseSum) => {
|
||
if (err) return res.status(400).json({ error: err.message });
|
||
const basePrice = baseSum * (parseInt(adults) || 1);
|
||
|
||
validatePromocode(promocode, (err, promo) => {
|
||
if (err) return res.status(500).json({ error: 'Database error' });
|
||
let discountPercent = 0;
|
||
let promocodeId = null;
|
||
if (promo) {
|
||
discountPercent = promo.discount_percent;
|
||
promocodeId = promo.id;
|
||
}
|
||
const safeBasePrice = basePrice || 0;
|
||
const discountAmount = Math.round(safeBasePrice * discountPercent / 100);
|
||
const totalPrice = safeBasePrice - discountAmount;
|
||
|
||
const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, room_id, base_price, discount_percent, discount_amount, total_price, promocode_id)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, 'новая', ?, ?, ?, ?, ?, ?, ?)`);
|
||
stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null,
|
||
room || null, roomIdValue, safeBasePrice || null, discountPercent || 0, discountAmount || 0, totalPrice || null,
|
||
promocodeId, function(err) {
|
||
if (err) {
|
||
console.error(err);
|
||
return res.status(500).json({ error: 'Database error' });
|
||
}
|
||
const booking = {
|
||
id: this.lastID,
|
||
name, phone, adults: parseInt(adults), children: parseInt(children || 0),
|
||
checkin_date: checkin, checkout_date: checkout, wishes, room_type: room, room_id: roomIdValue,
|
||
base_price: safeBasePrice, discount_percent: discountPercent, discount_amount: discountAmount,
|
||
total_price: totalPrice, promocode_id: promocodeId
|
||
};
|
||
try { require('../email').sendBookingConfirmation(booking); } catch {}
|
||
try { require('../telegram').sendBookingNotification(booking); } catch {}
|
||
res.status(201).json({
|
||
id: this.lastID, message: 'Booking saved',
|
||
base_price: safeBasePrice, discount_percent: discountPercent,
|
||
discount_amount: discountAmount, total_price: totalPrice
|
||
});
|
||
});
|
||
stmt.finalize();
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function getBookings(req, res) {
|
||
const providedKey = req.headers['x-api-key'];
|
||
if (!providedKey) return res.status(401).json({ error: 'Invalid or missing API key' });
|
||
const API_KEY = process.env.HOTEL777KEY;
|
||
if (providedKey !== API_KEY) return res.status(401).json({ error: 'Invalid or missing API key' });
|
||
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b
|
||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||
ORDER BY b.checkin_date ASC`, (err, rows) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return res.status(500).json({ error: 'Database error' });
|
||
}
|
||
res.json(rows);
|
||
});
|
||
}
|
||
|
||
function getBookingHistory(req, res) {
|
||
const bookingId = parseInt(req.params.id);
|
||
db.all(`SELECT id, booking_id, user_id, user_login, field, old_value, new_value, created_at
|
||
FROM booking_history WHERE booking_id = ? ORDER BY created_at DESC`, [bookingId], (err, rows) => {
|
||
if (err) return res.status(500).json({ error: 'Database error' });
|
||
res.json(rows);
|
||
});
|
||
}
|
||
function setupRoutes(app, authenticateToken, requireAdmin) {
|
||
app.post('/api/bookings', createBooking);
|
||
app.get('/api/bookings', getBookings);
|
||
app.get('/api/admin/bookings/:id/history', authenticateToken, getBookingHistory);
|
||
}
|
||
|
||
module.exports = { init, setupRoutes, validatePromocode, calculateNightPrices, checkAvailability };
|