Payment
payment is a small, provider-neutral payment package for Go. It defines common contracts for creating, verifying, and optionally refunding payments while keeping provider configuration, protocols, and errors in provider packages.
The package deliberately does not manage gateway selection, routing, persistence, retries, reconciliation, or business rules. Applications can configure several gateways by creating one immutable client for each gateway.
Requirements
Installation
Install the module and import only the providers your application uses:
go get github.com/codenaline/payment@latest
import (
"github.com/codenaline/payment"
"github.com/codenaline/payment/zarinpal"
)
Supported providers
| Provider |
Purchase |
Verify |
Refund |
Currencies |
Sandbox |
| ZarinPal |
Yes |
Yes |
No |
IRR |
Yes |
| NextPay |
Yes |
Yes |
Yes |
IRR, IRT |
No |
Refund support is exposed as an optional capability. A custom gateway can implement the core payment.Gateway interface and, when applicable, payment.Refunder.
Quick start
Create a gateway, wrap it in a client, and initiate a payment:
package main
import (
"context"
"fmt"
"github.com/codenaline/payment"
"github.com/codenaline/payment/zarinpal"
)
func main() {
gateway, err := zarinpal.New(zarinpal.Config{
MerchantID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
Sandbox: true,
})
if err != nil {
panic(err)
}
client := payment.NewClient(gateway)
result, err := client.Purchase(context.Background(), payment.PurchaseRequest{
OrderID: "order-1234",
Amount: payment.Money{
Amount: 100_000,
Currency: payment.CurrencyIRR,
},
CallbackURL: "https://example.com/payments/callback",
Description: "Order #1234",
})
if err != nil {
panic(err)
}
// Persist result.Transaction.ID with the order before redirecting the payer.
fmt.Println(result.RedirectURL)
}
Redirect the payer to PurchaseResponse.RedirectURL. Persist the transaction ID, order ID, amount, currency, and status in your own database.
Money.Amount is an integer in the selected currency unit. The package does not convert between currencies or units; use the unit required by the configured provider.
Verify a payment
After the provider redirects the payer to your callback endpoint, verify the transaction on a trusted server using the stored transaction ID and original amount:
transaction, err := client.Verify(ctx, payment.VerifyRequest{
TransactionID: storedTransactionID,
Amount: payment.Money{
Amount: 100_000,
Currency: payment.CurrencyIRR,
},
})
if err != nil {
return err
}
if transaction.Status == payment.StatusPaid {
// Persist the paid state before fulfilling the order.
}
A browser redirect alone is not proof of payment. Callback handlers should be idempotent: persist the verified state and return the existing successful result when a paid callback is repeated.
ZarinPal verification codes 100 (verified now) and 101 (already verified) both produce a paid transaction without an error.
Multiple gateways
Create an independent client for each configured gateway. The application decides which client to use:
zarinpalGateway, err := zarinpal.New(zarinpal.Config{
MerchantID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
})
if err != nil {
return err
}
nextpayGateway, err := nextpay.New(nextpay.Config{
APIKey: "nextpay-api-key",
})
if err != nil {
return err
}
zarinpalClient := payment.NewClient(zarinpalGateway)
nextpayClient := payment.NewClient(nextpayGateway)
// Select a client using application-owned business rules.
_ = zarinpalClient
_ = nextpayClient
A client does not switch or mutate its gateway after construction. There is no global gateway registry.
Provider configuration
ZarinPal
gateway, err := zarinpal.New(zarinpal.Config{
MerchantID: "your-merchant-id",
Sandbox: true, // Optional; defaults to false.
HTTPClient: httpClient, // Optional.
})
ZarinPal accepts IRR. When HTTPClient is nil, the driver uses its default HTTP client with a 30-second timeout.
NextPay
gateway, err := nextpay.New(nextpay.Config{
APIKey: "your-api-key",
HTTPClient: httpClient, // Optional.
})
NextPay accepts IRR and IRT and requires PurchaseRequest.OrderID when creating a payment.
Refunds
Refund is an optional gateway capability. Client.Refund returns payment.ErrUnsupported when the configured gateway does not implement it:
refund, err := client.Refund(ctx, payment.RefundRequest{
TransactionID: transactionID,
Amount: amount,
Reason: "customer request",
})
if errors.Is(err, payment.ErrUnsupported) {
// Use a provider-specific or manual refund process.
}
Error handling
The root package exposes portable sentinel errors. Use errors.Is for provider-independent decisions:
switch {
case errors.Is(err, payment.ErrInvalidRequest):
// Correct the request; retrying it unchanged will not help.
case errors.Is(err, payment.ErrNetwork):
// Apply the application's retry and reconciliation policy.
case errors.Is(err, payment.ErrDeclined):
// Ask the customer to use another payment method.
case errors.Is(err, payment.ErrTransactionNotFound):
// Reconcile the stored transaction information.
case errors.Is(err, payment.ErrCanceled):
// Record that the payment was canceled.
case errors.Is(err, payment.ErrProvider):
// Handle an unclassified provider failure.
}
Provider packages expose their own error types. Use errors.As only when provider-specific diagnostics are needed:
var providerError *zarinpal.Error
if errors.As(err, &providerError) {
fmt.Printf("ZarinPal %s failed with code %d: %s\n",
providerError.Operation,
providerError.Code,
providerError.Message,
)
}
Do not expose provider errors directly to customers when they may contain operational details.
Custom gateways
Implement payment.Gateway to integrate another provider without registering it globally:
type CustomGateway struct{}
func (*CustomGateway) Purchase(
ctx context.Context,
request payment.PurchaseRequest,
) (payment.PurchaseResponse, error) {
return payment.PurchaseResponse{}, nil
}
func (*CustomGateway) Verify(
ctx context.Context,
request payment.VerifyRequest,
) (payment.Transaction, error) {
return payment.Transaction{}, nil
}
client := payment.NewClient(&CustomGateway{})
Implement payment.Refunder if the provider supports refunds. Custom gateways should wrap the portable sentinel errors and expose a provider-specific error type when callers need additional details.
Project scope
The package provides:
- Common payment types and gateway interfaces
- Immutable clients bound to one gateway
- Bundled ZarinPal and NextPay drivers
- Portable and provider-specific error handling
- Optional gateway capabilities such as refunds
Applications remain responsible for:
- Gateway selection and routing
- Transaction and order persistence
- Callback validation and idempotency
- Retry, timeout, and reconciliation policies
- Logging, metrics, and tracing
- Fulfillment and all other business rules
Contributing and support
See CONTRIBUTING.md before submitting a change. Use GitHub Discussions for usage questions and GitHub Issues for reproducible defects.
Security issues must be reported privately according to SECURITY.md.
License
payment is available under the MIT License.