Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 128 additions & 2 deletions billing_router.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# billing_router.py

from fastapi import APIRouter, Depends, HTTPException, Query
from typing import List, Dict, Any, Tuple
from typing import List, Dict, Any, Tuple, Optional
from pydantic import BaseModel, Field
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

# SaaS SDK のクライアント
from saasus_sdk_python.src.auth import TenantApi
from saasus_sdk_python.src.auth.models.plan_reservation import PlanReservation
from saasus_sdk_python.src.pricing import (
PricingPlansApi,
MeteringApi,
Expand All @@ -34,6 +35,11 @@ class UpdateCountBody(BaseModel):
method: str = Field(..., pattern="^(add|sub|direct)$")
count: int = Field(..., ge=0)

class UpdateTenantPlanRequest(BaseModel):
next_plan_id: Optional[str] = None
tax_rate_id: Optional[str] = None
using_next_plan_from: Optional[int] = None


# --- 認可ヘルパー ---
def has_billing_access(auth_user: Any, tenant_id: str) -> bool:
Expand Down Expand Up @@ -360,4 +366,124 @@ def update_count_of_now(
metering_unit_name=unit,
update_metering_unit_timestamp_count_now_param=param,
)
return resp
return resp

@router.get(
"/pricing_plans",
summary="Get pricing plans list"
)
def get_pricing_plans(auth_user: Any = Depends(fastapi_auth)):
# プラン一覧を取得する
try:
# 料金プラン一覧を取得
plans = PricingPlansApi(api_client=pricing_api_client).get_pricing_plans()
return plans.pricing_plans
except Exception as e:
raise HTTPException(status_code=500, detail="Failed to retrieve pricing plans")


@router.get(
"/tax_rates",
summary="Get tax rates list"
)
def get_tax_rates(auth_user: Any = Depends(fastapi_auth)):
# 税率一覧を取得する
try:
# 税率一覧を取得
tax_rates = TaxRateApi(api_client=pricing_api_client).get_tax_rates()
return tax_rates.tax_rates
except Exception as e:
print(f"TaxRateApi error: {e}")
raise HTTPException(status_code=500, detail="Failed to retrieve tax rates")
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message lacks context about the actual error. Consider including the exception details or a more specific error message to aid in debugging. For example: detail=f\"Failed to retrieve tax rates: {str(e)}\"

Suggested change
raise HTTPException(status_code=500, detail="Failed to retrieve tax rates")
raise HTTPException(status_code=500, detail=f"Failed to retrieve tax rates: {str(e)}")

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エラー詳細はprintでコンソール出力、フロントには出さない形式で対応。



@router.get(
"/tenants/{tenant_id}/plan",
summary="Get tenant plan information"
)
def get_tenant_plan_info(
tenant_id: str,
auth_user: Any = Depends(fastapi_auth)
):
# テナントプラン情報を取得する
# 管理者権限チェック
if not has_billing_access(auth_user, tenant_id):
raise HTTPException(status_code=403, detail="Insufficient permissions")

try:
# テナント詳細情報を取得
tenant = TenantApi(api_client=api_client).get_tenant(tenant_id=tenant_id)

# 現在のプランの税率情報を取得(プラン履歴の最新エントリから)
current_tax_rate_id = None
if tenant.plan_histories:
latest_plan_history = tenant.plan_histories[-1]
if latest_plan_history.tax_rate_id:
current_tax_rate_id = latest_plan_history.tax_rate_id

# レスポンスを構築
response = {
"id": tenant.id,
"name": tenant.name,
"plan_id": tenant.plan_id,
"tax_rate_id": current_tax_rate_id,
"plan_reservation": None,
}

# 予約情報がある場合は追加(using_next_plan_fromで判定)
if hasattr(tenant, 'using_next_plan_from') and tenant.using_next_plan_from is not None:
plan_reservation = {
"next_plan_id": getattr(tenant, 'next_plan_id', None),
"using_next_plan_from": tenant.using_next_plan_from,
"next_plan_tax_rate_id": getattr(tenant, 'next_plan_tax_rate_id', None),
}
response["plan_reservation"] = plan_reservation

return response
except Exception as e:
print(f"TenantApi error: {e}")
raise HTTPException(status_code=500, detail="Failed to retrieve tenant detail")
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message lacks context about the actual error. Consider including the exception details or a more specific error message to aid in debugging. For example: detail=f\"Failed to retrieve tenant detail: {str(e)}\"

Suggested change
raise HTTPException(status_code=500, detail="Failed to retrieve tenant detail")
raise HTTPException(status_code=500, detail=f"Failed to retrieve tenant detail: {str(e)}")

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エラー詳細はprintでコンソール出力、フロントには出さない形式で対応。



@router.put(
"/tenants/{tenant_id}/plan",
summary="Update tenant plan"
)
def update_tenant_plan(
tenant_id: str,
request: UpdateTenantPlanRequest,
auth_user: Any = Depends(fastapi_auth)
):
# テナントプランを更新する
# 管理者権限チェック
if not has_billing_access(auth_user, tenant_id):
raise HTTPException(status_code=403, detail="Insufficient permissions")

try:
# PlanReservationオブジェクトを作成(指定されたフィールドのみ設定)
plan_reservation_kwargs = {}

# next_plan_idがNoneでない場合は設定(空文字も含む)
if request.next_plan_id is not None:
plan_reservation_kwargs['next_plan_id'] = request.next_plan_id

plan_reservation = PlanReservation(**plan_reservation_kwargs)

# 税率IDが指定されている場合のみ設定
if request.tax_rate_id:
Comment on lines +472 to +473
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks for truthiness (if request.tax_rate_id:) which will be False for empty strings, but the comment on line 476 indicates empty strings should be handled. Change to if request.tax_rate_id is not None: to match the behavior of next_plan_id and allow empty strings to be set.

Suggested change
# 税率IDが指定されている場合のみ設定
if request.tax_rate_id:
# 税率IDがNoneでない場合は設定(空文字も含む)
if request.tax_rate_id is not None:

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@SasakiTakatsugu SasakiTakatsugu Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next_plan_idは解除予約で空文字が必要、tax_rate_idは不要のため対応しない。

plan_reservation.next_plan_tax_rate_id = request.tax_rate_id

# using_next_plan_fromが指定されている場合のみ設定
if request.using_next_plan_from is not None and request.using_next_plan_from > 0:
plan_reservation.using_next_plan_from = request.using_next_plan_from

# テナントプランを更新
TenantApi(api_client=api_client).update_tenant_plan(
tenant_id=tenant_id,
body=plan_reservation
)

return {"message": "Tenant plan updated successfully"}
except Exception as e:
print(f"TenantApi error: {e}")
raise HTTPException(status_code=500, detail="Failed to update tenant plan")
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message lacks context about the actual error. Consider including the exception details or a more specific error message to aid in debugging. For example: detail=f\"Failed to update tenant plan: {str(e)}\"

Suggested change
raise HTTPException(status_code=500, detail="Failed to update tenant plan")
raise HTTPException(status_code=500, detail=f"Failed to update tenant plan: {str(e)}")

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エラー詳細はprintでコンソール出力、フロントには出さない形式で対応。