- Treat an article that has become a father (nIstVater=1) as a free position (kArtikel=0) with the name kept: VersandIntern.vBestellPosOffen joins tArtikel with nIstVater=0, so such positions could never be reserved and failed with "insufficient stock after POS shortage booking" (e.g. Becks 42000030). - Recalculate tAuftragPositionEckdaten before deliverOrder so orders without payments get their positions reserved too (previously only insertPayment triggered the recalculation). Also adds debug logging across the delivery pipeline (reserve, commit, deliver, session, stock-shortage, create-order).
159 lines
4.4 KiB
JavaScript
159 lines
4.4 KiB
JavaScript
import fs from 'node:fs';
|
|
import http from 'node:http';
|
|
import https from 'node:https';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import 'dotenv/config';
|
|
|
|
import { connectDb, closeDb } from './src/db.js';
|
|
import { createJtlPosServer } from './src/jtl-server.js';
|
|
import { createPairingStore } from './src/pairing.js';
|
|
import { closeOrderLog } from './src/order-log.js';
|
|
import { closeRequestLog, logRequest } from './src/request-log.js';
|
|
import { logger } from './src/logger.js';
|
|
import { fetchActiveShop } from './src/shop.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
const PORT = Number(process.env.PORT) || 4443;
|
|
const HTTP_PORT = Number(process.env.HTTP_PORT) || 8080;
|
|
const AUTH_TOKEN = process.env.AUTH_TOKEN || 'df40ad2067954646abb0499548a52241';
|
|
const PAIRING_CODE = process.env.PAIRING_CODE || '307018';
|
|
|
|
const certsDir = path.join(__dirname, 'certs');
|
|
const keyPath = path.join(certsDir, 'key.pem');
|
|
const certPath = path.join(certsDir, 'cert.pem');
|
|
|
|
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
|
|
logger.error('Missing TLS certificate. Run: npm run cert');
|
|
process.exit(1);
|
|
}
|
|
|
|
const CONSOLE_URL_MAX_LENGTH = 100;
|
|
|
|
function truncateUrl(url) {
|
|
if (url.length <= CONSOLE_URL_MAX_LENGTH) {
|
|
return url;
|
|
}
|
|
return `${url.slice(0, CONSOLE_URL_MAX_LENGTH)}...`;
|
|
}
|
|
|
|
let lastLoggedInitUrl = null;
|
|
let suppressedInitCount = 0;
|
|
|
|
function flushSuppressedInitLogs() {
|
|
if (suppressedInitCount > 0) {
|
|
logger.info(`Suppressed ${suppressedInitCount} duplicate init log(s)`);
|
|
suppressedInitCount = 0;
|
|
}
|
|
}
|
|
|
|
function formatBody(buffer) {
|
|
if (!buffer.length) {
|
|
return '(empty)';
|
|
}
|
|
const text = buffer.toString('utf8');
|
|
if (/^[\x09\x0A\x0D\x20-\x7E\u0080-\uFFFF]*$/.test(text)) {
|
|
return text;
|
|
}
|
|
return `[binary ${buffer.length} bytes]`;
|
|
}
|
|
|
|
const pairingStore = createPairingStore();
|
|
pairingStore.setPairingCode(PAIRING_CODE, 'JTL-POS');
|
|
pairingStore.registerDevice(AUTH_TOKEN, 'JTL-POS');
|
|
|
|
const jtlHandler = createJtlPosServer(pairingStore, { authToken: AUTH_TOKEN });
|
|
|
|
const loggedJtlHandler = async (req, res) => {
|
|
const started = Date.now();
|
|
let responseBuffer = Buffer.alloc(0);
|
|
|
|
const originalEnd = res.end.bind(res);
|
|
res.end = (chunk, ...args) => {
|
|
if (chunk) {
|
|
responseBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
}
|
|
return originalEnd(chunk, ...args);
|
|
};
|
|
|
|
await jtlHandler(req, res);
|
|
|
|
const responseBody = formatBody(responseBuffer);
|
|
const durationMs = Date.now() - started;
|
|
const isInit = req.url.startsWith('/api/v1/init');
|
|
|
|
if (isInit) {
|
|
if (lastLoggedInitUrl === req.url) {
|
|
suppressedInitCount++;
|
|
return;
|
|
}
|
|
lastLoggedInitUrl = req.url;
|
|
} else {
|
|
flushSuppressedInitLogs();
|
|
lastLoggedInitUrl = null;
|
|
}
|
|
|
|
logger.info(`${req.socket.remoteAddress} ${req.method} ${truncateUrl(req.url)} ${res.statusCode} ${durationMs}ms`);
|
|
|
|
logRequest({
|
|
remoteAddress: req.socket.remoteAddress,
|
|
method: req.method,
|
|
url: req.url,
|
|
statusCode: res.statusCode,
|
|
durationMs,
|
|
response: responseBody,
|
|
});
|
|
};
|
|
|
|
const httpsServer = https.createServer(
|
|
{
|
|
key: fs.readFileSync(keyPath),
|
|
cert: fs.readFileSync(certPath),
|
|
},
|
|
loggedJtlHandler
|
|
);
|
|
|
|
const httpServer = http.createServer(loggedJtlHandler);
|
|
|
|
async function start() {
|
|
try {
|
|
const pool = await connectDb();
|
|
logger.success(`MSSQL connected: ${process.env.MSSQL_SERVER}/${process.env.MSSQL_DATABASE}`);
|
|
const activeShop = await fetchActiveShop(pool);
|
|
logger.info(`Active shop ID: ${activeShop}`);
|
|
} catch (err) {
|
|
logger.warn(`MSSQL connection skipped: ${err.message}`);
|
|
logger.warn('POS handshake will still work; sync from database is not available yet.');
|
|
}
|
|
|
|
httpsServer.listen(PORT, '0.0.0.0', () => {
|
|
logger.success(`HTTPS POS server listening on https://0.0.0.0:${PORT}`);
|
|
logger.info(`Certificate: ${certPath}`);
|
|
logger.info(`Pairing code: ${PAIRING_CODE}`);
|
|
logger.info(`Auth token: ${AUTH_TOKEN}`);
|
|
});
|
|
|
|
httpServer.listen(HTTP_PORT, '0.0.0.0', () => {
|
|
logger.success(`HTTP POS server listening on http://0.0.0.0:${HTTP_PORT}`);
|
|
});
|
|
}
|
|
|
|
async function shutdown() {
|
|
logger.info('Shutting down...');
|
|
httpsServer.close();
|
|
httpServer.close();
|
|
await closeDb();
|
|
closeRequestLog();
|
|
closeOrderLog();
|
|
}
|
|
|
|
process.on('SIGINT', () => {
|
|
shutdown().finally(() => process.exit(0));
|
|
});
|
|
process.on('SIGTERM', () => {
|
|
shutdown().finally(() => process.exit(0));
|
|
});
|
|
|
|
start();
|