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

プラン変更の実装

サンプルアプリのプラン設定変更機能を題材に、SaaSus Auth API と Pricing API を組み合わせて、テナントの料金プラン変更機能を実装する方法を解説します。

以下はプラン設定画面のスクリーンショットです。

プラン設定変更機能では以下の機能を提供します:

  • 現在のプラン情報の表示
  • プラン変更予約情報の表示
  • プラン変更の実行

フロントエンド実装

実装例リンク

以下のリンク先に、フロントエンドの実装が含まれています。

バックエンド実装

エンドポイント一覧

種別メソッド & パス説明
現在のプラン情報GET /tenants/{tenantId}/planテナントの現在のプラン情報と履歴を取得します。
プラン一覧GET /pricing_plans全料金プランの一覧を取得します。
税率一覧GET /tax_rates利用可能な税率の一覧を取得します。
プラン変更実行PUT /tenants/{tenantId}/planテナントの料金プランを変更します。
備考

以下のコードサンプルはバックエンドが Go を前提としています。

現在のプラン情報取得エンドポイント

実装例(履歴から現在の税率設定を取得)

func getTenantPlanInfo(c echo.Context) error {
tenantId := c.Param("tenant_id")
if tenantId == "" {
return c.JSON(http.StatusBadRequest, echo.Map{"error": "tenant_id is required"})
}

userInfo, ok := c.Get(string(ctxlib.UserInfoKey)).(*authapi.UserInfo)
if !ok {
c.Logger().Error("failed to get user info")
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Internal server error"})
}

// 管理者権限チェック
if !hasBillingAccess(userInfo, tenantId) {
return c.JSON(http.StatusForbidden, echo.Map{"error": "Insufficient permissions"})
}

// テナント詳細情報を取得
tenantDetailResp, err := authClient.GetTenantWithResponse(context.Background(), authapi.TenantId(tenantId))
if err != nil {
c.Logger().Errorf("Failed to retrieve tenant detail: %v", err)
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Failed to retrieve tenant detail"})
}

if tenantDetailResp.StatusCode() != http.StatusOK {
c.Logger().Errorf("Failed to retrieve tenant detail: status %d", tenantDetailResp.StatusCode())
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Failed to retrieve tenant detail"})
}

if tenantDetailResp.JSON200 == nil {
return c.JSON(http.StatusNotFound, echo.Map{"error": "Tenant not found"})
}

// 現在の税率設定を取得(プラン履歴の最新エントリから)
var currentTaxRateId *string
if len(tenantDetailResp.JSON200.PlanHistories) > 0 {
latestPlanHistory := tenantDetailResp.JSON200.PlanHistories[len(tenantDetailResp.JSON200.PlanHistories)-1]
if latestPlanHistory.TaxRateId != nil {
taxRateIdStr := string(*latestPlanHistory.TaxRateId)
currentTaxRateId = &taxRateIdStr
}
}

// レスポンスを構築
response := echo.Map{
"id": tenantDetailResp.JSON200.Id,
"name": tenantDetailResp.JSON200.Name,
"plan_id": tenantDetailResp.JSON200.PlanId,
"tax_rate_id": currentTaxRateId,
"plan_reservation": nil,
}

// 予約情報がある場合は追加
if tenantDetailResp.JSON200.NextPlanId != nil {
planReservation := echo.Map{
"next_plan_id": *tenantDetailResp.JSON200.NextPlanId,
"using_next_plan_from": tenantDetailResp.JSON200.UsingNextPlanFrom,
"next_plan_tax_rate_id": tenantDetailResp.JSON200.NextPlanTaxRateId,
}
response["plan_reservation"] = planReservation
}

return c.JSON(http.StatusOK, response)
}

実装例リンク

以下のリンク先に、本エンドポイントの実装が含まれています。
関数名で検索して該当箇所をご確認ください。

プラン一覧取得エンドポイント

プラン変更セレクトボックスの実装

ユーザーが選択可能なプランをセレクトボックスで表示します。
このエンドポイントで取得したプラン一覧を使用してUIを構築します。

実装例リンク

以下のリンク先に、本エンドポイントの実装が含まれています。
関数名で検索して該当箇所をご確認ください。

税率一覧取得エンドポイント

税率選択セレクトボックスの実装

プラン変更時に適用する税率をユーザーが選択できるよう、利用可能な税率一覧をセレクトボックスで表示します。
このエンドポイントで取得した税率一覧を使用してUIを構築します。

実装例リンク

以下のリンク先に、本エンドポイントの実装が含まれています。
関数名で検索して該当箇所をご確認ください。

プラン変更実行エンドポイント

リクエストパラメータによる操作

同一エンドポイント PUT /tenants/{tenantId}/plan で以下の操作が可能です:

プラン変更予約

{
"nextPlanId": "plan-id",
"taxRateId": "tax-rate-id",
"usingNextPlanFrom": 1640995200
}

プラン解約

{
"nextPlanId": "",
"usingNextPlanFrom": 1640995200
}

予約取り消し

{}

バックエンドでの処理ロジック

func updateTenantPlan(c echo.Context) error {
tenantId := c.Param("tenant_id")
if tenantId == "" {
return c.JSON(http.StatusBadRequest, echo.Map{"error": "tenant_id is required"})
}

var request UpdateTenantPlanRequest
if err := c.Bind(&request); err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{"error": "Invalid request"})
}
nextPlanId := request.NextPlanId
taxRateId := request.TaxRateId
usingNextPlanFrom := request.UsingNextPlanFrom

userInfo, ok := c.Get(string(ctxlib.UserInfoKey)).(*authapi.UserInfo)
if !ok {
c.Logger().Error("failed to get user info")
return c.String(http.StatusInternalServerError, "internal server error")
}

// 管理者権限チェック
if !hasBillingAccess(userInfo, tenantId) {
return c.String(http.StatusForbidden, "Insufficient permissions")
}

// テナントプランを更新
updateTenantPlanParam := authapi.UpdateTenantPlanParam{
NextPlanId: (*authapi.Uuid)(&nextPlanId),
}

// 税率IDが指定されている場合のみ設定
if taxRateId != nil && *taxRateId != "" {
updateTenantPlanParam.NextPlanTaxRateId = (*authapi.Uuid)(taxRateId)
}

// using_next_plan_fromが指定されている場合のみ設定
if usingNextPlanFrom != nil && *usingNextPlanFrom > 0 {
usingNextPlanFromInt := int(*usingNextPlanFrom)
updateTenantPlanParam.UsingNextPlanFrom = &usingNextPlanFromInt
}

resp, err := authClient.UpdateTenantPlanWithResponse(context.Background(), tenantId, updateTenantPlanParam)
if err != nil {
c.Logger().Errorf("failed to update tenant plan: %v", err)
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Failed to update tenant plan"})
}

// レスポンスのステータスコードをチェック
if resp.StatusCode() != http.StatusOK {
c.Logger().Errorf("tenant plan update failed with status %d: %s", resp.StatusCode(), string(resp.Body))
return c.JSON(resp.StatusCode(), echo.Map{"error": "Failed to update tenant plan"})
}

return c.JSON(http.StatusOK, echo.Map{"message": "Tenant plan updated successfully"})
}

実装例リンク

以下のリンク先に、本エンドポイントの実装が含まれています。
関数名で検索して該当箇所をご確認ください。