const express = require('express'); const path = require('path'); const fs = require('fs'); const base64url = require('base64url'); const multer = require('multer'); const { log } = require('console'); const bodyParser = require('body-parser'); const { convertSvgToPng } = require('./utils/imageConverter'); const websocket = require('./websocket'); const translations = require('./translations'); const app = express(); const upload = multer({ storage: multer.memoryStorage() }); // Configure EJS app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); app.use(express.urlencoded({ extended: true })); app.use(bodyParser.json({ limit: "50mb" })); // Serve static files app.use(express.static(path.join(__dirname, 'public'))); // Initialize WebSocket server const server = app.listen(80, () => { console.log('Server running on port 80'); }); websocket.initialize(server); // Static images mapping const staticImages = { mulberry: 'am_mulberry_logo_wide_color.png' , home: 'payment.png', scan: 'Scanner.png', printer: 'Check.png', settings: 'Settings.png', pinpad: 'pinpad.png', cashier: 'cashier.png', menu: 'Menu.png', }; // Topics list const topics = [ { deviceName: 'M50F_Second', deviceID: '01534090202502210057' }, { deviceName: 'M50F_Igor', deviceID: '01534090202502210142' }, { deviceName: 'M60_Igor', deviceID: '01620013202312220735' }, { deviceName: 'M70_Igor', deviceID: '01723060202412010280' }, { deviceName: 'M30_Igor', deviceID: '01364100202503060196' }, { deviceName: 'M80F_Ahmed', deviceID: '01804042025072100015' }, { deviceName: 'M80K_Ahmed', deviceID: '78040132025082600001' }, { deviceName: 'M50F_Ahmed', deviceID: '01534090202502210065' }, { deviceName: 'M50F_Ahmed', deviceID: '01534090202502210065' }, { deviceName: 'M60_Ahmed', deviceID: '01620013202312221500' }, { deviceName: 'M30_Ahmed', deviceID: '01364100202503060115' }, { deviceName: 'M20_Ahmed', deviceID: '01220110202305150514' }, { deviceName: 'M70_Ahmed', deviceID: '01723060202412010160' }, { deviceName: 'M60F-NOSN', deviceID: 'm60f_pos_no_sn' } ]; // Sample configuration const defaultConfig = { type: "configuration", navItems: [ { navPage: "NAV_HOME", navName: "Оплата", icon: { type: "resource", value: "home" }, position: 0, enabled: true }, { navPage: "NAV_SCAN", navName: "Сканнер", icon: { type: "resource", value: "scan" }, position: 1, enabled: true }, { navPage: "NAV_PRINTER", navName: "Принтер", icon: { type: "resource", value: "printer" }, position: 2, enabled: true }, { navPage: "NAV_SETTINGS", navName: "Настройки", icon: { type: "resource", value: "settings" }, position: 3, enabled: true } ], mainLogo: { type: "base64", value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABlUAAAE " }, footerLogo: { type: "base64", value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaQAAAWY" }, printLogo: { type: "base64", value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaQAAAWY" }, printURL: "www.mulberrypos.ru", footerLogoVisibility: "true", bankMode: false, footerGreetingText: "Привет Игорь! ))", footerText: "Mulberry, OOO Demo ©2025", primaryColor: "#002233", secondaryColor: "#67777eff", }; // Routes app.get('/', (req, res) => { res.render('index', { staticImages: Object.keys(staticImages), topics, defaultConfig, connectedDevices: websocket.getConnectedDevices() }); }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', websocketConnections: websocket.connectedDevices.size }); }); // Graceful shutdown process.on('SIGTERM', gracefulShutdown); process.on('SIGINT', gracefulShutdown); function gracefulShutdown() { console.log('Shutting down gracefully...'); websocket.cleanup(); server.close(() => { console.log('Server closed'); process.exit(0); }); } app.post('/sendToDevice', (req, res) => { const { config, topic } = req.body; console.log(`Sending config to device ${topic}`); const success = websocket.sendToDevice(topic, config); res.status(success ? 200 : 404).send(success ? 'Message sent' : 'Device not connected'); }); // Payment endpoints // Payment endpoint app.get('/pay', (req, res) => { try { const paymentData = JSON.parse(base64url.decode(req.query.data)); if (!paymentData.sn || !paymentData.amount) { return res.redirect(`/error?message=${encodeURIComponent('Invalid payment data')}`); } console.log(paymentData); // Convert the amount from cents to dollars const amountInDollars = parseFloat(paymentData.amount) / 100; // Format the amount to two decimal places const formattedAmount = amountInDollars.toFixed(2); const language = paymentData.lang; // Get translations for the selected language const t = translations[language] || translations.en; // const event = new Date(paymentData.time); // const russianDate = event.toLocaleString("ru-RU", { timeZone: "Etc/GMT-3" }) const event = new Date(paymentData.time); const russianDate = event.toLocaleString("ru-RU", { timeZone: "Europe/Moscow", year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(',', ''); // Format data for the receipt const receiptData = { deviceId: paymentData.sn, t:t, appLogo: paymentData.appLogo, appColor: paymentData.appColor, currencyS: paymentData.currencyS, paymentMethod: "QR Code", amount: formattedAmount, timestamp: russianDate , transactionId: `TXN-${Math.random().toString(36).substr(2, 9).toUpperCase()}` }; // Render EJS template with data res.render('receipt', { title: 'DatexPay Virtual Bank - Receipt', ...receiptData }); } catch (error) { res.redirect(`/error?message=${encodeURIComponent(error.message)}`); } }); app.post('/pay-command', (req, res) => { console.log("pay command"); const id = req.query.id; websocket.sendToDevice(id, { type: 'payment', action: 'PRINT_RECEIPT', status : "success" }); res.send({ message: 'Receipt print command sent!' }); }); app.post('/cancel-command', (req, res) => { const id = req.query.id; console.log("cancel command id :" + id); websocket.sendToDevice(id, { type: 'payment', action: 'REJECT_RECEIPT', status : "failed" }); res.send({ message: 'Receipt rejection command sent!' }); }); // Error page app.get('/error', (req, res) => { res.render('error', { title: 'Payment Error', message: req.query.message || 'An unknown error occurred' }); }); // File upload app.post('/upload', upload.single('image'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } let base64Data; const isSvg = req.body.isSvg === 'true'; if (isSvg) { const svgContent = req.file.buffer.toString('utf-8'); const pngBuffer = await convertSvgToPng(svgContent, 'LARGE'); base64Data = pngBuffer.toString('base64'); } else { base64Data = req.file.buffer.toString('base64'); } res.json({ success: true, base64: `data:image/${isSvg ? 'png' : req.file.mimetype.split('/')[1]};base64,${base64Data}`, fileName: req.file.originalname, inputName: req.body.inputName }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ error: 'Image processing failed' }); } });