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.goQRProvider Interface
All QR payment providers must implement the QRProvider interface:
// 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
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:
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:
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:
// 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:
# 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:
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:
// 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:
webhooks := router.Group("/v1/webhooks")
webhooks.POST("/provider_name", webhookHandler.HandleProviderWebhook)Currently Supported Providers
| Provider | Status | Payment Types | Notes |
|---|---|---|---|
| PayPay | Implemented | MPM (QR) | Full lifecycle support |
| OnePay | Implemented | MPM (QR) | ALIPAY_PLUS, WeChat Pay |
| LINE Pay | Planned | MPM (QR) | |
| Rakuten Pay | Planned | MPM (QR) | |
| au PAY | Planned | MPM (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)