メインコンテンツまでスキップ

決済ゲートウェイ

Contract Billing Coreは、決済処理を抽象化するPaymentGatewayインターフェースを定義しています。お使いの決済プロバイダー(Stripe、Braintree、PayPayなど)向けにこのインターフェースを実装します。

インターフェース

type PaymentGateway interface {
ID() string
SupportedMethods() []PaymentMethodType

// 直接チャージ
Charge(ctx context.Context, req *ChargeRequest) (*ChargeResponse, error)

// 2フェーズ: オーソリ → キャプチャ
Authorize(ctx context.Context, req *AuthorizeRequest) (*AuthorizeResponse, error)
Capture(ctx context.Context, req *CaptureRequest) (*CaptureResponse, error)
Void(ctx context.Context, req *VoidRequest) (*VoidResponse, error)

// 返金
Refund(ctx context.Context, req *RefundRequest) (*RefundResponse, error)
Cancel(ctx context.Context, req *CancelRequest) (*CancelResponse, error)

// トランザクションクエリ
GetTransaction(ctx context.Context, transactionID string) (*Transaction, error)

// 決済手段管理
RegisterPaymentMethod(ctx context.Context, req *RegisterPaymentMethodRequest) (*PaymentMethodDetail, error)
DeletePaymentMethod(ctx context.Context, paymentMethodID string) error
GetPaymentMethod(ctx context.Context, paymentMethodID string) (*PaymentMethodDetail, error)
ListPaymentMethods(ctx context.Context, customerID string) ([]*PaymentMethodDetail, error)
}

決済手段タイプ

const (
PaymentMethodTypeCreditCard = "credit_card" // クレジットカード
PaymentMethodTypeDebitCard = "debit_card" // デビットカード
PaymentMethodTypeBankTransfer = "bank_transfer" // 銀行振込
PaymentMethodTypeConvenienceStore = "convenience_store" // コンビニ決済
PaymentMethodTypeQRCode = "qr_code" // QRコード決済
PaymentMethodTypeCarrier = "carrier" // キャリア決済
PaymentMethodTypePostpay = "postpay" // 後払い
PaymentMethodTypeDirectDebit = "direct_debit" // 口座振替
)

チャージフロー

最もシンプルな決済フロー — 即時チャージ:

pmID := "pm-visa-1234"
resp, err := gateway.Charge(ctx, &port.ChargeRequest{
CustomerID: "cust-001",
Amount: invoiceTotal,
PaymentMethodID: &pmID,
IdempotencyKey: "charge-inv-001",
})

オーソリ/キャプチャフロー

サービス提供前に決済を確認する必要がある場合(決済ゲート付きプロビジョニング):

// 1. オーソリ(資金を確保)
pmID := "pm-visa-1234"
authResp, _ := gateway.Authorize(ctx, &port.AuthorizeRequest{
CustomerID: "cust-001",
Amount: invoiceTotal,
PaymentMethodID: &pmID,
IdempotencyKey: "auth-inv-001",
})

// 2. サービスプロビジョニング...

// 3. キャプチャ(チャージを確定)
captureResp, _ := gateway.Capture(ctx, &port.CaptureRequest{
AuthorizationID: authResp.AuthorizationID,
Amount: &invoiceTotal, // 一部キャプチャも可能
})

決済ゲート付きプロビジョニング

ホスティング/クラウドプロバイダーで一般的なパターン。決済完了がサービス有効化の条件:

1. Contract: Draft     → 契約作成
2. Contract: Active → 有効化
3. Contract: Suspended → 即時停止(決済待ち)
4. Invoice: Finalized → 請求書の生成・確定
5. Payment: Completed → 決済処理
6. Contract: Active → 再開 → サービスプロビジョニング

Suspended状態を以下の両方に再利用:

  • 初回有効化: 初回決済を待つ新規契約
  • 未払い停止: 支払い遅延の既存契約(ダニング)

両方とも同じ方法で解決: 決済 → 再開。

OnContractResumeHookでサービスプロビジョニングをトリガー:

注記

AfterChargeHookが受け取るPaymentContextでは、Contract()はデフォルトでnilを返します。契約情報にアクセスするには、Invoice.ContractID()経由で自分で契約をルックアップする必要があります。そのため、決済ゲート付きプロビジョニングでは、フックではなくProcessPayment成功後にアプリケーションコードでResumeを処理する方法が推奨されます(決済統合ガイド参照)。

func (p *ProvisioningPlugin) OnContractResume(ctx *plugin.Context, c *contract.ContractAggregate) error {
return p.provisioningService.Activate(ctx.Context(), c.ContractID())
}
既知の制約

OnContractResumeHook初回有効化(初回決済完了)と再有効化(停止後の決済完了)を区別できません。これは集約のApply()メソッドでSuspensionConfigurationがフック発火前にnilにクリアされるためです。

回避策:

  • プロビジョニング状態を外部で追跡(例: データベースに「プロビジョニング済み」フラグ)
  • plugin.Contextのメタデータを使って停止理由をオーケストレーションコードから渡す
  • プロビジョニング前にサービスが既に存在するか確認する

詳細はIssue #5を参照。

決済手段のフォールバック解決

決済手段は階層的に解決されます(Stripeスタイル):

1. ProcessPaymentInput内の明示的PaymentMethodID
2. Invoice.PaymentMethodID
3. Contract.PaymentMethodID
4. Customer.DefaultPaymentMethodID
注記

ContractおよびCustomerレベルのフォールバックには、NewPaymentServicecontractRepoを渡す必要があります。contractReponilの場合、解決はInvoiceレベルで止まります。

より具体的なレベルがより一般的なレベルをオーバーライドします。

ゲートウェイの実装

type StripeGateway struct {
client *stripe.Client
}

func (g *StripeGateway) ID() string { return "stripe" }

func (g *StripeGateway) SupportedMethods() []port.PaymentMethodType {
return []port.PaymentMethodType{
port.PaymentMethodTypeCreditCard,
port.PaymentMethodTypeDebitCard,
}
}

func (g *StripeGateway) Charge(ctx context.Context, req *port.ChargeRequest) (*port.ChargeResponse, error) {
// Stripe APIにマッピング
// ...
}