Documentation
¶
Overview ¶
Package webhook — приймач вебхуків PRRO.cloud: перевірка підпису, розбір конверта доставки і типізовані дані подій.
Сервіс надсилає кожну подію POST-запитом із тілом {delivery_id, event, attempt, sequence, occurred_at, data} і заголовком X-Signature: "sha256=<hex>" — HMAC-SHA256 тіла на секреті вебхука.
Секрет ¶
Секрет — рядок, яким сервіс підписує доставки, а приймач перевіряє підпис. Його видають рівно один раз, коли вебхук створюють: у кабінеті (розділ «Webhooks» → «Додати webhook») або через prro.WebhooksService.Create — поле prro.CreatedWebhook.Secret. Повторно отримати секрет неможливо: збережіть його одразу, наприклад у змінну оточення.
Приймач ¶
Найпростіший приймач — Handler:
secret := os.Getenv("PRRO_WEBHOOK_SECRET")
http.Handle("/prro-hook", webhook.Handler(secret, func(ctx context.Context, e *webhook.Event) error {
switch e.Type {
case prro.EventTaskCompleted, prro.EventTaskFailed:
d, err := e.Task()
if err != nil {
return err
}
return markOrder(ctx, d.TaskID, d.Status)
}
return nil
}))
Доставка гарантована «щонайменше один раз»: та сама подія може прийти повторно з тим самим Event.DeliveryID — відкидайте повтори за ним.
Події однієї каси надсилаються по одній і по порядку, але подія, яку приймач не прийняв, не затримує наступні: після повтору чи replay вона може прийти пізніше за них. Упорядковуйте події каси за Event.Sequence (зростає, пропуски можливі); актуальний стан каси — prro.CashRegistersService.Get.
Index ¶
- Constants
- Variables
- func Handler(secret string, fn HandlerFunc) http.Handler
- func Sign(secret string, body []byte) string
- func Verify(secret string, body []byte, signature string) error
- type AttentionData
- type BalanceData
- type DPSData
- type Event
- func (e *Event) Attention() (*AttentionData, error)
- func (e *Event) Balance() (*BalanceData, error)
- func (e *Event) DPS() (*DPSData, error)
- func (e *Event) Key() (*KeyData, error)
- func (e *Event) Offline() (*OfflineData, error)
- func (e *Event) OfflineAbandoned() (*OfflineAbandonedData, error)
- func (e *Event) Ping() (*PingData, error)
- func (e *Event) Receipt() (*ReceiptData, error)
- func (e *Event) Remediation() (*RemediationData, error)
- func (e *Event) Shift() (*ShiftData, error)
- func (e *Event) Task() (*TaskData, error)
- type HandlerFunc
- type KeyData
- type OfflineAbandonedData
- type OfflineData
- type PingData
- type ReceiptData
- type ReceiptLine
- type ReceiptPayment
- type ReceiptSource
- type ReceiptTax
- type RemediationData
- type ShiftData
- type TaskData
Examples ¶
Constants ¶
const MaxBodyBytes = 10 << 20
MaxBodyBytes — найбільше тіло доставки, яке читає ParseRequest. Подія register.offline_abandoned перелічує всі документи сесії, тож межа має запас.
const SignatureHeader = "X-Signature"
SignatureHeader — заголовок із підписом доставки.
Variables ¶
var ErrInvalidSignature = errors.New("webhook: invalid signature")
ErrInvalidSignature — підпис відсутній, має не той формат або не збігається з тілом: доставку надіслав не PRRO.cloud або тіло змінено.
Functions ¶
func Handler ¶
func Handler(secret string, fn HandlerFunc) http.Handler
Handler повертає http.Handler, що приймає доставки вебхуків: перевіряє підпис, розбирає конверт і викликає fn. secret — секрет вебхука, виданий під час його створення (див. розділ «Секрет» в описі пакета).
Відповіді: 200 — подію оброблено; 401 — підпис недійсний; 405 — не POST; 413 — тіло понад MaxBodyBytes; 400 — конверт не розібрано; 500 — fn повернула помилку. Сервіс вважає успіхом будь-яку 2xx-відповідь і повторює решту, тож fn має бути ідемпотентною щодо Event.DeliveryID.
Example ¶
package main
import (
"context"
"log"
"net/http"
"os"
"github.com/prro-cloud/prro-go"
"github.com/prro-cloud/prro-go/webhook"
)
func main() {
secret := os.Getenv("PRRO_WEBHOOK_SECRET")
http.Handle("/prro-hook", webhook.Handler(secret, func(ctx context.Context, e *webhook.Event) error {
switch e.Type {
case prro.EventTaskCompleted:
d, err := e.Task()
if err != nil {
return err
}
if r := d.Result.Receipt; r != nil {
log.Printf("чек %s: %s", r.FiscalNumber, r.ReceiptURL)
}
case prro.EventClientBalanceLow:
d, err := e.Balance()
if err != nil {
return err
}
log.Printf("баланс добігає кінця: лишилося %d чеків", d.ReceiptsLeft)
}
return nil
}))
log.Fatal(http.ListenAndServe(":8080", nil))
}
Output:
Types ¶
type AttentionData ¶
type AttentionData struct {
CashRegisterID string `json:"cash_register_id"`
Reason prro.AttentionReason `json:"reason"`
// Remedy і Count — яке виправлення повторюється і скільки разів; для
// [prro.AttentionRecurringRemediation].
Remedy string `json:"remedy,omitempty"`
Count int `json:"count,omitempty"`
// SessionID і Error — зупинена сесія і причина; для
// [prro.AttentionOfflineSubmissionPaused].
SessionID string `json:"session_id,omitempty"`
Error string `json:"error,omitempty"`
}
AttentionData — дані події register.needs_attention: автоматичні виправлення не тримаються, на касу має поглянути людина.
type BalanceData ¶
type BalanceData struct {
Balance prro.Kopecks `json:"balance"`
SoftLimit prro.Kopecks `json:"soft_limit"`
PricePerReceipt prro.Kopecks `json:"price_per_receipt"`
// ReceiptsLeft — скільки чеків ще вміщує залишок (Balance − SoftLimit).
ReceiptsLeft int64 `json:"receipts_left"`
// ThresholdReceipts — поріг, за яким надіслано попередження.
ThresholdReceipts int `json:"threshold_receipts"`
}
BalanceData — дані події client.balance_low. Подія стосується клієнта, а не каси.
type DPSData ¶
type DPSData struct {
Verdict prro.DPSMode `json:"verdict"`
Override string `json:"override"`
Effective prro.DPSMode `json:"effective"`
Cause string `json:"cause"`
OccurredAt time.Time `json:"occurred_at"`
}
DPSData — дані подій system.dps_*: режим роботи з ДПС після переходу і його причина.
type Event ¶
type Event struct {
// DeliveryID — ідентифікатор доставки; однаковий в усіх повторах, тож
// за ним відкидають дублікати.
DeliveryID string `json:"delivery_id"`
// Type — тип події.
Type prro.WebhookEvent `json:"event"`
// Attempt — номер спроби доставки, від 1.
Attempt int `json:"attempt"`
// Sequence — номер події в межах каси: зростає, пропуски можливі; за ним
// упорядковують події каси. 0 у подій рівня клієнта.
Sequence int64 `json:"sequence,omitempty"`
// OccurredAt — коли подія сталася.
OccurredAt time.Time `json:"occurred_at"`
// Data — корисне навантаження; типізовано його повертають методи
// [Event.Task], [Event.Shift] тощо.
Data json.RawMessage `json:"data"`
}
Event — конверт доставки події.
func ParseRequest ¶
ParseRequest читає тіло запиту (не більше MaxBodyBytes), перевіряє підпис із заголовка SignatureHeader і розбирає конверт.
Example ¶
package main
import (
"errors"
"log"
"net/http"
"os"
"github.com/prro-cloud/prro-go/webhook"
)
func main() {
secret := os.Getenv("PRRO_WEBHOOK_SECRET")
http.HandleFunc("POST /prro-hook", func(w http.ResponseWriter, r *http.Request) {
e, err := webhook.ParseRequest(r, secret)
if errors.Is(err, webhook.ErrInvalidSignature) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
// Доставка — «щонайменше один раз»: відкидайте повтори за DeliveryID.
log.Printf("подія %s, доставка %s, спроба %d", e.Type, e.DeliveryID, e.Attempt)
w.WriteHeader(http.StatusOK)
})
}
Output:
func (*Event) Attention ¶
func (e *Event) Attention() (*AttentionData, error)
Attention повертає дані події register.needs_attention.
func (*Event) Balance ¶
func (e *Event) Balance() (*BalanceData, error)
Balance повертає дані події client.balance_low.
func (*Event) Offline ¶
func (e *Event) Offline() (*OfflineData, error)
Offline повертає дані події register.offline, register.online або register.offline_limit.
func (*Event) OfflineAbandoned ¶
func (e *Event) OfflineAbandoned() (*OfflineAbandonedData, error)
OfflineAbandoned повертає дані події register.offline_abandoned.
func (*Event) Receipt ¶ added in v0.2.0
func (e *Event) Receipt() (*ReceiptData, error)
Receipt повертає дані події receipt.registered.
func (*Event) Remediation ¶
func (e *Event) Remediation() (*RemediationData, error)
Remediation повертає дані події register.remediated.
type HandlerFunc ¶
HandlerFunc обробляє одну перевірену доставку. Помилка означає «не оброблено»: приймач відповідає 500, і сервіс повторить доставку пізніше.
type KeyData ¶
type KeyData struct {
KeyID string `json:"key_id"`
// CashRegisterIDs — каси, що підписують цим ключем.
CashRegisterIDs []string `json:"cash_register_ids,omitempty"`
// CertSubject — власник сертифіката, як його вказав АЦСК.
CertSubject string `json:"cert_subject,omitempty"`
// CertValidTo — коли сертифікат спливає.
CertValidTo time.Time `json:"cert_valid_to"`
}
KeyData — дані події key.expiring.
type OfflineAbandonedData ¶
type OfflineAbandonedData struct {
CashRegisterID string `json:"cash_register_id"`
SessionID string `json:"session_id"`
DPSSessionID int64 `json:"dps_session_id"`
TaskID string `json:"task_id"`
Documents []prro.AbandonedDocument `json:"documents"`
}
OfflineAbandonedData — дані події register.offline_abandoned: сесія, від якої відмовлено, і документи, яких ДПС так і не отримала. Чи видавати ці продажі знову, вирішує інтеграція.
type OfflineData ¶
type OfflineData struct {
CashRegisterID string `json:"cash_register_id"`
SessionID string `json:"session_id"`
DPSSessionID int64 `json:"dps_session_id"`
StartedAt time.Time `json:"started_at"`
// EndedAt — кінець сесії; лише для register.online.
EndedAt time.Time `json:"ended_at,omitzero"`
// Documents — скільки документів видано в сесії; лише для
// register.online.
Documents int64 `json:"documents,omitempty"`
// SessionUsed і MonthUsed — витрачений офлайн-час сесії і місяця; лише
// для register.offline_limit (межі — 36 і 168 годин).
SessionUsed time.Duration `json:"-"`
MonthUsed time.Duration `json:"-"`
}
OfflineData — дані подій register.offline, register.online і register.offline_limit.
func (*OfflineData) UnmarshalJSON ¶
func (d *OfflineData) UnmarshalJSON(b []byte) error
UnmarshalJSON розбирає тривалості, які сервіс передає рядками Go ("35h12m0s").
type PingData ¶
type PingData struct {
Message string `json:"message"`
WebhookID string `json:"webhook_id"`
SentAt time.Time `json:"sent_at"`
}
PingData — дані тестової відправки webhook.ping.
type ReceiptData ¶ added in v0.2.0
type ReceiptData struct {
CashRegisterID string `json:"cash_register_id"`
// RegisterFiscalNumber — фіскальний номер ПРРО.
RegisterFiscalNumber string `json:"register_fiscal_number"`
// DocumentID — документ у журналі сервісу.
DocumentID string `json:"document_id"`
// TaskID — завдання, яке зареєструвало документ.
TaskID string `json:"task_id"`
Type prro.ReceiptType `json:"type"`
Source ReceiptSource `json:"source"`
// UserID — обліковий запис панелі, з якого подано чек; лише для
// [SourceCabinet].
UserID string `json:"user_id,omitempty"`
// Cashier — ім'я касира, надруковане на чеку.
Cashier string `json:"cashier,omitempty"`
LocalNumber int64 `json:"local_number"`
// FiscalNumber — фіскальний номер документа; в офлайні — обчислений
// локально.
FiscalNumber string `json:"fiscal_number"`
// Offline — чек подано в офлайн-сесії.
Offline bool `json:"offline"`
// Testing — тестовий документ: каса в режимі test.
Testing bool `json:"testing"`
ShiftID string `json:"shift_id"`
ShiftNumber int64 `json:"shift_number,omitempty"`
// IssuedAt — дата й час документа, як їх надруковано.
IssuedAt time.Time `json:"issued_at"`
// Total — сума до сплати; для службових документів — сума руху готівки.
Total prro.Amount `json:"total"`
// Rounding — заокруглення готівки, якщо було.
Rounding prro.Amount `json:"rounding,omitempty"`
Items []ReceiptLine `json:"items,omitempty"`
Payments []ReceiptPayment `json:"payments,omitempty"`
Taxes []ReceiptTax `json:"taxes,omitempty"`
// Comment — коментар, надрукований на чеку.
Comment string `json:"comment,omitempty"`
// Original — чек, який повертає повернення чи скасовує сторно.
Original *prro.ReturnOriginal `json:"original,omitempty"`
// ReceiptURL — копія чека для покупця.
ReceiptURL string `json:"receipt_url,omitempty"`
// TaxURL — чек у кабінеті платника ДПС; відсутнє, доки офлайн-чек не
// прийнято ДПС.
TaxURL string `json:"tax_url,omitempty"`
}
ReceiptData — дані події receipt.registered: документ у тому вигляді, в якому його отримала ДПС. Подія приходить на кожен зареєстрований чек — і поданий через API, і поданий з панелі.
type ReceiptLine ¶ added in v0.2.0
type ReceiptLine struct {
Code string `json:"code,omitempty"`
Barcode string `json:"barcode,omitempty"`
UKTZED string `json:"uktzed,omitempty"`
DKPP string `json:"dkpp,omitempty"`
Name string `json:"name"`
UnitName string `json:"unit_name,omitempty"`
// Quantity — кількість десятковим рядком: "1", "0.750".
Quantity string `json:"quantity"`
Price prro.Amount `json:"price"`
// Cost — вартість рядка до знижки.
Cost prro.Amount `json:"cost,omitempty"`
TaxLetters string `json:"tax_letters,omitempty"`
// Discount — сума знижки.
Discount prro.Amount `json:"discount,omitempty"`
}
ReceiptLine — рядок зареєстрованого чека.
type ReceiptPayment ¶ added in v0.2.0
type ReceiptPayment struct {
// Type — форма оплати, як у запиті чека.
Type prro.PaymentType `json:"type,omitempty"`
// Name — назва, надрукована на чеку.
Name string `json:"name"`
Sum prro.Amount `json:"sum"`
}
ReceiptPayment — форма оплати зареєстрованого чека.
type ReceiptSource ¶ added in v0.2.0
type ReceiptSource string
ReceiptSource — звідки подано чек.
const ( SourceAPI ReceiptSource = "api" // через машинне API SourceCabinet ReceiptSource = "cabinet" // з панелі кабінету )
Джерела чека.
type ReceiptTax ¶ added in v0.2.0
type ReceiptTax struct {
Letter string `json:"letter,omitempty"`
Name string `json:"name"`
// Rate — ставка у відсотках: "20.00".
Rate string `json:"rate"`
// Turnover — оборот за ставкою.
Turnover prro.Amount `json:"turnover"`
// Sum — сума податку.
Sum prro.Amount `json:"sum"`
// Excluded — податок нараховано понад ціну.
Excluded bool `json:"excluded,omitempty"`
}
ReceiptTax — податок зареєстрованого чека.
type RemediationData ¶
type RemediationData struct {
CashRegisterID string `json:"cash_register_id"`
TaskID string `json:"task_id,omitempty"`
// Action — що виправлено: local_number_synced, shift_adopted,
// shift_abandoned, zreport_recovered, zreport_autofiled,
// register_config_filled.
Action string `json:"action"`
// FaultCode — збій, який спричинив виправлення.
FaultCode string `json:"fault_code"`
Details map[string]any `json:"details,omitempty"`
}
RemediationData — дані події register.remediated: одне автоматичне виправлення розбіжності з ДПС.
type ShiftData ¶
type ShiftData struct {
CashRegisterID string `json:"cash_register_id"`
ShiftID string `json:"shift_id"`
TaskID string `json:"task_id"`
// Testing — зміна тестова: каса в режимі test.
Testing bool `json:"testing"`
}
ShiftData — дані подій shift.opened і shift.closed.
type TaskData ¶
type TaskData struct {
TaskID string `json:"task_id"`
Type prro.TaskType `json:"type"`
Status prro.TaskStatus `json:"status"`
// CashRegisterID — каса завдання; порожній для завдань рівня клієнта.
CashRegisterID string `json:"cash_register_id,omitempty"`
// Result — той самий результат, що повертає [prro.TasksService.Get];
// лише для task.completed.
Result *prro.TaskResult `json:"result,omitempty"`
// Error і Fault — опис збою; лише для task.failed.
Error string `json:"error,omitempty"`
Fault *prro.Fault `json:"fault,omitempty"`
}
TaskData — дані подій task.completed і task.failed.