payment

package module
v0.1.0 Latest Latest
Warning

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

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

README

Payment Package

The payment package provides a modular and extensible payment orchestration system. It follows a gateway-based architecture, separating protocol-level concerns from domain logic, and is designed to be imported and integrated into other repositories.

Architecture Overview

The system consists of several key components:

  1. PaymentGateway: An interface for gateway-specific integrations (e.g., LankaPay, GovPay). It handles session creation, webhook parsing, and real-time validation formatting.
  2. GatewayRegistry: A pure discovery and lookup service. It manages gateway registration, configuration injection, and provides sanitized metadata for the UI.
  3. PaymentRepository: Handles persistence for PaymentTransaction records using GORM.
  4. PaymentService: The high-level orchestrator. It uses the Registry to find the correct Gateway and coordinates between the gateway logic, database, and internal events.
  5. HTTPHandler: Exposes the payment service via RESTful endpoints for both public and internal use.

Integration

This package is designed to be imported into other repositories. To use it:

import "github.com/OpenNSW/core/payment"

Getting Started

1. Implement a PaymentGateway

Each payment gateway requires a dedicated implementation of the PaymentGateway interface.

type MyGateway struct {}

func (g *MyGateway) ApplyConfig(config json.RawMessage) error {
    // Inject gateway-specific settings from JSON
    return nil
}

func (g *MyGateway) GetFlowType() payment.InteractionType {
    return payment.FlowTypeRedirect
}

func (g *MyGateway) CreateSession(ctx context.Context, req payment.SessionRequest) (*payment.SessionResponse, error) {
    // Logic to initialize session with gateway
    return &payment.SessionResponse{...}, nil
}

func (g *MyGateway) ExtractReferenceNumber(ctx context.Context, reqData json.RawMessage) (string, error) {
    // Parse gateway-specific validation request to find the reference
    return "REF-123", nil
}

func (g *MyGateway) HandleValidateReference(ctx context.Context, tx *payment.ValidationTransaction, isPayable bool, reqData json.RawMessage) (*payment.ValidationResponse, error) {
    // Format the final response for the gateway
    return &payment.ValidationResponse{...}, nil
}

func (g *MyGateway) ParseWebhook(ctx context.Context, body []byte, headers map[string][]string) (*payment.WebhookPayload, error) {
    // Logic to parse and validate gateway webhook
    return &payment.WebhookPayload{...}, nil
}
2. Configure Payment Methods

The payment_methods.json file is the source of truth for available methods.

{
  "version": "1.0",
  "methods": [
    {
      "id": "lankapay",
      "is_active": true,
      "render_info": {
        "display_name": "Credit/Debit Card (LankaPay)",
        "description": "Pay securely using your card.",
        "display_order": 1
      },
      "config": {
        "base_url": "https://sandbox.govpay.lk"
      }
    }
  ]
}
3. Instantiate the Registry

The GatewayRegistry loads the configuration and maps each method ID to its implementation.

gateways := map[string]payment.PaymentGateway{
    "lankapay": &lankapay.Gateway{},
    "govpay":   &govpay.Gateway{},
}

registry, err := payment.NewRegistry("configs/payment_methods.json", gateways)
4. Setup the Orchestrator

The PaymentService acts as the orchestrator using the Registry as a lookup.

repo := payment.NewPaymentRepository(db)
service := payment.NewPaymentService(repo, registry)

handler := payment.NewHTTPHandler(service)

Key Flows

Checkout Initialization

The frontend calls CreateCheckoutSession. The Service generates an NSW reference, looks up the gateway implementation via the Registry, and delegates the session creation to that gateway.

Real-Time Validation

When a user enters a reference in a bank app, the gateway calls NSW.

  1. The Service uses the Gateway to Extract the reference number.
  2. The Service fetches the transaction from the Database.
  3. The Service passes the record back to the Gateway to Validate and format the protocol-specific response.
Webhook Processing

Gateways notify the payment service of results. The Service looks up the gateway via the Registry, delegates the parsing, and then performs domain actions: updating status, persisting metadata, and firing internal events.

Exported Types and Functions

Core Interfaces
  • PaymentGateway: Interface for gateway implementations
  • PaymentService: Main orchestrator service
  • PaymentRepository: Database persistence layer
Data Types
  • SessionRequest: Checkout session initialization
  • SessionResponse: Session response with checkout details
  • WebhookPayload: Incoming webhook data
  • ValidationTransaction: Transaction details for validation
  • ValidationResponse: Validation response format
  • InteractionType: Enum for flow types (REDIRECT, INSTRUCTION)
  • WebhookStatus: Canonical webhook status (PENDING, SUCCESS, FAILED)
Constructor Functions
  • NewRegistry(configPath string, gateways map[string]PaymentGateway): Create a gateway registry
  • NewPaymentService(repo PaymentRepository, registry *GatewayRegistry): Create payment service
  • NewPaymentRepository(db *gorm.DB): Create payment repository
  • NewHTTPHandler(service PaymentService): Create HTTP handler
Error Types
  • ErrUnsupportedWebhookStatus: Gateway status cannot be normalized
  • ErrTransactionNotFound: Payment transaction not found
  • ErrAmountMismatch: Payment amount or currency mismatch

Integration Example

In your consuming repository:

package main

import (
    "github.com/OpenNSW/core/payment"
)

func setupPayments(db *gorm.DB) *payment.HTTPHandler {
    // Create your gateway implementations
    gateways := map[string]payment.PaymentGateway{
        "your-gateway": &yourgateway.Gateway{},
    }
    
    // Initialize registry
    registry, err := payment.NewRegistry("path/to/config.json", gateways)
    if err != nil {
        panic(err)
    }
    
    // Setup service
    repo := payment.NewPaymentRepository(db)
    service := payment.NewPaymentService(repo, registry)
    
    // Return handler for HTTP endpoints
    return payment.NewHTTPHandler(service)
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrAmountMismatch = errors.New("webhook amount/currency mismatch")

ErrAmountMismatch indicates a successful-payment webhook reported an amount or currency that doesn't match the recorded transaction. Permanent and suspicious, so it is never marked paid and the gateway should not retry.

View Source
var ErrTransactionNotFound = errors.New("payment transaction not found")

ErrTransactionNotFound indicates no payment transaction matches a given reference. This is a permanent condition, so callers (e.g. the webhook handler) should not ask the gateway to retry.

View Source
var ErrUnsupportedWebhookStatus = errors.New("unsupported webhook status")

ErrUnsupportedWebhookStatus indicates a gateway status that could not be normalized into a WebhookStatus. It is a permanent condition (retrying the same payload won't help), so callers should not signal the gateway to retry.

Functions

This section is empty.

Types

type CreateCheckoutRequest

type CreateCheckoutRequest struct {
	GatewayID          string            `json:"gateway_id"`
	Amount             decimal.Decimal   `json:"amount"`
	Currency           string            `json:"currency"`
	SuccessRedirectURL string            `json:"success_redirect_url,omitempty"` // Optional: User redirect on success
	CancelRedirectURL  string            `json:"cancel_redirect_url,omitempty"`  // Optional: User redirect on cancel
	ExpiresAt          time.Time         `json:"expires_at"`                     // Aligned with Task TTL
	Metadata           map[string]string `json:"metadata"`                       // Pass-through data (e.g., TaskID)
}

CreateCheckoutRequest is the payload sent to initialize a session.

type CreateCheckoutResponse

type CreateCheckoutResponse struct {
	ReferenceNumber string          `json:"reference_number"` // The generated NSW reference
	SessionID       string          `json:"session_id"`
	Type            InteractionType `json:"type"`
	CheckoutURL     string          `json:"checkout_url,omitempty"` // The hosted URL to redirect the user to
	Instructions    string          `json:"instructions,omitempty"`
	ExpiresIn       int             `json:"expires_in_seconds"`
}

CreateCheckoutResponse is the expected reply from LankaPay.

type EventData

type EventData struct {
	TaskID               string          `json:"task_id"`
	ReferenceNumber      string          `json:"reference_number"`
	GatewayTransactionID string          `json:"gateway_transaction_id"`
	Status               PaymentStatus   `json:"status"`
	AmountPaid           decimal.Decimal `json:"amount_paid"`
	Currency             string          `json:"currency"`
	ConfirmedAt          string          `json:"confirmed_at"`
}

type Factory

type Factory func(config json.RawMessage) (PaymentGateway, error)

Factory constructs a configured, ready-to-use gateway from its raw config. One factory per gateway type; the registry calls it once at init so gateways are immutable after construction (no post-init config mutation).

type GatewayInfo

type GatewayInfo struct {
	ID         string          `json:"id"`
	IsActive   bool            `json:"is_active"`
	RenderInfo RenderInfo      `json:"render_info"`
	Config     json.RawMessage `json:"config,omitempty"`
}

GatewayInfo is the aggregate DTO used for gateway discovery.

type GatewayRegistry

type GatewayRegistry interface {
	// Get retrieves a gateway implementation by its ID.
	Get(id string) (PaymentGateway, error)

	// ListInfo returns the aggregated metadata for all supported gateways.
	ListInfo() []GatewayInfo
}

GatewayRegistry manages the discovery and lookup of payment gateways.

func NewRegistry

func NewRegistry(configPath string, factories map[string]Factory) (GatewayRegistry, error)

NewRegistry initializes a new registry by loading configuration from a file. For each configured gateway it invokes the matching factory to construct a fully configured implementation, so gateways are immutable after init.

type HTTPHandler

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

HTTPHandler handles public HTTP requests for the Payment Service.

func NewHTTPHandler

func NewHTTPHandler(service PaymentService) *HTTPHandler

NewHTTPHandler creates a new handler.

func (*HTTPHandler) HandleValidateReference

func (h *HTTPHandler) HandleValidateReference(w http.ResponseWriter, r *http.Request)

HandleValidateReference handles POST /api/v1/payments/:gatewayId/validate Called by gateways to query if a reference number is valid and payable.

func (*HTTPHandler) HandleWebhook

func (h *HTTPHandler) HandleWebhook(w http.ResponseWriter, r *http.Request)

HandleWebhook handles POST /api/v1/payments/:gatewayID/webhook Called by payment gateways to notify about payment successes and failures.

type InteractionType

type InteractionType string
const (
	FlowTypeRedirect    InteractionType = "REDIRECT"
	FlowTypeInstruction InteractionType = "INSTRUCTION"
)

type InternalPaymentEvent

type InternalPaymentEvent struct {
	EventType string    `json:"event_type"`
	Data      EventData `json:"data"`
}

InternalPaymentEvent represents the internal event the Payment Service fires for the Task Engine.

type PaymentGateway

type PaymentGateway interface {
	// GetFlowType returns the flow type of the gateway (REDIRECT or INSTRUCTION).
	GetFlowType() InteractionType

	// CreateSession initializes a payment session with the gateway.
	CreateSession(ctx context.Context, req SessionRequest) (*SessionResponse, error)

	// ExtractReferenceNumber parses the gateway-specific validation request to extract the reference number.
	ExtractReferenceNumber(ctx context.Context, reqData json.RawMessage) (string, error)

	// HandleValidateReference formats the gateway-specific validation response.
	// tx is nil when no matching transaction exists (unknown reference or a
	// mismatched gateway); isPayable is the domain decision (exists, owned by
	// this gateway, pending, and not expired) the gateway should reflect back.
	HandleValidateReference(ctx context.Context, tx *ValidationTransaction, isPayable bool, reqData json.RawMessage) (*ValidationResponse, error)

	// ParseWebhook processes raw gateway notifications into a domain-neutral
	// payload (for the service to act on) together with the gateway-specific
	// acknowledgement to relay back once the notification has been accepted.
	ParseWebhook(ctx context.Context, body []byte, headers map[string][]string) (*WebhookPayload, *WebhookResponse, error)
}

PaymentGateway defines the interface for external payment gateway integration.

type PaymentRepository

type PaymentRepository interface {
	Create(ctx context.Context, tx *PaymentTransaction) error
	GetByReferenceNumber(ctx context.Context, referenceNumber string) (*PaymentTransaction, error)
	// GetByReferenceNumberForUpdate reads a transaction while holding a row-level
	// write lock (SELECT ... FOR UPDATE). Must be called inside RunInTransaction.
	GetByReferenceNumberForUpdate(ctx context.Context, referenceNumber string) (*PaymentTransaction, error)
	GetByTaskID(ctx context.Context, taskID string) (*PaymentTransaction, error)
	Update(ctx context.Context, tx *PaymentTransaction) error
	UpdateStatus(ctx context.Context, referenceNumber string, status PaymentStatus) error
	// RunInTransaction runs fn inside a DB transaction, passing a repository bound
	// to that transaction. The transaction commits when fn returns nil and rolls
	// back on error.
	RunInTransaction(ctx context.Context, fn func(repo PaymentRepository) error) error
	WithTx(tx *gorm.DB) PaymentRepository
}

PaymentRepository defines the interface for managing PaymentTransactions.

func NewPaymentRepository

func NewPaymentRepository(db *gorm.DB) PaymentRepository

NewPaymentRepository creates a new instance of PaymentRepository.

type PaymentService

type PaymentService interface {
	// ListAvailableMethods returns the rendering information for all active payment gateways.
	ListAvailableMethods(ctx context.Context) ([]GatewayInfo, error)

	// CreateCheckoutSession initializes a payment session and generates a ReferenceNumber.
	CreateCheckoutSession(ctx context.Context, req CreateCheckoutRequest) (*CreateCheckoutResponse, error)

	// ValidateReference is used for real-time validation requests from gateways.
	ValidateReference(ctx context.Context, gatewayID string, rawBody json.RawMessage) (*ValidationResponse, error)

	// ProcessWebhook handles asynchronous notifications from payment gateways and
	// returns the gateway-specific acknowledgement to relay back to the gateway.
	ProcessWebhook(ctx context.Context, gatewayID string, body []byte, headers map[string][]string) (*WebhookResponse, error)

	// SetTaskCompleter injects the dependency used to advance the workflow when
	// a payment settles. Wired post-construction to avoid an import cycle with taskv2.
	SetTaskCompleter(completer TaskCompleter)
}

PaymentService defines the high-level orchestration for payments.

func NewPaymentService

func NewPaymentService(repo PaymentRepository, registry GatewayRegistry) PaymentService

NewPaymentService initializes a new payment service.

type PaymentStatus

type PaymentStatus string
const (
	PaymentStatusPending PaymentStatus = "PENDING"
	PaymentStatusSuccess PaymentStatus = "SUCCESS"
	PaymentStatusFailed  PaymentStatus = "FAILED"
)

type PaymentTransaction

type PaymentTransaction struct {
	ID              string            `json:"id" gorm:"type:text;not null;primaryKey"`
	ReferenceNumber string            `json:"reference_number" gorm:"uniqueIndex"` // Generated by Payment Service
	TaskID          string            `json:"task_id" gorm:"index"`                // Links back to the FSM Task Node
	GatewayID       string            `json:"gateway_id" gorm:"index"`             // e.g., "lankapay"
	SessionID       string            `json:"session_id"`                          // Gateway-specific session identifier
	Amount          decimal.Decimal   `json:"amount"`
	Currency        string            `json:"currency"`       // "LKR" or foreign currency
	Status          PaymentStatus     `json:"status"`         // PENDING, SUCCESS, FAILED, EXPIRED
	PaymentMethod   string            `json:"payment_method"` // CC, BANK_TRANSFER (populated on webhook)
	ExpiryDate      time.Time         `json:"expiry_date"`
	GatewayMetadata map[string]string `json:"gateway_metadata" gorm:"serializer:json"`
	CreatedAt       time.Time         `json:"created_at"`
	UpdatedAt       time.Time         `json:"updated_at"`
}

PaymentTransaction represents the internal state of a payment

type RenderInfo

type RenderInfo struct {
	DisplayName  string `json:"display_name"`
	Description  string `json:"description"`
	LogoURL      string `json:"logo_url"`
	DisplayOrder int    `json:"display_order"`
	PrimaryColor string `json:"primary_color,omitempty"`
}

RenderInfo contains UI-specific metadata for displaying a payment method.

type SessionRequest

type SessionRequest struct {
	Amount             decimal.Decimal `json:"amount"`
	Currency           string          `json:"currency"`
	SuccessRedirectURL string          `json:"success_redirect_url"`
	CancelRedirectURL  string          `json:"cancel_redirect_url"`
}

type SessionResponse

type SessionResponse struct {
	SessionID    string          `json:"session_id"`
	Type         InteractionType `json:"type"`
	CheckoutURL  string          `json:"checkout_url,omitempty"`
	Instructions string          `json:"instructions,omitempty"`
}

type TaskCompleter

type TaskCompleter interface {
	CompleteTaskStep(ctx context.Context, taskID string, payload map[string]any) error
}

TaskCompleter resumes a suspended workflow step once a payment reaches a terminal outcome. It is satisfied by the taskv2 TaskManager.

type ValidateReferenceRequest

type ValidateReferenceRequest struct {
	PaymentReference string `json:"paymentReference"` // Maps to our ReferenceNumber
	ServiceType      string `json:"serviceType"`      // e.g., NSW_IMPORT_PERMIT_CD
}

ValidateReferenceRequest is the payload GovPay sends when a user enters a reference in their bank app.

type ValidateReferenceResponse

type ValidateReferenceResponse struct {
	Amount     decimal.Decimal `json:"amount"`
	Currency   string          `json:"currency"`
	TraderName string          `json:"traderName"`
	OGAName    string          `json:"ogaName"`
	ExpiryDate string          `json:"expiryDate"` // ISO8601 format string
	IsPayable  bool            `json:"isPayable"`  // false if already paid or expired
	Remarks    string          `json:"remarks,omitempty"`
}

ValidateReferenceResponse is the payload we return to GovPay to auto-populate the user's screen.

type ValidationResponse

type ValidationResponse struct {
	Payload    json.RawMessage
	HTTPStatus int
}

ValidationResponse represents a structured response for a validation request.

type ValidationTransaction

type ValidationTransaction struct {
	ReferenceNumber string            `json:"reference_number"`
	Amount          decimal.Decimal   `json:"amount"`
	Currency        string            `json:"currency"`
	Status          string            `json:"status"`
	ExpiryDate      time.Time         `json:"expiry_date"`
	Metadata        map[string]string `json:"metadata"`
}

ValidationTransaction represents a minimal view of a payment transaction for validation purposes.

type WebhookPayload

type WebhookPayload struct {
	ReferenceNumber      string            `json:"reference_number"`
	SessionID            string            `json:"session_id"`
	GatewayTransactionID string            `json:"gateway_transaction_id"`
	Status               WebhookStatus     `json:"status"`
	Amount               decimal.Decimal   `json:"amount"`
	Currency             string            `json:"currency"`
	PaymentMethod        string            `json:"payment_method"`
	Timestamp            string            `json:"timestamp"`
	Metadata             map[string]string `json:"metadata"`
}

WebhookPayload represents the external callback from LankaPay to the Payment Service.

type WebhookResponse

type WebhookResponse struct {
	Payload    json.RawMessage
	HTTPStatus int
}

WebhookResponse is the gateway-specific acknowledgement returned to the gateway after a webhook (payment-completion) notification has been processed. For GovPay+ this carries the UpdateResponse (paymentData receipt).

type WebhookStatus

type WebhookStatus string

WebhookStatus is the canonical, gateway-neutral outcome a gateway must normalize its own status vocabulary into when parsing a webhook.

const (
	WebhookStatusPending WebhookStatus = "PENDING"
	WebhookStatusSuccess WebhookStatus = "SUCCESS"
	WebhookStatusFailed  WebhookStatus = "FAILED"
)

Jump to

Keyboard shortcuts

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