This commit is contained in:
romantarkin 2026-05-31 21:18:41 +05:00
parent f078fa0b3b
commit 2b3ba383fb
2 changed files with 101 additions and 13 deletions

View File

@ -90,3 +90,7 @@ Content-Type: application/json
] ]
} }
``` ```
Поле `email` обязательно: на этот адрес сервис отправит письмо со ссылкой на чек после успешного создания чека в ФНС.
Если чек создан в ФНС, но письмо клиенту не отправилось, API вернет `success: true`, `receiptCreated: true`, `emailSent: false`, `receiptId`, `printLink` и `technicalError`. Ошибка отправки сохранится в `error.json`.

110
server.js
View File

@ -1,4 +1,5 @@
import express from "express"; import express from "express";
import net from "net";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import dotenv from "dotenv"; import dotenv from "dotenv";
import pkg from "lknpd-nalog-api"; import pkg from "lknpd-nalog-api";
@ -80,6 +81,43 @@ function withTimeout(promise, timeoutMs, label) {
}); });
} }
function formatTechnicalError(err) {
return {
message: err.message || "Unknown error",
code: err.code,
command: err.command,
responseCode: err.responseCode,
response: err.response
};
}
function smtpConfigForResponse() {
return {
host: process.env.SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_SECURE,
user: process.env.SMTP_USER,
from: process.env.SMTP_MAIL_FROM
};
}
function checkTcpConnection(host, port, timeoutMs) {
return new Promise((resolve, reject) => {
const socket = net.createConnection({ host, port });
socket.setTimeout(timeoutMs);
socket.once("connect", () => {
socket.destroy();
resolve();
});
socket.once("timeout", () => {
socket.destroy();
reject(new Error(`TCP connection timeout after ${timeoutMs}ms`));
});
socket.once("error", reject);
});
}
async function createReceiptWithRetry(income, retries = MAX_RETRIES) { async function createReceiptWithRetry(income, retries = MAX_RETRIES) {
for (let attempt = 1; attempt <= retries; attempt++) { for (let attempt = 1; attempt <= retries; attempt++) {
try { try {
@ -208,14 +246,30 @@ app.get("/health/deep", async (req, res) => {
}); });
app.get("/health/smtp", async (req, res) => { app.get("/health/smtp", async (req, res) => {
const smtp = smtpConfigForResponse();
try {
await checkTcpConnection(process.env.SMTP_HOST, SMTP_PORT, SMTP_TIMEOUT_MS);
} catch (err) {
return res.status(500).json({
status: "error",
smtp: "error",
step: "tcp_connect",
config: smtp,
technicalError: formatTechnicalError(err)
});
}
try { try {
await withTimeout(transporter.verify(), SMTP_TIMEOUT_MS, "SMTP health check"); await withTimeout(transporter.verify(), SMTP_TIMEOUT_MS, "SMTP health check");
res.json({ status: "ok", smtp: "ok" }); res.json({ status: "ok", smtp: "ok", config: smtp });
} catch (err) { } catch (err) {
res.status(500).json({ res.status(500).json({
status: "error", status: "error",
smtp: "error", smtp: "error",
message: err.message || "SMTP check failed" step: "smtp_verify",
config: smtp,
technicalError: formatTechnicalError(err)
}); });
} }
}); });
@ -304,19 +358,47 @@ ${process.env.APPNAME}
</html> </html>
`; `;
await withTimeout( try {
transporter.sendMail({ await withTimeout(
from: process.env.SMTP_MAIL_FROM, transporter.sendMail({
to: email, from: process.env.SMTP_MAIL_FROM,
subject: `Чек ${process.env.APPNAME}`, to: email,
html subject: `Чек ${process.env.APPNAME}`,
}), html
SMTP_TIMEOUT_MS, }),
"Client email sending" SMTP_TIMEOUT_MS,
); "Client email sending"
);
} catch (emailErr) {
console.error("Чек создан, но email клиенту не отправлен:", emailErr);
const technicalError = formatTechnicalError(emailErr);
await saveToErrorFile({
type: "email_send_failed",
email,
items,
amount: total,
receiptId,
printLink,
error: emailErr.message || "Не удалось отправить email клиенту",
technicalError
});
return res.json({
success: true,
receiptCreated: true,
emailSent: false,
receiptId,
printLink,
warning: "Чек создан в ФНС, но письмо клиенту не отправлено. Данные сохранены в error.json.",
technicalError
});
}
res.json({ res.json({
success: true, success: true,
receiptCreated: true,
emailSent: true,
receiptId, receiptId,
printLink printLink
}); });
@ -329,6 +411,7 @@ ${process.env.APPNAME}
items: req.body?.items, items: req.body?.items,
amount: calculateTotal(req.body?.items), amount: calculateTotal(req.body?.items),
error: err.message || "Неизвестная ошибка", error: err.message || "Неизвестная ошибка",
technicalError: formatTechnicalError(err),
api_pass: req.body?.api_pass api_pass: req.body?.api_pass
}; };
@ -337,7 +420,8 @@ ${process.env.APPNAME}
res.status(500).json({ res.status(500).json({
error: "Не удалось создать чек. Данные сохранены для повторной попытки.", error: "Не удалось создать чек. Данные сохранены для повторной попытки.",
saved_to_error_file: true saved_to_error_file: true,
technicalError: formatTechnicalError(err)
}); });
} }
}); });