Initial release: qbirr Go SDK v1.0.0
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// Package qbirr is a thin Go client for the qbirr payment verification API.
|
||||
//
|
||||
// qb := qbirr.New("your_api_key")
|
||||
// r, err := qb.Verify(context.Background(), qbirr.VerifyRequest{
|
||||
// Provider: "cbe",
|
||||
// Ref: "FT23001234ABC",
|
||||
// Amount: 500,
|
||||
// ReceiverName: "YOUR NAME",
|
||||
// ReceiverAccount: "1000017692643",
|
||||
// })
|
||||
// if err != nil { return err }
|
||||
// if r.Verified { fmt.Println("Paid by", r.Payer) }
|
||||
package qbirr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "1.0.0"
|
||||
DefaultBaseURL = "https://verify.qbirr.com"
|
||||
DefaultTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// Provider identifies which bank or wallet the reference belongs to.
|
||||
type Provider = string
|
||||
|
||||
const (
|
||||
ProviderCBE Provider = "cbe"
|
||||
ProviderTelebirr Provider = "telebirr"
|
||||
ProviderAbyssinia Provider = "abyssinia"
|
||||
ProviderDashen Provider = "dashen"
|
||||
ProviderMpesa Provider = "mpesa"
|
||||
ProviderAwash Provider = "awash"
|
||||
ProviderEbirr Provider = "ebirr"
|
||||
ProviderZamZam Provider = "zamzam"
|
||||
)
|
||||
|
||||
// VerifyRequest is the payload for /api/v1/verify.
|
||||
type VerifyRequest struct {
|
||||
Provider Provider `json:"provider"`
|
||||
Ref string `json:"ref"`
|
||||
Amount float64 `json:"amount"`
|
||||
ReceiverName string `json:"receiver_name"`
|
||||
ReceiverAccount string `json:"receiver_account,omitempty"` // required for CBE & Abyssinia
|
||||
}
|
||||
|
||||
// VerifyResult is the API response. Verification failures arrive here (Verified=false + Error set),
|
||||
// NOT as Go errors. Errors are reserved for network/HTTP problems.
|
||||
type VerifyResult struct {
|
||||
Verified bool `json:"verified"`
|
||||
Payer string `json:"payer"`
|
||||
Amount float64 `json:"amount"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// UsageStatus is the response from /api/v1/usage.
|
||||
type UsageStatus struct {
|
||||
Used int `json:"used"`
|
||||
Limit int `json:"limit"`
|
||||
Remaining int `json:"remaining"`
|
||||
Plan string `json:"plan"`
|
||||
PeriodStart string `json:"period_start"`
|
||||
PeriodEnd string `json:"period_end"`
|
||||
}
|
||||
|
||||
// APIError wraps a non-2xx HTTP response.
|
||||
type APIError struct {
|
||||
Status int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string { return fmt.Sprintf("qbirr: HTTP %d: %s", e.Status, e.Message) }
|
||||
|
||||
// Client talks to the qbirr API.
|
||||
type Client struct {
|
||||
APIKey string
|
||||
BaseURL string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// New returns a client with sensible defaults.
|
||||
func New(apiKey string) *Client {
|
||||
return &Client{
|
||||
APIKey: apiKey,
|
||||
BaseURL: DefaultBaseURL,
|
||||
HTTPClient: &http.Client{Timeout: DefaultTimeout},
|
||||
}
|
||||
}
|
||||
|
||||
// Verify performs a verification. Returns (result, nil) for both verified=true and verified=false
|
||||
// (real verification failures). Returns (nil, error) only for transport/HTTP issues.
|
||||
func (c *Client) Verify(ctx context.Context, req VerifyRequest) (*VerifyResult, error) {
|
||||
if c.APIKey == "" {
|
||||
return nil, errors.New("qbirr: APIKey is empty")
|
||||
}
|
||||
var out VerifyResult
|
||||
if err := c.do(ctx, "POST", "/api/v1/verify", req, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// Usage returns the current calendar-month quota status.
|
||||
func (c *Client) Usage(ctx context.Context) (*UsageStatus, error) {
|
||||
var out UsageStatus
|
||||
if err := c.do(ctx, "GET", "/api/v1/usage", nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path string, body any, out any) error {
|
||||
var rdr io.Reader
|
||||
if body != nil {
|
||||
buf, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("qbirr: marshal: %w", err)
|
||||
}
|
||||
rdr = bytes.NewReader(buf)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, rdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-API-Key", c.APIKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set("User-Agent", "qbirr-go/"+Version)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("qbirr: network: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("qbirr: read body: %w", err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
// Try to extract { error: "..." }
|
||||
var apiErr struct{ Error string `json:"error"` }
|
||||
_ = json.Unmarshal(raw, &apiErr)
|
||||
msg := apiErr.Error
|
||||
if msg == "" {
|
||||
msg = string(raw)
|
||||
}
|
||||
return &APIError{Status: resp.StatusCode, Message: msg}
|
||||
}
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
return fmt.Errorf("qbirr: decode (HTTP %d): %w", resp.StatusCode, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user