Skip to content
Merged
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
168 changes: 168 additions & 0 deletions src/routes/billingRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@ import { Router, Request, Response, NextFunction } from "express";
import { AuthClient, AuthMiddleware, PricingClient } from "saasus-sdk";
import type { PricingPlan } from "saasus-sdk/dist/generated/Pricing";

// --- 型定義 -----------------------------------------------------------
interface TenantPlanResponse {
id: string;
name: string;
plan_id: string | undefined;
tax_rate_id: string | null;
plan_reservation: {
next_plan_id: string | undefined;
using_next_plan_from: number;
next_plan_tax_rate_id: string | undefined;
} | null;
}

interface UpdateTenantPlanParam {
next_plan_id: string;
next_plan_tax_rate_id?: string;
using_next_plan_from?: number;
}

const router = Router();

router.use(AuthMiddleware);
Expand Down Expand Up @@ -400,4 +419,153 @@ router.post(
}
}
);

/**
* GET /pricing_plans
* 料金プラン一覧を取得
*/
router.get("/pricing_plans", async (req: Request, res: Response) => {
try {
const userInfo = req.userInfo;
if (!userInfo) {
return res.status(401).json({ detail: "No user" });
}

const pricingCli = new PricingClient();
const plans = (await pricingCli.pricingPlansApi.getPricingPlans()).data;
res.json(plans.pricing_plans);
} catch (error) {
console.error(error);
res.status(500).json({ detail: "Internal server error" });
}
});

/**
* GET /tax_rates
* 税率一覧を取得
*/
router.get("/tax_rates", async (req: Request, res: Response) => {
try {
const userInfo = req.userInfo;
if (!userInfo) {
return res.status(401).json({ detail: "No user" });
}

const pricingCli = new PricingClient();
const taxRates = (await pricingCli.taxRateApi.getTaxRates()).data;
res.json(taxRates.tax_rates);
} catch (error) {
console.error(error);
res.status(500).json({ detail: "Internal server error" });
}
});

/**
* GET /tenants/:tenant_id/plan
* テナントプラン情報を取得
*/
router.get("/tenants/:tenant_id/plan", async (req: Request, res: Response) => {
try {
const tenantId = req.params.tenant_id;
if (!tenantId) {
return res.status(400).json({ error: "tenant_id is required" });
}

const userInfo = req.userInfo;
if (!userInfo) {
return res.status(401).json({ error: "Unauthorized" });
}

// 管理者権限チェック
if (!hasBillingAccess(userInfo, tenantId)) {
return res.status(403).json({ error: "Insufficient permissions" });
}

const authCli = new AuthClient();
const tenant = (await authCli.tenantApi.getTenant(tenantId)).data;

// 現在のプランの税率情報を取得(プラン履歴の最新エントリから)
let currentTaxRateId: string | null = null;
if (tenant.plan_histories && tenant.plan_histories.length > 0) {
const latestPlanHistory = tenant.plan_histories[tenant.plan_histories.length - 1];
if (latestPlanHistory.tax_rate_id) {
currentTaxRateId = latestPlanHistory.tax_rate_id;
}
}

// レスポンスを構築
const response: TenantPlanResponse = {
id: tenant.id,
name: tenant.name,
plan_id: tenant.plan_id,
tax_rate_id: currentTaxRateId,
plan_reservation: null,
};

// 予約情報がある場合は追加(通常の予約または解除予約)
if (tenant.using_next_plan_from) {
const planReservation = {
next_plan_id: tenant.next_plan_id,
using_next_plan_from: tenant.using_next_plan_from,
next_plan_tax_rate_id: tenant.next_plan_tax_rate_id,
};
response.plan_reservation = planReservation;
}

res.json(response);
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to retrieve tenant detail" });
}
});

/**
* PUT /tenants/:tenant_id/plan
* テナントプランを更新
*/
router.put("/tenants/:tenant_id/plan", async (req: Request, res: Response) => {
try {
const tenantId = req.params.tenant_id;
if (!tenantId) {
return res.status(400).json({ error: "tenant_id is required" });
}

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.

Missing input validation for required field 'next_plan_id'. The field should be validated before proceeding with the request body destructuring to ensure it exists and is a non-empty string.

Suggested change
// Validate next_plan_id before destructuring
if (
!req.body.next_plan_id ||
typeof req.body.next_plan_id !== "string" ||
req.body.next_plan_id.trim() === ""
) {
return res.status(400).json({ error: "next_plan_id is required and must be a non-empty string" });
}

Copilot uses AI. Check for mistakes.
const { next_plan_id, tax_rate_id, using_next_plan_from } = req.body;

const userInfo = req.userInfo;
if (!userInfo) {
return res.status(401).json({ error: "Unauthorized" });
}

// 管理者権限チェック
if (!hasBillingAccess(userInfo, tenantId)) {
return res.status(403).json({ error: "Insufficient permissions" });
}

const authCli = new AuthClient();

// テナントプランを更新
const updateTenantPlanParam: UpdateTenantPlanParam = {
next_plan_id: next_plan_id,
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.

[nitpick] Redundant property assignment. Use shorthand property notation 'next_plan_id' instead of 'next_plan_id: next_plan_id' for cleaner code.

Suggested change
next_plan_id: next_plan_id,
next_plan_id,

Copilot uses AI. Check for mistakes.
};

// 税率IDが指定されている場合のみ設定
if (tax_rate_id && tax_rate_id !== "") {
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.

[nitpick] Inconsistent checking pattern. The condition 'tax_rate_id && tax_rate_id !== ""' can be simplified to just 'tax_rate_id' since the truthiness check already handles empty strings in JavaScript/TypeScript.

Suggested change
if (tax_rate_id && tax_rate_id !== "") {
if (tax_rate_id) {

Copilot uses AI. Check for mistakes.
updateTenantPlanParam.next_plan_tax_rate_id = tax_rate_id;
}

// using_next_plan_fromが指定されている場合のみ設定
if (using_next_plan_from && using_next_plan_from > 0) {
updateTenantPlanParam.using_next_plan_from = using_next_plan_from;
}

await authCli.tenantApi.updateTenantPlan(tenantId, updateTenantPlanParam);

res.json({ message: "Tenant plan updated successfully" });
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to update tenant plan" });
}
});

export default router;