paygate

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

paygate

Go Reference License

paygate is a small, gateway-agnostic billing core for Go: it manages encrypted gateway credentials, a money-movement ledger, prepaid tenant balances, top-ups, saved payment methods (saved with or without a charge, then charged off-session), and webhook handling — across Stripe and PayPal behind one interface.

It is self-contained (only the Stripe/PayPal SDKs, google/uuid, and the standard library) so independent applications can share it instead of re-implementing payment plumbing. The host supplies a database, a 32-byte encryption key, HTTP handlers, and UI; paygate owns the gateways, ledger, balance, idempotency, and webhooks.

Features

  • Two gateways, one interface — Stripe (PaymentIntents → webhook) and PayPal (Orders → capture), selectable at runtime.
  • Prepaid balances — exact NUMERIC(12,4) decimal math in Postgres, never floats.
  • Saved methods (Stripe & PayPal) — save during a top-up or with no charge (Stripe SetupIntent / PayPal Vault setup token), list, set-default, delete, and off-session charging.
  • Encrypted credentials at rest — AES-256-GCM, with each ciphertext bound to (namespace, gateway).
  • Idempotent & money-safe — DB-level uniqueness on transactions/webhooks and a pending-guarded balance credit, so redelivered webhooks never double-credit.

Install

go get github.com/farhanahmed1/paygate

Requires Go 1.25+ and PostgreSQL.

Quick start

import "github.com/farhanahmed1/paygate"

// db satisfies paygate.DB (a *sql.DB does); key is 32 bytes (AES-256);
// namespace binds ciphertexts to this host (e.g. "payment_gateway").
svc := paygate.NewService(db, key, "payment_gateway")

// Configure a gateway (admin) — credentials are encrypted at rest:
_ = svc.UpsertGatewaySetting(paygate.GatewayStripe, "Credit Card", true, true,
    map[string]string{
        "publishable_key": "pk_test_…",
        "secret_key":      "sk_test_…",
        "webhook_secret":  "whsec_…",
    })

// Start a top-up (tenant), optionally saving the card for later:
res, err := svc.CreateTopup(ctx, paygate.TopupRequest{
    TenantID:      tenantID,
    Gateway:       paygate.GatewayStripe,
    AmountCents:   2500,
    SaveCard:      true,
    CustomerEmail: "user@example.com",
})
// res.ClientSecret → confirm with Stripe.js in the browser; the
// payment_intent.succeeded webhook credits the balance (and saves the card).

// Later: top up off-session with a saved card:
res, err = svc.ChargeSavedMethod(ctx, tenantID, methodID, 2500)

// Or save a method WITHOUT charging (Stripe SetupIntent / PayPal Vault token):
setup, err := svc.CreateSaveSetup(ctx, paygate.SaveSetupRequest{
    TenantID:      tenantID,
    Gateway:       paygate.GatewayStripe,
    CustomerEmail: "user@example.com",
})
// setup.ClientSecret (Stripe) or setup.ApprovalURL + setup.SetupTokenID (PayPal)
// → confirm/approve in the browser, then persist with no charge:
err = svc.PersistSavedMethodFromSetup(ctx, tenantID, paygate.GatewayStripe, setup.SetupIntentID)

// Inbound webhook (mount on a public, unauthenticated route):
err = svc.ProcessWebhook(ctx, paygate.GatewayStripe, body, req.Header)

Gateways

Gateway Completion model Saved methods
stripe PaymentIntent confirmed client-side → credited by the payment_intent.succeeded webhook Cards — SavedMethodGateway + SetupGateway
paypal Order approved in-browser → captured server-side by CaptureTopup Vaulted wallet — SavedMethodGateway + SetupGateway

Gateways implement the Gateway interface; PayPal additionally implements CapturableGateway. Both Stripe and PayPal implement SavedMethodGateway (off-session charging) and SetupGateway (saving a method without a charge). Building a gateway only requires it to be configured; the enabled flag is enforced on the payment path.

What the host provides

paygate is the core only. The host application provides:

  1. A database implementing the DB interface (*sql.DB satisfies it) and the schema below.
  2. HTTP handlers & routes — authenticated tenant endpoints (overview, top-up, saved-method management) and public, signature-verified webhook endpoints.
  3. UI — e.g. Stripe Elements / PayPal Buttons for the browser side.
Required schema

paygate does not run migrations; create these tables (Postgres). Reference DDL — adapt the tenant_id type and UUID default to your app:

tenants
  id           UUID PRIMARY KEY
  balance      NUMERIC(12,4) NOT NULL DEFAULT 0

payment_gateway_settings
  name             VARCHAR(50) PRIMARY KEY   -- 'stripe' | 'paypal'
  display_name     VARCHAR
  is_enabled       BOOLEAN NOT NULL DEFAULT false
  is_test_mode     BOOLEAN NOT NULL DEFAULT true
  encrypted_config TEXT NOT NULL DEFAULT ''  -- AES-256-GCM blob

payment_methods
  id, tenant_id (FK→tenants), gateway, payment_type ('card'|'paypal_wallet'),
  gateway_customer_id NOT NULL, gateway_payment_method_id NOT NULL,
  card_brand, card_last4, card_exp_month, card_exp_year,   -- cards
  paypal_email, paypal_payer_id,                           -- vaulted wallets
  nickname, is_default, is_active, last_used_at,
  UNIQUE (tenant_id, gateway, gateway_payment_method_id)

payment_transactions
  id, tenant_id (FK→tenants), gateway,
  transaction_type ('topup' | host-defined, e.g. 'subscription'),
  amount NUMERIC(12,4), currency, status ('pending'|'completed'|'failed'),
  gateway_transaction_id, gateway_customer_id, description,
  UNIQUE (gateway, gateway_transaction_id)        -- DB-level idempotency

payment_webhook_logs
  id, gateway, event_id, event_type, payload JSONB, status, ...,
  UNIQUE (gateway, event_id)

Saved methods

Stripe stores cards; PayPal stores vaulted wallet tokens. Both expose the same operations, and the tenant's first saved method becomes its default.

  • Save during top-up: CreateTopup with SaveCard: true ensures a gateway customer (looked up in payment_methods, then the ledger, else created) and saves the method on success — Stripe via setup_future_usage=off_session (persisted by the success webhook), PayPal via vault-on-purchase (persisted by CaptureTopup).
  • Save without charging: CreateSaveSetup starts a zero-amount save and returns what the browser needs to confirm it — Stripe a SetupIntent ClientSecret, PayPal a Vault SetupTokenID + buyer ApprovalURL. After the browser confirms (Stripe) / the buyer approves (PayPal), PersistSavedMethodFromSetup resolves and stores the method. No money moves.
  • Manage: ListPaymentMethods, SetDefaultPaymentMethod, DeletePaymentMethod (soft-delete + best-effort provider detach + default promotion). The default is one per tenant.
  • Pay off-session: ChargeSavedMethod charges a stored method and credits the balance synchronously when it settles immediately (the webhook is an idempotent backstop). ChargeSavedMethodDirect charges a stored method for a non-top-up purpose (e.g. a subscription fee) without crediting the balance. Both wrap ErrChargeDeclined when the gateway rejects the method for a payment-method reason (declined card / declined PayPal wallet / off-session authentication required) — errors.Is it to return a clean 402 rather than a 5xx, distinct from gateway/infra errors.

Webhooks & money-safety

ProcessWebhook verifies the provider signature, records the event for audit (UNIQUE(gateway, event_id)), and applies it. Balance credits run inside a transaction guarded by WHERE status='pending', so a redelivered or duplicate webhook credits nothing the second time.

Security

  • Gateway credentials are encrypted with AES-256-GCM (internal/crypto), the ciphertext bound to (namespace, gateway) as additional authenticated data — a Stripe blob cannot be decrypted as PayPal, nor reused across hosts.
  • Public keys (publishable_key / client_id) are the only credential values surfaced to clients; secret keys never leave the server.

Versioning

Released as Go module versions; see CHANGELOG.md.

License

Apache-2.0.

Documentation

Overview

Package paygate is the gateway-agnostic core of a payment system: gateway credential storage (encrypted at rest), saved payment methods, the transaction ledger, tenant balance crediting, topup orchestration, and webhook handling. Gateway-specific behaviour (Stripe, PayPal) is layered on top via the Gateway and SavedMethodGateway interfaces.

It is self-contained — depending only on a minimal DB surface, the Stripe and PayPal SDKs, and the standard library — so multiple hosts can import it. Each host supplies a *sql.DB-compatible DB, a 32-byte credential-encryption key, and a namespace string to NewService, plus the HTTP handlers, routes, and UI; paygate owns the gateways, ledger, balance, idempotency, and webhooks.

The host is responsible for the database schema. paygate expects:

  • tenants(id, balance NUMERIC(12,4))
  • payment_gateway_settings(name, display_name, is_enabled, is_test_mode, encrypted_config)
  • payment_methods(...) — saved cards / wallet tokens
  • payment_transactions(...) — the money-movement ledger
  • payment_webhook_logs(gateway, event_id, ...) with UNIQUE(gateway, event_id)

See the README for the full column list and the reference migrations.

Index

Constants

View Source
const (
	GatewayStripe = "stripe"
	GatewayPayPal = "paypal"
)

Gateway identifiers. These match the CHECK constraint on payment_gateway_settings.name (migration 069) and the `gateway` column on payment_methods / payment_transactions / payment_webhook_logs.

View Source
const MinTopupCents = 100

MinTopupCents is the smallest topup paygate accepts ($1.00). Keeps amounts above gateway minimums and avoids dust transactions. Exported so hosts can validate against it instead of hardcoding the value.

Variables

View Source
var ErrAmountTooSmall = errors.New("paygate: amount below minimum")

ErrAmountTooSmall is returned (wrapped) by CreateTopup / ChargeSavedMethod when the amount is below MinTopupCents. Hosts can errors.Is it to return a 400.

View Source
var ErrChargeDeclined = errors.New("paygate: payment method declined")

ErrChargeDeclined is returned (wrapped) by ChargeSavedMethod / ChargeSavedMethodDirect when the gateway rejects a saved-method charge for a payment-method reason — a declined card / declined PayPal wallet, or a card that needs authentication an off-session charge cannot perform. It is client-correctable (use another method / add a new one), distinct from gateway/infra errors. Hosts can errors.Is it to surface a clean 402 instead of a 5xx. The original gateway error is preserved in the chain.

View Source
var ErrGatewayDisabled = errors.New("paygate: gateway disabled")

ErrGatewayDisabled is returned when a topup targets a configured-but-disabled gateway. (Configuration/testing build the gateway regardless; only the payment path enforces the enabled flag.)

View Source
var ErrGatewayNotFound = errors.New("paygate: gateway not configured")

ErrGatewayNotFound is returned by GatewaySetting when no row exists for the requested gateway name.

View Source
var ErrPaymentMethodNotFound = errors.New("paygate: payment method not found")

ErrPaymentMethodNotFound is returned when a saved-method operation targets a method that does not exist (or is no longer active) for the requesting tenant.

View Source
var ErrTenantNotFound = errors.New("paygate: tenant not found")

ErrTenantNotFound is returned when a balance operation targets a tenant id that does not exist.

View Source
var ErrTopupNotFound = errors.New("paygate: topup not found")

ErrTopupNotFound is returned when a capture targets a topup that does not exist for the requesting tenant.

View Source
var ErrUnknownGateway = errors.New("paygate: unknown gateway")

ErrUnknownGateway is returned (wrapped) when a gateway name is not one this build supports (not Stripe/PayPal) — distinct from ErrGatewayNotFound, which means a supported gateway has no configuration row. Hosts can errors.Is it to return a 400.

Functions

func CentsToAmount

func CentsToAmount(cents int64) string

CentsToAmount converts integer cents to a NUMERIC(12,4) decimal string for the ledger, e.g. 2500 → "25.0000". Exact for 2-decimal (USD) amounts.

Types

type CapturableGateway

type CapturableGateway interface {
	Gateway
	// CaptureTopupPayment captures a previously-created payment identified by
	// its gateway transaction id (PayPal order id). The result reports whether
	// the gateway completed the payment and, when a method was vaulted during
	// the capture (PayPal vault-on-purchase), its details to persist.
	CaptureTopupPayment(ctx context.Context, gatewayTxnID string) (CaptureResult, error)
}

CapturableGateway is a Gateway whose payments need an explicit server-side capture after the buyer approves (PayPal Orders). Stripe does not implement it — its PaymentIntents are confirmed client-side and settle via webhook.

type CaptureResult added in v0.3.0

type CaptureResult struct {
	Completed   bool
	SavedMethod *SavedMethodDetails
}

CaptureResult is the outcome of capturing a CapturableGateway payment. SavedMethod is non-nil when the capture vaulted a reusable method, for the Service to persist.

type CardDetails

type CardDetails struct {
	Brand    string
	Last4    string
	ExpMonth int
	ExpYear  int
}

CardDetails is the card information a gateway returns for a saved method.

type CreateTopupInput

type CreateTopupInput struct {
	TenantID          uuid.UUID
	Gateway           string // GatewayStripe | GatewayPayPal
	Amount            string // positive decimal, ≤4 fractional digits, e.g. "25.0000"
	GatewayTxnID      string // gateway id (Stripe PaymentIntent / PayPal order); "" if not yet known
	GatewayCustomerID string // optional
	Description       string // optional
}

CreateTopupInput is the data needed to open a pending topup transaction.

type CreateTopupResult

type CreateTopupResult struct {
	TransactionID uuid.UUID
	GatewayTxnID  string
	ClientSecret  string // Stripe.js client secret; empty for PayPal
}

CreateTopupResult is what the browser needs to complete a topup.

type DB

type DB interface {
	QueryRow(query string, args ...interface{}) *sql.Row
	Query(query string, args ...interface{}) (*sql.Rows, error)
	Exec(query string, args ...interface{}) (sql.Result, error)
	// Begin starts a transaction for multi-statement work — the atomic
	// topup-complete + balance-credit path.
	Begin() (*sql.Tx, error)
}

DB is the minimal database surface the billing service needs. A standard *sql.DB satisfies it.

type DirectChargeInput added in v0.4.0

type DirectChargeInput struct {
	TenantID    uuid.UUID
	MethodID    uuid.UUID // saved payment_methods.id to charge
	AmountCents int64     // must be > 0
	// TransactionType is the ledger transaction_type (e.g. "subscription"). It
	// must be non-empty and != "topup" (so a direct charge can never land on the
	// crediting path). The host schema's CHECK governs the allowed set.
	TransactionType string
	Description     string            // ledger description, e.g. "Growth plan — 2026-06"
	Metadata        map[string]string // optional extra gateway metadata; merged with tenant_id/purpose/saved_method_id
}

DirectChargeInput asks the service to charge a tenant's saved method off-session for a purpose OTHER than a wallet top-up (e.g. a subscription fee). Unlike a top-up, the tenant balance is never credited — the card charge is the payment itself.

type DirectChargeResult added in v0.4.0

type DirectChargeResult struct {
	TransactionID uuid.UUID
	GatewayTxnID  string
	Succeeded     bool
}

DirectChargeResult reports the recorded transaction and whether the charge settled synchronously. Succeeded=false means the gateway accepted the payment but it has not settled yet; the transaction is recorded 'pending' and settled later by the webhook (still without crediting the balance).

type Gateway

type Gateway interface {
	Name() string
	// Validate confirms the configured credentials work via a cheap
	// authenticated call (used by the admin "test connection" action).
	Validate(ctx context.Context) error
	CreateTopupPayment(ctx context.Context, in TopupPaymentInput) (TopupPaymentResult, error)
	ParseWebhook(payload []byte, headers http.Header) (WebhookEvent, error)
}

Gateway is the payment-gateway contract. An implementation wraps one provider's API and webhook verification; the gateway-agnostic billing.Service owns persistence, balance, and idempotency.

type GatewaySetting

type GatewaySetting struct {
	Name        string
	DisplayName string
	IsEnabled   bool
	IsTestMode  bool
	Config      map[string]string
}

GatewaySetting is one payment gateway's stored configuration.

Config holds the decrypted credential key/values. Keys are gateway-specific and interpreted by the gateway implementations, not by this core package:

stripe → publishable_key, secret_key, webhook_secret
paypal → client_id, client_secret, webhook_id, return_url, cancel_url
         (return_url/cancel_url are required only to save methods / vault)

Config is empty when the gateway row exists but has not been configured yet.

type PaymentMethod

type PaymentMethod struct {
	ID           uuid.UUID `json:"id"`
	Gateway      string    `json:"gateway"`
	PaymentType  string    `json:"payment_type"`
	CardBrand    string    `json:"card_brand"`
	CardLast4    string    `json:"card_last4"`
	CardExpMonth int       `json:"card_exp_month"`
	CardExpYear  int       `json:"card_exp_year"`
	Nickname     string    `json:"nickname"`
	IsDefault    bool      `json:"is_default"`

	GatewayCustomerID      string `json:"-"`
	GatewayPaymentMethodID string `json:"-"`
}

PaymentMethod is a saved payment method (a stored card / wallet token) a tenant can reuse. The gateway reference ids are kept internal and never serialized to clients.

type SaveSetupRequest added in v0.6.0

type SaveSetupRequest struct {
	TenantID      uuid.UUID
	Gateway       string // GatewayStripe | GatewayPayPal
	CustomerEmail string // labels the gateway customer if one is created
	CustomerName  string
}

SaveSetupRequest is the input to CreateSaveSetup.

type SaveSetupResult added in v0.6.0

type SaveSetupResult struct {
	Gateway       string `json:"gateway"`
	ClientSecret  string `json:"client_secret,omitempty"`   // Stripe
	SetupIntentID string `json:"setup_intent_id,omitempty"` // Stripe (reference)
	SetupTokenID  string `json:"setup_token_id,omitempty"`  // PayPal (reference)
	ApprovalURL   string `json:"approval_url,omitempty"`    // PayPal
	CustomerID    string `json:"-"`                         // gateway customer id (internal)
}

SaveSetupResult carries what the browser needs to finish saving a payment method WITHOUT a charge. Stripe returns a SetupIntent client secret (confirmed in the browser with Stripe.js confirmSetup); PayPal returns a Vault setup-token id plus the buyer approval URL (approved with the PayPal Buttons SDK). Exactly one gateway's fields are populated. The reference the host later passes to PersistSavedMethodFromSetup is SetupIntentID (Stripe) or SetupTokenID (PayPal).

type SavedChargeInput

type SavedChargeInput struct {
	CustomerID      string            // gateway customer id owning the method
	PaymentMethodID string            // stored payment-method id to charge
	AmountCents     int64             // gateway minor units (e.g. cents)
	Currency        string            // ISO 4217, lower-case (e.g. "usd")
	Metadata        map[string]string // optional, attached at the gateway
}

SavedChargeInput asks a gateway to charge an already-stored payment method off-session (no buyer present), to top up from a saved card.

type SavedMethodDetails added in v0.3.0

type SavedMethodDetails struct {
	CustomerID      string
	PaymentMethodID string
	PaymentType     string // "card" | "paypal_wallet"
	Card            *CardDetails
	PayPalEmail     string
	PayPalPayerID   string
}

SavedMethodDetails describes a payment method to persist, produced by a gateway when a method is saved during a payment: a Stripe card (retrieved after its webhook) or a PayPal wallet (vaulted during capture). Card is set for cards; PayPalEmail/PayPalPayerID for a PayPal wallet.

type SavedMethodGateway

type SavedMethodGateway interface {
	Gateway
	// CreateCustomer returns the provider customer id that stored methods are
	// grouped under (Stripe creates one via the API; PayPal derives a stable
	// synthetic id, as PayPal has no customer object).
	CreateCustomer(ctx context.Context, email, name string, metadata map[string]string) (customerID string, err error)
	// ChargeSavedMethod charges a stored method off-session. It returns the
	// gateway transaction id and whether the charge settled synchronously
	// (succeeded); when false the payment settles later via webhook.
	ChargeSavedMethod(ctx context.Context, in SavedChargeInput) (gatewayTxnID string, succeeded bool, err error)
	// DetachPaymentMethod removes a stored method at the provider.
	DetachPaymentMethod(ctx context.Context, paymentMethodID string) error
}

SavedMethodGateway is a Gateway that can store a payment method for a tenant and reuse it for off-session charges — Stripe (customers + cards) and PayPal (vaulted wallet tokens). The gateway-agnostic Service owns the saved-method ledger (payment_methods); this interface is only the provider-side calls.

type Service

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

Service is the gateway-agnostic billing core.

func NewService

func NewService(db DB, key []byte, namespace string) *Service

NewService constructs the billing service. key is a 32-byte AES-256 key used to encrypt gateway credentials at rest; namespace is mixed into the AAD so a ciphertext is bound to (namespace, gateway) — pass a stable host-specific value such as "payment_gateway".

func (*Service) CaptureTopup

func (s *Service) CaptureTopup(ctx context.Context, tenantID uuid.UUID, gateway, gatewayTxnID string) (bool, error)

CaptureTopup captures a gateway payment the tenant created (PayPal order) and completes the topup. Returns credited=true when the balance was credited. Ownership is verified: the transaction must belong to the requesting tenant.

func (*Service) ChargeSavedMethod

func (s *Service) ChargeSavedMethod(ctx context.Context, tenantID, methodID uuid.UUID, amountCents int64) (CreateTopupResult, error)

ChargeSavedMethod tops up a tenant's balance using a stored card, charged off-session at the gateway. It records a pending topup transaction and, when the charge settles synchronously, credits the balance immediately (the webhook is an idempotent backstop for the asynchronous case). A declined card surfaces as an error and records no transaction.

func (*Service) ChargeSavedMethodDirect added in v0.4.0

func (s *Service) ChargeSavedMethodDirect(ctx context.Context, in DirectChargeInput) (DirectChargeResult, error)

ChargeSavedMethodDirect charges a tenant's stored method off-session and records the payment as a non-topup transaction WITHOUT crediting the tenant's balance — the money-out counterpart to ChargeSavedMethod (which tops the wallet up). Here the card charge IS the payment (e.g. a subscription fee).

A declined / authentication-required card surfaces as an error and records no transaction, mirroring ChargeSavedMethod. The caller owns per-purpose idempotency (e.g. charging a given subscription period at most once): each call creates a new gateway payment.

func (*Service) CompleteTopup

func (s *Service) CompleteTopup(transactionID uuid.UUID) (credited bool, err error)

CompleteTopup marks a pending topup completed and credits the tenant's balance — atomically and idempotently. The status flip and the balance credit run in one transaction; the flip's WHERE status='pending' guard means a redelivered/duplicate call matches zero rows and credits nothing. Returns credited=true only when this call performed the transition.

func (*Service) CreateSaveSetup added in v0.6.0

func (s *Service) CreateSaveSetup(ctx context.Context, in SaveSetupRequest) (SaveSetupResult, error)

CreateSaveSetup starts saving a tenant's payment method WITHOUT a charge: it ensures the tenant's gateway customer exists, then asks the gateway to begin the save (Stripe SetupIntent / PayPal Vault setup-token). The browser completes it, after which the host calls PersistSavedMethodFromSetup. The gateway must be enabled and support setup, else an error is returned.

func (*Service) CreateTopup

func (s *Service) CreateTopup(ctx context.Context, in TopupRequest) (CreateTopupResult, error)

CreateTopup starts a topup: enforces the enabled flag, creates the payment at the gateway, and records a pending transaction. The browser then completes it (Stripe.js confirm → webhook; PayPal approve → CaptureTopup). When in.SaveCard is set (Stripe), the tenant's gateway customer is ensured and the confirmed card is saved for reuse by the webhook once the payment succeeds.

func (*Service) CreateTopupTransaction

func (s *Service) CreateTopupTransaction(in CreateTopupInput) (uuid.UUID, error)

CreateTopupTransaction inserts a pending topup row and returns its id. The amount is credited to the tenant's balance only later, by CompleteTopup, once the gateway confirms payment; transaction_type is 'topup', status 'pending'.

func (*Service) DeletePaymentMethod

func (s *Service) DeletePaymentMethod(ctx context.Context, tenantID, methodID uuid.UUID) error

DeletePaymentMethod removes a tenant's saved method: it detaches the method at the provider (best-effort — the local row is always removed, as in LeanPBX) then soft-deletes it (is_active=false) and promotes another active method to default if the deleted one was the default. Returns ErrPaymentMethodNotFound for an unknown or already-removed method.

func (*Service) Gateway

func (s *Service) Gateway(name string) (Gateway, error)

Gateway returns a configured gateway implementation, built from its stored (decrypted) settings — regardless of the enabled flag, so credentials can be tested before the gateway is enabled. Returns ErrGatewayNotFound if the gateway has not been configured. The enabled flag is enforced separately by the payment path.

func (*Service) GatewaySetting

func (s *Service) GatewaySetting(name string) (*GatewaySetting, error)

GatewaySetting loads one gateway's configuration and decrypts its credentials. Returns ErrGatewayNotFound if no row exists for the gateway.

func (*Service) ListPaymentMethods

func (s *Service) ListPaymentMethods(tenantID uuid.UUID) ([]PaymentMethod, error)

ListPaymentMethods returns a tenant's active saved methods, default first then most-recently-used. Gateway reference ids are populated but not serialized to clients (json:"-").

func (*Service) MarkTopupFailed

func (s *Service) MarkTopupFailed(transactionID uuid.UUID) error

MarkTopupFailed marks a pending topup failed. Idempotent — it only flips a row still in 'pending', so a duplicate failure event is a no-op and it never overrides an already-completed topup.

func (*Service) PersistSavedMethodFromSetup added in v0.6.0

func (s *Service) PersistSavedMethodFromSetup(ctx context.Context, tenantID uuid.UUID, gateway, reference string) error

PersistSavedMethodFromSetup finalizes a no-charge save after the browser confirmed/approved it: it resolves the method at the gateway and stores it in the saved-method ledger (idempotent; the tenant's first method becomes the default). reference is the Stripe SetupIntent id or the PayPal approved setup-token id. The gateway need only be configured (not enabled), so a method the buyer already approved can still be saved if the gateway was toggled off between setup and persist.

func (*Service) ProcessWebhook

func (s *Service) ProcessWebhook(ctx context.Context, gateway string, payload []byte, headers http.Header) error

ProcessWebhook verifies an inbound gateway webhook, records it for audit, and applies its effect to the matching topup. Signature verification is delegated to the gateway. Money-safety against reprocessing comes from CompleteTopup's pending-guard (and MarkTopupFailed's), not from webhook dedup — so a redelivered webhook is safe even though the audit row is inserted only once.

func (*Service) SetDefaultPaymentMethod

func (s *Service) SetDefaultPaymentMethod(tenantID, methodID uuid.UUID) error

SetDefaultPaymentMethod makes one of the tenant's active methods the default (default is one per tenant). The flip is a single statement so there is never a window with two defaults. Returns ErrPaymentMethodNotFound for an unknown or inactive method.

func (*Service) TenantBalance

func (s *Service) TenantBalance(tenantID uuid.UUID) (string, error)

TenantBalance returns the tenant's prepaid balance as an exact decimal string (e.g. "25.0000"). Money is never represented as a float in Go — NUMERIC arithmetic stays in Postgres. Returns ErrTenantNotFound for an unknown tenant id.

func (*Service) TopupOptions

func (s *Service) TopupOptions(tenantID uuid.UUID) (TopupOptions, error)

TopupOptions returns the tenant balance plus each *enabled* gateway's public key, for initializing the topup page. Secret values are never included.

func (*Service) UpsertGatewaySetting

func (s *Service) UpsertGatewaySetting(name, displayName string, isEnabled, isTestMode bool, config map[string]string) error

UpsertGatewaySetting creates or updates a gateway's configuration. config is encrypted at rest. Pass a nil/empty map to clear credentials while keeping the row (e.g. to disable a gateway without losing its toggles). updated_at is maintained by the update_payment_gateway_settings_updated_at trigger.

type SetupGateway added in v0.6.0

type SetupGateway interface {
	Gateway
	// CreateSaveSetup starts an off-session save with no charge. customerID is the
	// gateway customer the method attaches to (Stripe); PayPal ignores it.
	CreateSaveSetup(ctx context.Context, customerID string, metadata map[string]string) (SaveSetupResult, error)
	// PersistSavedMethodFromSetup resolves the saved method after the buyer
	// confirmed/approved. reference is the Stripe SetupIntent id or the PayPal
	// approved setup-token id.
	PersistSavedMethodFromSetup(ctx context.Context, reference string) (SavedMethodDetails, error)
}

SetupGateway is an optional Gateway capability: saving a tenant's payment method WITHOUT charging, then persisting it once the buyer confirms (Stripe SetupIntent) or approves (PayPal Vault setup-token → payment-token). It mirrors the optional-interface pattern of CapturableGateway. The gateway-agnostic Service owns the saved-method ledger; this is only the provider-side calls.

type TopupGatewayOption

type TopupGatewayOption struct {
	Name      string `json:"name"`
	TestMode  bool   `json:"test_mode"`
	PublicKey string `json:"public_key"` // Stripe publishable_key / PayPal client_id (browser-safe)
}

TopupGatewayOption tells the browser how to render a gateway payment option.

type TopupOptions

type TopupOptions struct {
	Balance  string               `json:"balance"`
	Gateways []TopupGatewayOption `json:"gateways"`
}

TopupOptions is the data the topup page needs to initialize.

type TopupPaymentInput

type TopupPaymentInput struct {
	AmountCents int64             // gateway minor units (e.g. cents)
	Currency    string            // ISO 4217, lower-case (e.g. "usd")
	Metadata    map[string]string // optional, attached at the gateway

	// CustomerID + SaveCard request that the card be saved for future
	// off-session topups: the payment is attached to CustomerID with
	// setup_future_usage. Honoured only by gateways implementing
	// SavedMethodGateway; ignored otherwise. When SaveCard is set, CustomerID
	// must be non-empty.
	CustomerID string
	SaveCard   bool
}

TopupPaymentInput asks a gateway to start a topup payment.

type TopupPaymentResult

type TopupPaymentResult struct {
	GatewayTxnID string // gateway transaction id (Stripe PaymentIntent id / PayPal order id)
	ClientSecret string // Stripe.js client secret; empty for gateways without one (PayPal)
}

TopupPaymentResult carries the gateway transaction id plus the client data the browser needs to complete the payment.

type TopupRequest

type TopupRequest struct {
	TenantID    uuid.UUID
	Gateway     string // GatewayStripe | GatewayPayPal
	AmountCents int64

	// SaveCard stores the card for future off-session topups (Stripe only).
	// CustomerEmail/CustomerName label the gateway customer if one is created.
	SaveCard      bool
	CustomerEmail string
	CustomerName  string
}

TopupRequest is the input to CreateTopup.

type WebhookEvent

type WebhookEvent struct {
	EventID      string           // gateway event id (e.g. Stripe evt_…, PayPal WH-…)
	EventType    string           // raw gateway event type, for the log
	Kind         WebhookEventKind // normalized action
	GatewayTxnID string           // gateway transaction/resource id this event concerns

	// Saved-method hints, populated for a successful topup that set up a method
	// for future use (Stripe). The webhook handler uses them to persist the
	// card after crediting the balance. Empty/false when no method was saved.
	PaymentMethodID    string // gateway payment-method id (Stripe pm_…)
	CustomerID         string // gateway customer id the method is attached to
	SavesPaymentMethod bool   // the payment was set up for future off-session use
}

WebhookEvent is a gateway-agnostic view of a verified webhook. The HTTP webhook handler uses EventID for idempotency logging and (Kind, GatewayTxnID) to act on the payment.

type WebhookEventKind

type WebhookEventKind int

WebhookEventKind is the normalized meaning of a gateway webhook, mapped from each provider's native event types so the webhook handler stays gateway-agnostic.

const (
	WebhookIgnored        WebhookEventKind = iota // acknowledged, no action
	WebhookTopupSucceeded                         // a topup payment completed
	WebhookTopupFailed                            // a topup payment failed
)

Directories

Path Synopsis
internal
crypto
Package crypto provides the authenticated symmetric encryption used to protect gateway credentials at rest: AES-256-GCM with the ciphertext bound to caller-supplied additional authenticated data (AAD).
Package crypto provides the authenticated symmetric encryption used to protect gateway credentials at rest: AES-256-GCM with the ciphertext bound to caller-supplied additional authenticated data (AAD).

Jump to

Keyboard shortcuts

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