fix #3

Merged
romtuck merged 1 commits from dd into main 2026-05-31 19:19:04 +03:00
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 net from "net";
import nodemailer from "nodemailer";
import dotenv from "dotenv";
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) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
@ -208,14 +246,30 @@ app.get("/health/deep", 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 {
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) {
res.status(500).json({
status: "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>
`;
await withTimeout(
transporter.sendMail({
from: process.env.SMTP_MAIL_FROM,
to: email,
subject: `Чек ${process.env.APPNAME}`,
html
}),
SMTP_TIMEOUT_MS,
"Client email sending"
);
try {
await withTimeout(
transporter.sendMail({
from: process.env.SMTP_MAIL_FROM,
to: email,
subject: `Чек ${process.env.APPNAME}`,
html
}),
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({
success: true,
receiptCreated: true,
emailSent: true,
receiptId,
printLink
});
@ -329,6 +411,7 @@ ${process.env.APPNAME}
items: req.body?.items,
amount: calculateTotal(req.body?.items),
error: err.message || "Неизвестная ошибка",
technicalError: formatTechnicalError(err),
api_pass: req.body?.api_pass
};
@ -337,7 +420,8 @@ ${process.env.APPNAME}
res.status(500).json({
error: "Не удалось создать чек. Данные сохранены для повторной попытки.",
saved_to_error_file: true
saved_to_error_file: true,
technicalError: formatTechnicalError(err)
});
}
});