Chuyển đến nội dung chính

レッスン 6: マルチ PSP 統合 - VNPay、MoMo、ZaloPay、Stripe

決済サービスプロバイダーの抽象化レイヤーを設計します。複数の PSP 用のアダプター パターン。 VNPay、MoMo、ZaloPay、Stripe との統合。 PSP のルーティングとフォールバック戦略。

🏗️ アーキテクチャ — レッスン 6 レッスン 6: マルチ PSP 統合 - VNPay、 MoMo、ZaloPay、ストライプ

FinTech と決済プラットフォームのアーキテクチャ

パート 2: コア決済エンジン

xdev.asia

レッスン 6: マルチ PSP 統合 - VNPay、MoMo、ZaloPay、Stripe

はじめに

実際の決済プラットフォームでは、成功率、コスト、適用範囲を最適化するために、常に複数の PSP (決済サービス プロバイダー) を統合する必要があります。この記事では、スマート ルーティングを使用したマルチ PSP 統合のための抽象化レイヤーの設計について説明します。


1. PSP 抽象化レイヤー

1.1 アダプターのパターン

┌────────────────────────────────────────────────────┐
│                 PAYMENT SERVICE                     │
│                                                     │
│  ┌──────────────────────────────────────────────┐  │
│  │            PSP Interface (Port)               │  │
│  │  authorize() / capture() / refund() / void() │  │
│  └──────────────────┬───────────────────────────┘  │
│                     │                               │
│      ┌──────────────┼──────────────┐               │
│      │              │              │               │
│  ┌───▼────┐   ┌─────▼────┐  ┌─────▼────┐         │
│  │ VNPay  │   │  MoMo    │  │ Stripe   │  ...    │
│  │Adapter │   │ Adapter  │  │ Adapter  │         │
│  └───┬────┘   └─────┬────┘  └─────┬────┘         │
└──────┼──────────────┼─────────────┼────────────────┘
       │              │             │
   ┌───▼────┐   ┌─────▼────┐  ┌────▼─────┐
   │ VNPay  │   │  MoMo    │  │ Stripe   │
   │  API   │   │   API    │  │   API    │
   └────────┘   └──────────┘  └──────────┘

1.2 PSP インターフェース

public interface PaymentServiceProvider {
    String getId();
    Set<PaymentMethod> supportedMethods();
    Set<Currency> supportedCurrencies();

    AuthResult authorize(AuthRequest request);
    CaptureResult capture(CaptureRequest request);
    RefundResult refund(RefundRequest request);
    VoidResult voidPayment(VoidRequest request);
    PaymentStatus queryStatus(String pspReference);
}

1.3 VNPay アダプター

@Component
public class VNPayAdapter implements PaymentServiceProvider {

    @Override
    public String getId() { return "vnpay"; }

    @Override
    public Set<PaymentMethod> supportedMethods() {
        return Set.of(
            PaymentMethod.ATM_CARD,
            PaymentMethod.CREDIT_CARD,
            PaymentMethod.QR_CODE,
            PaymentMethod.BANK_TRANSFER
        );
    }

    @Override
    public AuthResult authorize(AuthRequest request) {
        // Build VNPay-specific parameters
        var params = new TreeMap<String, String>();
        params.put("vnp_Version", "2.1.0");
        params.put("vnp_Command", "pay");
        params.put("vnp_TmnCode", config.getTmnCode());
        params.put("vnp_Amount", String.valueOf(
            request.getAmount().multiply(100).longValue()));
        params.put("vnp_CurrCode", "VND");
        params.put("vnp_TxnRef", request.getOrderId());
        params.put("vnp_OrderInfo", request.getDescription());
        params.put("vnp_ReturnUrl", config.getReturnUrl());

        // Sign with HMAC-SHA512
        String signature = HmacUtils.hmacSha512(
            config.getHashSecret(), buildQueryString(params));
        params.put("vnp_SecureHash", signature);

        // Return redirect URL for customer
        String paymentUrl = config.getPayUrl() + "?" + buildQueryString(params);
        return AuthResult.redirect(paymentUrl);
    }
}

2. スマートルーティング

2.1 ルーティング エンジン

@Service
public class PSPRouter {
    private final List<RoutingRule> rules;
    private final PSPHealthMonitor healthMonitor;

    public PaymentServiceProvider selectPSP(PaymentRequest request) {
        // 1. Filter by capability
        var candidates = pspRegistry.getAll().stream()
            .filter(psp -> psp.supportedMethods().contains(request.getMethod()))
            .filter(psp -> psp.supportedCurrencies().contains(request.getCurrency()))
            .filter(psp -> healthMonitor.isHealthy(psp.getId()))
            .toList();

        // 2. Apply routing rules
        for (var rule : rules) {
            var result = rule.evaluate(request, candidates);
            if (result.isPresent()) return result.get();
        }

        // 3. Default: cost-optimized selection
        return candidates.stream()
            .min(Comparator.comparing(psp -> getFeeRate(psp, request)))
            .orElseThrow(() -> new NoPSPAvailableException());
    }
}

2.2 ルーティング戦略

戦略ロジック使用例
コストの最適化最低料金のPSPデフォルト
成功率最高の成功率 PSP高額取引
レイテンシ最適化最速のPSPリアルタイム支払い
地理お客様に一番近いPSP国境を越えて
負荷分散ラウンドロビン/加重トラフィック分散
A/B テストランダム分割新しい PSP のテスト

2.3 フェイルオーバー パターン

Primary PSP (VNPay)
    │
    ├── Success → Done
    │
    ├── Timeout/5xx → Retry once
    │       │
    │       ├── Success → Done
    │       │
    │       └── Fail → Fallback PSP (Stripe)
    │                   │
    │                   ├── Success → Done
    │                   │
    │                   └── Fail → Return error to customer
    │
    └── Terminal error (declined) → Return error (no fallback)

3. PSP ヘルスモニタリング

@Component
public class PSPHealthMonitor {
    // Track success rate per PSP with sliding window
    private final Map<String, SlidingWindowCounter> successRates;

    @Scheduled(fixedRate = 60000) // Every minute
    public void checkHealth() {
        for (var entry : successRates.entrySet()) {
            var pspId = entry.getKey();
            var counter = entry.getValue();
            double rate = counter.getSuccessRate();

            if (rate < 0.5) { // < 50% success rate
                circuitBreaker.open(pspId);
                alertService.send("PSP " + pspId + " degraded: " + rate);
            }
        }
    }

    public boolean isHealthy(String pspId) {
        return !circuitBreaker.isOpen(pspId);
    }
}

4. Webhook の処理

4.1 PSP コールバックの受信

@RestController
@RequestMapping("/webhooks")
public class WebhookController {

    @PostMapping("/vnpay")
    public ResponseEntity<String> handleVNPayCallback(
            @RequestParam Map<String, String> params) {
        // 1. Verify signature
        if (!vnpayAdapter.verifySignature(params)) {
            return ResponseEntity.badRequest().body("Invalid signature");
        }

        // 2. Process callback
        var paymentId = params.get("vnp_TxnRef");
        var responseCode = params.get("vnp_ResponseCode");

        webhookProcessor.process(WebhookEvent.builder()
            .pspId("vnpay")
            .paymentId(paymentId)
            .status(mapStatus(responseCode))
            .rawPayload(params)
            .build());

        return ResponseEntity.ok("OK");
    }

    @PostMapping("/stripe")
    public ResponseEntity<Void> handleStripeWebhook(
            @RequestBody String payload,
            @RequestHeader("Stripe-Signature") String signature) {
        // Verify Stripe webhook signature
        var event = stripeAdapter.verifyAndParse(payload, signature);
        webhookProcessor.process(event);
        return ResponseEntity.ok().build();
    }
}

概要

マルチ PSP 統合には以下が必要です。

  • PSP 固有の API を抽象化するための アダプター パターン
  • スマート ルーティング により、コスト、成功率、遅延を最適化します。
  • PSP がクラッシュすると自動的に フェイルオーバー
  • ヘルスモニタリング、サーキットブレーカー付き
  • 署名検証による各 PSP の Webhook 処理

次の記事: 調整および決済エンジン — 制御システムと自動決済プロセス。