webhook

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 10, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

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

Examples

Constants

View Source
const MaxBodyBytes = 10 << 20

MaxBodyBytes — найбільше тіло доставки, яке читає ParseRequest. Подія register.offline_abandoned перелічує всі документи сесії, тож межа має запас.

View Source
const SignatureHeader = "X-Signature"

SignatureHeader — заголовок із підписом доставки.

Variables

View Source
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))
}

func Sign

func Sign(secret string, body []byte) string

Sign повертає значення заголовка X-Signature для тіла body — так його рахує сервіс. Знадобиться для тестів власного приймача.

func Verify

func Verify(secret string, body []byte, signature string) error

Verify перевіряє підпис signature тіла body за сталий час. Перевіряйте підпис до будь-якої обробки тіла.

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 Parse

func Parse(secret string, body []byte, signature string) (*Event, error)

Parse перевіряє підпис signature і розбирає тіло доставки body.

func ParseRequest

func ParseRequest(r *http.Request, secret string) (*Event, error)

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)
	})
}

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) DPS

func (e *Event) DPS() (*DPSData, error)

DPS повертає дані подій system.dps_*.

func (*Event) Key

func (e *Event) Key() (*KeyData, error)

Key повертає дані події key.expiring.

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) Ping

func (e *Event) Ping() (*PingData, error)

Ping повертає дані тестової відправки webhook.ping.

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.

func (*Event) Shift

func (e *Event) Shift() (*ShiftData, error)

Shift повертає дані події shift.opened або shift.closed.

func (*Event) Task

func (e *Event) Task() (*TaskData, error)

Task повертає дані події task.completed або task.failed.

type HandlerFunc

type HandlerFunc func(ctx context.Context, e *Event) error

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.

Jump to

Keyboard shortcuts

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