fns-receipt-service/server.js
romantarkin f078fa0b3b fix
2026-05-31 21:12:01 +05:00

349 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from "express";
import nodemailer from "nodemailer";
import dotenv from "dotenv";
import pkg from "lknpd-nalog-api";
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
dotenv.config();
const { NalogApi } = pkg;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.use(express.json({ limit: "1mb" }));
const PORT = process.env.PORT || 4000;
const HOST = process.env.HOST || "0.0.0.0";
const MAX_RETRIES = 3;
const FNS_TIMEOUT_MS = Number(process.env.FNS_TIMEOUT_MS || 30000);
const SMTP_TIMEOUT_MS = Number(process.env.SMTP_TIMEOUT_MS || 15000);
const ERROR_FILE = path.join(__dirname, "error.json");
const ADMIN_EMAIL = process.env.ADMIN_EMAIL;
const SMTP_PORT = Number(process.env.SMTP_PORT || 587);
const SMTP_SECURE = process.env.SMTP_SECURE
? process.env.SMTP_SECURE === "true"
: SMTP_PORT === 465;
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_SECURE,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
},
connectionTimeout: SMTP_TIMEOUT_MS,
greetingTimeout: SMTP_TIMEOUT_MS,
socketTimeout: SMTP_TIMEOUT_MS
});
let nalogApi;
function getNalogApi() {
if (!process.env.INN || !process.env.PASSWORD) {
throw new Error("INN and PASSWORD environment variables are required");
}
if (!nalogApi) {
nalogApi = new NalogApi({
inn: process.env.INN,
password: process.env.PASSWORD
});
}
return nalogApi;
}
function calculateTotal(items = []) {
return items.reduce((sum, item) => {
const price = Number(item.price) || 0;
const quantity = Number(item.quantity) || 1;
return sum + price * quantity;
}, 0);
}
function withTimeout(promise, timeoutMs, label) {
let timeoutId;
const timeout = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`${label} timeout after ${timeoutMs}ms`));
}, timeoutMs);
});
return Promise.race([promise, timeout]).finally(() => {
clearTimeout(timeoutId);
});
}
async function createReceiptWithRetry(income, retries = MAX_RETRIES) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await withTimeout(
getNalogApi().addIncome(income),
FNS_TIMEOUT_MS,
"FNS receipt creation"
);
} catch (err) {
console.error(`Попытка ${attempt} не удалась`, err.message || err);
if (attempt === retries) throw err;
await new Promise(r => setTimeout(r, 2000));
}
}
}
async function saveToErrorFile(errorData) {
try {
let errors = [];
try {
const data = await fs.readFile(ERROR_FILE, "utf8");
const parsedData = JSON.parse(data);
if (Array.isArray(parsedData)) {
errors = parsedData;
}
} catch (err) {
}
errors.push({
...errorData,
timestamp: new Date().toISOString(),
retryAttempt: 0
});
await fs.writeFile(ERROR_FILE, JSON.stringify(errors, null, 2));
console.log(`Ошибка сохранена в ${ERROR_FILE}`);
} catch (err) {
console.error("Не удалось сохранить ошибку в файл:", err);
}
}
async function notifyAdmin(errorData) {
try {
const html = `
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Ошибка создания чека</title>
</head>
<body>
<h2>⚠️ Ошибка при создании чека</h2>
<p><b>Время:</b> ${new Date().toLocaleString()}</p>
<p><b>Email клиента:</b> ${errorData.email}</p>
<p><b>Сумма:</b> ${errorData.amount} ₽</p>
<p><b>Ошибка:</b> ${errorData.error}</p>
<p><b>Данные заказа:</b></p>
<pre>${JSON.stringify(errorData.items, null, 2)}</pre>
<p>Ошибка сохранена в error.json для последующей обработки.</p>
<p>Пробейте чек вручную через приложение Мой налог и вручную отправте клиенту чек по email.</p>
</body>
</html>
`;
await withTimeout(
transporter.sendMail({
from: process.env.SMTP_MAIL_FROM,
to: ADMIN_EMAIL,
subject: `Ошибка создания чека ${process.env.APPNAME}`,
html
}),
SMTP_TIMEOUT_MS,
"Admin email sending"
);
console.log(`Администратор ${ADMIN_EMAIL} уведомлен об ошибке`);
} catch (err) {
console.error("Не удалось отправить уведомление администратору:", err);
}
}
app.get("/", (req, res) => {
res.json({
service: "fns-receipt-service",
status: "ok"
});
});
app.get(["/health", "/fns-receipt-service/health"], (req, res) => {
res.json({
status: "ok"
});
});
app.get("/fns-receipt-service", (req, res) => {
res.json({
service: "fns-receipt-service",
status: "ok"
});
});
app.get("/health/deep", async (req, res) => {
const result = {
status: "ok",
connect_to_fns: "ok",
smtp: "ok",
};
try {
await transporter.verify();
} catch (err) {
result.smtp = "error";
result.status = "degraded";
}
try {
await getNalogApi().getUserInfo();
} catch (err) {
console.error("FNS health error:", err.message || err);
result.connect_to_fns = "error";
result.status = "degraded";
}
res.json(result);
});
app.get("/health/smtp", async (req, res) => {
try {
await withTimeout(transporter.verify(), SMTP_TIMEOUT_MS, "SMTP health check");
res.json({ status: "ok", smtp: "ok" });
} catch (err) {
res.status(500).json({
status: "error",
smtp: "error",
message: err.message || "SMTP check failed"
});
}
});
app.post("/api/v1/create-receipt", async (req, res) => {
try {
const { api_pass, email, items } = req.body;
if (api_pass !== process.env.API_PASS) {
return res.status(401).json({ error: "Unauthorized" });
}
if (!email || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ error: "Неверные данные" });
}
const total = calculateTotal(items);
const income = {
name: `${process.env.APPNAME}`,
amount: Number(total.toFixed(2)),
quantity: 1
};
const receiptId = await createReceiptWithRetry(income);
const printLink = `https://lknpd.nalog.ru/api/v1/receipt/${process.env.INN}/${receiptId}/print`;
const rows = items.map(i => {
const price = Number(i.price) || 0;
const qty = Number(i.quantity) || 1;
return `
<tr>
<td>${i.id}</td>
<td>${i.name}</td>
<td>${price.toFixed(2)}</td>
<td>${qty}</td>
<td>${(price * qty).toFixed(2)}</td>
</tr>
`;
}).join("");
const html = `
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Чек</title>
<link rel="stylesheet" href="https://cdn.email.ga1maz.ru/emails/styles.css">
</head>
<body style="margin:0;padding:0;font-family:Arial,sans-serif;color:#333;background:#f5f6f7;">
<table width="100%" bgcolor="#f5f6f7">
<tr>
<td align="center">
<table width="600" bgcolor="#ffffff" style="margin:40px auto;">
<tr>
<td style="padding:24px;text-align:center;color:#333;">
<img src="https://cdn.email.ga1maz.ru/emails/main.png" width="536" />
<h2 style="color:#333;">Ваш чек</h2>
<p style="color:#333;">Чек сформирован в ФНС (Мой налог)</p>
<table width="100%" border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;color:#333;">
<tr style="background:#eee;">
<th>ID</th><th>Название</th><th>Цена</th><th>Кол-во</th><th>Сумма</th>
</tr>
${rows}
</table>
<p><b>Итого:</b> ${total.toFixed(2)} ₽</p>
<a href="${printLink}" target="_blank"
style="display:inline-block;background:#ffdd2d;padding:16px 36px;border-radius:4px;color:#333;text-decoration:none;">
Посмотреть чек
</a>
<p style="font-size:12px;color:#999;margin-top:24px;">
${process.env.APPNAME}
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`;
await withTimeout(
transporter.sendMail({
from: process.env.SMTP_MAIL_FROM,
to: email,
subject: `Чек ${process.env.APPNAME}`,
html
}),
SMTP_TIMEOUT_MS,
"Client email sending"
);
res.json({
success: true,
receiptId,
printLink
});
} catch (err) {
console.error("Ошибка создания чека:", err);
const errorData = {
email: req.body?.email,
items: req.body?.items,
amount: calculateTotal(req.body?.items),
error: err.message || "Неизвестная ошибка",
api_pass: req.body?.api_pass
};
await saveToErrorFile(errorData);
await notifyAdmin(errorData);
res.status(500).json({
error: "Не удалось создать чек. Данные сохранены для повторной попытки.",
saved_to_error_file: true
});
}
});
app.listen(PORT, HOST, () => {
console.log(`✅ Сервер запущен: http://${HOST}:${PORT}`);
console.log(`📁 Файл ошибок: ${ERROR_FILE}`);
});