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

カスタムプラグインガイド

課金動作を拡張するカスタムプラグインの構築方法を学びます。

基本構造

すべてのプラグインはPluginインターフェースに加え、1つ以上のフックインターフェースを実装します:

package myplugin

import (
"context"
"github.com/contract-to-cash/core/plugin"
)

type MyPlugin struct {
priority int
}

func (p *MyPlugin) Name() string { return "my-plugin" }
func (p *MyPlugin) Version() string { return "1.0.0" }
func (p *MyPlugin) Priority() int { return p.priority }

func (p *MyPlugin) Initialize(_ context.Context, config plugin.Config) error {
if v, ok := config["priority"]; ok {
if n, ok := v.(int); ok {
p.priority = n
}
}
return nil
}

func (p *MyPlugin) Shutdown(_ context.Context) error { return nil }

例: ロイヤリティ割引プラグイン

全請求書に5%割引を適用するプラグイン:

type LoyaltyDiscountPlugin struct {
priority int
rate *big.Rat
}

func NewLoyaltyDiscountPlugin(discountPercent int) *LoyaltyDiscountPlugin {
return &LoyaltyDiscountPlugin{
rate: new(big.Rat).SetFrac64(int64(discountPercent), 100),
}
}

// DiscountHookを実装
func (p *LoyaltyDiscountPlugin) CalculateDiscount(ctx *plugin.CalculationContext) (shared.Money, error) {
discount := ctx.Subtotal().Multiply(p.rate)
ctx.RecordDiscount(plugin.AppliedDiscount{
PluginName: "loyalty-discount",
Code: "LOYALTY",
Amount: discount,
})
return discount, nil
}

例: 監査ログプラグイン

請求書計算ライフサイクルをログするプラグイン:

type AuditLogPlugin struct {
priority int
logger *slog.Logger
}

// InvoiceLifecycleHookを実装
func (p *AuditLogPlugin) BeforeCalculation(ctx *plugin.CalculationContext) error {
p.logger.Info("請求書計算開始",
"contract_id", ctx.ContractID(),
"subtotal", ctx.Subtotal().Amount().RatString(),
)
return nil
}

func (p *AuditLogPlugin) AfterCalculation(ctx *plugin.CalculationContext, inv *invoice.Invoice) error {
p.logger.Info("請求書計算完了",
"contract_id", ctx.ContractID(),
"total", inv.Total().Amount().RatString(),
"discounts_applied", len(ctx.AppliedDiscounts()),
)
return nil
}

例: ホスティングプロビジョニングプラグイン

契約ライフサイクルイベントでホスティングリソースをプロビジョニング/デプロビジョニング:

type HostingPlugin struct {
priority int
provisioningClient ProvisioningClient
}

// OnContractActivateHookを実装
func (p *HostingPlugin) OnContractActivate(ctx *plugin.Context, c *contract.ContractAggregate) error {
return p.provisioningClient.CreateServer(ctx.Context(), c.ContractID(), c.PriceID())
}

// OnContractSuspendHookを実装
func (p *HostingPlugin) OnContractSuspend(ctx *plugin.Context, c *contract.ContractAggregate) error {
return p.provisioningClient.StopServer(ctx.Context(), c.ContractID())
}

// OnContractResumeHookを実装
func (p *HostingPlugin) OnContractResume(ctx *plugin.Context, c *contract.ContractAggregate) error {
return p.provisioningClient.StartServer(ctx.Context(), c.ContractID())
}

// OnContractCancelHookを実装
func (p *HostingPlugin) OnContractCancel(ctx *plugin.Context, c *contract.ContractAggregate) error {
return p.provisioningClient.DeleteServer(ctx.Context(), c.ContractID())
}
注意

OnContractResumeフックは初回プロビジョニング(初回決済)と再有効化(停止後の決済)を区別できません。プロビジョニング状態を外部で追跡し、OnContractResume内で確認することを検討してください:

func (p *HostingPlugin) OnContractResume(ctx *plugin.Context, c *contract.ContractAggregate) error {
if p.provisioningClient.Exists(ctx.Context(), c.ContractID()) {
return p.provisioningClient.StartServer(ctx.Context(), c.ContractID())
}
return p.provisioningClient.CreateServer(ctx.Context(), c.ContractID(), c.PriceID())
}

カスタムプラグインの登録

registry := plugin.NewRegistry()

// レジストリはインターフェースによりプラグインを自動分類
registry.Register(&LoyaltyDiscountPlugin{}) // → DiscountHook
registry.Register(&AuditLogPlugin{}) // → InvoiceLifecycleHook
registry.Register(&HostingPlugin{}) // → 複数の契約フック

configs := map[string]plugin.Config{
"loyalty-discount": {"priority": plugin.PriorityNormal},
"audit-log": {"priority": plugin.PriorityHighest},
"hosting": {"priority": plugin.PriorityHigh},
}
registry.InitializeAll(ctx, configs)

マルチフックプラグイン

1つのプラグインで複数のフックインターフェースを実装できます:

type MetricsPlugin struct {
priority int
metrics MetricsCollector
}

// OnContractChangeHook + OnInvoiceIssuedHook + OnPaymentProcessedHookを実装
func (p *MetricsPlugin) OnContractChange(ctx *plugin.Context, event plugin.ContractChangeEvent) error {
p.metrics.RecordContractChange(event.ChangeType, event.MRRChange)
return nil
}

func (p *MetricsPlugin) OnInvoiceIssued(ctx *plugin.Context, inv *invoice.Invoice) error {
p.metrics.RecordRevenue(inv.Total())
return nil
}

// OnPaymentProcessed は支払いフックと同じ *PaymentContext を受け取るため、
// ctx.ContractID() / ctx.AccountID() で支払いを契約・アカウントへ帰属できます。
func (p *MetricsPlugin) OnPaymentProcessed(ctx *plugin.PaymentContext) error {
pay := ctx.Payment()
p.metrics.RecordPayment(pay.Status(), pay.Amount())
return nil
}

プラグイン実行順序

請求書生成時のプラグイン実行順序:

  1. InvoiceLifecycleHook.BeforeCalculation(優先度順)
  2. 小計計算
  3. DiscountHook.CalculateDiscount(優先度順、複数)
  4. 割引キャップ
  5. TaxHook.CalculateTax(優先度順)
  6. 請求書作成
  7. InvoiceLifecycleHook.AfterCalculation(優先度順)