let db; let settingsModule; let emailModule; let telegramModule; let jwt = null; let syncTimer = null; let running = false; const PRIMARY_URL = (process.env.PRIMARY_URL || '').replace(/\/+$/, ''); const PRIMARY_LOGIN = process.env.PRIMARY_LOGIN || ''; const PRIMARY_PASSWORD = process.env.PRIMARY_PASSWORD || ''; const PRIMARY_SYNC_INTERVAL = parseInt(process.env.PRIMARY_SYNC_INTERVAL) || 60; function init(database, settings, email, telegram) { db = database; settingsModule = settings; emailModule = email; telegramModule = telegram; } function start() { if (!PRIMARY_URL || !PRIMARY_LOGIN || !PRIMARY_PASSWORD) { console.log('[SYNC] PRIMARY_URL/LOGIN/PASSWORD not set — replica mode disabled'); return; } console.log(`[SYNC] Replica mode: pulling from ${PRIMARY_URL} every ${PRIMARY_SYNC_INTERVAL}s`); loginAndSync(); } function stop() { running = false; if (syncTimer) { clearTimeout(syncTimer); syncTimer = null; } } function loginAndSync() { login((err) => { if (err) { console.error(`[SYNC] Login failed: ${err.message}. Retrying in 30s...`); syncTimer = setTimeout(loginAndSync, 30000); return; } running = true; settingsModule.get('sync_first_sync_done', (err, val) => { if (err || val !== 'true') { firstSync(); } else { syncLoop(); } }); }); } function login(callback) { fetch(`${PRIMARY_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ login: PRIMARY_LOGIN, password: PRIMARY_PASSWORD }) }) .then(res => { if (!res.ok) return res.json().then(d => { throw new Error(`HTTP ${res.status}: ${d.error || 'unknown'}`); }); return res.json(); }) .then(data => { jwt = data.token; console.log('[SYNC] Logged in to primary server'); callback(null); }) .catch(err => { callback(err); }); } function firstSync() { console.log('[SYNC] First sync — pulling ALL bookings...'); apiGet('/api/admin/bookings?limit=99999', (err, result) => { if (err) { console.error(`[SYNC] First sync failed: ${err.message}. Retrying in 30s...`); syncTimer = setTimeout(firstSync, 30000); return; } const bookings = result.data || []; insertBatch(bookings, false, (insertCount) => { console.log(`[SYNC] First sync done: ${insertCount} bookings imported (no notifications)`); settingsModule.set('sync_first_sync_done', 'true', (err) => { if (err) console.error('[SYNC] Failed to save sync_first_sync_done:', err); }); saveLastSync(() => { scheduleNext(); }); }); }); } function syncLoop() { settingsModule.get('sync_last_sync_at', (err, lastSyncAt) => { if (err) { console.error('[SYNC] Failed to read last_sync_at:', err); scheduleNext(); return; } const since = lastSyncAt ? encodeURIComponent(lastSyncAt) : ''; const url = since ? `/api/admin/bookings?limit=99999&since=${since}` : '/api/admin/bookings?limit=99999'; apiGet(url, (err, result) => { if (err) { if (err.message.includes('401') || err.message.includes('token') || err.message.includes('jwt')) { console.log('[SYNC] Token expired, re-logging in...'); loginAndSync(); return; } console.error(`[SYNC] Sync failed: ${err.message}. Retry at next interval.`); scheduleNext(); return; } const bookings = result.data || []; if (bookings.length === 0) { saveLastSync(() => scheduleNext()); return; } insertBatch(bookings, true, (insertCount, notifyCount) => { console.log(`[SYNC] Pulled ${bookings.length}, new: ${insertCount}, notified: ${notifyCount}`); saveLastSync(() => scheduleNext()); }); }); }); } function scheduleNext() { if (!running) return; syncTimer = setTimeout(syncLoop, PRIMARY_SYNC_INTERVAL * 1000); } function saveLastSync(callback) { const now = new Date().toISOString(); settingsModule.set('sync_last_sync_at', now, (err) => { if (err) console.error('[SYNC] Failed to save last_sync_at:', err); callback(); }); } function apiGet(path, callback) { if (!jwt) return callback(new Error('Not authenticated')); fetch(PRIMARY_URL + path, { method: 'GET', headers: { 'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json' } }) .then(res => { if (!res.ok) { if (res.status === 401) { jwt = null; return res.json().then(d => { throw Object.assign(new Error('401 Unauthorized'), { code: 401 }); }); } return res.json().then(d => { throw new Error(`HTTP ${res.status}: ${d.error || 'unknown'}`); }); } return res.json(); }) .then(data => callback(null, data)) .catch(err => callback(err)); } function insertBatch(bookings, withNotifications, callback) { let inserted = 0; let notified = 0; const total = bookings.length; const stmt = db.prepare(`INSERT OR IGNORE INTO bookings (remote_id, name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, room_id, base_price, discount_percent, discount_amount, total_price, promocode_id, comment, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); bookings.forEach((b, index) => { stmt.run( b.id, b.name, b.phone, b.adults, b.children, b.checkin_date, b.checkout_date, b.wishes, b.status || 'новая', b.room_type, b.room_id, b.base_price, b.discount_percent, b.discount_amount, b.total_price, b.promocode_id, b.comment, b.created_at, function(err) { if (err) { console.error('[SYNC] Insert error:', err.message); } else if (this.changes > 0) { inserted++; if (withNotifications && b.status !== 'отменена') { sendNotifications(b); notified++; } } if (index === total - 1) { stmt.finalize(); callback(inserted, notified); } } ); }); if (total === 0) { stmt.finalize(); callback(0, 0); } } function sendNotifications(booking) { try { if (telegramModule && typeof telegramModule.sendBookingNotification === 'function') { telegramModule.sendBookingNotification(booking); } } catch(e) { console.error('[SYNC] Telegram notify error:', e.message); } try { if (emailModule && typeof emailModule.sendBookingConfirmation === 'function') { emailModule.sendBookingConfirmation(booking); } } catch(e) { console.error('[SYNC] Email notify error:', e.message); } } module.exports = { init, start, stop };