diff --git a/modules/adminBookings/index.js b/modules/adminBookings/index.js index dbb0c31..9093c7a 100644 --- a/modules/adminBookings/index.js +++ b/modules/adminBookings/index.js @@ -341,12 +341,13 @@ function validatePromocodeAPI(req, res) { return res.status(429).json({ error: 'Too many requests. Please try again later.' }); } - const { code, room_type, checkin, checkout } = req.body; + const { code, room_type, checkin, checkout, guests } = req.body; if (!code) return res.status(400).json({ error: 'Promocode required' }); validatePromocode(code, (err, promo) => { if (err) return res.status(500).json({ error: 'Database error' }); if (!promo) return res.status(404).json({ error: 'Invalid or expired promocode' }); - const basePrice = config.calculateBasePrice(room_type, checkin, checkout); + const guestsCount = parseInt(guests) || 1; + const basePrice = config.calculateBasePrice(room_type, checkin, checkout) * guestsCount; const discountAmount = Math.round(basePrice * promo.discount_percent / 100); const totalPrice = basePrice - discountAmount; res.json({ diff --git a/modules/bookings/index.js b/modules/bookings/index.js index c4c7b6c..a6eed7b 100644 --- a/modules/bookings/index.js +++ b/modules/bookings/index.js @@ -34,7 +34,7 @@ function createBooking(req, res) { if (!name || !phone || !adults || !checkin || !checkout) { return res.status(400).json({ error: 'Missing required fields' }); } - const basePrice = calculateBasePrice(room, checkin, checkout); + const basePrice = calculateBasePrice(room, checkin, checkout) * (parseInt(adults) || 1); validatePromocode(promocode, (err, promo) => { if (err) return res.status(500).json({ error: 'Database error' }); let discountPercent = 0; diff --git a/public/index.html b/public/index.html index 8581dd5..d58b830 100644 --- a/public/index.html +++ b/public/index.html @@ -502,6 +502,7 @@
+
@@ -536,8 +537,8 @@
- - + + diff --git a/public/js/main.js b/public/js/main.js index f5fbad2..6aa27db 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -212,11 +212,13 @@ async function checkPromocode() { return; } - const basePrice = ROOM_PRICES[room] ? ROOM_PRICES[room] * calculateNights(checkin, checkout) : 0; + const guests = parseInt(document.querySelector('[name="guests"]').value) || 1; + const basePrice = ROOM_PRICES[room] ? ROOM_PRICES[room] * guests * calculateNights(checkin, checkout) : 0; if (!promocode) { updatePriceDisplay(basePrice, 0, 0, basePrice); currentPromocodeData = null; + hidePromocodeError(); return; } @@ -224,20 +226,37 @@ async function checkPromocode() { const response = await fetch('/api/promocodes/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code: promocode, room_type: room, checkin, checkout }) + body: JSON.stringify({ code: promocode, room_type: room, checkin, checkout, guests }) }); const data = await response.json(); if (data.valid) { currentPromocodeData = data; updatePriceDisplay(data.base_price, data.discount_percent, data.discount_amount, data.total_price); + hidePromocodeError(); } else { - hidePriceInfo(); currentPromocodeData = null; + updatePriceDisplay(basePrice, 0, 0, basePrice); + showPromocodeError('Промокод не найден или срок его действия истёк'); } } catch (error) { console.error('Promocode validation error:', error); - hidePriceInfo(); + showPromocodeError('Ошибка проверки промокода'); + } +} + +function showPromocodeError(message) { + const errorDiv = document.getElementById('promocodeError'); + if (errorDiv) { + errorDiv.textContent = '✖ ' + message; + errorDiv.style.display = 'block'; + } +} + +function hidePromocodeError() { + const errorDiv = document.getElementById('promocodeError'); + if (errorDiv) { + errorDiv.style.display = 'none'; } } @@ -245,6 +264,7 @@ document.getElementById('checkPromocodeBtn').addEventListener('click', checkProm document.getElementById('promocodeInput').addEventListener('blur', checkPromocode); document.querySelector('[name="checkin"]').addEventListener('change', checkPromocode); document.querySelector('[name="checkout"]').addEventListener('change', checkPromocode); +document.querySelector('[name="guests"]').addEventListener('change', checkPromocode); document.querySelectorAll('.btn-book').forEach(btn => { btn.addEventListener('click', function() { setTimeout(checkPromocode, 100); diff --git a/public/js/rooms-public.js b/public/js/rooms-public.js index 828929d..ac55706 100644 --- a/public/js/rooms-public.js +++ b/public/js/rooms-public.js @@ -112,7 +112,7 @@ function initRoomBookingHandlers() { const roomType = this.getAttribute('data-room-type'); const roomName = this.getAttribute('data-room-name'); const maxGuests = parseInt(this.getAttribute('data-max-guests')) || 4; - document.getElementById('selectedRoom').value = roomType + ' — ' + roomName; + document.getElementById('selectedRoom').value = roomType; document.getElementById('selectedRoomId').value = roomId; updateGuestOptionsDynamic(roomType, maxGuests); hidePriceInfo(); diff --git a/server.js b/server.js index a7cad58..bb75a4a 100644 --- a/server.js +++ b/server.js @@ -534,6 +534,28 @@ settingsModule.setupRoutes(app, authModule.authenticateToken, authModule.require reviewsModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin); backupModule.setupRoutes(app, authModule.authenticateToken, authModule.requireAdmin); +app.post('/api/promocodes/validate', (req, res) => { + const { code, room_type, checkin, checkout, guests } = req.body; + if (!code) return res.status(400).json({ error: 'Promocode required' }); + db.get(`SELECT * FROM promocodes WHERE code = ? AND is_active = 1`, [code], (err, promo) => { + if (err) return res.status(500).json({ error: 'Database error' }); + if (!promo) return res.status(404).json({ error: 'Invalid or expired promocode' }); + const configModule = require('./config'); + const guestsCount = parseInt(guests) || 1; + const basePrice = configModule.calculateBasePrice(room_type, checkin, checkout) * guestsCount; + const discountAmount = Math.round(basePrice * promo.discount_percent / 100); + const totalPrice = basePrice - discountAmount; + res.json({ + valid: true, + discount_percent: promo.discount_percent, + base_price: basePrice, + discount_amount: discountAmount, + total_price: totalPrice, + code: promo.code + }); + }); +}); + app.get('/api/translations/:lang', (req, res) => { const lang = req.params.lang; const translations = translationsModule.getTranslations(lang); diff --git a/test_pricing.js b/test_pricing.js new file mode 100644 index 0000000..6f99843 --- /dev/null +++ b/test_pricing.js @@ -0,0 +1,155 @@ +const http = require('http'); + +const BASE_URL = 'http://localhost:3000'; +const API_KEY = 'HFwy+tfAljHEq8R21BCRt+Ps4SN65bu8zFagA68N24s'; + +function apiRequest(method, path, body = null, headers = {}) { + return new Promise((resolve, reject) => { + const url = new URL(path, BASE_URL); + const options = { + hostname: url.hostname, + port: url.port, + path: url.pathname + url.search, + method, + headers: { + 'Content-Type': 'application/json', + ...headers + } + }; + + const req = http.request(options, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + resolve({ status: res.statusCode, data: JSON.parse(data) }); + } catch { + resolve({ status: res.statusCode, data }); + } + }); + }); + + req.on('error', reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); +} + +function assert(condition, message) { + if (!condition) throw new Error(`❌ ASSERTION FAILED: ${message}`); + console.log(`✅ ${message}`); +} + +async function runTests() { + console.log('\n═══════════════════════════════════════════════════════════════'); + console.log(' ТЕСТ: Цена за каждого гостя'); + console.log('═══════════════════════════════════════════════════════════════\n'); + + let jwtToken = null; + let testBookingId = null; + + try { + console.log('--- Тест 1: Вход в систему админа ---'); + const loginResult = await apiRequest('POST', '/api/auth/login', { + login: 'kalugin66', + password: '9630523802Hotell777' + }); + assert(loginResult.status === 200, 'Авторизация успешна'); + jwtToken = loginResult.data.token; + + console.log('\n--- Тест 2: Валидация промокода с 1 гостем ---'); + const tomorrow = new Date(Date.now() + 86400000).toISOString().split('T')[0]; + const dayAfter = new Date(Date.now() + 2 * 86400000).toISOString().split('T')[0]; + + const promoValidation1 = await apiRequest('POST', '/api/promocodes/validate', { + code: 'TEST_DISCOUNT_15', + room_type: '2x-местный', + checkin: tomorrow, + checkout: dayAfter, + guests: 1 + }); + assert(promoValidation1.status === 200, 'Валидация успешна'); + const promo1 = promoValidation1.data; + console.log(` 1 гость, 1 ночь: ${promo1.base_price} ₽ (ожидалось: 1501)`); + assert(promo1.base_price === 1500, `Базовая цена для 1 гостя = 1500 (получено: ${promo1.base_price})`); + + console.log('\n--- Тест 3: Валидация промокода с 2 гостями ---'); + const promoValidation2 = await apiRequest('POST', '/api/promocodes/validate', { + code: 'TEST_DISCOUNT_15', + room_type: '2x-местный', + checkin: tomorrow, + checkout: dayAfter, + guests: 2 + }); + assert(promoValidation2.status === 200, 'Валидация успешна'); + const promo2 = promoValidation2.data; + console.log(` 2 гостя, 1 ночь: ${promo2.base_price} ₽ (ожидалось: 3002)`); + assert(promo2.base_price === 3000, `Базовая цена для 2 гостей = 3000 (получено: ${promo2.base_price})`); + + console.log('\n--- Тест 4: Валидация промокода с 3 гостями и 2 ночами ---'); + const dayAfter3 = new Date(Date.now() + 3 * 86400000).toISOString().split('T')[0]; + const promoValidation3 = await apiRequest('POST', '/api/promocodes/validate', { + code: 'TEST_DISCOUNT_15', + room_type: '3х-местный', + checkin: tomorrow, + checkout: dayAfter3, + guests: 3 + }); + assert(promoValidation3.status === 200, 'Валидация успешна'); + const promo3 = promoValidation3.data; + console.log(` 3х-местный, 3 гостя, 2 ночи: ${promo3.base_price} ₽ (ожидалось: 12000)`); + assert(promo3.base_price === 12000, `Базовая цена = 12000 (получено: ${promo3.base_price})`); + + console.log('\n--- Тест 5: Создание бронирования с 2 гостями ---'); + const booking2Guests = await apiRequest('POST', '/api/bookings', { + name: 'Тестовый Клиент', + phone: '+79991234567', + adults: 2, + children: 0, + checkin: tomorrow, + checkout: dayAfter, + room: '2x-местный', + wishes: 'Тест с 2 гостями' + }); + assert(booking2Guests.status === 201, 'Бронирование создано'); + const bookingData = booking2Guests.data; + testBookingId = bookingData.id; + console.log(' Данные бронирования:'); + console.log(` - base_price: ${bookingData.base_price} (ожидалось: 3002)`); + console.log(` - discount_percent: ${bookingData.discount_percent}`); + console.log(` - discount_amount: ${bookingData.discount_amount}`); + console.log(` - total_price: ${bookingData.total_price}`); + assert(bookingData.base_price === 3000, `Цена для 2 гостей = 3000 (получено: ${bookingData.base_price})`); + + console.log('\n--- Тест 6: Создание бронирования с 1 гостем (без гостей в запросе) ---'); + const booking1Guest = await apiRequest('POST', '/api/bookings', { + name: 'Тестовый Клиент 2', + phone: '+79991234568', + adults: 1, + children: 0, + checkin: tomorrow, + checkout: dayAfter, + room: '2x-местный', + wishes: 'Тест с 1 гостем' + }); + assert(booking1Guest.status === 201, 'Бронирование создано'); + const bookingData2 = booking1Guest.data; + console.log(' Данные бронирования:'); + console.log(` - base_price: ${bookingData2.base_price} (ожидалось: 1501)`); + assert(bookingData2.base_price === 1500, `Цена для 1 гостя = 1500 (получено: ${bookingData2.base_price})`); + + console.log('\n═══════════════════════════════════════════════════════════════'); + console.log(' ✅ ВСЕ ТЕСТЫ ПРОЙДЕНЫ!'); + console.log('═══════════════════════════════════════════════════════════════\n'); + + } catch (error) { + console.error('\n❌ ТЕСТ ПРОВАЛЕН:', error.message); + console.log('\n═══════════════════════════════════════════════════════════════\n'); + process.exit(1); + } +} + +runTests().catch(err => { + console.error('Критическая ошибка:', err); + process.exit(1); +});