diff --git a/SampleWebAppDotNet48/Controllers/BillingController.cs b/SampleWebAppDotNet48/Controllers/BillingController.cs index 755bd4b..c89ebfc 100644 --- a/SampleWebAppDotNet48/Controllers/BillingController.cs +++ b/SampleWebAppDotNet48/Controllers/BillingController.cs @@ -6,6 +6,7 @@ using System.Net; using System.Threading.Tasks; using System.Web.Http; +using Newtonsoft.Json; using SampleWebAppDotNet48.Helpers; using authapi.Api; @@ -290,8 +291,8 @@ public async Task GetBillingDashboard( } catch (Exception ex) { - var handled = SaasusApiHelpers.HandleApiException(ex); - return Content(handled.Item1, handled.Item2); + System.Diagnostics.Debug.WriteLine($"Failed to get billing dashboard: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get billing dashboard" }); } } @@ -397,8 +398,8 @@ string Label(DateTime start, DateTime end) => } catch (Exception ex) { - var handled = SaasusApiHelpers.HandleApiException(ex); - return Content(handled.Item1, handled.Item2); + System.Diagnostics.Debug.WriteLine($"Failed to get plan periods: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get plan periods" }); } } @@ -478,8 +479,8 @@ public async Task UpdateMeteringCount( } catch (Exception ex) { - var handled = SaasusApiHelpers.HandleApiException(ex); - return Content(handled.Item1, handled.Item2); + System.Diagnostics.Debug.WriteLine($"Failed to update metering count: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to update metering count" }); } } @@ -535,8 +536,194 @@ public async Task UpdateMeteringCountNow( } catch (Exception ex) { - var handled = SaasusApiHelpers.HandleApiException(ex); - return Content(handled.Item1, handled.Item2); + System.Diagnostics.Debug.WriteLine($"Failed to update metering count now: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to update metering count now" }); + } + } + + // ──────────────────────────── GET /pricing_plans ──────────────────────────── + [HttpGet, Route("~/pricing_plans")] + public async Task GetPricingPlans() + { + try + { + // 1. 認証チェック + var authCfg = SaasusApiHelpers.CreateClientConfiguration(c => c.GetAuthApiClientConfig(), Request); + var userInfoApi = new UserInfoApi(authCfg); + var idToken = SaasusApiHelpers.GetBearerToken(Request); + if (string.IsNullOrEmpty(idToken)) + return Content(HttpStatusCode.Unauthorized, new { error = "Unauthorized" }); + var userInfo = await userInfoApi.GetUserInfoAsync(idToken); + + // 2. 料金プラン一覧を取得 + var pricingCfg = SaasusApiHelpers.CreateClientConfiguration(c => c.GetPricingApiClientConfig(), Request); + var plansApi = new PricingPlansApi(pricingCfg); + var plans = await plansApi.GetPricingPlansAsync(); + + // JSON形式でレスポンスを返す + return Ok(plans.VarPricingPlans); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to get pricing plans: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get pricing plans" }); + } + } + + // ──────────────────────────── GET /tax_rates ──────────────────────────── + [HttpGet, Route("~/tax_rates")] + public async Task GetTaxRates() + { + try + { + // 1. 認証チェック + var authCfg = SaasusApiHelpers.CreateClientConfiguration(c => c.GetAuthApiClientConfig(), Request); + var userInfoApi = new UserInfoApi(authCfg); + var idToken = SaasusApiHelpers.GetBearerToken(Request); + if (string.IsNullOrEmpty(idToken)) + return Content(HttpStatusCode.Unauthorized, new { error = "Unauthorized" }); + var userInfo = await userInfoApi.GetUserInfoAsync(idToken); + + // 2. 税率一覧を取得 + var pricingCfg = SaasusApiHelpers.CreateClientConfiguration(c => c.GetPricingApiClientConfig(), Request); + var taxRatesApi = new TaxRateApi(pricingCfg); + var taxRates = await taxRatesApi.GetTaxRatesAsync(); + + // JSON形式でレスポンスを返す + return Ok(taxRates.VarTaxRates); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to get tax rates: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get tax rates" }); + } + } + + // ──────────────────────────── GET /tenants/{tenantId}/plan ──────────────────────────── + [HttpGet, Route("~/tenants/{tenantId}/plan")] + public async Task GetTenantPlanInfo(string tenantId) + { + try + { + if (string.IsNullOrWhiteSpace(tenantId)) + { + return Content(HttpStatusCode.BadRequest, new { error = "tenant_id is required" }); + } + + // 1. 認証・認可チェック + var authCfg = SaasusApiHelpers.CreateClientConfiguration(c => c.GetAuthApiClientConfig(), Request); + var userInfoApi = new UserInfoApi(authCfg); + var idToken = SaasusApiHelpers.GetBearerToken(Request); + if (string.IsNullOrEmpty(idToken)) + return Content(HttpStatusCode.Unauthorized, new { error = "Unauthorized" }); + var userInfo = await userInfoApi.GetUserInfoAsync(idToken); + + if (!HasBillingAccess(userInfo, tenantId)) + { + return Content(HttpStatusCode.Forbidden, new { error = "Insufficient permissions" }); + } + + // 2. テナント詳細情報を取得 + var tenantApi = new TenantApi(authCfg); + var tenant = await tenantApi.GetTenantAsync(tenantId); + + // 3. 現在のプランの税率情報を取得(プラン履歴の最新エントリから) + string currentTaxRateId = null; + if (tenant.PlanHistories != null && tenant.PlanHistories.Any()) + { + var latestPlanHistory = tenant.PlanHistories.Last(); + if (!string.IsNullOrEmpty(latestPlanHistory.TaxRateId)) + { + currentTaxRateId = latestPlanHistory.TaxRateId; + } + } + + // 4. レスポンスを構築 + object planReservation = null; + if (tenant.UsingNextPlanFrom > 0) + { + planReservation = new + { + next_plan_id = tenant.NextPlanId, + using_next_plan_from = tenant.UsingNextPlanFrom, + next_plan_tax_rate_id = tenant.NextPlanTaxRateId + }; + } + + var response = new + { + id = tenant.Id, + name = tenant.Name, + plan_id = tenant.PlanId, + tax_rate_id = currentTaxRateId, + plan_reservation = planReservation + }; + + return Ok(response); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to retrieve tenant detail: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to retrieve tenant detail" }); + } + } + + // ──────────────────────────── PUT /tenants/{tenantId}/plan ──────────────────────────── + [HttpPut, Route("~/tenants/{tenantId}/plan")] + public async Task UpdateTenantPlan( + string tenantId, + [FromBody] UpdateTenantPlanRequest request) + { + try + { + if (string.IsNullOrWhiteSpace(tenantId)) + { + return Content(HttpStatusCode.BadRequest, new { error = "tenant_id is required" }); + } + + // 1. 認証・認可チェック + var authCfg = SaasusApiHelpers.CreateClientConfiguration(c => c.GetAuthApiClientConfig(), Request); + var userInfoApi = new UserInfoApi(authCfg); + var idToken = SaasusApiHelpers.GetBearerToken(Request); + if (string.IsNullOrEmpty(idToken)) + return Content(HttpStatusCode.Unauthorized, new { error = "Unauthorized" }); + var userInfo = await userInfoApi.GetUserInfoAsync(idToken); + + if (!HasBillingAccess(userInfo, tenantId)) + { + return Content(HttpStatusCode.Forbidden, new { error = "Insufficient permissions" }); + } + + // 2. テナントプランを更新 + var tenantApi = new TenantApi(authCfg); + var updateParam = new authapi.Model.PlanReservation(); + + // NextPlanIdが指定されている場合のみ設定(プラン解除時は空文字列またはnull) + if (!string.IsNullOrEmpty(request.NextPlanId)) + { + updateParam.NextPlanId = request.NextPlanId; + } + + // 税率IDが指定されている場合のみ設定 + if (!string.IsNullOrEmpty(request.TaxRateId)) + { + updateParam.NextPlanTaxRateId = request.TaxRateId; + } + + // using_next_plan_fromが指定されている場合のみ設定 + if (request.UsingNextPlanFrom.HasValue && request.UsingNextPlanFrom.Value > 0) + { + updateParam.UsingNextPlanFrom = (int)request.UsingNextPlanFrom.Value; + } + + await tenantApi.UpdateTenantPlanAsync(tenantId, updateParam); + + return Ok(new { message = "Tenant plan updated successfully" }); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to update tenant plan: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to update tenant plan" }); } } } @@ -552,4 +739,19 @@ public sealed class UpdateMeteringCountRequest [Range(0, int.MaxValue)] public int Count { get; set; } } + + /// + /// テナントプラン更新リクエスト + /// + public class UpdateTenantPlanRequest + { + [JsonProperty("next_plan_id")] + public string NextPlanId { get; set; } + + [JsonProperty("tax_rate_id")] + public string TaxRateId { get; set; } + + [JsonProperty("using_next_plan_from")] + public long? UsingNextPlanFrom { get; set; } + } } \ No newline at end of file diff --git a/SampleWebAppDotNet48/Controllers/MainController.cs b/SampleWebAppDotNet48/Controllers/MainController.cs index 84c27ec..e6ab211 100644 --- a/SampleWebAppDotNet48/Controllers/MainController.cs +++ b/SampleWebAppDotNet48/Controllers/MainController.cs @@ -47,8 +47,8 @@ public async Task GetCredentials(string code) } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get credentials: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get credentials" }); } } @@ -73,8 +73,8 @@ public async Task Refresh() } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to refresh credentials: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to refresh credentials" }); } } @@ -94,8 +94,8 @@ public async Task GetUserInfo() } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get user info: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get user info" }); } } @@ -143,8 +143,8 @@ public async Task GetUsers(string tenant_id) } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get users: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get users" }); } } @@ -207,8 +207,8 @@ public async Task GetTenantAttributes(string tenant_id) } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get tenant attributes: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get tenant attributes" }); } } @@ -228,8 +228,8 @@ public async Task GetUserAttributes() } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get user attributes: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get user attributes" }); } } @@ -262,8 +262,8 @@ public async Task GetPricingPlan(string plan_id) } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get pricing plan: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get pricing plan" }); } } @@ -354,8 +354,8 @@ public async Task RegisterUser([FromBody] UserRegisterRequest } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to register user: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to register user" }); } } @@ -416,8 +416,8 @@ public async Task DeleteUser([FromBody] UserDeleteRequest req } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to delete user: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to delete user" }); } } @@ -471,8 +471,8 @@ public async Task GetDeleteUserLogs(string tenant_id) } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get delete user log: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get delete user log" }); } } @@ -492,8 +492,8 @@ public async Task GetTenantAttributes() } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get tenant attributes list: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get tenant attributes list" }); } } @@ -595,8 +595,8 @@ public async Task SelfSignUp([FromBody] SelfSignUpRequest req } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to self sign up: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to self sign up" }); } } @@ -655,8 +655,8 @@ public async Task GetInvitations(string tenant_id) } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get invitations: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get invitations" }); } } @@ -703,8 +703,8 @@ public async Task UserInvitation([FromBody] UserInvitationReq } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to invite user: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to invite user" }); } } @@ -734,8 +734,8 @@ public async Task GetMfaStatus() } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to get MFA status: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to get MFA status" }); } } @@ -776,8 +776,8 @@ public async Task SetupMfa() } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to setup MFA: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to setup MFA" }); } } @@ -814,8 +814,8 @@ await saasUserApi.UpdateSoftwareTokenAsync( } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to verify MFA: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to verify MFA" }); } } @@ -839,8 +839,8 @@ public async Task EnableMfa() } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to enable MFA: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to enable MFA" }); } } @@ -864,8 +864,8 @@ public async Task DisableMfa() } catch (Exception ex) { - var (status, body) = SaasusApiHelpers.HandleApiException(ex); - return Content(status, body); + System.Diagnostics.Debug.WriteLine($"Failed to disable MFA: {ex.Message}"); + return Content(HttpStatusCode.InternalServerError, new { error = "Failed to disable MFA" }); } } diff --git a/SampleWebAppDotNet48/Helpers/SaasusApiHelpers.cs b/SampleWebAppDotNet48/Helpers/SaasusApiHelpers.cs index f03e3ef..4d7293e 100644 --- a/SampleWebAppDotNet48/Helpers/SaasusApiHelpers.cs +++ b/SampleWebAppDotNet48/Helpers/SaasusApiHelpers.cs @@ -67,16 +67,16 @@ public static (HttpStatusCode, object) HandleApiException(Exception ex) { case authapi.Client.ApiException a: Debug.WriteLine($"Auth API Error: {a.ErrorCode} - {a.Message}"); - return ((HttpStatusCode)a.ErrorCode, new { error = a.Message }); + return ((HttpStatusCode)a.ErrorCode, new { detail = a.ErrorContent ?? a.Message }); case pricingapi.Client.ApiException p: Debug.WriteLine($"Pricing API Error: {p.ErrorCode} - {p.Message}"); - return ((HttpStatusCode)p.ErrorCode, new { error = p.Message }); + return ((HttpStatusCode)p.ErrorCode, new { detail = p.ErrorContent ?? p.Message }); default: Debug.WriteLine($"Unhandled Error: {ex.Message}"); - return (HttpStatusCode.InternalServerError, new { error = ex.Message }); + return (HttpStatusCode.InternalServerError, new { detail = ex.Message }); } } } -} +} \ No newline at end of file diff --git a/SampleWebAppDotNet8/Controllers/BillingController.cs b/SampleWebAppDotNet8/Controllers/BillingController.cs index 61655d8..3b06beb 100644 --- a/SampleWebAppDotNet8/Controllers/BillingController.cs +++ b/SampleWebAppDotNet8/Controllers/BillingController.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using System.Text.Json.Serialization; using System.Collections.Concurrent; using System.ComponentModel.DataAnnotations; using SampleWebAppDotNet8.Helpers; @@ -334,7 +336,8 @@ public async Task GetBillingDashboard( } catch (Exception ex) { - return SaasusApiHelpers.HandleApiExceptionMvc(ex, this); + _logger.LogError(ex, "Failed to get billing dashboard"); + return StatusCode(500, new { error = "Failed to get billing dashboard" }); } } @@ -448,7 +451,8 @@ public async Task GetPlanPeriods([FromQuery(Name = "tenant_id")] } catch (Exception ex) { - return SaasusApiHelpers.HandleApiExceptionMvc(ex, this); + _logger.LogError(ex, "Failed to get plan periods"); + return StatusCode(500, new { error = "Failed to get plan periods" }); } } @@ -548,7 +552,8 @@ public async Task UpdateMeteringCount( } catch (Exception ex) { - return SaasusApiHelpers.HandleApiExceptionMvc(ex, this); + _logger.LogError(ex, "Failed to update metering count"); + return StatusCode(500, new { error = "Failed to update metering count" }); } } @@ -609,7 +614,205 @@ public async Task UpdateMeteringCountNow( } catch (Exception ex) { - return SaasusApiHelpers.HandleApiExceptionMvc(ex, this); + _logger.LogError(ex, "Failed to update metering count now"); + return StatusCode(500, new { error = "Failed to update metering count now" }); + } + } + + /// + /// 料金プラン一覧取得 + /// + [HttpGet("/pricing_plans")] + public async Task GetPricingPlans() + { + try + { + // 1. 認証チェック + var authApiClientConfig = SaasusApiHelpers.CreateClientConfiguration(c => c.GetAuthApiClientConfig(), HttpContext); + var userInfoApi = new UserInfoApi(authApiClientConfig); + var idToken = SaasusApiHelpers.GetBearerToken(HttpContext); + if (string.IsNullOrEmpty(idToken)) + return Unauthorized(); + var userInfo = await userInfoApi.GetUserInfoAsync(idToken); + + // 2. 料金プラン一覧を取得 + var pricingConfig = SaasusApiHelpers.CreateClientConfiguration(c => c.GetPricingApiClientConfig(), HttpContext); + var plansApi = new PricingPlansApi(pricingConfig); + var plans = await plansApi.GetPricingPlansAsync(); + + // JSON形式でレスポンスを返す + var jsonResponse = JsonConvert.SerializeObject(plans.VarPricingPlans); + return Content(jsonResponse, "application/json"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get pricing plans"); + return StatusCode(500, new { detail = "Internal server error" }); + } + } + + /// + /// 税率一覧取得 + /// + [HttpGet("/tax_rates")] + public async Task GetTaxRates() + { + try + { + // 1. 認証チェック + var authApiClientConfig = SaasusApiHelpers.CreateClientConfiguration(c => c.GetAuthApiClientConfig(), HttpContext); + var userInfoApi = new UserInfoApi(authApiClientConfig); + var idToken = SaasusApiHelpers.GetBearerToken(HttpContext); + if (string.IsNullOrEmpty(idToken)) + return Unauthorized(); + var userInfo = await userInfoApi.GetUserInfoAsync(idToken); + + // 2. 税率一覧を取得 + var pricingConfig = SaasusApiHelpers.CreateClientConfiguration(c => c.GetPricingApiClientConfig(), HttpContext); + var taxRatesApi = new TaxRateApi(pricingConfig); + var taxRates = await taxRatesApi.GetTaxRatesAsync(); + + // JSON形式でレスポンスを返す + var jsonResponse = JsonConvert.SerializeObject(taxRates.VarTaxRates); + return Content(jsonResponse, "application/json"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get tax rates"); + return StatusCode(500, new { detail = "Internal server error" }); + } + } + + /// + /// テナントプラン情報取得 + /// + [HttpGet("/tenants/{tenantId}/plan")] + public async Task GetTenantPlanInfo([FromRoute] string tenantId) + { + try + { + if (string.IsNullOrWhiteSpace(tenantId)) + { + return BadRequest(new { error = "tenant_id is required" }); + } + + // 1. 認証・認可チェック + var authApiClientConfig = SaasusApiHelpers.CreateClientConfiguration(c => c.GetAuthApiClientConfig(), HttpContext); + var userInfoApi = new UserInfoApi(authApiClientConfig); + var idToken = SaasusApiHelpers.GetBearerToken(HttpContext); + if (string.IsNullOrEmpty(idToken)) + return Unauthorized(); + var userInfo = await userInfoApi.GetUserInfoAsync(idToken); + + if (!HasBillingAccess(userInfo, tenantId)) + { + return StatusCode(403, new { error = "Insufficient permissions" }); + } + + // 2. テナント詳細情報を取得 + var tenantApi = new TenantApi(authApiClientConfig); + var tenant = await tenantApi.GetTenantAsync(tenantId); + + // 3. 現在のプランの税率情報を取得(プラン履歴の最新エントリから) + string? currentTaxRateId = null; + if (tenant.PlanHistories != null && tenant.PlanHistories.Any()) + { + var latestPlanHistory = tenant.PlanHistories.Last(); + if (!string.IsNullOrEmpty(latestPlanHistory.TaxRateId)) + { + currentTaxRateId = latestPlanHistory.TaxRateId; + } + } + + // 4. レスポンスを構築 + var response = new Dictionary + { + ["id"] = tenant.Id, + ["name"] = tenant.Name, + ["plan_id"] = tenant.PlanId, + ["tax_rate_id"] = currentTaxRateId, + ["plan_reservation"] = null + }; + + // 5. 予約情報がある場合は追加 + if (tenant.UsingNextPlanFrom > 0) + { + var planReservation = new Dictionary + { + ["next_plan_id"] = tenant.NextPlanId, + ["using_next_plan_from"] = tenant.UsingNextPlanFrom, + ["next_plan_tax_rate_id"] = tenant.NextPlanTaxRateId + }; + response["plan_reservation"] = planReservation; + } + + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to retrieve tenant detail"); + return StatusCode(500, new { error = "Failed to retrieve tenant detail" }); + } + } + + /// + /// テナントプラン更新 + /// + [HttpPut("/tenants/{tenantId}/plan")] + public async Task UpdateTenantPlan( + [FromRoute] string tenantId, + [FromBody] UpdateTenantPlanRequest request) + { + try + { + if (string.IsNullOrWhiteSpace(tenantId)) + { + return BadRequest(new { error = "tenant_id is required" }); + } + + // 1. 認証・認可チェック + var authApiClientConfig = SaasusApiHelpers.CreateClientConfiguration(c => c.GetAuthApiClientConfig(), HttpContext); + var userInfoApi = new UserInfoApi(authApiClientConfig); + var idToken = SaasusApiHelpers.GetBearerToken(HttpContext); + if (string.IsNullOrEmpty(idToken)) + return Unauthorized(); + var userInfo = await userInfoApi.GetUserInfoAsync(idToken); + + if (!HasBillingAccess(userInfo, tenantId)) + { + return StatusCode(403, new { error = "Insufficient permissions" }); + } + + // 2. テナントプランを更新 + var tenantApi = new TenantApi(authApiClientConfig); + var updateParam = new authapi.Model.PlanReservation(); + + // NextPlanIdが指定されている場合のみ設定(プラン解除時は空文字列またはnull) + if (!string.IsNullOrEmpty(request.NextPlanId)) + { + updateParam.NextPlanId = request.NextPlanId; + } + + // 税率IDが指定されている場合のみ設定 + if (!string.IsNullOrEmpty(request.TaxRateId)) + { + updateParam.NextPlanTaxRateId = request.TaxRateId; + } + + // using_next_plan_fromが指定されている場合のみ設定 + if (request.UsingNextPlanFrom.HasValue && request.UsingNextPlanFrom.Value > 0) + { + updateParam.UsingNextPlanFrom = (int)request.UsingNextPlanFrom.Value; + } + + await tenantApi.UpdateTenantPlanAsync(tenantId, updateParam); + + return Ok(new { message = "Tenant plan updated successfully" }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update tenant plan"); + return StatusCode(500, new { error = "Failed to update tenant plan" }); } } @@ -627,4 +830,19 @@ public class UpdateMeteringCountRequest [Range(0, int.MaxValue, ErrorMessage = "Count must be greater than or equal to 0")] public int Count { get; set; } } + + /// + /// テナントプラン更新リクエスト + /// + public class UpdateTenantPlanRequest + { + [JsonPropertyName("next_plan_id")] + public string? NextPlanId { get; set; } + + [JsonPropertyName("tax_rate_id")] + public string? TaxRateId { get; set; } + + [JsonPropertyName("using_next_plan_from")] + public long? UsingNextPlanFrom { get; set; } + } } \ No newline at end of file diff --git a/SampleWebAppDotNet8/Helpers/SaasusApiHelpers.cs b/SampleWebAppDotNet8/Helpers/SaasusApiHelpers.cs index 65c2bc0..dae78f2 100644 --- a/SampleWebAppDotNet8/Helpers/SaasusApiHelpers.cs +++ b/SampleWebAppDotNet8/Helpers/SaasusApiHelpers.cs @@ -43,37 +43,5 @@ public static T CreateClientConfiguration( return null; } - /// - /// SaaSus SDK の ApiException → HTTP レスポンス変換 - /// Minimal-API 用 IResult を返す版 - /// - public static IResult HandleApiException(Exception ex) - { - return ex switch - { - authapi.Client.ApiException aex => Results.Problem(aex.Message, - statusCode: (int)aex.ErrorCode), - pricingapi.Client.ApiException pex => Results.Problem(pex.Message, - statusCode: (int)pex.ErrorCode), - /* 例外型を追加したいときは ↓ にケースを足すだけ */ - // billingapi.Client.ApiException bex => Results.Problem(bex.Message, (int)bex.ErrorCode), - - _ => Results.Problem(ex.Message, statusCode: 500) - }; - } - - public static IActionResult HandleApiExceptionMvc(Exception ex, ControllerBase ctrl) - { - return ex switch - { - authapi.Client.ApiException a => - ctrl.Problem(detail: a.Message, statusCode: (int)a.ErrorCode), - - pricingapi.Client.ApiException p => - ctrl.Problem(detail: p.Message, statusCode: (int)p.ErrorCode), - - _ => ctrl.Problem(detail: ex.Message, statusCode: 500) - }; - } } } diff --git a/SampleWebAppDotNet8/Program.cs b/SampleWebAppDotNet8/Program.cs index a22549f..376fe09 100644 --- a/SampleWebAppDotNet8/Program.cs +++ b/SampleWebAppDotNet8/Program.cs @@ -129,7 +129,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to get credentials: {ex.Message}"); + return Results.Problem("Failed to get credentials", statusCode: 500); } }); @@ -153,7 +154,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to refresh credentials: {ex.Message}"); + return Results.Problem("Failed to refresh credentials", statusCode: 500); } }); @@ -177,7 +179,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to get user info: {ex.Message}"); + return Results.Problem("Failed to get user info", statusCode: 500); } }); @@ -232,7 +235,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to get users: {ex.Message}"); + return Results.Problem("Failed to get users", statusCode: 500); } }); @@ -300,7 +304,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to get tenant attributes: {ex.Message}"); + return Results.Problem("Failed to get tenant attributes", statusCode: 500); } }); @@ -324,7 +329,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to get user attributes: {ex.Message}"); + return Results.Problem("Failed to get user attributes", statusCode: 500); } }); @@ -353,7 +359,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to get pricing plan: {ex.Message}"); + return Results.Problem("Failed to get pricing plan", statusCode: 500); } }); @@ -444,8 +451,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); - return Results.Problem(detail: ex.Message, statusCode: 500); + Console.WriteLine($"Failed to register user: {ex.Message}"); + return Results.Problem("Failed to register user", statusCode: 500); } }); @@ -508,7 +515,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to delete user: {ex.Message}"); + return Results.Problem("Failed to delete user", statusCode: 500); } }); @@ -565,8 +573,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); - return Results.Problem(detail: ex.Message, statusCode: 500); + Console.WriteLine($"Failed to get delete user log: {ex.Message}"); + return Results.Problem("Failed to get delete user log", statusCode: 500); } }); @@ -590,7 +598,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to get tenant attributes list: {ex.Message}"); + return Results.Problem("Failed to get tenant attributes list", statusCode: 500); } }); @@ -684,8 +693,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); - return Results.Problem(detail: ex.Message, statusCode: 500); + Console.WriteLine($"Failed to self sign up: {ex.Message}"); + return Results.Problem("Failed to self sign up", statusCode: 500); } }); @@ -742,7 +751,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to get invitations: {ex.Message}"); + return Results.Problem("Failed to get invitations", statusCode: 500); } }); @@ -803,8 +813,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); - return Results.Problem(detail: ex.Message, statusCode: 500); + Console.WriteLine($"Failed to invite user: {ex.Message}"); + return Results.Problem("Failed to invite user", statusCode: 500); } }); @@ -833,7 +843,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to get MFA status: {ex.Message}"); + return Results.Problem("Failed to get MFA status", statusCode: 500); } }); @@ -868,7 +879,8 @@ static object ConvertToExpectedType(object value, string expectedType) } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to setup MFA: {ex.Message}"); + return Results.Problem("Failed to setup MFA", statusCode: 500); } }); @@ -902,7 +914,8 @@ await saasUserApi.UpdateSoftwareTokenAsync( } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to verify MFA: {ex.Message}"); + return Results.Problem("Failed to verify MFA", statusCode: 500); } }); @@ -931,7 +944,8 @@ await saasUserApi.UpdateSoftwareTokenAsync( } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to enable MFA: {ex.Message}"); + return Results.Problem("Failed to enable MFA", statusCode: 500); } }); @@ -960,7 +974,8 @@ await saasUserApi.UpdateSoftwareTokenAsync( } catch (Exception ex) { - return SaasusApiHelpers.HandleApiException(ex); + Console.WriteLine($"Failed to disable MFA: {ex.Message}"); + return Results.Problem("Failed to disable MFA", statusCode: 500); } });