Files
open-isp/services/voucherCacheWarmer.js
oxpoe eaf05f2811 OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor,
admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS.

Kredit & lisensi (ISC):
- Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak
  atas base kode dan repo yang bermanfaat.
- Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder.
- Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
2026-10-09 05:51:21 +07:00

257 lines
9.1 KiB
JavaScript

/**
* Voucher Cache Warmer Service
* Melakukan pre-warming cache untuk voucher profiles dan payment channels
* agar halaman voucher selalu cepat diakses
*/
const { logger } = require('../config/logger');
const { parseMikhmonOnLogin } = require('../utils/mikhmonParser');
// Flag untuk mencegah multiple warming berjalan bersamaan
let isWarming = false;
let warmingInterval = null;
/**
* Warm up voucher cache
* Dipanggil secara berkala untuk memastikan cache selalu fresh
*/
async function warmVoucherCache() {
if (isWarming) {
logger.debug('[VoucherCacheWarmer] Already warming, skip');
return;
}
isWarming = true;
const startTime = Date.now();
try {
// Import di dalam fungsi untuk menghindari circular dependency
const mikrotikService = require('./mikrotikService');
const paymentSvc = require('./paymentService');
const { getSettingsWithCache } = require('../config/settingsManager');
const db = require('../config/database');
logger.info('[VoucherCacheWarmer] Starting cache warming...');
// Get settings
const settings = getSettingsWithCache();
// 1. Warm voucher profiles cache
const CACHE_KEY = 'voucher_profiles_cache';
const routers = mikrotikService.getAllRouters().filter(r => r.is_active);
const routerList = routers.length > 0 ? routers : [{ id: null, name: '' }];
// Query database dan MikroTik secara parallel
const [configuredPricesMap, mikrotikResults] = await Promise.all([
(async () => {
const map = new Map();
try {
const rows = db.prepare(`
SELECT router_id, profile_name, price, validity
FROM voucher_batches
WHERE price > 0
GROUP BY router_id, profile_name
HAVING id = MAX(id)
`).all();
for (const row of rows) {
const key = `${row.router_id}_${row.profile_name}`;
map.set(key, { price: row.price, validity: row.validity });
}
} catch (e) {
logger.error('[VoucherCacheWarmer] Error loading configured prices: ' + e.message);
}
return map;
})(),
(async () => {
try {
// Query semua router secara parallel dengan timeout per router
const results = await Promise.allSettled(
routerList.map(r =>
Promise.race([
mikrotikService.getHotspotUserProfiles(r.id),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout for router ${r.name || r.id}`)), 4000)
)
])
)
);
return results;
} catch (e) {
logger.error('[VoucherCacheWarmer] Error querying MikroTik: ' + e.message);
return [];
}
})()
]);
// Process profiles
const allRows = [];
if (Array.isArray(mikrotikResults)) {
for (let i = 0; i < mikrotikResults.length; i++) {
const result = mikrotikResults[i];
const router = routerList[i];
if (result.status !== 'fulfilled' || !Array.isArray(result.value)) {
logger.warn(`[VoucherCacheWarmer] Router ${router.name || router.id} query failed or returned invalid data`);
continue;
}
for (const p of result.value) {
allRows.push({
routerId: router.id ?? null,
name: p?.name,
onLogin: p?.onLogin ?? p?.['on-login']
});
}
}
}
logger.info(`[VoucherCacheWarmer] Found ${allRows.length} profiles from MikroTik`);
const bestByName = new Map();
let skippedCount = 0;
for (const row of allRows) {
const name = String(row.name || '').trim();
if (!name) continue;
// Parse Mikhmon metadata (gunakan shared parser agar konsisten dengan route handler)
const onLogin = String(row.onLogin || '').trim();
const meta = parseMikhmonOnLogin(onLogin);
let price = Number(meta?.price || 0) || 0;
let validity = String(meta?.validity || '').trim();
if (price <= 0 || !validity) {
const key = `${row.routerId}_${name}`;
const configured = configuredPricesMap.get(key);
if (configured) {
price = Number(configured.price || 0) || 0;
validity = String(configured.validity || '').trim();
}
}
if (price <= 0) {
skippedCount++;
logger.debug(`[VoucherCacheWarmer] Skipped profile "${name}" - no price (onLogin: ${onLogin || 'empty'})`);
continue;
}
if (!validity) validity = '-';
const existing = bestByName.get(name);
if (!existing || Number(price) < Number(existing.price || 0)) {
bestByName.set(name, { name, validity, price, router_id: row.routerId });
}
}
if (skippedCount > 0) {
logger.warn(`[VoucherCacheWarmer] Skipped ${skippedCount} profiles without price metadata. Add Mikhmon metadata (e.g., $10000^1d) to profile's on-login script or configure price in voucher_batches table.`);
}
const profiles = Array.from(bestByName.values()).sort((a, b) => Number(a.price || 0) - Number(b.price || 0));
// Save to cache
global[CACHE_KEY] = {
data: profiles,
timestamp: Date.now()
};
logger.info(`[VoucherCacheWarmer] Profiles cached: ${profiles.length} profiles`);
// 2. Warm payment channels cache
const gateway = settings.default_gateway || 'tripay';
const gatewayEnabled = {
tripay: settings.tripay_enabled && settings.tripay_api_key,
midtrans: settings.midtrans_enabled && settings.midtrans_server_key,
xendit: settings.xendit_enabled && settings.xendit_api_key,
duitku: settings.duitku_enabled && settings.duitku_api_key
};
if (gatewayEnabled[gateway]) {
const PAYMENT_CACHE_KEY = `voucher_payment_channels_cache_${gateway}`;
let channels = [];
if (gateway === 'tripay') {
try {
channels = await Promise.race([
paymentSvc.getTripayChannels(),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 1500))
]);
} catch (e) {
logger.warn('[VoucherCacheWarmer] Tripay channels fetch failed: ' + e.message);
channels = [];
}
} else {
const base = [
{ code: 'QRIS', name: 'QRIS', group: 'QRIS', active: true },
{ code: 'BCAVA', name: 'BCA Virtual Account', group: 'Virtual Account', active: true },
{ code: 'BNIVA', name: 'BNI Virtual Account', group: 'Virtual Account', active: true },
{ code: 'BRIVA', name: 'BRI Virtual Account', group: 'Virtual Account', active: true },
{ code: 'PERMATAVA', name: 'Permata Virtual Account', group: 'Virtual Account', active: true },
{ code: 'MANDIRIVA', name: 'Mandiri Virtual Account', group: 'Virtual Account', active: true }
];
if (gateway === 'midtrans') channels = [{ code: 'SNAP', name: 'Semua Metode (Snap)', group: 'E-Wallet', active: true }, ...base];
else if (gateway === 'xendit') channels = [{ code: 'XENDIT', name: 'Semua Metode', group: 'E-Wallet', active: true }, ...base];
else if (gateway === 'duitku') channels = [{ code: 'DUITKU', name: 'Semua Metode', group: 'E-Wallet', active: true }, ...base];
else channels = base;
}
global[PAYMENT_CACHE_KEY] = {
data: channels,
timestamp: Date.now()
};
logger.info(`[VoucherCacheWarmer] Payment channels cached: ${channels.length} channels`);
}
const duration = Date.now() - startTime;
logger.info(`[VoucherCacheWarmer] Cache warming completed in ${duration}ms`);
} catch (error) {
logger.error('[VoucherCacheWarmer] Error warming cache:', error.message);
} finally {
isWarming = false;
}
}
/**
* Start background cache warming
* Warm cache setiap 8 menit (sebelum cache 10 menit expired)
*/
function startCacheWarming() {
if (warmingInterval) {
logger.warn('[VoucherCacheWarmer] Already started');
return;
}
// Warm immediately on start (500ms delay agar app sudah siap)
setTimeout(() => {
warmVoucherCache().catch(err => {
logger.error('[VoucherCacheWarmer] Initial warming failed:', err.message);
});
}, 500); // 500ms setelah aplikasi start (lebih cepat siap)
// Then warm every 8 minutes
warmingInterval = setInterval(() => {
warmVoucherCache().catch(err => {
logger.error('[VoucherCacheWarmer] Periodic warming failed:', err.message);
});
}, 3 * 60 * 1000); // 3 menit (agar profile selalu up-to-date)
logger.info('[VoucherCacheWarmer] Background cache warming started (every 3 minutes)');
}
/**
* Stop background cache warming
*/
function stopCacheWarming() {
if (warmingInterval) {
clearInterval(warmingInterval);
warmingInterval = null;
logger.info('[VoucherCacheWarmer] Background cache warming stopped');
}
}
module.exports = {
warmVoucherCache,
startCacheWarming,
stopCacheWarming
};
// Made with Bob