From 830b63e70185b1a673b76213ecdfeeff552445b4 Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 30 Sep 2025 14:39:08 +0300 Subject: [PATCH] initial commit --- .gitignore | 1 + README.md | 167 +++++++++++++++++++++++++++++++++++++++++++++++++ api/kasa.go | 150 ++++++++++++++++++++++++++++++++++++++++++++ api/structs.go | 93 +++++++++++++++++++++++++++ go.mod | 7 +++ go.sum | 4 ++ vchasno.go | 50 +++++++++++++++ 7 files changed, 472 insertions(+) create mode 100644 .gitignore create mode 100644 api/kasa.go create mode 100644 api/structs.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 vchasno.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b726568 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +examples \ No newline at end of file diff --git a/README.md b/README.md index 3ede1c4..479901c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,169 @@ # go-vchasno-kassa +Go SDK для работы с API кассы ВЧАСНО - украинской системы фискализации. + +## Установка + +```bash +go get git.jeezft.xyz/rk/go-vchasno-kassa +``` + +## Быстрый старт + +```go +package main + +import ( + "context" + "log" + "time" + + "git.jeezft.xyz/rk/go-vchasno-kassa" +) + +func main() { + client := vchasno.NewClient(vchasno.Config{ + Token: "your-api-token-here", + Timeout: 30 * time.Second, + }) + + ctx := context.Background() + + // Проверка подключения + if err := client.Ping(ctx); err != nil { + log.Fatal(err) + } + + // Создание фискального чека + receipt := vchasno.FiscalReceipt{ + Cashier: "Иванов И.И.", + CashierTaxID: "1234567890", + Items: []vchasno.ReceiptItem{ + { + Name: "Parking on obj1", + Code: "PARK001", + Price: vchasno.NewMoney(10.00), + Quantity: 2.0, + Amount: vchasno.NewMoney(20.00), + Tax: vchasno.NewTax(4.0, vchasno.NewMoney(0.80)), + }, + }, + Payments: []vchasno.Payment{ + { + Type: "cash", + Amount: vchasno.NewMoney(20.00), + }, + }, + Total: vchasno.NewMoney(20.00), + TaxTotal: vchasno.NewMoney(0.80), + } + + response, err := client.CreateFiscalReceipt(ctx, receipt) + if err != nil { + log.Fatal(err) + } + + log.Printf("Fiscal receipt created: %s", response.FiscalNumber) +} +``` + +## Функциональность + +### Работа с фискальными чеками +- Создание фискальных чеков через единый API эндпоинт `/api/v3/fiscal/execute` +- Получение информации о фискализованном чеке +- Отмена фискальных чеков +- Поддержка различных типов оплаты (наличные, карта) +- Автоматический расчет НДС и налогов + +### Отчеты +- X-отчет (промежуточный отчет без обнуления) +- Z-отчет (итоговый отчет с обнулением кассы) + +### Валидация данных +- Проверка корректности сумм и количества +- Валидация обязательных полей +- Проверка соответствия общей суммы и оплат + +## Примеры использования + +Полные примеры использования находятся в папке `examples/`. + +```bash +cd examples +go run main.go +``` + +## Валидация + +SDK включает встроенную валидацию фискальных чеков: + +```go +receipt := vchasno.FiscalReceipt{ + Cashier: "Иванов И.И.", + CashierTaxID: "1234567890", + Items: []vchasno.ReceiptItem{...}, + Payments: []vchasno.Payment{...}, + Total: vchasno.NewMoney(100.00), + TaxTotal: vchasno.NewMoney(4.00), +} + +if err := receipt.Validate(); err != nil { + log.Fatal("Validation error:", err) +} +``` + +## Обработка ошибок + +```go +response, err := client.CreateFiscalReceipt(ctx, receipt) +if err != nil { + switch { + case errors.Is(err, vchasno.ErrMissingToken): + log.Println("Отсутствует токен авторизации") + default: + log.Printf("API error: %v", err) + } +} +``` + +## Работа с деньгами + +SDK использует копейки для точных денежных расчетов: + +```go +// Создание суммы в копейках +price := vchasno.NewMoney(10.50) // 1050 копеек + +// Конвертация обратно в гривны +amount := price.ToFloat64() // 10.50 + +// Создание налога +tax := vchasno.NewTax(4.0, vchasno.NewMoney(0.42)) // 4% НДС +``` + +## Типы операций + +Через эндпоинт `/api/v3/fiscal/execute` выполняются следующие команды: + +- `create_receipt` - создание фискального чека +- `get_receipt` - получение информации о чеке +- `cancel_receipt` - отмена чека +- `x_report` - получение X-отчета +- `z_report` - получение Z-отчета +- `ping` - проверка соединения + +## Конфигурация + +```go +client := vchasno.NewClient(vchasno.Config{ + BaseURL: "https://kasa.vchasno.ua", // необязательно, по умолчанию + Token: "your-token", // обязательно + Timeout: 30 * time.Second, // необязательно + HTTPClient: &http.Client{...}, // необязательно +}) +``` + +## API Reference + +Полная документация API ВЧАСНО доступна по адресу: https://documenter.getpostman.com/view/26351974/2s93shy9To \ No newline at end of file diff --git a/api/kasa.go b/api/kasa.go new file mode 100644 index 0000000..c535f74 --- /dev/null +++ b/api/kasa.go @@ -0,0 +1,150 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "resty.dev/v3" +) + +func NewKasaInstance(token string) *Kasa { + return &Kasa{ + token: token, + resty: resty.New(), + } +} + +type Kasa struct { + token string + resty *resty.Client +} + +func createReceiptRow(name string, cnt int, price float64, comment string, disc float64, taxgrp string) ReceiptRow { + return ReceiptRow{ + Name: name, + Cnt: cnt, + Price: price, + Disc: disc, + Taxgrp: taxgrp, + Comment: comment, + } +} + +func createReceiptPayCash(PayType int, sum float64, comment string) ReceiptPay { + return ReceiptPay{ + Type: PayType, + Sum: sum, + Comment: comment, + } +} + +func createReceiptPayCard(PayType int, sum float64, comment string, cardmask string, bankID string) ReceiptPay { + return ReceiptPay{ + Type: PayType, + Sum: sum, + Comment: comment, + Paysys: "parking_pos", + Cardmask: cardmask, + BankID: bankID, + } +} + +func getReceiptSum(rows []ReceiptRow, taxgrp string) float64 { + sum := 0.0 + for _, row := range rows { + sum += (row.Price - row.Disc) * float64(row.Cnt) + } + return sum +} + +func createReceipt(PayType int, sum float64, comment string, cardmask string, bankID string, name string, cnt int, price float64, disc float64, taxgrp string) Receipt { + rows := []ReceiptRow{createReceiptRow(name, cnt, price, comment, disc, taxgrp)} + sum = getReceiptSum(rows, taxgrp) + + r := Receipt{ + Sum: sum, + Round: 0.00, + Disc: 0, + DiscType: 0, + Rows: rows, + Pays: []ReceiptPay{createReceiptPayCard(PayType, sum, comment, cardmask, bankID)}, + } + + fmt.Println("Receipt sum:", r.Sum) + return r +} + +// "fiscal": { +// "task": 1, +// "cashier": "Парковка", +// "receipt": { +// "sum": 40.00, +// "round": 0.00, +// "comment_up": "Квитанція за паркування", +// "comment_down": "Дякуємо за користування паркінгом!", +// "disc": 0.00, +// "disc_type": 0, +// "rows": [ +// { +// "code": "PARK-3H", +// "pop": "Оплата за послуги паркування", +// "name": "Парковка, 3 години", +// "cnt": 1, +// "price": 40.00, +// "disc": 0.00, +// "taxgrp": "4", +// "comment": "Тариф: 3 години" +// } +// ], +// "pays": [ +// { +// "type": 0, +// "sum": 40.00, +// "change": 0.00, +// "comment": "Оплата готівкою" +// } +// ] +// } +// } + +func createFiscal(source string, cashier string, PayType int, sum float64, comment string, cardmask string, bankID string, name string, cnt int, price float64, disc float64, taxgrp string) FiskalCheck { + return FiskalCheck{ + Source: source, + Fiscal: Fiscal{ + Task: 1, + Cashier: cashier, + Receipt: createReceipt(PayType, sum, comment, cardmask, bankID, name, cnt, price, disc, taxgrp), + }, + } +} + +func (k *Kasa) NewSell(ctx context.Context, PayType int, sum float64, comment string, cardmask string, bankID string, name string, cnt int, price float64, disc float64, taxgrp string) (*KasaResponse, error) { + fiscal := createFiscal("kasa", "test", PayType, sum, comment, cardmask, bankID, name, cnt, price, disc, taxgrp) + + // create a POST request to the kasa api https://kasa.vchasno.ua/api/v3/fiscal/execute with resty + request, err := k.resty.R().SetBody(fiscal).SetHeader("Authorization", k.token).Post("https://kasa.vchasno.ua/api/v3/fiscal/execute") + if err != nil { + return nil, fmt.Errorf("failed to create a POST request to the kasa api: %w", err) + } + if request.IsError() { + return nil, fmt.Errorf("failed to create a POST request to the kasa api: %v", request.Error()) + } + if request.StatusCode() != 200 { + return nil, fmt.Errorf("failed to create a POST request to the kasa api: %v", request.Error()) + } + + body, err := io.ReadAll(request.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + fmt.Println(string(body)) + var response KasaResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return &response, nil +} diff --git a/api/structs.go b/api/structs.go new file mode 100644 index 0000000..60ffdce --- /dev/null +++ b/api/structs.go @@ -0,0 +1,93 @@ +package api + +type FiskalCheck struct { + Source string `json:"source"` + Userinfo Userinfo `json:"userinfo"` + Fiscal Fiscal `json:"fiscal"` +} + +type Userinfo struct { + Email string `json:"email"` + Phone string `json:"phone"` +} + +type Fiscal struct { + Task int `json:"task"` + Cashier string `json:"cashier"` + Receipt Receipt `json:"receipt"` +} + +type Receipt struct { + Sum float64 `json:"sum"` + Round float64 `json:"round"` + CommentUp string `json:"comment_up"` + CommentDown string `json:"comment_down"` + Disc float64 `json:"disc"` + DiscType int `json:"disc_type"` + Rows []ReceiptRow `json:"rows"` + Pays []ReceiptPay `json:"pays"` +} + +type ReceiptRow struct { + Code string `json:"code"` + Pop string `json:"pop,omitempty"` + Code1 string `json:"code1"` + Code2 string `json:"code2"` + CodeAa []string `json:"code_aa,omitempty"` + Name string `json:"name"` + Cnt int `json:"cnt"` + Price float64 `json:"price"` + Disc float64 `json:"disc"` + Taxgrp string `json:"taxgrp"` + Comment string `json:"comment"` + CodeA string `json:"code_a,omitempty"` +} + +type ReceiptPay struct { + Type int `json:"type"` + Sum float64 `json:"sum"` + Change float64 `json:"change,omitempty"` + Comment string `json:"comment"` + Commission float64 `json:"commission,omitempty"` + Paysys string `json:"paysys,omitempty"` + Rrn string `json:"rrn,omitempty"` + OperType string `json:"oper_type,omitempty"` + Cardmask string `json:"cardmask,omitempty"` + TermID string `json:"term_id,omitempty"` + BankName string `json:"bank_name,omitempty"` + BankID string `json:"bank_id,omitempty"` + AuthCode string `json:"auth_code,omitempty"` + ShowAdditionalInfo bool `json:"show_additional_info,omitempty"` +} + +type KasaResponse struct { + Task int `json:"task"` + Type int `json:"type"` + Ver int `json:"ver"` + Source string `json:"source"` + Device string `json:"device"` + Tag string `json:"tag"` + Dt string `json:"dt"` + Res int `json:"res"` + ResAction int `json:"res_action"` + Errortxt string `json:"errortxt"` + Warnings []string `json:"warnings"` + Info Info `json:"info"` + ErrorExtra interface{} `json:"error_extra"` +} + +type Info struct { + Task int `json:"task"` + Fisid string `json:"fisid"` + Dataid int `json:"dataid"` + Doccode string `json:"doccode"` + Dt string `json:"dt"` + Cashier string `json:"cashier"` + Dtype int `json:"dtype"` + Isprint int `json:"isprint"` + Isoffline bool `json:"isoffline"` + Safe float64 `json:"safe"` + ShiftLink int `json:"shift_link"` + Docno int `json:"docno"` + Cancelid string `json:"cancelid"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..94f65f7 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module git.jeezft.xyz/rk/go-vchasno-kassa + +go 1.23.5 + +require resty.dev/v3 v3.0.0-beta.3 + +require golang.org/x/net v0.33.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6ab8da4 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E= +resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4= diff --git a/vchasno.go b/vchasno.go new file mode 100644 index 0000000..6181ebd --- /dev/null +++ b/vchasno.go @@ -0,0 +1,50 @@ +package vchasno + +import ( + "context" + "time" + + "git.jeezft.xyz/rk/go-vchasno-kassa/api" +) + +type Vchasno struct { + api *api.Kasa +} + +const ( + PayTypeCash = 0 + PayTypeCard = 2 +) + +type SellParams struct { + PayType int + Sum float64 + Comment string + Cardmask string + BankID string + Name string + Cnt int + Price float64 + Disc float64 + Taxgrp string +} + +func NewVchasno(token string) *Vchasno { + return &Vchasno{ + api: api.NewKasaInstance(token), + } +} + +func (v *Vchasno) NewSell(params SellParams) (*api.KasaResponse, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + return v.api.NewSell(ctx, params.PayType, params.Sum, params.Comment, params.Cardmask, params.BankID, params.Name, params.Cnt, params.Price, params.Disc, params.Taxgrp) +} + +func SetDefaultParams() SellParams { + return SellParams{ + PayType: PayTypeCard, + Taxgrp: "1", + Cnt: 1, + } +}