diff --git a/README.md b/README.md index 479901c..1121509 100644 --- a/README.md +++ b/README.md @@ -5,165 +5,318 @@ Go SDK для работы с API кассы ВЧАСНО - украинской ## Установка ```bash -go get git.jeezft.xyz/rk/go-vchasno-kassa +go get gitea.jeezft.xyz/jeezft/go-vchasno-kassa ``` ## Быстрый старт +### Базовое использование + ```go package main import ( "context" "log" - "time" - "git.jeezft.xyz/rk/go-vchasno-kassa" + "gitea.jeezft.xyz/jeezft/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) + response, err := client.QuickSell(ctx, 100.00) if err != nil { log.Fatal(err) } - log.Printf("Fiscal receipt created: %s", response.FiscalNumber) + log.Printf("Sale created: %s", response.Info.Doccode) } ``` +### С дефолтными параметрами + +```go +client := vchasno.NewClient(vchasno.Config{ + Token: "your-api-token-here", + Cashier: "Иванов", + Source: "parking", + Defaults: &vchasno.DefaultParams{ + ProductName: "Парковка", + Comment: "Оплата парковки", + Taxgrp: "1", + PayType: api.PayTypeCash, + DefaultTimeout: 30 * time.Second, + }, +}) + +response, _ := client.QuickSell(ctx, 50.00) +``` + +### С Builder Pattern + +```go +response, err := client.NewSellParams(). + Name("Парковка VIP"). + Price(150.00). + Cnt(2). + Comment("2 часа"). + PayCash(). + ExecuteDefault() +``` + ## Функциональность -### Работа с фискальными чеками -- Создание фискальных чеков через единый API эндпоинт `/api/v3/fiscal/execute` -- Получение информации о фискализованном чеке -- Отмена фискальных чеков -- Поддержка различных типов оплаты (наличные, карта) -- Автоматический расчет НДС и налогов +### Работа со сменами +- Открытие смены (`OpenShift`) +- Закрытие смены с Z-отчетом (`CloseShift`) -### Отчеты -- X-отчет (промежуточный отчет без обнуления) -- Z-отчет (итоговый отчет с обнулением кассы) +### Продажи +- Создание чеков с оплатой наличными +- Создание чеков с оплатой картой +- Поддержка дополнительных данных клиента +- Автоматический расчет сумм -### Валидация данных -- Проверка корректности сумм и количества -- Валидация обязательных полей -- Проверка соответствия общей суммы и оплат +### Z-отчет содержит +- Количество чеков (продажа/возврат) +- Сводку по налогам +- Информацию о платежах +- Остатки в кассе +- Детальную информацию по налоговым группам + +## Структура проекта + +``` +api/ +├── client.go - Клиент API +├── constants.go - Константы (типы задач, платежей) +├── requests.go - Структуры запросов +├── responses.go - Структуры ответов +├── helpers.go - Вспомогательные функции +└── fiscal.go - Методы API +``` ## Примеры использования -Полные примеры использования находятся в папке `examples/`. - -```bash -cd examples -go run main.go -``` - -## Валидация - -SDK включает встроенную валидацию фискальных чеков: +### 1. Быстрая продажа (QuickSell) ```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) -} +response, err := client.QuickSell(ctx, 100.00) ``` -## Обработка ошибок +Использует все дефолтные параметры из конфигурации. + +### 2. Быстрая продажа с названием ```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) - } -} +response, err := client.QuickSellNamed(ctx, "Парковка VIP", 150.00) ``` -## Работа с деньгами - -SDK использует копейки для точных денежных расчетов: +### 3. Builder Pattern - базовый пример ```go -// Создание суммы в копейках -price := vchasno.NewMoney(10.50) // 1050 копеек - -// Конвертация обратно в гривны -amount := price.ToFloat64() // 10.50 - -// Создание налога -tax := vchasno.NewTax(4.0, vchasno.NewMoney(0.42)) // 4% НДС +response, err := client.NewSellParams(). + Price(100.00). + ExecuteDefault() ``` -## Типы операций - -Через эндпоинт `/api/v3/fiscal/execute` выполняются следующие команды: - -- `create_receipt` - создание фискального чека -- `get_receipt` - получение информации о чеке -- `cancel_receipt` - отмена чека -- `x_report` - получение X-отчета -- `z_report` - получение Z-отчета -- `ping` - проверка соединения - -## Конфигурация +### 4. Builder Pattern - полный пример ```go -client := vchasno.NewClient(vchasno.Config{ - BaseURL: "https://kasa.vchasno.ua", // необязательно, по умолчанию - Token: "your-token", // обязательно - Timeout: 30 * time.Second, // необязательно - HTTPClient: &http.Client{...}, // необязательно +response, err := client.NewSellParams(). + Name("Парковка premium"). + Price(200.00). + Cnt(2). + Disc(20.00). + Comment("Скидка 10%"). + Taxgrp("1"). + PayCash(). + ExecuteDefault() +``` + +### 5. Оплата картой через Builder + +```go +response, err := client.NewSellParams(). + Name("Услуга парковки"). + Price(150.00). + PayCard("411111****1111", "305299", "123456789012", "123456"). + Userinfo("user@example.com", "+380501234567"). + ExecuteDefault() +``` + +### 6. Традиционный способ (без Builder) + +```go +response, err := client.Sell(ctx, vchasno.SellParams{ + Name: "Товар", + Cnt: 2, + Price: 50.00, + Taxgrp: "1", + PayType: api.PayTypeCash, }) ``` +### 7. Полный рабочий цикл + +```go +client := vchasno.NewClient(vchasno.Config{ + Token: "your-token", + Defaults: &vchasno.DefaultParams{ + ProductName: "Парковка", + Taxgrp: "1", + }, +}) + +ctx := context.Background() + +client.OpenShift(ctx) + +client.NewSellParams().Price(50.00).PayCash().ExecuteDefault() +client.NewSellParams().Price(100.00).PayCash().ExecuteDefault() +client.NewSellParams().Price(75.00).PayCard("411111****1111", "305299", "123456789012", "123456").ExecuteDefault() + +zReport, _ := client.CloseShift(ctx) +fmt.Printf("Итого за смену: %.2f\n", zReport.Info.Safe) +``` + +### 8. Изменение дефолтов в процессе работы + +```go +client.SetDefaults(vchasno.DefaultParams{ + ProductName: "VIP Парковка", + Comment: "VIP зона", + Taxgrp: "2", + PayType: api.PayTypeCard, + DefaultTimeout: 60 * time.Second, +}) + +response, _ := client.QuickSell(ctx, 200.00) +``` + +### 9. Множественные продажи + +```go +prices := []float64{50.00, 75.00, 100.00, 125.00} + +for _, price := range prices { + client.NewSellParams(). + Price(price). + PayCash(). + ExecuteDefault() +} +``` + +### 10. Собственный таймаут + +```go +response, err := client.NewSellParams(). + Price(100.00). + ExecuteWithTimeout(45 * time.Second) +``` + +## Хелперы и удобные функции + +### DefaultParams - дефолтные параметры + +При создании клиента можно задать дефолтные значения, которые будут использоваться автоматически: + +```go +client := vchasno.NewClient(vchasno.Config{ + Token: "your-token", + Defaults: &vchasno.DefaultParams{ + ProductName: "Парковка", + Comment: "Оплата услуг", + Taxgrp: "1", + PayType: api.PayTypeCash, + DefaultTimeout: 30 * time.Second, + }, +}) +``` + +### Builder Pattern + +Builder Pattern позволяет строить параметры продажи в цепочке вызовов: + +```go +client.NewSellParams(). + Name("Товар"). + Price(100.00). + Cnt(2). + Disc(10.00). + Comment("Комментарий"). + Taxgrp("1"). + PayCash(). + ExecuteDefault() +``` + +Доступные методы Builder: +- `Name(string)` - название товара +- `Price(float64)` - цена +- `Cnt(int)` - количество +- `Disc(float64)` - скидка +- `Comment(string)` - комментарий +- `Taxgrp(string)` - налоговая группа +- `PayCash()` - оплата наличными +- `PayCard(cardmask, bankID, rrnCode, authCode)` - оплата картой +- `Userinfo(email, phone)` - данные клиента +- `Build()` - получить SellParams +- `Execute(ctx)` - выполнить с контекстом +- `ExecuteWithTimeout(duration)` - выполнить с таймаутом +- `ExecuteDefault()` - выполнить с дефолтным таймаутом + +### QuickSell методы + +Для быстрых продаж: + +```go +client.QuickSell(ctx, 100.00) + +client.QuickSellNamed(ctx, "Парковка", 100.00) +``` + +### Изменение дефолтов + +Дефолтные параметры можно изменить в любой момент: + +```go +client.SetDefaults(vchasno.DefaultParams{ + ProductName: "Новое название", + PayType: api.PayTypeCard, +}) + +defaults := client.GetDefaults() +``` + +## Константы + +### Типы задач +- `api.TaskOpenShift = 0` - Открытие смены +- `api.TaskSell = 1` - Продажа +- `api.TaskZReport = 11` - Z-отчет + +### Типы платежей +- `api.PayTypeCash = 0` - Оплата наличными +- `api.PayTypeCard = 2` - Оплата картой + +## Структуры ответов + +### SellResponse +Используется для продаж и открытия смены. Содержит базовую информацию о документе. + +### ZReportResponse +Используется для закрытия смены. Содержит детальную информацию: +- `Receipt` - статистика по чекам +- `Summary` - итоговые суммы +- `Taxes` - разбивка по налогам +- `Pays` - способы оплаты +- `Money` - движение наличных +- `Cash` - остатки по безналичным + ## API Reference -Полная документация API ВЧАСНО доступна по адресу: https://documenter.getpostman.com/view/26351974/2s93shy9To \ No newline at end of file +Полная документация API ВЧАСНО: https://documenter.getpostman.com/view/26351974/2s93shy9To \ No newline at end of file diff --git a/api/client.go b/api/client.go new file mode 100644 index 0000000..9959b56 --- /dev/null +++ b/api/client.go @@ -0,0 +1,17 @@ +package api + +import "resty.dev/v3" + +type Client struct { + token string + resty *resty.Client + apiBaseURL string +} + +func NewClient(token string) *Client { + return &Client{ + token: token, + resty: resty.New(), + apiBaseURL: "https://kasa.vchasno.ua/api/v3", + } +} diff --git a/api/constants.go b/api/constants.go new file mode 100644 index 0000000..610e30d --- /dev/null +++ b/api/constants.go @@ -0,0 +1,16 @@ +package api + +const ( + TaskOpenShift = 0 + TaskSell = 1 + TaskZReport = 11 +) + +const ( + PayTypeCash = 0 + PayTypeCard = 2 +) + +const ( + PaySystemParkingPos = "parking_pos" +) diff --git a/api/fiscal.go b/api/fiscal.go new file mode 100644 index 0000000..8a4fc5d --- /dev/null +++ b/api/fiscal.go @@ -0,0 +1,103 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" +) + +func (c *Client) executeRequest(ctx context.Context, request FiscalRequest, response interface{}) error { + resp, err := c.resty.R(). + SetContext(ctx). + SetHeader("Authorization", c.token). + SetBody(request). + Post(c.apiBaseURL + "/fiscal/execute") + + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + + if resp.IsError() { + return fmt.Errorf("api error: %v", resp.Error()) + } + + if resp.StatusCode() != 200 { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode()) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response: %w", err) + } + + if err := json.Unmarshal(body, response); err != nil { + return fmt.Errorf("failed to unmarshal response: %w", err) + } + + return nil +} + +func (c *Client) OpenShift(ctx context.Context, cashier string) (*SellResponse, error) { + request := FiscalRequest{ + Fiscal: Fiscal{ + Task: TaskOpenShift, + Cashier: cashier, + }, + } + + var response SellResponse + if err := c.executeRequest(ctx, request, &response); err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) CloseShift(ctx context.Context, cashier string) (*ZReportResponse, error) { + request := FiscalRequest{ + Fiscal: Fiscal{ + Task: TaskZReport, + Cashier: cashier, + }, + } + + var response ZReportResponse + if err := c.executeRequest(ctx, request, &response); err != nil { + return nil, err + } + + return &response, nil +} + +type SellParams struct { + Cashier string + Source string + Rows []ReceiptRow + Pays []ReceiptPay + Userinfo *Userinfo +} + +func (c *Client) Sell(ctx context.Context, params SellParams) (*SellResponse, error) { + receipt := NewReceipt(params.Rows, params.Pays) + + request := FiscalRequest{ + Source: params.Source, + Fiscal: Fiscal{ + Task: TaskSell, + Cashier: params.Cashier, + Receipt: &receipt, + }, + } + + if params.Userinfo != nil { + request.Userinfo = *params.Userinfo + } + + var response SellResponse + if err := c.executeRequest(ctx, request, &response); err != nil { + return nil, err + } + + return &response, nil +} diff --git a/api/helpers.go b/api/helpers.go new file mode 100644 index 0000000..df32317 --- /dev/null +++ b/api/helpers.go @@ -0,0 +1,49 @@ +package api + +func NewReceiptRow(name string, cnt int, price float64, taxgrp string) ReceiptRow { + return ReceiptRow{ + Name: name, + Cnt: cnt, + Price: price, + Taxgrp: taxgrp, + } +} + +func NewReceiptPayCash(sum float64, comment string) ReceiptPay { + return ReceiptPay{ + Type: PayTypeCash, + Sum: sum, + Comment: comment, + } +} + +func NewReceiptPayCard(sum float64, cardmask, bankID, rrnCode, authCode string) ReceiptPay { + return ReceiptPay{ + Type: PayTypeCard, + Sum: sum, + Paysys: PaySystemParkingPos, + Cardmask: cardmask, + BankID: bankID, + Rrn: rrnCode, + AuthCode: authCode, + } +} + +func CalculateReceiptSum(rows []ReceiptRow) float64 { + sum := 0.0 + for _, row := range rows { + sum += (row.Price - row.Disc) * float64(row.Cnt) + } + return sum +} + +func NewReceipt(rows []ReceiptRow, pays []ReceiptPay) Receipt { + return Receipt{ + Sum: CalculateReceiptSum(rows), + Round: 0.00, + Disc: 0, + DiscType: 0, + Rows: rows, + Pays: pays, + } +} diff --git a/api/kasa.go b/api/kasa.go deleted file mode 100644 index e1ded75..0000000 --- a/api/kasa.go +++ /dev/null @@ -1,152 +0,0 @@ -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, rrnCode string, authCode string) ReceiptPay { - return ReceiptPay{ - Type: PayType, - Sum: sum, - Comment: comment, - Paysys: "parking_pos", - Cardmask: cardmask, - BankID: bankID, - Rrn: rrnCode, - AuthCode: authCode, - } -} - -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, rrnCode string, authCode 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, rrnCode, authCode)}, - } - - 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, rrnCode string, authCode string) FiskalCheck { - return FiskalCheck{ - Source: source, - Fiscal: Fiscal{ - Task: 1, - Cashier: cashier, - Receipt: createReceipt(PayType, sum, comment, cardmask, bankID, name, cnt, price, disc, taxgrp, rrnCode, authCode), - }, - } -} - -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, rrnCode string, authCode string) (*KasaResponse, error) { - fiscal := createFiscal("kasa", "test", PayType, sum, comment, cardmask, bankID, name, cnt, price, disc, taxgrp, rrnCode, authCode) - - // 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/requests.go b/api/requests.go new file mode 100644 index 0000000..68c97ad --- /dev/null +++ b/api/requests.go @@ -0,0 +1,61 @@ +package api + +type FiscalRequest struct { + Source string `json:"source"` + Userinfo Userinfo `json:"userinfo,omitempty"` + Fiscal Fiscal `json:"fiscal"` +} + +type Userinfo struct { + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` +} + +type Fiscal struct { + Task int `json:"task"` + Cashier string `json:"cashier"` + Receipt *Receipt `json:"receipt,omitempty"` +} + +type Receipt struct { + Sum float64 `json:"sum"` + Round float64 `json:"round"` + CommentUp string `json:"comment_up,omitempty"` + CommentDown string `json:"comment_down,omitempty"` + Disc float64 `json:"disc"` + DiscType int `json:"disc_type"` + Rows []ReceiptRow `json:"rows"` + Pays []ReceiptPay `json:"pays"` +} + +type ReceiptRow struct { + Code string `json:"code,omitempty"` + Pop string `json:"pop,omitempty"` + Code1 string `json:"code1,omitempty"` + Code2 string `json:"code2,omitempty"` + 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,omitempty"` + 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,omitempty"` + 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"` +} diff --git a/api/responses.go b/api/responses.go new file mode 100644 index 0000000..88d482d --- /dev/null +++ b/api/responses.go @@ -0,0 +1,124 @@ +package api + +type BaseResponse 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"` + ErrorExtra interface{} `json:"error_extra"` +} + +type SellResponse struct { + BaseResponse + Info SellInfo `json:"info"` +} + +type SellInfo 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,omitempty"` +} + +type ZReportResponse struct { + BaseResponse + Info ZReportInfo `json:"info"` +} + +type ZReportInfo 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"` + Receipt ZReportReceipt `json:"receipt"` + Summary ZReportSummary `json:"summary"` + Taxes []ZReportTax `json:"taxes"` + Pays []ZReportPay `json:"pays"` + Money []ZReportMoney `json:"money"` + Cash []ZReportMoney `json:"cash"` + MoneyTransfer []interface{} `json:"money_transfer"` +} + +type ZReportReceipt struct { + CountP int `json:"count_p"` + CountM int `json:"count_m"` + Count14 int `json:"count_14"` + CountTransfer int `json:"count_transfer"` + LastDocnoP int `json:"last_docno_p"` + LastDocnoM int `json:"last_docno_m"` +} + +type ZReportSummary struct { + BaseP float64 `json:"base_p"` + BaseM float64 `json:"base_m"` + TaxexP float64 `json:"taxex_p"` + TaxexM float64 `json:"taxex_m"` + DiscP float64 `json:"disc_p"` + DiscM float64 `json:"disc_m"` +} + +type ZReportTax struct { + GrCode int `json:"gr_code"` + BaseSumP float64 `json:"base_sum_p"` + BaseSumM float64 `json:"base_sum_m"` + BaseTaxSumP float64 `json:"base_tax_sum_p"` + BaseTaxSumM float64 `json:"base_tax_sum_m"` + BaseExSumP float64 `json:"base_ex_sum_p"` + BaseExSumM float64 `json:"base_ex_sum_m"` + TaxName string `json:"tax_name"` + TaxFname string `json:"tax_fname"` + TaxLit string `json:"tax_lit"` + TaxPercent float64 `json:"tax_percent"` + TaxSumP float64 `json:"tax_sum_p"` + TaxSumM float64 `json:"tax_sum_m"` + ExName string `json:"ex_name"` + ExPercent float64 `json:"ex_percent"` + ExSumP float64 `json:"ex_sum_p"` + ExSumM float64 `json:"ex_sum_m"` +} + +type ZReportPay struct { + Type int `json:"type"` + Name string `json:"name"` + SumP float64 `json:"sum_p"` + SumM float64 `json:"sum_m"` + RoundPu float64 `json:"round_pu"` + RoundPd float64 `json:"round_pd"` + RoundMu float64 `json:"round_mu"` + RoundMd float64 `json:"round_md"` +} + +type ZReportMoney struct { + Type int `json:"type"` + Name string `json:"name"` + SumP float64 `json:"sum_p"` + SumM float64 `json:"sum_m"` + RoundPu float64 `json:"round_pu"` + RoundPd float64 `json:"round_pd"` + RoundMu float64 `json:"round_mu"` + RoundMd float64 `json:"round_md"` +} diff --git a/api/structs.go b/api/structs.go deleted file mode 100644 index 60ffdce..0000000 --- a/api/structs.go +++ /dev/null @@ -1,93 +0,0 @@ -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/examples.go b/examples.go new file mode 100644 index 0000000..f1ce4e9 --- /dev/null +++ b/examples.go @@ -0,0 +1,158 @@ +package vchasno + +import ( + "context" + "time" + + "gitea.jeezft.xyz/jeezft/go-vchasno-kassa/api" +) + +func ExampleBasicUsage() { + client := NewClient(Config{ + Token: "your-token", + Cashier: "Иванов", + Source: "parking", + }) + + ctx := context.Background() + + response, _ := client.QuickSell(ctx, 100.00) + _ = response +} + +func ExampleWithDefaults() { + client := NewClient(Config{ + Token: "your-token", + Cashier: "Иванов", + Defaults: &DefaultParams{ + ProductName: "Парковка", + Comment: "Оплата парковки", + Taxgrp: "1", + PayType: api.PayTypeCash, + DefaultTimeout: 30 * time.Second, + }, + }) + + ctx := context.Background() + + response, _ := client.QuickSell(ctx, 50.00) + _ = response +} + +func ExampleBuilderPattern() { + client := NewClient(Config{ + Token: "your-token", + Cashier: "Иванов", + Defaults: &DefaultParams{ + ProductName: "Парковка", + Taxgrp: "1", + PayType: api.PayTypeCash, + }, + }) + + response, _ := client.NewSellParams(). + Price(100.00). + Cnt(2). + Comment("Парковка на 2 часа"). + ExecuteDefault() + + _ = response +} + +func ExampleBuilderWithCard() { + client := NewClient(Config{ + Token: "your-token", + }) + + response, _ := client.NewSellParams(). + Name("Услуга парковки"). + Price(150.00). + PayCard("411111****1111", "305299", "123456789012", "123456"). + Userinfo("user@example.com", "+380501234567"). + ExecuteDefault() + + _ = response +} + +func ExampleChangingDefaults() { + client := NewClient(Config{ + Token: "your-token", + }) + + client.SetDefaults(DefaultParams{ + ProductName: "VIP Парковка", + Comment: "VIP зона", + Taxgrp: "2", + PayType: api.PayTypeCard, + DefaultTimeout: 60 * time.Second, + }) + + ctx := context.Background() + response, _ := client.QuickSell(ctx, 200.00) + _ = response +} + +func ExampleFullWorkflow() { + client := NewClient(Config{ + Token: "your-token", + Cashier: "Петров", + Defaults: &DefaultParams{ + ProductName: "Парковка", + Taxgrp: "1", + }, + }) + + ctx := context.Background() + + openResp, _ := client.OpenShift(ctx) + _ = openResp + + client.NewSellParams(). + Price(50.00). + PayCash(). + ExecuteDefault() + + client.NewSellParams(). + Price(100.00). + Cnt(2). + PayCard("411111****1111", "305299", "123456789012", "123456"). + ExecuteDefault() + + zReport, _ := client.CloseShift(ctx) + _ = zReport +} + +func ExampleMultipleSales() { + client := NewClient(Config{ + Token: "your-token", + Defaults: &DefaultParams{ + ProductName: "Парковка", + Taxgrp: "1", + }, + }) + + prices := []float64{50.00, 75.00, 100.00, 125.00} + + for _, price := range prices { + client.NewSellParams(). + Price(price). + PayCash(). + ExecuteDefault() + } +} + +func ExampleWithDiscount() { + client := NewClient(Config{ + Token: "your-token", + }) + + response, _ := client.NewSellParams(). + Name("Парковка premium"). + Price(200.00). + Disc(20.00). + Comment("Скидка 10%"). + PayCash(). + ExecuteDefault() + + _ = response +} diff --git a/vchasno.go b/vchasno.go index d586f80..a54da7c 100644 --- a/vchasno.go +++ b/vchasno.go @@ -7,51 +7,247 @@ import ( "gitea.jeezft.xyz/jeezft/go-vchasno-kassa/api" ) -type Vchasno struct { - api *api.Kasa +type Client struct { + api *api.Client + cashier string + source string + defaults *DefaultParams } -const ( - PayTypeCash = 0 - PayTypeCard = 2 -) +type Config struct { + Token string + Cashier string + Source string + Defaults *DefaultParams +} + +type DefaultParams struct { + ProductName string + Comment string + Taxgrp string + PayType int + DefaultTimeout time.Duration +} + +func NewClient(config Config) *Client { + if config.Cashier == "" { + config.Cashier = "cashier" + } + if config.Source == "" { + config.Source = "api" + } + + defaults := config.Defaults + if defaults == nil { + defaults = &DefaultParams{ + Taxgrp: "1", + PayType: api.PayTypeCash, + DefaultTimeout: 30 * time.Second, + } + } + + if defaults.Taxgrp == "" { + defaults.Taxgrp = "1" + } + if defaults.DefaultTimeout == 0 { + defaults.DefaultTimeout = 30 * time.Second + } + + return &Client{ + api: api.NewClient(config.Token), + cashier: config.Cashier, + source: config.Source, + defaults: defaults, + } +} + +func (c *Client) SetDefaults(defaults DefaultParams) { + c.defaults = &defaults +} + +func (c *Client) GetDefaults() DefaultParams { + return *c.defaults +} + +func (c *Client) OpenShift(ctx context.Context) (*api.SellResponse, error) { + return c.api.OpenShift(ctx, c.cashier) +} + +func (c *Client) CloseShift(ctx context.Context) (*api.ZReportResponse, error) { + return c.api.CloseShift(ctx, c.cashier) +} type SellParams struct { - PayType int - Sum float64 - Comment string Name string Cnt int Price float64 Disc float64 Taxgrp string - CardParams CardParams + Comment string + PayType int + CardParams *CardParams + Userinfo *api.Userinfo } type CardParams struct { Cardmask string BankID string - RrnCode string AuthCode string } -func NewVchasno(token string) *Vchasno { - return &Vchasno{ - api: api.NewKasaInstance(token), +func (c *Client) NewSellParams() *SellParamsBuilder { + return &SellParamsBuilder{ + client: c, + params: SellParams{ + Name: c.defaults.ProductName, + Cnt: 1, + Taxgrp: c.defaults.Taxgrp, + Comment: c.defaults.Comment, + PayType: c.defaults.PayType, + }, } } -func (v *Vchasno) NewSell(params SellParams) (*api.KasaResponse, error) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +type SellParamsBuilder struct { + client *Client + params SellParams +} + +func (b *SellParamsBuilder) Name(name string) *SellParamsBuilder { + b.params.Name = name + return b +} + +func (b *SellParamsBuilder) Cnt(cnt int) *SellParamsBuilder { + b.params.Cnt = cnt + return b +} + +func (b *SellParamsBuilder) Price(price float64) *SellParamsBuilder { + b.params.Price = price + return b +} + +func (b *SellParamsBuilder) Disc(disc float64) *SellParamsBuilder { + b.params.Disc = disc + return b +} + +func (b *SellParamsBuilder) Taxgrp(taxgrp string) *SellParamsBuilder { + b.params.Taxgrp = taxgrp + return b +} + +func (b *SellParamsBuilder) Comment(comment string) *SellParamsBuilder { + b.params.Comment = comment + return b +} + +func (b *SellParamsBuilder) PayCash() *SellParamsBuilder { + b.params.PayType = api.PayTypeCash + b.params.CardParams = nil + return b +} + +func (b *SellParamsBuilder) PayCard(cardmask, bankID, rrnCode, authCode string) *SellParamsBuilder { + b.params.PayType = api.PayTypeCard + b.params.CardParams = &CardParams{ + Cardmask: cardmask, + BankID: bankID, + RrnCode: rrnCode, + AuthCode: authCode, + } + return b +} + +func (b *SellParamsBuilder) Userinfo(email, phone string) *SellParamsBuilder { + b.params.Userinfo = &api.Userinfo{ + Email: email, + Phone: phone, + } + return b +} + +func (b *SellParamsBuilder) Build() SellParams { + return b.params +} + +func (b *SellParamsBuilder) Execute(ctx context.Context) (*api.SellResponse, error) { + return b.client.Sell(ctx, b.params) +} + +func (b *SellParamsBuilder) ExecuteWithTimeout(timeout time.Duration) (*api.SellResponse, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - return v.api.NewSell(ctx, params.PayType, params.Sum, params.Comment, params.CardParams.Cardmask, params.CardParams.BankID, params.Name, params.Cnt, params.Price, params.Disc, params.Taxgrp, params.CardParams.RrnCode, params.CardParams.AuthCode) + return b.client.Sell(ctx, b.params) } -func SetDefaultParams() SellParams { - return SellParams{ - PayType: PayTypeCard, - Taxgrp: "1", - Cnt: 1, - } +func (b *SellParamsBuilder) ExecuteDefault() (*api.SellResponse, error) { + return b.ExecuteWithTimeout(b.client.defaults.DefaultTimeout) +} + +func (c *Client) Sell(ctx context.Context, params SellParams) (*api.SellResponse, error) { + if params.Taxgrp == "" { + params.Taxgrp = c.defaults.Taxgrp + } + if params.Name == "" { + params.Name = c.defaults.ProductName + } + if params.Comment == "" { + params.Comment = c.defaults.Comment + } + if params.Cnt == 0 { + params.Cnt = 1 + } + + row := api.NewReceiptRow(params.Name, params.Cnt, params.Price, params.Taxgrp) + if params.Disc > 0 { + row.Disc = params.Disc + } + if params.Comment != "" { + row.Comment = params.Comment + } + + var pay api.ReceiptPay + sum := (params.Price - params.Disc) * float64(params.Cnt) + + if params.PayType == api.PayTypeCard && params.CardParams != nil { + pay = api.NewReceiptPayCard( + sum, + params.CardParams.Cardmask, + params.CardParams.BankID, + params.CardParams.RrnCode, + params.CardParams.AuthCode, + ) + } else { + pay = api.NewReceiptPayCash(sum, params.Comment) + } + + return c.api.Sell(ctx, api.SellParams{ + Cashier: c.cashier, + Source: c.source, + Rows: []api.ReceiptRow{row}, + Pays: []api.ReceiptPay{pay}, + Userinfo: params.Userinfo, + }) +} + +func (c *Client) SellWithTimeout(params SellParams, timeout time.Duration) (*api.SellResponse, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return c.Sell(ctx, params) +} + +func (c *Client) QuickSell(ctx context.Context, price float64) (*api.SellResponse, error) { + return c.Sell(ctx, SellParams{ + Price: price, + }) +} + +func (c *Client) QuickSellNamed(ctx context.Context, name string, price float64) (*api.SellResponse, error) { + return c.Sell(ctx, SellParams{ + Name: name, + Price: price, + }) }