websocket works and qr code

This commit is contained in:
Ahmed-DatexPay 2025-09-01 17:07:40 +00:00
parent 050245b523
commit f2e663f1d8
33 changed files with 591 additions and 796 deletions

264
app.js Normal file
View File

@ -0,0 +1,264 @@
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 = {
home: 'payment.png',
scan: 'Scanner.png',
printer: 'Check.png',
settings: 'Settings.png',
pinpad: 'pinpad.png',
cashier: 'cashier.png',
};
// Topics list
const topics = [
{ deviceName: 'M50F_Igor', deviceID: '01534090202502210142' },
{ deviceName: 'M60_Igor', deviceID: '01620013202312220735' },
{ deviceName: 'M70_Igor', deviceID: '01723060202412010280' },
{ deviceName: 'M30_Igor', deviceID: '01364100202503060196' },
{ 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"
},
footerLogoVisibility: "true",
footerGreetingText: "Привет Игорь! ))",
footerText: "Mulberry, OOO Demo ©2025",
primaryColor: "#002233"
};
// 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' });
}
});

View File

@ -1,71 +0,0 @@
const express = require('express');
const mysql = require('mysql2/promise');
const path = require('path');
const app = express();
const config = require("./config/db.config.js");
const port = 8080;
const dbConfig = {
host: config.host,
user: config.user,
password: config.password,
database: config.database,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
};
app.use(express.json({ limit: '50mb' }));
app.use(express.static(path.join(__dirname, 'public'))); // Serve static files
async function uploadPriomToMySQL(jsonRows, tableName) {
const conn = await mysql.createConnection(dbConfig);
// This SQL will be used to check for existing records
const checkSql = `SELECT COUNT(*) as count FROM ${tableName} WHERE SerialNumber = ? AND filename = ?`;
// This SQL will be used to insert new records
const insertSql = `INSERT INTO ${tableName} (PriomID, Model, SerialNumber, Problem, filename) VALUES (?, ?, ?, ?, ?)`;
try {
await Promise.all(jsonRows.map(async (jsonRow) => {
const values = [
jsonRow['№ п/п'] || null,
jsonRow['Оборудование'] || null,
jsonRow['Серийный номер'] || null,
jsonRow['Неисправность'] || null,
jsonRow['filename'] || null
];
// Check if the combination of SerialNumber and filename exists
const [rows] = await conn.execute(checkSql, [values[2], values[4]]);
if (rows[0].count === 0) {
// Insert only if the combination doesn't exist
await conn.execute(insertSql, values);
}
}));
} catch (err) {
throw new Error(err.message);
} finally {
await conn.end();
}
}
app.post('/api/upload', async (req, res) => {
// console.log("post upload works")
const jsonRows = req.body;
if (!Array.isArray(jsonRows) ) {
return res.status(400).json({ error: 'Invalid request data' });
}
try {
await uploadPriomToMySQL(jsonRows , 'priom');
res.status(200).json({ message: 'Data uploaded successfully' });
} catch (error) {
res.status(500).json({ error: 'Internal server error: ' + error.message });
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});

227
main.js
View File

@ -1,227 +0,0 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const base64url = require('base64url');
const multer = require('multer');
const mqtt = require('mqtt');
const { log } = require('console');
const bodyParser = require('body-parser');
const { convertSvgToPng } = require('./utils/imageConverter');
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')));
// Static images mapping (matches Android resources)
const staticImages = {
home: 'am_card_pay_black.png',
scan: 'am_barcode_black.png',
printer: 'am_receipt_cart_black.png',
settings: 'am_settings_black.png',
mulberry: 'am_mulberry_logo_wide_color.png',
// ... add other images
};
// MQTT Client setup
const client = mqtt.connect('mqtt://192.168.1.199', {
username: 'ahkad',
password: 'Ksenia11241124m'
});
// Topics list
const topics = ['pos/update', 'pos/update2', 'pos/update3'];
// Sample configuration data structure
const defaultConfig = {
type: "configuration",
navItems: [
{
navPage: "NAV_HOME",
navName: "Payment",
icon: { type: "resource", value: "home" },
position: 0,
enabled: true
},
{
navPage: "NAV_SCAN",
navName: "Scanner",
icon: { type: "resource", value: "scan" },
position: 1,
enabled: true
},
{
navPage: "NAV_PRINTER",
navName: "Printer",
icon: { type: "resource", value: "printer" },
position: 2,
enabled: true
},
{
navPage: "NAV_SETTINGS",
navName: "Settings",
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"
},
footerLogoVisibility: "ture",
footerGreetingText: "Привет Игорь! ))",
footerText: "Mulberry, OOO Demo ©2025",
primaryColor: "#002233"
};
client.on('connect', () => {
console.log('Connected to MQTT broker');
});
client.on('error', (err) => {
console.error('MQTT error:', err);
});
// Payment endpoint
app.get('/pay', (req, res) => {
try {
const paymentData = JSON.parse(base64url.decode(req.query.data));
if (!paymentData.deviceId || !paymentData.paymentMethod || !paymentData.amount) {
return res.redirect(`/error?message=${encodeURIComponent('Invalid payment data')}`);
}
console.log(paymentData);
// Format data for the receipt
const receiptData = {
deviceId: paymentData.deviceId,
appLogo: paymentData.appLogo,
appColor: paymentData.appColor,
paymentMethod: paymentData.paymentMethod,
amount: paymentData.amount.toFixed(2),
timestamp: new Date(paymentData.timestamp).toLocaleString(),
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)}`);
}
});
// ANSI escape code for grey text
const grey = '\x1b[90m';
const reset = '\x1b[0m'; // Reset to default color
// Handle Pay command
app.post('/pay-command', (req, res) => {
const id = req.query.id; // Get the id from the query parameter
const timestamp = new Date().toLocaleString(); // Get the current date and time
console.log(`${grey}[${timestamp}] pay command on id: ${id}${reset}`);
client.publish(id, 'PRINT_RECEIPT');
res.send({ message: 'Receipt printed successfully!' });
});
// Handle Cancel command
app.post('/cancel-command', (req, res) => {
const id = req.query.id; // Get the id from the query parameter
const timestamp = new Date().toLocaleString(); // Get the current date and time
console.log(`${grey}[${timestamp}] cancel command on id: ${id}${reset}`);
client.publish(id, 'REJECT_RECEIPT');
res.send({ message: 'Receipt rejected!' });
});
// Error page
app.get('/error', (req, res) => {
res.render('error', {
title: 'Payment Error',
message: req.query.message || 'An unknown error occurred'
});
});
// app.post('/upload', upload.single('image'), (req, res) => {
// const base64 = fs.readFileSync(req.file.path, 'base64');
// fs.unlinkSync(req.file.path); // Clean up
// res.json({ base64: `data:image/png;base64,${base64}` });
// });
// In your upload route handler
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) {
// Convert SVG to PNG
const svgContent = req.file.buffer.toString('utf-8');
const pngBuffer = await convertSvgToPng(svgContent, 'LARGE');
base64Data = pngBuffer.toString('base64');
} else {
// Use original image for non-SVG
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' });
}
});
// Routes
app.get('/', (req, res) => {
res.render('index', {
staticImages: Object.keys(staticImages),
topics,
defaultConfig
});
});
app.post('/publish', (req, res) => {
const { config, topic } = req.body;
console.log("published config on topic" + topic);
client.publish(topic, JSON.stringify(config));
res.sendStatus(200);
});
app.listen(80, () => {
console.log('Server running on port 80');
});

View File

@ -1,174 +0,0 @@
const express = require('express');
const crypto = require('crypto');
const app = express();
const port = 8080;
const base64url = require('base64url'); // npm install base64url
app.get('/pay', (req, res) => {
try {
// Decode and parse the payment data
const paymentData = JSON.parse(base64url.decode(req.query.data));
// Validate required fields
if (!paymentData.deviceId || !paymentData.paymentMethod || !paymentData.amount) {
return res.status(400).send(invalidRequestResponse());
}
// Generate a beautiful receipt
res.send(generateReceiptHTML(
paymentData.deviceId,
paymentData.paymentMethod,
paymentData.amount,
paymentData.timestamp
));
} catch (error) {
res.status(400).send(errorResponse(error));
}
});
// Helper function to generate receipt HTML
function generateReceiptHTML(deviceId, paymentMethod, amount, timestamp) {
const paymentDate = new Date(timestamp).toLocaleString();
return `
<!DOCTYPE html>
<html>
<head>
<title>DatexPay Virtual Bank - Receipt</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: 'Courier New', monospace;
background: #f5f5f5;
display: flex;
justify-content: center;
padding: 20px;
}
.receipt {
width: 300px;
background: white;
padding: 25px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
border-top: 5px solid #4CAF50;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h1 {
color: #4CAF50;
font-size: 24px;
margin: 0;
}
.header p {
margin: 5px 0;
color: #666;
}
.divider {
border-top: 1px dashed #ccc;
margin: 15px 0;
}
.receipt-details {
margin-bottom: 20px;
}
.receipt-row {
display: flex;
justify-content: space-between;
margin: 8px 0;
}
.label {
font-weight: bold;
color: #333;
}
.footer {
text-align: center;
font-size: 12px;
color: #999;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="receipt">
<div class="header">
<h1>DatexPay Virtual Bank</h1>
<p>Transaction Receipt</p>
</div>
<div class="divider"></div>
<div class="receipt-details">
<div class="receipt-row">
<span class="label">Device ID:</span>
<span>${deviceId}</span>
</div>
<div class="receipt-row">
<span class="label">Payment Method:</span>
<span>${paymentMethod}</span>
</div>
<div class="receipt-row">
<span class="label">Amount:</span>
<span>$${amount.toFixed(2)}</span>
</div>
<div class="receipt-row">
<span class="label">Date:</span>
<span>${paymentDate}</span>
</div>
</div>
<div class="divider"></div>
<div class="footer">
<p>Thank you for using DatexPay</p>
<p>Transaction ID: ${generateTransactionId()}</p>
</div>
</div>
</body>
</html>
`;
}
// Helper function to generate error response
function errorResponse(error) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Error - DatexPay</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
.error-box {
border: 1px solid #ff4444;
background: #ffeeee;
padding: 20px;
max-width: 500px;
margin: 0 auto;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="error-box">
<h2>Payment Processing Error</h2>
<p>${error.message}</p>
<p>Please try again or contact support.</p>
</div>
</body>
</html>
`;
}
// Generate a random transaction ID
function generateTransactionId() {
return 'TXN-' + Math.random().toString(36).substr(2, 9).toUpperCase();
}
app.listen(port, () => {
console.log(`Test server running at http://localhost:${port}/`);
});

View File

@ -26,7 +26,8 @@
"mysql2": "^3.11.0", "mysql2": "^3.11.0",
"sharp": "^0.34.3", "sharp": "^0.34.3",
"svg2img": "^1.0.0-beta.2", "svg2img": "^1.0.0-beta.2",
"tmp": "^0.2.3" "tmp": "^0.2.3",
"ws": "^8.18.3"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.0" "nodemon": "^3.1.0"

View File

@ -13,7 +13,7 @@ body {
width: 300px; width: 300px;
background: white; background: white;
padding: 25px; padding: 25px;
box-shadow: 0 0 10px rgba(0,0,0,0.1); /* box-shadow: 0 0 10px rgba(0,0,0,0.1); */
border-top: 5px solid #00b8d7; border-top: 5px solid #00b8d7;
} }
.header { .header {

BIN
public/images/Card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
public/images/Check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
public/images/Discard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
public/images/Menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
public/images/Refund.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
public/images/Scaner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
public/images/Settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
public/images/cashier.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
public/images/payment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
public/images/pinpad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -5,10 +5,12 @@ const imageStore = {
// Static image resources // Static image resources
staticImages: { staticImages: {
home: 'am_card_pay_black.png', home: 'payment.png',
scan: 'am_barcode_black.png', scan: 'Scaner.png',
printer: 'am_receipt_cart_black.png', printer: 'Check.png',
settings: 'am_settings_black.png', settings: 'Settings.png',
pinpad: 'pinpad.png',
cashier: 'cashier.png',
mulberry: 'am_mulberry_logo_wide_color.png', mulberry: 'am_mulberry_logo_wide_color.png',
overtec: 'am_overtec_logo_wide_color.png', overtec: 'am_overtec_logo_wide_color.png',
datexpay: 'am_datexpay_logo_wide_color.png', datexpay: 'am_datexpay_logo_wide_color.png',
@ -130,7 +132,7 @@ document.addEventListener('DOMContentLoaded', function () {
} catch (error) { } catch (error) {
console.error('[ERROR] Upload failed:', error); console.error('[ERROR] Upload failed:', error);
alert('Error uploading image. Please try again.');
} }
}); });
}); });
@ -227,7 +229,7 @@ document.addEventListener('DOMContentLoaded', function () {
// .then(response => response.json()) // .then(response => response.json())
// .then(data => console.log('Success:', data)) // .then(data => console.log('Success:', data))
// .catch(error => console.error('Error:', error)); // .catch(error => console.error('Error:', error));
fetch('/publish', { fetch('/sendToDevice', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@ -237,7 +239,7 @@ document.addEventListener('DOMContentLoaded', function () {
}) })
.then(() => { .then(() => {
console.log('[DEBUG] Configuration published successfully'); console.log('[DEBUG] Configuration published successfully');
alert('Configuration published successfully!');
}) })
.catch(err => { .catch(err => {
console.error('[ERROR] Publish failed:', err); console.error('[ERROR] Publish failed:', err);

41
translations.js Normal file
View File

@ -0,0 +1,41 @@
// translations.js
const translations = {
en: {
receiptNumber: "Receipt №",
paymentType: "Payment Type:",
testPayment: "Test Payment:",
paymentAddress: "PAYMENT ADDRESS:",
issueDate: "ISSUE DATE:",
total: "TOTAL:",
thankYou: "Thank you for using Mulberry",
transactionId: "Transaction ID:",
status: "Status:",
receiptRejected: "Receipt Rejected",
paymentDate: "PAYMENT DATE:",
amountToPay: "Amount to Pay:",
cancel: "Cancel",
pay: "Pay",
paymentAddressExample:"Uzbekistan, Tashkent",
// Add more translations as needed
},
ru: {
receiptNumber: "Квитанция №",
paymentType: "Тип оплаты:",
testPayment: "Тестовый платеж:",
paymentAddress: "АДРЕС ПЛАТЕЖА:",
issueDate: "ДАТА ВЫПУСКА:",
total: "ИТОГО:",
thankYou: "Спасибо за использование Mulberry",
transactionId: "ID транзакции:",
status: "Статус:",
receiptRejected: "Квитанция отклонена",
paymentDate: "ДАТА ПЛАТЕЖА:",
amountToPay: "Сумма к оплате:",
cancel: "Отмена",
pay: "Оплатить",
paymentAddressExample:"Волгоград, пр. им. В. И. Ленина",
// Add more translations as needed
}
};
module.exports = translations;

View File

@ -56,8 +56,8 @@
<label>MQTT Topic</label> <label>MQTT Topic</label>
<select name="topic"> <select name="topic">
<% topics.forEach(topic=> { %> <% topics.forEach(topic=> { %>
<option value="<%= topic %>"> <option value="<%= topic.deviceID %>">
<%= topic %> <%= topic.deviceName %> (<%= topic.deviceID %>)
</option> </option>
<% }); %> <% }); %>
</select> </select>
@ -104,6 +104,14 @@
>Printer</option> >Printer</option>
<option value="NAV_SETTINGS" <%=item.navPage==='NAV_SETTINGS' ? 'selected' : '' <option value="NAV_SETTINGS" <%=item.navPage==='NAV_SETTINGS' ? 'selected' : ''
%>>Settings</option> %>>Settings</option>
<option value="NAV_PINPAD" <%=item.navPage==='NAV_PINPAD' ? 'selected' : ''
%>>PinPad</option>
<option value="NAV_CASHIER" <%=item.navPage==='NAV_CASHIER' ? 'selected' : ''
%>>Cashier</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -113,7 +121,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Icon</label> <label>Icon</label>
<select name="navItems[<%= index %>][icon][type]" onchange="toggleUploadButton(this)"> <select name="navItems[<%= index %>][icon][type]"
onchange="toggleUploadButton(this)">
<option value="resource" <%=item.icon.type==='resource' ? 'selected' : '' %> <option value="resource" <%=item.icon.type==='resource' ? 'selected' : '' %>
>Static</option> >Static</option>
<option value="base64" <%=item.icon.type==='base64' ? 'selected' : '' %>>Upload <option value="base64" <%=item.icon.type==='base64' ? 'selected' : '' %>>Upload
@ -129,8 +138,7 @@
</select> </select>
<input type="file" name="navIconUpload<%= index %>" class="icon-upload" <input type="file" name="navIconUpload<%= index %>" class="icon-upload"
style="display: none;"> style="display: none;">
<input type="hidden" name="navIconValue<%= index %>" <input type="hidden" name="navIconValue<%= index %>" navItems
navItems
style="display: none;"> style="display: none;">
</div> </div>
<div class="form-group"> <div class="form-group">
@ -173,7 +181,8 @@
<div class="form-group"> <div class="form-group">
<label>Footer Logo Visibility</label> <label>Footer Logo Visibility</label>
<label class="switch"> <label class="switch">
<input type="checkbox" name="footerLogoVisibility" <%= defaultConfig.footerLogoVisibility ? 'checked' : '' %>> <input type="checkbox" name="footerLogoVisibility" <%=defaultConfig.footerLogoVisibility
? 'checked' : '' %>>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
</div> </div>

View File

@ -1,182 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/receipt.css">
</head>
<body>
<!-- Accepted Receipt -->
<div id="acceptedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
<div class="header">
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset="">
<p>чек № 2949</p>
<p>https://www.mulberrypos.com</p>
<img class="mulberry-logo" src="/images/mulberry-logo-small.png" alt="" srcset="">
</div>
<div class="divider"></div>
<div class="receipt-details">
<div class="receipt-row">
<span class="label">Тип оплаты:</span>
<span><%= paymentMethod %></span>
</div>
<div class="receipt-row">
<span class="label">Тестовая оплата:</span>
<span><%= amount %> UZS</span>
</div>
<div class="receipt-row">
<span class="label">АДРЕС РАСЧЁТОВ:</span>
<span style="width: 135px;"> Узбекистан, г. Ташкент</span>
</div>
<div class="receipt-row">
<span class="label">ДАТА ВЫДАЧИ:</span>
<span><%= timestamp %></span>
</div>
</div>
<div class="divider"></div>
<div class="receipt-details">
<div style="font-size: large;" class="receipt-row">
<span class="label">ИТОГ:</span>
<span><%= amount %> UZS </span>
</div>
</div>
<div class="divider"></div>
<div class="footer">
<p>Thank you for using Mulberry</p>
<p>Transaction ID: <%= transactionId %></p>
</div>
</div>
<!-- Rejected Receipt -->
<div id="rejectedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
<div class="header">
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset="">
<p>чек № 2949</p>
<p>https://www.mulberrypos.com</p>
<img class="mulberry-logo" src="/images/mulberry-logo-small.png" alt="" srcset="">
</div>
<div class="divider"></div>
<div class="receipt-details">
<div class="receipt-row">
<span class="label">Статус:</span>
<span style="color: red;">Чек отклонен</span>
</div>
<div class="receipt-row">
<span class="label">ДАТА ПЛАТЕЖА:</span>
<span><%= timestamp %></span>
</div>
</div>
<div class="divider"></div>
<div class="footer">
<p>Transaction ID: <%= transactionId %></p>
</div>
</div>
<div id="buttons" class="buttons-section" style="border-top: 5px solid <%= appColor %>;">
<div class="header">
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset="">
<p>чек № 2949</p>
<p>https://www.mulberrypos.com</p>
<img class="mulberry-logo" src="/images/mulberry-logo-small.png" alt="" srcset="">
</div>
<div class="divider"></div>
<div class="receipt-details">
<div style="font-size: large;" class="receipt-row">
<span class="label">Сумма к оплате:</span>
<span><%= amount %> UZS </span>
</div>
<div class="receipt-row">
<span class="label">ДАТА ПЛАТЕЖА:</span>
<span><%= timestamp %></span>
</div>
</div>
<div class="divider"></div>
<div class="buttons">
<button id="cancelButton">Отмена</button>
<button id="payButton">Оплата</button>
</div>
<div class="divider"></div>
<div class="footer">
<p>Transaction ID: <%= transactionId %></p>
</div>
</div>
<!-- Buttons -->
<div id="buttonsXX" class="buttons">
<!-- <button id="payButton">Pay</button>
<button id="cancelButton">Cancel</button> -->
</div>
<!-- Loading Circle -->
<div id="loading" class="loading"> </div>
<script>
const acceptedReceipt = document.getElementById('acceptedReceipt');
const rejectedReceipt = document.getElementById('rejectedReceipt');
const buttons = document.getElementById('buttons');
const id = '<%= deviceId %>'; // Pass the id from EJS
document.getElementById('payButton').addEventListener('click', async () => {
const response = await fetch(`/pay-command?id=${id}`, { method: 'POST' });
const result = await response.json();
loading.style.display = 'block';
// Simulate payment process for 1 second
setTimeout(() => {
// Hide loading circle
loading.style.display = 'none';
// Show accepted receipt and hide rejected receipt
acceptedReceipt.style.display = 'block';
rejectedReceipt.style.display = 'none';
buttons.style.display = 'none';
}, 1000);
});
document.getElementById('cancelButton').addEventListener('click', async () => {
const response = await fetch(`/cancel-command?id=${id}`, { method: 'POST' });
const result = await response.json();
// alert(result.message);
setTimeout(() => {
// Hide loading circle
loading.style.display = 'none';
// Show rejected receipt and hide accepted receipt
rejectedReceipt.style.display = 'block';
acceptedReceipt.style.display = 'none';
buttons.style.display = 'none';
}, 1000);
});
</script>
</body>
</html>

View File

@ -10,29 +10,30 @@
<!-- Accepted Receipt --> <!-- Accepted Receipt -->
<div id="acceptedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;"> <div id="acceptedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
<div class="header"> <div class="header">
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset=""> <!-- <img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset=""> -->
<p>Receipt № 2949</p> <img class="datex-pay-logo" src="/images/am_mulberry_logo_wide_color.png" alt="" srcset="">
<p><%= t.receiptNumber %> 2949</p>
<!-- <p>https://www.mulberrypos.com</p> --> <!-- <p>https://www.mulberrypos.com</p> -->
<img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> <!-- <img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> -->
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="receipt-details"> <div class="receipt-details">
<div class="receipt-row"> <div class="receipt-row">
<span class="label">Payment Type:</span> <span class="label"><%= t.paymentType %></span>
<span><%= paymentMethod %></span> <span><%= paymentMethod %></span>
</div> </div>
<div class="receipt-row"> <div class="receipt-row">
<span class="label">Test Payment:</span> <span class="label"><%= t.testPayment %></span>
<span><%= amount %> $</span> <span><%= amount %> <%= currencyS %></span>
</div> </div>
<!-- <div class="receipt-row">
<span class="label"><%= t.paymentAddress %></span>
<span style="width: 135px;"><%= t.paymentAddressExample %></span>
</div> -->
<div class="receipt-row"> <div class="receipt-row">
<span class="label">PAYMENT ADDRESS:</span> <span class="label"><%= t.issueDate %></span>
<span style="width: 135px;">Uzbekistan, Tashkent</span>
</div>
<div class="receipt-row">
<span class="label">ISSUE DATE:</span>
<span><%= timestamp %></span> <span><%= timestamp %></span>
</div> </div>
</div> </div>
@ -41,37 +42,37 @@
<div class="receipt-details"> <div class="receipt-details">
<div style="font-size: large;" class="receipt-row"> <div style="font-size: large;" class="receipt-row">
<span class="label">TOTAL:</span> <span class="label"><%= t.total %></span>
<span><%= amount %> $</span> <span><%= amount %> <%= currencyS %></span>
</div> </div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="footer"> <div class="footer">
<p>Thank you for using Mulberry</p> <p><%= t.thankYou %></p>
<p>Transaction ID: <%= transactionId %></p> <p><%= t.transactionId %> <%= transactionId %></p>
</div> </div>
</div> </div>
<!-- Rejected Receipt --> <!-- Rejected Receipt -->
<div id="rejectedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;"> <div id="rejectedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
<div class="header"> <div class="header">
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset=""> <img class="datex-pay-logo" src="/images/am_mulberry_logo_wide_color.png" alt="" srcset="">
<p>Receipt № 2949</p> <p><%= t.receiptNumber %> 2949</p>
<!-- <p>https://www.mulberrypos.com</p> --> <!-- <p>https://www.mulberrypos.com</p> -->
<img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> <!-- <img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> -->
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="receipt-details"> <div class="receipt-details">
<div class="receipt-row"> <div class="receipt-row">
<span class="label">Status:</span> <span class="label"><%= t.status %></span>
<span style="color: red;">Receipt Rejected</span> <span style="color: red;"><%= t.receiptRejected %></span>
</div> </div>
<div class="receipt-row"> <div class="receipt-row">
<span class="label">PAYMENT DATE:</span> <span class="label"><%= t.paymentDate %></span>
<span><%= timestamp %></span> <span><%= timestamp %></span>
</div> </div>
</div> </div>
@ -79,29 +80,29 @@
<div class="divider"></div> <div class="divider"></div>
<div class="footer"> <div class="footer">
<p>Transaction ID: <%= transactionId %></p> <p><%= t.transactionId %> <%= transactionId %></p>
</div> </div>
</div> </div>
<!-- Buttons Section --> <!-- Buttons Section -->
<div id="buttons" class="buttons-section" style="border-top: 5px solid <%= appColor %>;"> <div id="buttons" class="buttons-section" style="border-top: 5px solid <%= appColor %>;">
<div class="header"> <div class="header">
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset=""> <img class="datex-pay-logo" src="/images/am_mulberry_logo_wide_color.png" alt="" srcset="">
<p>Receipt № 2949</p> <p><%= t.receiptNumber %> 2949</p>
<!-- <p>https://www.mulberrypos.com</p> --> <!-- <p>https://www.mulberrypos.com</p> -->
<img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> <!-- <img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> -->
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="receipt-details"> <div class="receipt-details">
<div style="font-size: large;" class="receipt-row"> <div style="font-size: large;" class="receipt-row">
<span class="label">Amount to Pay:</span> <span class="label"><%= t.amountToPay %></span>
<span><%= amount %> $</span> <span><%= amount %> <%= currencyS %></span>
</div> </div>
<div class="receipt-row"> <div class="receipt-row">
<span class="label">PAYMENT DATE:</span> <span class="label"><%= t.paymentDate %></span>
<span><%= timestamp %></span> <span><%= timestamp %></span>
</div> </div>
</div> </div>
@ -109,14 +110,14 @@
<div class="divider"></div> <div class="divider"></div>
<div class="buttons"> <div class="buttons">
<button id="cancelButton">Cancel</button> <button id="cancelButton"><%= t.cancel %></button>
<button id="payButton">Pay</button> <button id="payButton"><%= t.pay %></button>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="footer"> <div class="footer">
<p>Transaction ID: <%= transactionId %></p> <p><%= t.transactionId %> <%= transactionId %></p>
</div> </div>
</div> </div>
@ -128,6 +129,11 @@
const rejectedReceipt = document.getElementById('rejectedReceipt'); const rejectedReceipt = document.getElementById('rejectedReceipt');
const buttons = document.getElementById('buttons'); const buttons = document.getElementById('buttons');
const id = '<%= deviceId %>'; // Pass the id from EJS const id = '<%= deviceId %>'; // Pass the id from EJS
console.log(id);
console.log("Data in receipt.ejs :");
document.getElementById('payButton').addEventListener('click', async () => { document.getElementById('payButton').addEventListener('click', async () => {
const response = await fetch(`/pay-command?id=${id}`, { method: 'POST' }); const response = await fetch(`/pay-command?id=${id}`, { method: 'POST' });

126
websocket.js Normal file
View File

@ -0,0 +1,126 @@
const WebSocket = require('ws');
class WebSocketManager {
constructor() {
this.connectedDevices = new Map(); // deviceId -> WebSocket
this.wss = null;
}
initialize(server) {
this.wss = new WebSocket.Server({ server });
this.wss.on('connection', (ws) => {
console.log('New client connected');
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
// Handle device identification
if (data.type === 'identification') {
this.connectedDevices.set(data.deviceId, ws);
console.log(`Device ${data.deviceId} (${data.model}) connected`);
// Send configuration immediately after identification
if (data.requestConfig) {
this.sendConfiguration(data.deviceId);
}
return;
}
if (data.type === 'redirect') {
console.log(`message ${data.amount}
${data.currencySymbol}
${data.id} `);
const ws = this.connectedDevices.get(data.id);
if (ws && ws.readyState === WebSocket.OPEN) {
message = JSON.stringify(data);
ws.send(message);
return true;
}
return;
}
if (data.type === 'message') {
console.log(`message ${data.content} `);
return;
}
// Handle other message types...
} catch (e) {
console.error('Error parsing message:', e);
}
});
ws.on('close', () => {
// Clean up disconnected devices
for (let [id, connection] of this.connectedDevices.entries()) {
if (connection === ws) {
this.connectedDevices.delete(id);
console.log(`Device ${id} disconnected`);
break;
}
}
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
});
}
sendToDevice(deviceId, message) {
const ws = this.connectedDevices.get(deviceId);
if (ws && ws.readyState === WebSocket.OPEN) {
if (typeof message !== 'string') {
message = JSON.stringify(message);
}
ws.send(message);
return true;
}
console.log(`Device ${deviceId} not connected`);
return false;
}
sendConfiguration(deviceId) {
// In a real app, you would fetch device-specific config from database
const config = {
type: "configuration",
deviceId: deviceId,
navItems: [{
navPage: "NAV_HOME",
navName: "PaymentZ",
icon: { type: "resource", value: "home" },
position: 0,
enabled: true
},
{
navPage: "NAV_SCAN",
navName: "ScannerZ",
icon: { type: "resource", value: "scan" },
position: 1,
enabled: true
},
],
timestamp: new Date().toISOString()
};
this.sendToDevice(deviceId, config);
}
broadcast(message) {
this.connectedDevices.forEach((ws, deviceId) => {
this.sendToDevice(deviceId, message);
});
}
getConnectedDevices() {
return Array.from(this.connectedDevices.keys());
}
}
module.exports = new WebSocketManager();