-
Notifications
You must be signed in to change notification settings - Fork 0
Extending
Amir Iranmanesh edited this page Jul 31, 2026
·
1 revision
A provider Payvand does not support — or one that is in-house, or under NDA — becomes a first class gateway by implementing the interface and registering a factory. Nothing has to be upstreamed.
package acme
import (
"context"
"net/http"
"strings"
"github.com/amiranmanesh/payvand/core"
)
// Name is the registry name of this gateway.
const Name core.Name = "acme"
func init() {
core.Register(Name, func(cfg core.Config, opts ...core.Option) (core.Gateway, error) {
return New(cfg, opts...)
})
}
// Gateway is the Acme implementation of core.Gateway.
type Gateway struct {
// Unsupported answers Refund, Inquiry and ParseCallback with
// ErrNotSupported; override only what Acme really offers.
core.Unsupported
cfg core.Config
opts *core.Options
client *http.Client
baseURL string
}
func New(cfg core.Config, opts ...core.Option) (*Gateway, error) {
options := core.NewOptions(opts...)
if strings.TrimSpace(cfg.MerchantKey) == "" {
return nil, core.NewError(Name, "new", core.ErrInvalidConfig).
WithMessage("MerchantKey is required")
}
baseURL := "https://api.acme.example"
if options.BaseURL != "" { // keeps the gateway testable
baseURL = options.BaseURL
}
return &Gateway{
Unsupported: core.Unsupported{GatewayName: Name},
cfg: cfg,
opts: options,
client: &http.Client{Timeout: options.Timeout},
baseURL: baseURL,
}, nil
}
func (g *Gateway) Name() core.Name { return Name }
func (g *Gateway) Capabilities() core.Capabilities {
return core.Capabilities{
Verify: true,
Callback: true,
RedirectMethod: http.MethodGet,
Currencies: []core.Currency{core.IRR},
}
}
func (g *Gateway) Purchase(ctx context.Context, req core.PurchaseRequest) (core.PurchaseResponse, error) {
// … call the provider, then:
return core.PurchaseResponse{
Token: token,
OrderID: req.OrderID,
Amount: req.Amount,
Redirect: core.Redirect{
Method: http.MethodGet,
URL: g.baseURL + "/pay/" + token,
},
}, nil
}
func (g *Gateway) Verify(ctx context.Context, req core.VerifyRequest) (core.VerifyResponse, error) {
// … then:
return core.VerifyResponse{
ReferenceNumber: rrn,
OrderID: req.OrderID,
Amount: req.Amount,
}, nil
}
func (g *Gateway) ParseCallback(r *http.Request) (core.Callback, error) {
values, err := core.CallbackValues(r)
if err != nil {
return core.Callback{}, core.NewError(Name, "callback", err)
}
return core.Callback{
Gateway: Name,
Succeeded: core.FirstValue(values, "status") == "OK",
Token: core.FirstValue(values, "token", "Token"),
Values: values,
}, nil
}Then, anywhere in your program:
import _ "yourmodule/gateway/acme"
gw, err := payvand.New("acme", payvand.Config{MerchantKey: key})Use the same mechanism the built-in gateways use, so your options compose with the shared ones:
type config struct {
description string
}
func settings(o *core.Options) *config {
if existing, ok := o.Extra(string(Name)).(*config); ok {
return existing
}
created := &config{}
o.SetExtra(string(Name), created)
return created
}
// WithDefaultDescription sets the description used when a request carries none.
func WithDefaultDescription(description string) core.Option {
return func(o *core.Options) { settings(o).description = description }
}(Inside the Payvand repository, internal/gwopt.From[config] does the same in
one line.)
| Convention | Why |
|---|---|
Honour Options.BaseURL everywhere |
it is what makes the gateway testable without the provider |
Validate credentials in New
|
fail at wiring time, not at payment time |
Tell the truth in Capabilities
|
callers branch on it instead of on the provider name |
Convert with Money.Rial() / Money.Toman()
|
never assume the caller's unit |
Wrap errors with core.NewError(...).WithCode(...).WithMessage(...)
|
keeps errors.Is working and the provider code visible |
Put the raw body in Raw
|
support tickets need it |
Expose Message(code) if the provider publishes a table |
turns numbers into sentences |
Decoration works because everything is an interface. A gateway that records metrics:
type instrumented struct {
payvand.Gateway
metrics *Metrics
}
func (i instrumented) Purchase(ctx context.Context, req payvand.PurchaseRequest) (payvand.PurchaseResponse, error) {
start := time.Now()
res, err := i.Gateway.Purchase(ctx, req)
i.metrics.Observe(string(i.Name()), "purchase", time.Since(start), err)
return res, err
}The same shape covers retries with your own policy, circuit breaking, or routing a payment to a second terminal when the first one is down.
Next: Testing · Contributing
Payvand · MIT licensed · built on the Go standard library alone · report an issue