Skip to content

Payment Provider Adapters

This guide explains how to add new payment provider integrations to mpac-pgw.

Architecture

mpac-pgw uses a provider adapter pattern to abstract payment provider integrations. Each provider implements a common interface, and a registry manages provider lookup at runtime.

internal/
├── domain/provider/
│   ├── registry.go          # Provider registry
│   └── provider.go          # QRProvider interface
└── infrastructure/provider/
    ├── paypay/              # PayPay adapter (implemented)
    │   ├── adapter.go
    │   ├── types.go
    │   └── adapter_test.go
    ├── onepay/              # OnePay adapter (implemented)
    │   ├── adapter.go
    │   ├── types.go
    │   └── adapter_test.go
    └── <new-provider>/      # Your new provider
        ├── adapter.go
        ├── types.go
        └── adapter_test.go

QRProvider Interface

All QR payment providers must implement the QRProvider interface:

go
// QRProvider defines the contract for QR payment provider adapters.
type QRProvider interface {
    // CreateQRCode generates a QR code for a payment.
    // Returns the QR content URL and provider reference ID.
    CreateQRCode(ctx context.Context, req CreateQRRequest) (*CreateQRResponse, error)

    // GetPaymentStatus checks the current status of a payment at the provider.
    GetPaymentStatus(ctx context.Context, providerRefID string) (*PaymentStatusResponse, error)

    // CancelPayment cancels a pending payment at the provider.
    CancelPayment(ctx context.Context, providerRefID string) error

    // RefundPayment initiates a refund for a completed payment.
    RefundPayment(ctx context.Context, req RefundRequest) (*RefundResponse, error)

    // ProviderName returns the provider identifier (e.g., "PAYPAY").
    ProviderName() string
}

Adding a New Provider

Step 1: Create the adapter directory

bash
mkdir -p pgw/internal/infrastructure/provider/<provider_name>

Step 2: Define provider-specific types

Create types.go with request/response structures specific to the provider's API:

go
package providername

// CreateQRRequest is the provider-specific API request.
type providerCreateQRRequest struct {
    MerchantPaymentID string `json:"merchantPaymentId"`
    Amount            struct {
        Amount   int    `json:"amount"`
        Currency string `json:"currency"`
    } `json:"amount"`
    OrderDescription string `json:"orderDescription,omitempty"`
}

// CreateQRResponse is the provider-specific API response.
type providerCreateQRResponse struct {
    ResultInfo struct {
        Code    string `json:"code"`
        Message string `json:"message"`
    } `json:"resultInfo"`
    Data struct {
        CodeID  string `json:"codeId"`
        URL     string `json:"url"`
        DeepLink string `json:"deeplink"`
    } `json:"data"`
}

Step 3: Implement the adapter

Create adapter.go implementing the QRProvider interface:

go
package providername

import (
    "context"
    "fmt"
    "net/http"

    "github.com/mp-solution-inc/mpac-pgw/internal/domain/provider"
)

type Adapter struct {
    baseURL    string
    httpClient *http.Client
    // Provider-specific credentials
    apiKey     string
    apiSecret  string
    merchantID string
}

func NewAdapter(baseURL, apiKey, apiSecret, merchantID string) *Adapter {
    return &Adapter{
        baseURL:    baseURL,
        httpClient: &http.Client{},
        apiKey:     apiKey,
        apiSecret:  apiSecret,
        merchantID: merchantID,
    }
}

func (a *Adapter) ProviderName() string {
    return "PROVIDER_NAME"
}

func (a *Adapter) CreateQRCode(ctx context.Context, req provider.CreateQRRequest) (*provider.CreateQRResponse, error) {
    // 1. Map generic request to provider-specific format
    // 2. Call provider API
    // 3. Map provider response back to generic format
    // 4. Return result or error
    return nil, fmt.Errorf("not implemented")
}

func (a *Adapter) GetPaymentStatus(ctx context.Context, providerRefID string) (*provider.PaymentStatusResponse, error) {
    return nil, fmt.Errorf("not implemented")
}

func (a *Adapter) CancelPayment(ctx context.Context, providerRefID string) error {
    return fmt.Errorf("not implemented")
}

func (a *Adapter) RefundPayment(ctx context.Context, req provider.RefundRequest) (*provider.RefundResponse, error) {
    return nil, fmt.Errorf("not implemented")
}

Step 4: Register the provider

Add the new provider to the registry in the application startup:

go
// In cmd/server/main.go or similar initialization code
providerRegistry.Register(providername.NewAdapter(
    cfg.ProviderBaseURL,
    cfg.ProviderAPIKey,
    cfg.ProviderAPISecret,
    cfg.ProviderMerchantID,
))

Step 5: Add configuration

Add environment variables for the new provider:

yaml
# configs/config.yaml
provider_name:
  base_url: "https://api.provider.com"
  api_key: "${PROVIDER_API_KEY}"
  api_secret: "${PROVIDER_API_SECRET}"

Step 6: Write tests

Create adapter_test.go with unit tests using mocked HTTP responses:

go
package providername_test

import (
    "context"
    "net/http"
    "net/http/httptest"
    "testing"

    providername "github.com/mp-solution-inc/mpac-pgw/internal/infrastructure/provider/providername"
    "github.com/mp-solution-inc/mpac-pgw/internal/domain/provider"
)

func TestCreateQRCode(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"resultInfo":{"code":"SUCCESS"},"data":{"url":"https://qr.provider.com/..."}}`))
    }))
    defer server.Close()

    adapter := providername.NewAdapter(server.URL, "test-key", "test-secret", "test-merchant")

    result, err := adapter.CreateQRCode(context.Background(), provider.CreateQRRequest{
        Amount:   1000,
        Currency: "JPY",
    })

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if result.QRContent == "" {
        t.Error("expected QR content to be non-empty")
    }
}

Step 7: Add webhook handler (if applicable)

If the provider sends webhooks for payment status updates:

go
// In internal/api/handler/webhook_handler.go
func (h *WebhookHandler) HandleProviderWebhook(c *gin.Context) {
    // 1. Verify webhook signature
    // 2. Parse webhook payload
    // 3. Map to internal status
    // 4. Update PaymentIntent status
    // 5. Return 200 OK
}

Register the webhook route:

go
webhooks := router.Group("/v1/webhooks")
webhooks.POST("/provider_name", webhookHandler.HandleProviderWebhook)

Currently Supported Providers

ProviderStatusPayment TypesNotes
PayPayImplementedMPM (QR)Full lifecycle support
OnePayImplementedMPM (QR)ALIPAY_PLUS, WeChat Pay
LINE PayPlannedMPM (QR)
Rakuten PayPlannedMPM (QR)
au PAYPlannedMPM (QR)

Testing Checklist

When adding a new provider, ensure the following tests pass:

  • [ ] Unit tests for all interface methods
  • [ ] Error handling for network failures
  • [ ] Error handling for provider API errors
  • [ ] Webhook signature verification
  • [ ] Idempotent request handling
  • [ ] Timeout and retry behavior
  • [ ] Integration tests with provider sandbox (optional, tagged)

MPAC — MP-Solution Advanced Cloud Service