payment

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 2 Imported by: 0

README

Payment for Go

CI Coverage Go Reference Go 1.24 or newer MIT License

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

  • Go 1.24 or newer

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.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidRequest      = errors.New("payment: invalid request")
	ErrNetwork             = errors.New("payment: network error")
	ErrTransactionNotFound = errors.New("payment: transaction not found")
	ErrCanceled            = errors.New("payment: canceled")
	ErrDeclined            = errors.New("payment: declined")
	ErrProvider            = errors.New("payment: provider error")
	ErrUnsupported         = errors.New("payment: operation unsupported")
)

Portable error categories returned by payment gateways. Gateway implementations may wrap these errors with provider-specific details.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client provides payment operations through one immutable gateway. Create a separate Client for each configured gateway.

func NewClient

func NewClient(gateway Gateway) *Client

NewClient creates a client backed by gateway. It panics if gateway is nil.

func (*Client) Purchase

func (c *Client) Purchase(ctx context.Context, request PurchaseRequest) (PurchaseResponse, error)

Purchase initiates a payment through the client's gateway.

func (*Client) Refund

func (c *Client) Refund(ctx context.Context, request RefundRequest) (RefundResponse, error)

Refund refunds a payment if the client's gateway supports refunds.

func (*Client) Verify

func (c *Client) Verify(ctx context.Context, request VerifyRequest) (Transaction, error)

Verify verifies a payment through the client's gateway.

type Currency

type Currency string

Currency is a payment currency code. It remains open-ended so applications and third-party gateways can use currencies that are not predefined by this package.

const (
	CurrencyIRR Currency = "IRR"
	CurrencyIRT Currency = "IRT"
)

type Gateway

type Gateway interface {
	Purchaser
	Verifier
}

Gateway defines the operations required from every payment provider.

type Money

type Money struct {
	Amount   int64
	Currency Currency
}

Money represents an amount in the currency's smallest unit.

type PurchaseRequest

type PurchaseRequest struct {
	OrderID     string
	Amount      Money
	CallbackURL string
	Description string
	Metadata    map[string]string
}

PurchaseRequest contains the information needed to initiate a payment.

type PurchaseResponse

type PurchaseResponse struct {
	Transaction Transaction
	RedirectURL string
}

PurchaseResponse contains the newly created transaction and the URL where the payer should be redirected.

type Purchaser

type Purchaser interface {
	Purchase(context.Context, PurchaseRequest) (PurchaseResponse, error)
}

Purchaser initiates payments.

type RefundRequest

type RefundRequest struct {
	TransactionID string
	Amount        Money
	Reason        string
}

RefundRequest contains the information needed to issue a full or partial refund.

type RefundResponse

type RefundResponse struct {
	ID            string
	TransactionID string
	Amount        Money
}

RefundResponse identifies a refund accepted by the payment provider.

type Refunder

type Refunder interface {
	Refund(context.Context, RefundRequest) (RefundResponse, error)
}

Refunder refunds payments when supported by a provider.

type Status

type Status string

Status describes the current state of a transaction.

const (
	// StatusPending indicates that a transaction has not completed yet.
	StatusPending Status = "pending"
	// StatusPaid indicates that a transaction completed successfully.
	StatusPaid Status = "paid"
	// StatusFailed indicates that a transaction failed.
	StatusFailed Status = "failed"
	// StatusRefunded indicates that a transaction was refunded.
	StatusRefunded Status = "refunded"
	// StatusCanceled indicates that a transaction was canceled.
	StatusCanceled Status = "canceled"
)

type Transaction

type Transaction struct {
	ID          string
	Amount      Money
	Status      Status
	ReferenceID string
	Provider    string
}

Transaction represents a payment-provider transaction.

type Verifier

type Verifier interface {
	Verify(context.Context, VerifyRequest) (Transaction, error)
}

Verifier verifies payments.

type VerifyRequest

type VerifyRequest struct {
	TransactionID string
	Amount        Money
}

VerifyRequest contains the information needed to verify a transaction.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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