Documentation
¶
Index ¶
- Variables
- func ContextWithRequest(ctx context.Context, r *http.Request) context.Context
- func NewWebhookVerificationError(reason string) error
- func RequestFromContext(ctx context.Context) *http.Request
- type CreateCheckoutRequest
- type CreateCheckoutResponse
- type EventData
- type Factory
- type GatewayInfo
- type GatewayRegistry
- type HTTPHandler
- type InteractionType
- type InternalPaymentEvent
- type PaymentGateway
- type PaymentRepository
- type PaymentService
- type PaymentStatus
- type PaymentTransaction
- type RenderInfo
- type SessionRequest
- type SessionResponse
- type TaskCompleter
- type ValidateReferenceRequest
- type ValidateReferenceResponse
- type ValidationResponse
- type ValidationTransaction
- type WebhookPayload
- type WebhookResponse
- type WebhookStatus
Constants ¶
This section is empty.
Variables ¶
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.
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.
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.
var ErrWebhookVerificationFailed = errors.New("webhook verification failed")
ErrWebhookVerificationFailed indicates a caller — either a real-time validation request or an asynchronous webhook notification — could not be verified as genuinely originating from the gateway it claims to be. No transaction may be settled, and no presentment information may be disclosed, on the strength of an unverified caller, so this must be checked (and satisfied) before any gateway-specific parsing of the request runs.
Functions ¶
func ContextWithRequest ¶ added in v0.2.0
ContextWithRequest returns a new context with the given inbound HTTP request attached. HTTPHandler calls this — for every request, on both HandleValidateReference and HandleWebhook — before invoking PaymentService, so that by the time a gateway's VerifyWebhook runs, anything about the request beyond the explicit body and headers parameters (method, URL, query parameters, TLS connection state, remote address, cookies, trailers, or any other field of *http.Request) is reachable via RequestFromContext. core/payment never has to anticipate which specific dimension a given verification scheme needs; a gateway pulls out whatever it requires.
By the time this is called, HTTPHandler has already drained r.Body via io.ReadAll to produce the explicit body []byte parameter passed to VerifyWebhook/ParseWebhook/etc. — gateways must use that parameter for the payload; r.Body here will read as empty, not the original payload. This mechanism is for everything else about the request, not for re-reading the body.
r itself is stored as given — its own r.Context() at this point is whatever it was before this call (e.g. net/http's server-assigned context), not the context this function returns. RequestFromContext reconciles that on retrieval: see its doc comment.
func NewWebhookVerificationError ¶ added in v0.2.0
NewWebhookVerificationError builds the error a VerifyWebhook implementation should return once it has positively determined the caller is NOT genuinely this gateway (see VerifyWebhook's error-classification contract below). It wraps ErrWebhookVerificationFailed via %w, so errors.Is(err, ErrWebhookVerificationFailed) succeeds and HTTPHandler maps the response to 401.
Using this instead of hand-rolling fmt.Errorf("%s: %w", reason, ErrWebhookVerificationFailed) is entirely optional — any error that already wraps ErrWebhookVerificationFailed some other way is equally valid — but it gives gateway authors an easy, hard-to-get-wrong default, reducing the risk of forgetting the %w and misclassifying a genuine rejection as an operational failure.
func RequestFromContext ¶ added in v0.2.0
RequestFromContext extracts the inbound *http.Request previously attached via ContextWithRequest, or nil if none was ever attached — e.g. when PaymentService is invoked directly, bypassing HTTPHandler (as most unit tests do). Callers (typically a gateway's VerifyWebhook) must treat a nil return as "not available" and fall back to whatever the explicit body/headers parameters allow, rather than treating nil as a zero-value request. The returned *http.Request must not be mutated: HTTPHandler is still using the underlying request to serve the response after PaymentService returns, and other callers may hold their own copy.
The returned request is rewrapped via r.WithContext(ctx) using the exact ctx this function is called with, so its own .Context() is that same ctx — not just whatever context ContextWithRequest happened to return earlier. This stays consistent even if ctx is wrapped further after ContextWithRequest ran, since context.Value lookups delegate to parent contexts regardless of how many layers were added afterward. One consequence: repeated calls to RequestFromContext, even with the same ctx, return distinct *http.Request instances (shallow copies sharing the same underlying Header/URL/Body/etc.), not the exact same pointer every time.
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
// ValidateMetadata checks that a checkout request's pass-through metadata
// carries whatever this gateway cannot operate without.
//
// It is called once per checkout, before a reference number is generated
// and before anything is persisted, so a caller that omitted a required
// key fails immediately — while the configuration responsible is still
// nameable in the error — instead of surfacing much later as an opaque
// callback failure against a transaction nobody can trace back.
//
// This is a presence/well-formedness check on the request only; it must
// not perform I/O and must not depend on any state outside metadata.
// Deciding whether the declared values are the *correct* ones for a given
// transaction belongs on the callback paths, not here.
//
// A gateway with no metadata requirements returns nil. The method is
// required rather than optional so that every gateway author has to make
// that choice deliberately instead of silently forgetting to opt in.
ValidateMetadata(metadata map[string]string) error
// CreateSession initializes a payment session with the gateway.
CreateSession(ctx context.Context, req SessionRequest) (*SessionResponse, error)
// VerifyWebhook authenticates an inbound request — a real-time
// validation request or an asynchronous webhook notification — as
// genuinely originating from this gateway, using whatever scheme the
// gateway requires (e.g. an HMAC signature over body, a bearer token
// extracted from headers, an IP allowlist, or a server-side status
// check against the gateway's own API — not every scheme needs to be
// cryptographic). It is called before ExtractReferenceNumber and
// ParseWebhook, and a non-nil return blocks both: no reference lookup, no
// settlement, and no presentment info may reach an unverified caller.
// There is no default/no-op — every implementation must perform a real
// check.
//
// Error contract: return ErrWebhookVerificationFailed (wrapped via %w)
// only when verification has positively determined the caller is NOT
// genuinely this gateway (e.g. an invalid signature, an expired or
// unrecognized token). HTTPHandler maps that sentinel to 401. Return any
// other error for a failure to complete verification for an operational
// reason (a timeout reaching an upstream introspection/JWKS endpoint, a
// missing local configuration, a cancelled context) — those are NOT proof
// the caller is invalid and must not use this sentinel; they are treated
// as transient (mapped to 500, so the gateway's retry can re-drive it),
// exactly like an unclassified error from any of this interface's other
// methods. Forgetting to wrap the sentinel for a genuine rejection has a
// concrete cost: HTTPHandler responds 500 instead of 401, so the caller
// burns its retry budget retrying a request that can never succeed, and
// monitoring records it as a transient/internal failure rather than an
// auth rejection. See NewWebhookVerificationError for a helper that
// builds a correctly-wrapped rejection.
//
// For a scheme needing anything beyond body/headers — e.g. query
// parameters, HTTP method, request path, TLS connection state, or remote
// address — see RequestFromContext, which HTTPHandler populates with the
// full inbound *http.Request before this is invoked.
VerifyWebhook(ctx context.Context, body []byte, headers map[string][]string) 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, headers map[string][]string) (*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"`
// Metadata is the checkout request's pass-through metadata, forwarded
// verbatim so a gateway can read whatever it declared as required in
// ValidateMetadata. It is the same map persisted on the transaction as
// GatewayMetadata, so anything readable here is recoverable later by
// reference number.
Metadata map[string]string `json:"metadata,omitempty"`
}
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" )