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 ERROR_FILE = path.join(__dirname, "error.json"); const ADMIN_EMAIL = process.env.ADMIN_EMAIL; const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: Number(process.env.SMTP_PORT), secure: false, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } }); 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); } async function createReceiptWithRetry(income, retries = MAX_RETRIES) { for (let attempt = 1; attempt <= retries; attempt++) { try { return await getNalogApi().addIncome(income); } 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 = ` Ошибка создания чека

⚠️ Ошибка при создании чека

Время: ${new Date().toLocaleString()}

Email клиента: ${errorData.email}

Сумма: ${errorData.amount} ₽

Ошибка: ${errorData.error}

Данные заказа:

${JSON.stringify(errorData.items, null, 2)}

Ошибка сохранена в error.json для последующей обработки.

Пробейте чек вручную через приложение Мой налог и вручную отправте клиенту чек по email.

`; await transporter.sendMail({ from: process.env.SMTP_MAIL_FROM, to: ADMIN_EMAIL, subject: `Ошибка создания чека ${process.env.APPNAME}`, html }); 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.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 ` ${i.id} ${i.name} ${price.toFixed(2)} ${qty} ${(price * qty).toFixed(2)} `; }).join(""); const html = ` Чек

Ваш чек

Чек сформирован в ФНС (Мой налог)

${rows}
IDНазваниеЦенаКол-воСумма

Итого: ${total.toFixed(2)} ₽

Посмотреть чек

${process.env.APPNAME}

`; await transporter.sendMail({ from: process.env.SMTP_MAIL_FROM, to: email, subject: `Чек ${process.env.APPNAME}`, html }); 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}`); });