first commit
This commit is contained in:
commit
7b143e6372
17
.env.example
Normal file
17
.env.example
Normal 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
44
.gitignore
vendored
Normal 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
296
README.MD
Normal 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
19
package.json
Normal 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
176
server.js
Normal 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}`);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user