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