Files
hotell777_260507/modules/sync/index.js
kalugin66 d539e1f0c0
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 42s
апи
2026-07-21 20:06:43 +05:00

226 lines
7.4 KiB
JavaScript

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 };