Initial release: qbirr Go SDK v1.0.0
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
# qbirr-go
|
||||||
|
|
||||||
|
Go client for the qbirr Ethiopian bank verification API.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/qbirr/sdk-go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/qbirr/sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
qb := qbirr.New(os.Getenv("QBIRR_API_KEY"))
|
||||||
|
|
||||||
|
r, err := qb.Verify(context.Background(), qbirr.VerifyRequest{
|
||||||
|
Provider: qbirr.ProviderCBE,
|
||||||
|
Ref: "FT23001234ABC",
|
||||||
|
Amount: 500,
|
||||||
|
ReceiverName: "YOUR NAME",
|
||||||
|
ReceiverAccount: "1000017692643",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("API error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Verified {
|
||||||
|
fmt.Printf("✓ Paid by %s, amount: %.2f\n", r.Payer, r.Amount)
|
||||||
|
} else {
|
||||||
|
fmt.Println("✗", r.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
Network and HTTP errors return as Go `error`. Verification failures (wrong amount, wrong receiver, etc.) are NOT errors — they're returned as `VerifyResult{Verified: false, Error: "..."}`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var apiErr *qbirr.APIError
|
||||||
|
if errors.As(err, &apiErr) {
|
||||||
|
fmt.Println("HTTP status:", apiErr.Status)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -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