nalogo

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 8, 2026 License: MIT Imports: 16 Imported by: 0

README

nalogo-go

Tests Go Version Go Report Card Coverage

Go-клиент для API ФНС «Мой налог» (lknpd.nalog.ru) — сервис для самозанятых.

Порт Python-библиотеки rusik636/nalogo с полным покрытием функциональности.

Возможности

  • Авторизация по ИНН + пароль
  • Авторизация по SMS (challenge → verify)
  • Автоматическое обновление access-токена по refresh-токену (single-flight, thread-safe)
  • Регистрация дохода: одна позиция и несколько позиций
  • Аннулирование чека
  • Получение чека: JSON и URL печати
  • Профиль пользователя
  • Способы оплаты
  • История начислений налогов

Установка

go get github.com/AlexZzz/nalogo-go

Быстрый старт

import "github.com/AlexZzz/nalogo-go"

ctx := context.Background()

client := nalogo.New(
    nalogo.WithTokenStore(nalogo.NewFileStore("token.json")),
)

// Авторизация
_, err := client.CreateAccessToken(ctx, "123456789012", "password")

// Зарегистрировать доход
resp, err := client.Income().Create(ctx, "Консультация", nalogo.MustMoneyAmount("5000"), nalogo.MustQuantity("1"))
fmt.Println(resp.ApprovedReceiptUUID)

// Аннулировать чек
_, err = client.Income().Cancel(ctx, resp.ApprovedReceiptUUID, nalogo.CancelCommentRefund)

// URL для печати чека
url, err := client.Receipt().PrintURL(resp.ApprovedReceiptUUID)

Авторизация

ИНН + пароль
tokenJSON, err := client.CreateAccessToken(ctx, inn, password)
SMS
challenge, err := client.CreatePhoneChallenge(ctx, "+79991234567")
// пользователь вводит код из SMS
resp, err := client.CreateAccessTokenByPhone(ctx, "+79991234567", challenge.ChallengeToken, "123456")
Восстановление сессии из сохранённого токена
err := client.Authenticate(ctx, savedTokenJSON)

Несколько позиций в чеке

resp, err := client.Income().CreateMultipleItems(ctx,
    []nalogo.IncomeServiceItem{
        {Name: "Разработка", Amount: nalogo.MustMoneyAmount("10000"), Quantity: nalogo.MustQuantity("1")},
        {Name: "Консультация", Amount: nalogo.MustMoneyAmount("2000"), Quantity: nalogo.MustQuantity("2")},
    },
    nalogo.AtomTimeNow(),
    nil, // nil = физическое лицо
)
Юридическое лицо или ИП
inn := "7707083893"
name := "ООО Ромашка"
resp, err := client.Income().CreateMultipleItems(ctx, items, nalogo.AtomTimeNow(),
    &nalogo.IncomeClientInfo{
        IncomeType:  nalogo.IncomeTypeFromLegalEntity,
        INN:         &inn,
        DisplayName: &name,
    },
)

Хранение токена

По умолчанию токен хранится в памяти (MemoryStore). Для сохранения между запусками:

// Файл (права 0600, создаётся автоматически)
store := nalogo.NewFileStore("/var/lib/myapp/nalog-token.json")
client := nalogo.New(nalogo.WithTokenStore(store))

Можно реализовать собственное хранилище, удовлетворив интерфейс TokenStore:

type TokenStore interface {
    Save(ctx context.Context, td *TokenData) error
    Load(ctx context.Context) (*TokenData, error)
    Clear(ctx context.Context) error
}

Опции клиента

Опция По умолчанию Описание
WithBaseURL(url) https://lknpd.nalog.ru/api Базовый URL API
WithTimeout(d) 10s Таймаут HTTP-запросов
WithDeviceID(id) случайный UUID-21 Идентификатор устройства
WithTokenStore(s) MemoryStore Хранилище токена
WithHTTPClient(c) nil Использовать c.Transport как базовый RoundTripper (для тестов/прокси)
WithLogger(l) slog.Default() Логгер

Примечание: WithHTTPClient использует только Transport переданного клиента; таймауты и auth-refresh управляются опциями WithTimeout и встроенным authTransport.

Обработка ошибок

Все ошибки API оборачивают sentinel-ошибки, совместимые с errors.Is:

var apiErr *nalogo.APIError
if errors.As(err, &apiErr) {
    fmt.Println(apiErr.StatusCode, apiErr.Body)
}

if errors.Is(err, nalogo.ErrUnauthorized) { /* 401 */ }
if errors.Is(err, nalogo.ErrNotAuthenticated) { /* токен не установлен */ }
if errors.Is(err, nalogo.ErrValidation) { /* неверные аргументы */ }
if errors.Is(err, nalogo.ErrDomain) { /* любая ошибка nalogo */ }

Запуск тестов

make test          # все тесты
make coverage      # покрытие (≥85%)
make lint          # go vet

Замечания

  • API «Мой налог» неофициальное (получено реверсом), может меняться без предупреждения.
  • Чувствительные поля (токены, пароли) автоматически маскируются в логах (***).

License

MIT.

Documentation

Overview

Package nalogo is a Go client for the Russian FNS "Moy Nalog" API (lknpd.nalog.ru) used by self-employed taxpayers.

Index

Constants

View Source
const (
	CancelCommentCancel = CancelComment("Чек сформирован ошибочно")
	CancelCommentRefund = CancelComment("Возврат средств")
)
View Source
const (
	IncomeTypeFromIndividual    = IncomeType("FROM_INDIVIDUAL")
	IncomeTypeFromLegalEntity   = IncomeType("FROM_LEGAL_ENTITY")
	IncomeTypeFromForeignAgency = IncomeType("FROM_FOREIGN_AGENCY")
)

Variables

View Source
var (
	ErrValidation       = fmt.Errorf("%w: validation (400)", ErrDomain)
	ErrUnauthorized     = fmt.Errorf("%w: unauthorized (401)", ErrDomain)
	ErrForbidden        = fmt.Errorf("%w: forbidden (403)", ErrDomain)
	ErrNotFound         = fmt.Errorf("%w: not found (404)", ErrDomain)
	ErrClient           = fmt.Errorf("%w: client error (406)", ErrDomain)
	ErrPhone            = fmt.Errorf("%w: phone error (422)", ErrDomain)
	ErrServer           = fmt.Errorf("%w: server error (500)", ErrDomain)
	ErrUnknown          = fmt.Errorf("%w: unknown error", ErrDomain)
	ErrNotAuthenticated = fmt.Errorf("%w: not authenticated", ErrDomain)
)

HTTP-status sentinels — mirror upstream Python exception hierarchy 1:1.

View Source
var ErrDomain = errors.New("nalogo")

ErrDomain is the root sentinel; all library errors wrap it.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	Sentinel   error
	StatusCode int
	Body       string
}

APIError carries the HTTP status code and (masked) response body alongside the appropriate sentinel. It satisfies both errors.Is (via Is) and errors.As (via type assertion) for callers that need status details.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

func (e *APIError) Is(target error) bool

Is reports true if target is e.Sentinel or ErrDomain.

func (*APIError) Unwrap

func (e *APIError) Unwrap() error

Unwrap returns e.Sentinel so errors.As can walk the chain.

type AtomTime

type AtomTime struct {
	time.Time
}

AtomTime wraps time.Time and serializes to/from the FNS ATOM datetime format.

func AtomTimeNow

func AtomTimeNow() AtomTime

AtomTimeNow returns the current UTC time wrapped in AtomTime.

func (AtomTime) MarshalJSON

func (a AtomTime) MarshalJSON() ([]byte, error)

func (*AtomTime) UnmarshalJSON

func (a *AtomTime) UnmarshalJSON(data []byte) error

type CancelComment

type CancelComment = string

CancelComment is the cancellation reason wire value (Russian string required by FNS API).

type CancelResponse

type CancelResponse struct {
	IncomeInfo map[string]any `json:"incomeInfo"`
}

CancelResponse is returned by Cancel.

type ChallengeResponse

type ChallengeResponse struct {
	ChallengeToken string `json:"challengeToken"`
	ExpireDate     string `json:"expireDate"`
	ExpireIn       int    `json:"expireIn"`
}

ChallengeResponse is returned by CreatePhoneChallenge.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the main facade for the FNS "Мой налог" API. Construct with New; all I/O methods require a context.Context first arg.

func New

func New(opts ...Option) *Client

New constructs a Client with the provided options.

func (*Client) Authenticate

func (c *Client) Authenticate(ctx context.Context, tokenJSON string) error

Authenticate loads a previously obtained token JSON into the client. After this call, all API requests will use the token.

func (*Client) CreateAccessToken

func (c *Client) CreateAccessToken(ctx context.Context, inn, password string) (string, error)

CreateAccessToken authenticates via INN + password. Returns the raw token JSON string (mirrors upstream). Persists the token to the configured TokenStore.

func (*Client) CreateAccessTokenByPhone

func (c *Client) CreateAccessTokenByPhone(ctx context.Context, phone, challengeToken, code string) (string, error)

CreateAccessTokenByPhone completes SMS authentication. Returns the raw token JSON string. Persists the token to the configured TokenStore.

func (*Client) CreatePhoneChallenge

func (c *Client) CreatePhoneChallenge(ctx context.Context, phone string) (*ChallengeResponse, error)

CreatePhoneChallenge starts the two-step SMS authentication (v2 endpoint).

func (*Client) INN

func (c *Client) INN() string

INN returns the INN of the authenticated user (empty before authentication).

func (*Client) Income

func (c *Client) Income() *Income

Income returns an Income API accessor.

func (*Client) PaymentType

func (c *Client) PaymentType() *PaymentType

PaymentType returns a PaymentType API accessor.

func (*Client) Receipt

func (c *Client) Receipt() *Receipt

Receipt returns a Receipt API accessor.

func (*Client) Tax

func (c *Client) Tax() *Tax

Tax returns a Tax API accessor.

func (*Client) User

func (c *Client) User() *User

User returns a User API accessor.

type FileStore

type FileStore struct {
	// contains filtered or unexported fields
}

FileStore is a file-based TokenStore that persists token JSON to disk (mode 0600).

func NewFileStore

func NewFileStore(path string) *FileStore

NewFileStore creates a FileStore that reads/writes to path.

func (*FileStore) Clear

func (f *FileStore) Clear(_ context.Context) error

func (*FileStore) Load

func (f *FileStore) Load(_ context.Context) (*TokenData, error)

func (*FileStore) Save

func (f *FileStore) Save(_ context.Context, t *TokenData) error

type Income

type Income struct {
	// contains filtered or unexported fields
}

Income is the income-receipt API accessor.

func (*Income) Cancel

func (a *Income) Cancel(ctx context.Context, receiptUUID string, comment CancelComment) (*CancelResponse, error)

Cancel annuls an income receipt. comment must be one of CancelCommentCancel or CancelCommentRefund.

func (*Income) Create

func (a *Income) Create(ctx context.Context, name string, amount MoneyAmount, quantity Quantity) (*IncomeResponse, error)

Create issues a single-item income receipt.

func (*Income) CreateMultipleItems

func (a *Income) CreateMultipleItems(ctx context.Context, services []IncomeServiceItem, operationTime AtomTime, client *IncomeClientInfo) (*IncomeResponse, error)

CreateMultipleItems issues an income receipt with one or more line items. operationTime is the time the service was rendered; pass AtomTimeNow() for "now". client is optional; pass nil for an individual payer (default).

type IncomeClientInfo

type IncomeClientInfo struct {
	ContactPhone *string    `json:"contactPhone,omitempty"`
	DisplayName  *string    `json:"displayName,omitempty"`
	IncomeType   IncomeType `json:"incomeType"`
	INN          *string    `json:"inn,omitempty"`
}

IncomeClientInfo carries payer information for an income receipt. For individual clients (default), all fields are optional. For legal entities (IncomeTypeFromLegalEntity), INN and DisplayName are required.

type IncomeResponse

type IncomeResponse struct {
	ApprovedReceiptUUID string `json:"approvedReceiptUuid"`
}

IncomeResponse is returned by Create and CreateMultipleItems.

type IncomeServiceItem

type IncomeServiceItem struct {
	Name     string      `json:"name"`
	Amount   MoneyAmount `json:"amount"`
	Quantity Quantity    `json:"quantity"`
}

IncomeServiceItem represents one line item in an income receipt.

type IncomeType

type IncomeType = string

IncomeType mirrors upstream IncomeType enum.

type MaskedString

type MaskedString string

MaskedString is a string whose slog representation is always "***". Use it for INN, phone numbers, tokens, and passwords in log records.

func (MaskedString) LogValue

func (m MaskedString) LogValue() slog.Value

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore is a thread-safe in-memory TokenStore (default).

func (*MemoryStore) Clear

func (m *MemoryStore) Clear(_ context.Context) error

func (*MemoryStore) Load

func (m *MemoryStore) Load(_ context.Context) (*TokenData, error)

func (*MemoryStore) Save

func (m *MemoryStore) Save(_ context.Context, t *TokenData) error

type MoneyAmount

type MoneyAmount struct {
	decimal.Decimal
}

MoneyAmount wraps decimal.Decimal and serializes to/from a JSON quoted string (e.g. "100.50") as required by the FNS API.

func MustMoneyAmount

func MustMoneyAmount(s string) MoneyAmount

MustMoneyAmount constructs a MoneyAmount from a decimal string and panics on error. For use in tests and compile-time constants only.

func NewMoneyAmount

func NewMoneyAmount(s string) (MoneyAmount, error)

NewMoneyAmount constructs a MoneyAmount from a decimal string (e.g. "100.50").

func (MoneyAmount) MarshalJSON

func (m MoneyAmount) MarshalJSON() ([]byte, error)

func (*MoneyAmount) UnmarshalJSON

func (m *MoneyAmount) UnmarshalJSON(data []byte) error

type Option

type Option func(*config)

Option mutates the client configuration.

func WithBaseURL

func WithBaseURL(u string) Option

WithBaseURL overrides the FNS API base URL (default: https://lknpd.nalog.ru/api).

func WithDeviceID

func WithDeviceID(id string) Option

WithDeviceID sets the device ID sent in every auth request.

func WithHTTPClient

func WithHTTPClient(cl *http.Client) Option

WithHTTPClient provides a custom base Transport for internal clients. If cl and cl.Transport are non-nil, the transport is used as the base for authTransport; auth refresh behavior remains enabled.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the structured logger (default: slog.Default()).

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the HTTP client timeout (default: 10s).

func WithTokenStore

func WithTokenStore(s TokenStore) Option

WithTokenStore plugs in a custom TokenStore (default: MemoryStore).

type PaymentType

type PaymentType struct {
	// contains filtered or unexported fields
}

PaymentType is the payment-type API accessor.

func (*PaymentType) Favorite

func (p *PaymentType) Favorite(ctx context.Context) (*PaymentTypeEntry, error)

Favorite returns the first payment type marked as favorite, or nil if none.

func (*PaymentType) Table

func (p *PaymentType) Table(ctx context.Context) ([]PaymentTypeEntry, error)

Table returns all available payment types.

type PaymentTypeEntry

type PaymentTypeEntry struct {
	ID       string         `json:"id"`
	Name     string         `json:"name"`
	Favorite bool           `json:"favorite"`
	Extra    map[string]any `json:"-"`
}

PaymentTypeEntry is a single entry from GET /v1/payment-type/table.

type Quantity added in v0.1.1

type Quantity struct {
	decimal.Decimal
}

Quantity wraps decimal.Decimal and represents a service unit count in an income receipt. It serializes identically to MoneyAmount (JSON quoted string, 2 decimal places) but is a distinct type to prevent accidental interchange with monetary amounts.

func MustQuantity added in v0.1.1

func MustQuantity(s string) Quantity

MustQuantity constructs a Quantity from a decimal string and panics on error. For use in tests and compile-time constants only.

func NewQuantity added in v0.1.1

func NewQuantity(s string) (Quantity, error)

NewQuantity constructs a Quantity from a decimal string (e.g. "1", "2.5").

func (Quantity) MarshalJSON added in v0.1.1

func (q Quantity) MarshalJSON() ([]byte, error)

func (*Quantity) UnmarshalJSON added in v0.1.1

func (q *Quantity) UnmarshalJSON(data []byte) error

type Receipt

type Receipt struct {
	// contains filtered or unexported fields
}

Receipt is the receipt API accessor.

func (*Receipt) JSON

func (r *Receipt) JSON(ctx context.Context, receiptUUID string) (map[string]any, error)

JSON retrieves the full JSON data for a receipt.

func (*Receipt) PrintURL

func (r *Receipt) PrintURL(receiptUUID string) (string, error)

PrintURL returns the print URL for a receipt without making an HTTP request. Requires the client to be authenticated (INN must be set via CreateAccessToken or Authenticate).

type Tax

type Tax struct {
	// contains filtered or unexported fields
}

Tax is the tax API accessor.

func (*Tax) Get

func (t *Tax) Get(ctx context.Context) (map[string]any, error)

Get returns current tax information (GET /v1/taxes).

func (*Tax) History

func (t *Tax) History(ctx context.Context, oktmo string) (map[string]any, error)

History returns tax history, optionally filtered by OKTMO code.

func (*Tax) Payments

func (t *Tax) Payments(ctx context.Context, oktmo string, onlyPaid bool) (map[string]any, error)

Payments returns tax payment records, optionally filtered by OKTMO.

type TokenData

type TokenData struct {
	Token                 string          `json:"token"`
	RefreshToken          string          `json:"refreshToken"`
	TokenExpireIn         json.RawMessage `json:"tokenExpireIn,omitempty"`
	RefreshTokenExpiresIn json.RawMessage `json:"refreshTokenExpiresIn,omitempty"`
	Profile               UserProfile     `json:"profile"`
}

TokenData is the token payload persisted by TokenStore implementations. TokenExpireIn and RefreshTokenExpiresIn are strings in the FNS API response (ISO datetime or null); leave as json.RawMessage to tolerate both.

type TokenStore

type TokenStore interface {
	Save(ctx context.Context, t *TokenData) error
	Load(ctx context.Context) (*TokenData, error)
	Clear(ctx context.Context) error
}

TokenStore is the persistence port for token data. Implement to swap between storage backends.

type User

type User struct {
	// contains filtered or unexported fields
}

User is the user API accessor.

func (*User) Get

func (u *User) Get(ctx context.Context) (*UserResponse, error)

Get returns the current user's profile.

type UserProfile

type UserProfile struct {
	ID  string `json:"id"`
	INN string `json:"inn"`
}

UserProfile holds the minimal user data returned alongside an access token.

type UserResponse

type UserResponse struct {
	ID          string `json:"id"`
	INN         string `json:"inn"`
	DisplayName string `json:"displayName"`
	Email       string `json:"email"`
	Phone       string `json:"phone"`
	Status      string `json:"status"`
}

UserResponse is the user profile response from GET /v1/user.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL