first commit

This commit is contained in:
Ga1maz 2026-02-01 02:34:25 +05:00
commit 7b143e6372
5 changed files with 552 additions and 0 deletions

17
.env.example Normal file
View File

@ -0,0 +1,17 @@
# === Данные ФНС ===
INN=123456789012 # ИНН самозанятого (12 цифр)
PASSWORD=your_fns_password # Пароль от приложения «Мой налог»
# === Настройки приложения ===
APPNAME=Моя компания # Название, отображаемое в чеке ФНС
# === SMTP настройки ===
SMTP_HOST=smtp.gmail.com # SMTP сервер
SMTP_PORT=587 # SMTP порт (обычно 587 или 465)
SMTP_USER=noreply@example.com # Email для отправки
SMTP_PASS=app_password # Пароль приложения для email
SMTP_MAIL_FROM=noreply@example.com # Email отправителя
# === Безопасность ===
API_PASS=your_secure_password_here # Пароль для доступа к API (используйте сложный!)
PORT=4000 # Порт, на котором будет работать сервер

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
# Node modules
node_modules/
# Environment variables (ignore real .env, keep example)
.env
!.env.example
# Logs
logs/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS files
.DS_Store
Thumbs.db
# Optional npm cache directory
.npm
# Temporary files
*.tmp
*.temp
# Build outputs
dist/
build/
# Ignore everything by default except these files
*
!.gitignore
!.env.example
!package.json
!README.md
!server.js

296
README.MD Normal file
View File

@ -0,0 +1,296 @@
# 🧾 FNS Receipt Service
> Автоматизированный сервис для создания чеков самозанятого через ФНС (Мой налог) с отправкой детализированных чеков клиентам по email
---
## 📋 Содержание
* [Описание](#-описание)
* [Возможности](#-возможности)
* [Установка](#-установка)
* [Настройка](#-настройка)
* [Использование](#-использование)
* [API Reference](#-api-reference)
* [Переменные окружения](#-переменные-окружения-env)
* [Ограничения и особенности](#-ограничения-и-особенности)
* [FAQ](#-faq)
* [Лицензия](#-лицензия)
---
## 🎯 Описание
**FNS Receipt Service** — Node.js сервис для автоматического создания чеков самозанятого через API ФНС («Мой налог») и отправки клиентам детализированных чеков по email.
Сервис учитывает ограничения официального API ФНС и использует практику, применяемую платёжными системами.
### Ключевая особенность
⚠️ **Ограничение API ФНС**: официальный API самозанятых **не поддерживает несколько позиций в одном чеке**.
Используется гибридный подход:
* **В ФНС** → отправляется **одна агрегированная позиция** с общей суммой дохода
* **Клиенту по email** → отправляется **детализированный HTMLчек** со всеми товарами/услугами
Это полностью соответствует требованиям ФНС: налоговый учёт ведётся по сумме дохода, а не по товарным позициям.
Если вы обнаружили баг или есть идея для улучшения сервиса, пожалуйста, создайте [Issue](https://github.com/ga1maz/fns-receipt-service/issues) в репозитории.
---
## ✨ Возможности
* ✅ Создание чеков самозанятого через официальный API ФНС
* ✅ Retryмеханизм при временных ошибках ФНС (до 3 попыток)
* ✅ Детализированные HTMLчеки для клиентов
* ✅ Отправка официальной ссылки на чек ФНС
* ✅ Защита API паролем
* ✅ Простая HTTPинтеграция с любыми сервисами
---
## 🚀 Установка
### Требования
* Node.js **16.x+**
* npm или yarn
* Активный статус самозанятого в «Мой налог»
### Установка
```bash
git clone https://github.com/ga1maz/fns-receipt-service.git
cd fns-receipt-service
npm install
```
### Основные зависимости
```json
{
"express": "^4.18.0",
"lknpd-nalog-api": "^1.0.0",
"nodemailer": "^6.9.0",
"dotenv": "^16.0.0"
}
```
---
## ⚙️ Настройка
### 1. Создание `.env`
```bash
cp .env.example .env
```
### 2. Переменные окружения
```env
# === ФНС (Мой налог) ===
INN=123456789012
PASSWORD=your_fns_password
# === Приложение ===
APPNAME=Моя компания
# === SMTP ===
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASS=app_password
SMTP_MAIL_FROM=noreply@example.com
# === Безопасность ===
API_PASS=your_secure_password_here
PORT=4000
```
> Поддерживаются любые SMTPпровайдеры (Gmail, Яндекс, Mail.ru и др.). Для Gmail рекомендуется использовать **пароль приложения**.
---
## 🎮 Использование
### Запуск сервера
```bash
node index.js
```
Сервер запускается на:
```
http://localhost:4000
```
### Проверка состояния сервиса
```bash
GET /health
```
Сервис возвращает состояние подключения и возможные ошибки:
```json
{
"status": "ok", // общий статус сервера (ok, degraded, error)
"connect_to_fns": "ok", // соединение с API ФНС
"smtp": "ok" // статус SMTP
}
```
Примеры возможных ошибок `health`:
* `connect_to_fns: error` — проблемы с авторизацией или доступностью API ФНС
* `smtp: error` — проблемы с подключением к SMTP серверу (неверный логин/пароль, блокировка сервера)
* `status: degraded` — сервис работает, но есть неполадки с одним из сервисов
Если вы что-то не заметили или есть идеи для доработки, создайте [Issue](https://github.com/ga1maz/fns-receipt-service/issues) в репозитории.
---
## 📡 API Reference
### POST `/api/v1/create-receipt`
Создаёт **один чек дохода в ФНС** на общую сумму всех позиций и отправляет клиенту **детализированный HTMLчек по email**.
### 🔐 Авторизация
Пароль передаётся **в теле запроса**:
```json
{
"api_pass": "..."
}
```
Значение сравнивается с `API_PASS` из `.env`.
### 📥 Request Body
```json
{
"api_pass": "your_secure_password_here",
"email": "client@example.com",
"items": [
{
"id": "sku-001",
"name": "Консультация по налогам",
"price": 5000,
"quantity": 1
}
]
}
```
### ⚙️ Логика обработки
* Общая сумма:
```
Σ (price × quantity)
```
* В ФНС отправляется:
* `name` → `APPNAME`
* `amount` → общая сумма
* `quantity` → `1`
* При ошибке ФНС:
* до **3 попыток**
* задержка **2 секунды** между попытками
### 📤 Response (Success)
**HTTP 200 OK**
```json
{
"success": true,
"receiptId": "200uhagtun",
"printLink": "https://lknpd.nalog.ru/api/v1/receipt/INN/receiptId/print"
}
```
> Поля `totalAmount` и `email` **не возвращаются** в ответе.
### ❌ Ошибки
#### 400 — Неверные данные
```json
{ "error": "Неверные данные" }
```
#### 401 — Unauthorized
```json
{ "error": "Unauthorized" }
```
#### 500 — Internal Server Error
```json
{ "error": "Не удалось создать чек" }
```
---
## 🧩 Переменные окружения (.env)
| Переменная | Назначение |
| ---------------- | -------------------- |
| `API_PASS` | Пароль доступа к API |
| `INN` | ИНН самозанятого |
| `PASSWORD` | Пароль «Мой налог» |
| `APPNAME` | Название в чеке ФНС |
| `SMTP_HOST` | SMTP сервер |
| `SMTP_PORT` | SMTP порт |
| `SMTP_USER` | SMTP логин |
| `SMTP_PASS` | SMTP пароль |
| `SMTP_MAIL_FROM` | Email отправителя |
| `PORT` | Порт сервера |
---
## 📝 Ограничения и особенности
* ❗ Один запрос = **один чек дохода**
* ❗ Детализация **не передаётся в ФНС**, только в email
* ✔️ Поддержка дробных количеств
* ✔️ HTMLчек с таблицей позиций
* ✔️ Официальная ссылка ФНС
---
## ❓ FAQ
**Это законно?**
Да. ФНС учитывает доход суммарно, детализация — дополнительный сервис.
**Можно ли создать несколько чеков на один платёж?**
Нет. Один платёж = один чек.
**Нужно ли хранить `receiptId`?**
Рекомендуется для поддержки клиентов и учёта.
**Нужен ли IP РФ?**
Да, API ФНС работает только с IP РФ.
---
## 📄 Лицензия
MIT License © 2026
---
**Сделано с ❤️ для самозанятых**

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "checks",
"version": "1.0.0",
"description": "Checks for the self-employed",
"license": "ISC",
"author": "Ga1maz",
"type": "module",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"body-parser": "^2.2.2",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"lknpd-nalog-api": "^1.0.1",
"nodemailer": "^7.0.13"
}
}

176
server.js Normal file
View File

@ -0,0 +1,176 @@
import express from "express";
import bodyParser from "body-parser";
import nodemailer from "nodemailer";
import dotenv from "dotenv";
import pkg from "lknpd-nalog-api";
dotenv.config();
const { NalogApi } = pkg;
const app = express();
app.use(bodyParser.json());
const PORT = process.env.PORT || 4000;
const MAX_RETRIES = 3;
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
}
});
const nalogApi = new NalogApi({
inn: process.env.INN,
password: process.env.PASSWORD
});
async function createReceiptWithRetry(income, retries = MAX_RETRIES) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await nalogApi.addIncome(income);
} catch (err) {
console.error(`Попытка ${attempt} не удалась`, err.message || err);
if (attempt === retries) throw err;
await new Promise(r => setTimeout(r, 2000));
}
}
}
app.get("/health", 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 nalogApi.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 = items.reduce(
(sum, i) => sum + i.price * (i.quantity || 1),
0
);
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 qty = i.quantity || 1;
return `
<tr>
<td>${i.id}</td>
<td>${i.name}</td>
<td>${i.price.toFixed(2)}</td>
<td>${qty}</td>
<td>${(i.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 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);
res.status(500).json({ error: "Не удалось создать чек" });
}
});
app.listen(PORT, () => {
console.log(`✅ Сервер запущен: http://localhost:${PORT}`);
});