1isp-billing
Собственная биллинговая система с IPAM-модулем для хостинг-провайдера. Замена связки ISPsystem BillManager 6 + IPmanager 5. Go + PostgreSQL 17. Активно разрабатывается с мая 2026 (Phase 2 — workers + бизнес-эндпоинты).
Зачем
Существующий стек ISPsystem стоит дорого, имеет закрытую базу, ограниченный API и примитивный IPAM (плоские пулы, без VRF-aware). Свой биллинг даёт:
- контроль над schema (миграции и аналитика на наших условиях),
- API-first архитектуру (web и admin — клиенты API, без прямого доступа к БД),
- multi-tenant через
organization_idс дня 1, - идемпотентный provisioning через адаптеры (Proxmox / cPanel / bare-metal),
- правильный учёт денег: double-entry ledger (ADR 0008), multi-currency, FX через явный
fx_gain_lossаккаунт, - РФ-совместимый документооборот (договор / акт / счёт-фактура / УПД / credit_note, ADR 0009).
Архитектура
+---------------------+
public users --> | web (Vue/Svelte) | -+
+---------------------+ |
| HTTPS
admin users --> +---------------------+ |
| admin (Python) | -+
+---------------------+ |
|
+-------------------------v---------+
| api (Go, chi+sqlc+pgx) |
| - billing core (ledger) |
| - ipam |
| - provisioning saga executor |
| - outbox publisher |
| - billing cron (renewals) |
| - reconciler (drift detector) |
| - auth |
+------+--------------+-------------+
| |
+------v-----+ +----v------------+
| PG 17 | | Provisioning |
| + ledger | | adapters |
| + outbox | | - Proxmox |
| + saga job | | - cPanel |
+------------+ | - Bare-metal |
| - fake (tests) |
+-----------------+
Один Go binary 1ispd, режим выбирается флагом --mode=api|worker|reconciler|all:
--mode=api— HTTP-сервер (chi).--mode=worker— три параллельные goroutine: saga executor (provisioning), outbox publisher (events), billing cron (renewals + ledger posting).--mode=reconciler— фоновый drift-detector (planned Phase 2.5).--mode=all— api + worker + reconciler в одном процессе (для dev и small deploys).
PG17 — единственный store. Очереди (provisioning saga, event outbox, billing cron) — embedded poller'ы поверх наших же таблиц через SELECT FOR UPDATE SKIP LOCKED. Никакого river / Redis / Kafka на текущем масштабе.
Стек
| Слой | Технология | Зачем |
|---|---|---|
| api/ | Go 1.25, chi v5, pgx v5, sqlc, slog (stdlib) | Single binary, типобезопасные SQL, минимум зависимостей |
| admin/ | Python, FastAPI или Django (TBD) | Богатый UI, async где имеет смысл |
| web/ | TBD (Vue / Svelte / HTMX) | Кабинет клиента и публичные страницы |
| БД | PostgreSQL 17 | Source of truth для всего, embedded queue, advisory_lock |
| Edge | nginx | TLS termination, статика, путь к ISPmanager-совместимым vhosts |
| Прод | VPS + systemd | Без Docker и Kubernetes |
| Migrations | golang-migrate, embed.FS | Forward-only, immutable artifact |
| Tests | testcontainers-go + PG17-alpine | Интеграция против реальной БД, не моки |
| CI | GitHub Actions (lint / unit / integration) | Готово; включается когда репо появится на remote |
Текущий статус
| Phase | Что | Статус |
|---|---|---|
| 0 | Архитектура, скелет схемы, ADR 0001-0007 | done |
| 0.5 | Design review, ADR 0008 ledger / 0009 documents / 0010 saga, 5 P0 schema fixes | done |
| 1 | Go scaffold: chi + pgx + slog, healthz, graceful shutdown, golang-migrate с embed.FS | done |
| 1.5 | docker-compose для local PG, GitHub Actions CI (lint / unit / integration), ADR 0011 worker mode | done |
| 2.1 | Базовые worker primitives (Backoff, WorkerID, config knobs) | done |
| 2.2 | Provisioning saga executor (create kind end-to-end), Adapter interface, fake adapter, 4 integration теста | done |
| 2.3a | Event outbox publisher (log sink); fan-out saga + outbox в --mode=worker; 3 integration теста | done |
| 2.3b | Suspend / resume / terminate saga kinds, adapter.ErrPermanent sentinel для non-retriable классификации; 4 lifecycle теста | done |
| 2.4 | Recurring billing cron + minimal double-entry ledger posting (pg_try_advisory_lock leader election); 3 cron теста. --mode=worker = 3 goroutine: saga + outbox + billing. | done |
| 2.5 | Reconciler drift detector (--mode=reconciler), outbox dead-letter, business CRUD endpoints (organizations / clients / tariffs / services) | next |
| 3+ | Реальные адаптеры (Proxmox, cPanel), admin UI, web cabinet, ОФД 54-ФЗ, ЭДО (Diadoc/SBIS/Kontur) | planned |
Тестовая база: 18 integration-тестов через testcontainers-go + PG17 — все зелёные, ~87 секунд на полный прогон.
Структура кода
api/
├── cmd/1ispd/main.go — entry, --mode switch, signal handling
├── internal/
│ ├── adapter/ — Adapter interface (ADR 0004) + fake impl
│ ├── billing/ — chart-of-accounts + RenewService helper
│ ├── config/ — envconfig + validation (20+ knobs)
│ ├── db/
│ │ ├── pool.go, migrate.go — pgxpool + embedded migrations
│ │ ├── migrations/0001..0004.sql — initial + ledger + documents + saga
│ │ └── queries/*.sql — sqlc input (Phase 2.5+)
│ ├── httpapi/ — chi server, healthz, error helpers
│ ├── log/ — slog JSON setup
│ ├── version/ — build/commit/date через ldflags
│ └── worker/
│ ├── backoff.go, workerid.go — общие primitives (Phase 2.1)
│ ├── saga/ — provisioning saga executor (Phase 2.2-2.3b)
│ ├── outbox/ — event_outbox publisher (Phase 2.3a)
│ └── billing/ — recurring billing cron (Phase 2.4)
└── tests/integration/ — 18 testcontainers тестов
ADR-карта
Все архитектурные решения зафиксированы как Architecture Decision Records в репозитории docs/adr/:
| # | Решение | Status |
|---|---|---|
| 0001 | Документировать решения как ADR (формат) | accepted |
| 0002 | Стек: Go (chi+sqlc+pgx) + Python admin + PG17 | accepted |
| 0003 | Multi-tenancy: row-level через organization_id | accepted |
| 0004 | Provisioning Adapter interface (Create/Suspend/Resume/Terminate/Status + idempotencyKey) | accepted |
| 0005 | Payment Gateway abstraction (MVP: manual + Yookassa + crypto) | accepted |
| 0006 | Money: numeric(20,4) в PG, shopspring/decimal в Go (modified by 0008) | accepted |
| 0007 | API contract: REST/JSON + OpenAPI 3.1, не gRPC для MVP | accepted |
| 0008 | Double-entry ledger + chart of accounts + FX rates | accepted (supersedes 0006 §balance_cents) |
| 0009 | РФ document model: parent document + 5 child tables + next_document_number() с advisory_lock | proposed |
| 0010 | Provisioning saga: provisioning_job state machine + per-step idempotency + reconciler (refined by 0011) | accepted |
| 0011 | Worker mode: embedded poller через SELECT FOR UPDATE SKIP LOCKED (rejected river/pgmq); 4 goroutine groups | proposed |
Будущие ADR (queued): 0012 OFD 54-ФЗ, 0013 ЭДО, 0014 УКЭП, 0015 tickets/support, 0016 dunning, 0017 VLAN/switch port, 0018 security hardening Phase 1.
Ключевые design-решения
Деньги — double-entry ledger
Каждая финансовая операция (issue invoice, receive payment, refund, FX) пишет минимум 2 строки в append-only ledger_entry с балансирующими debit/credit. Баланс клиента — производное значение (materialized view), а не отдельное поле. Multi-currency: один client → N балансов, по одному на валюту. Phase 2.4 шипит первый production use-case: billing cron на каждый renewal создаёт invoice + invoice_line + 2 ledger entries (debit customer_receivable_RUB, credit revenue_RUB) одной транзакцией. Аккаунты создаются lazy при первом обращении. См. ADR 0008.
Документы — РФ-совместимая иерархия
Родительская таблица document + child-таблицы (document_invoice, document_act, document_vat_invoice, document_contract, document_credit_note). Нумерация через document_series(organization_id, kind, year) + PL/pgSQL функция с pg_advisory_xact_lock — race-free, без gap'ов в пределах транзакции. tax_regime у client и organization определяет комплект документов (ОСНО / УСН / АУСН / ПСН). Phase 2 пока использует legacy invoice таблицу из initial-schema; миграция на document-модель — Phase 2.x.4. См. ADR 0009.
Provisioning — saga с явными шагами
Каждый provision (create / suspend / resume / terminate) разбит на именованные шаги, каждый идемпотентен по ключу <job.idempotency_key>:<step>. State хранится в provisioning_job; worker pull'ает row через FOR UPDATE SKIP LOCKED, выполняет step, advance state. Crash recovery через TTL-based lease (5 минут). Permanent errors через errors.Is(err, adapter.ErrPermanent) — сразу state='failed' без max_attempts retries. См. ADR 0010 и Phase 2.2/2.3b commits.
Очередь — embedded, не river
Наши доменные таблицы (provisioning_job, event_outbox) уже несут state, locked_by, locked_until, attempts. Добавление river дало бы второй источник истины для «что запускать дальше» — две миграции, два индекса, два failure mode. На нашем масштабе (десятки jobs/час) SKIP LOCKED справляется. Migration path к river зарезервирован: интерфейс Dispatcher допускает swap transport'а. См. ADR 0011.
Recurring billing — leader-locked cron
Многоинстансная безопасность через pg_try_advisory_lock(0x42494C4C00000001) session-scoped на каждый tick. Только один из N запущенных --mode=worker процессов делает renewal'ы за минуту; остальные silent skip. Per-service TX'ы внутри tick'а — failure одного service не откатывает остальные. Идемпотентность guaranteed внешним lock'ом; Phase 2.5 добавит UNIQUE (service_id, period_start) на invoice_line как defence-in-depth. См. ADR 0011 §"Cron idempotency".
Принципы
- Single source of truth = PG. Никакого in-memory state, который не восстанавливается из БД.
- Идемпотентность provisioning'а. Повторный вызов с тем же ключом не создаёт дубликатов.
- Audit log на каждое финансовое действие — append-only
audit_event. - Money =
numeric(20,4)в PG иdecimal.Decimalв коде. Никогда float. - Multi-tenancy через
organization_idв каждой бизнес-таблице с дня 1. - Migrations only forward. Без
DROP TABLEв проде, откаты — через compensating migrations. - Tests против реального PG, не моков. testcontainers-go + PG17-alpine.
- Single source for time/space. Все timestamps —
timestamptzUTC, все ID — uuid (gen_random_uuid()), все money —numeric(20,4)+ явная валюта.
Документация в репозитории
docs/architecture/overview.md— high-level архитектура и принципыdocs/architecture/services-boundaries.md— слои и граф зависимостейdocs/adr/0001-0011.md— 11 принятых архитектурных решенийdocs/db/initial-schema.sql+02-ledger+03-document+04-provisioning-saga— incremental migrationsdocs/plans/phase-1-scaffold.md,phase-2.md— детальные планы по фазамdocs/SCOPE.md,PARITY-billmanager.md,PARITY-ipmanager.md— scope и сравнение с ISPsystemCLAUDE.md— мандаты проекта (стиль, anti-patterns, deploy rules)web/billing/index.html— этот документ; гайд live наbilling.1isp.ru
Live deploy
Эта страница хостится на ru1.irbr.ru (92.63.192.50):
https://billing.1isp.ru/billing/— этот гайд (200 OK)https://billing.1isp.ru/— 302 →/billing/- HTTP → HTTPS 301 redirect (certbot --redirect)
- SSL: Let's Encrypt, валиден до 2026-08-18, auto-renew через
certbot renew - Бэкенд: nginx + ISPmanager vhost pattern (
/etc/nginx/vhosts/1isp/billing.1isp.ru.conf) - DNS: self-hosted BIND на ru1 (zone
1isp.ru)
Update flow: sudo rsync -a --delete --exclude='irbr/' --exclude='README.md' web/ /var/www/1isp/data/www/billing.1isp.ru/