From 425ef83dbab744b8003f3d90ec8271a1a1e37140 Mon Sep 17 00:00:00 2001 From: tema Date: Tue, 9 Sep 2025 15:40:13 +0900 Subject: [PATCH] Update SIM management service and UI components for Freebit API compliance - Adjusted quota validation in SimManagementService to enforce limits of 100MB to 51200MB for Freebit API compatibility. - Updated cost calculation to round up GB usage for billing, ensuring accurate invoice generation. - Modified top-up modal and related UI components to reflect new limits of 1-50 GB, aligning with Freebit API constraints. - Enhanced documentation to clarify pricing structure and API data flow adjustments. --- .../subscriptions/sim-management.service.ts | 18 ++++++-- .../src/vendors/freebit/freebit.service.ts | 46 +++++++++++++------ .../freebit/interfaces/freebit.types.ts | 10 +++- .../subscriptions/[id]/sim/top-up/page.tsx | 18 ++++---- .../sim-management/components/TopUpModal.tsx | 10 ++-- docs/FREEBIT-SIM-MANAGEMENT.md | 14 +++--- docs/SIM-MANAGEMENT-API-DATA-FLOW.md | 6 +-- 7 files changed, 78 insertions(+), 44 deletions(-) diff --git a/apps/bff/src/subscriptions/sim-management.service.ts b/apps/bff/src/subscriptions/sim-management.service.ts index ee5e1988..fda505e6 100644 --- a/apps/bff/src/subscriptions/sim-management.service.ts +++ b/apps/bff/src/subscriptions/sim-management.service.ts @@ -298,10 +298,15 @@ export class SimManagementService { throw new BadRequestException('Quota must be between 1MB and 100GB'); } - - // Calculate cost: 1GB = 500 JPY + // Calculate cost: 1GB = 500 JPY (rounded up to nearest GB) const quotaGb = request.quotaMb / 1000; - const costJpy = Math.round(quotaGb * 500); + const units = Math.ceil(quotaGb); + const costJpy = units * 500; + + // Validate quota against Freebit API limits (100MB - 51200MB) + if (request.quotaMb < 100 || request.quotaMb > 51200) { + throw new BadRequestException('Quota must be between 100MB and 51200MB (50GB) for Freebit API compatibility'); + } // Get client mapping for WHMCS const mapping = await this.mappingsService.findByUserId(userId); @@ -321,7 +326,7 @@ export class SimManagementService { // Step 1: Create WHMCS invoice const invoice = await this.whmcsService.createInvoice({ clientId: mapping.whmcsClientId, - description: `SIM Data Top-up: ${quotaGb.toFixed(1)}GB for ${account}`, + description: `SIM Data Top-up: ${units}GB for ${account}`, amount: costJpy, currency: 'JPY', dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now @@ -336,6 +341,11 @@ export class SimManagementService { }); // Step 2: Capture payment + this.logger.log(`Attempting payment capture`, { + invoiceId: invoice.id, + amount: costJpy, + }); + const paymentResult = await this.whmcsService.capturePayment({ invoiceId: invoice.id, amount: costJpy, diff --git a/apps/bff/src/vendors/freebit/freebit.service.ts b/apps/bff/src/vendors/freebit/freebit.service.ts index a69e8fbc..8e4c3745 100644 --- a/apps/bff/src/vendors/freebit/freebit.service.ts +++ b/apps/bff/src/vendors/freebit/freebit.service.ts @@ -380,30 +380,47 @@ export class FreebititService { scheduledAt?: string; } = {}): Promise { try { - const quotaKb = quotaMb * 1000; - - const request: Omit = { - account, - quota: quotaKb, - quotaCode: options.campaignCode, - expire: options.expiryDate, - }; + // Units per endpoint: + // - Immediate (PA04-04 /master/addSpec/): quota in MB (string), requires kind: 'MVNO' + // - Scheduled (PA05-22 /mvno/eachQuota/): quota in KB (string), accepts runTime + const quotaKb = quotaMb * 1000; // KB using decimal base to align with Freebit examples + const quotaMbStr = String(Math.round(quotaMb)); + const quotaKbStr = String(Math.round(quotaKb)); - // Use PA05-22 for scheduled top-ups, PA04-04 for immediate - const endpoint = options.scheduledAt ? '/mvno/eachQuota/' : '/master/addSpec/'; - - if (options.scheduledAt && endpoint === '/mvno/eachQuota/') { - (request as any).runTime = options.scheduledAt; + const isScheduled = !!options.scheduledAt; + const endpoint = isScheduled ? '/mvno/eachQuota/' : '/master/addSpec/'; + + let request: Omit; + if (isScheduled) { + // PA05-22: KB + runTime + request = { + account, + quota: quotaKbStr, + quotaCode: options.campaignCode, + expire: options.expiryDate, + runTime: options.scheduledAt, + }; + } else { + // PA04-04: MB + kind + request = { + account, + kind: 'MVNO', + quota: quotaMbStr, + quotaCode: options.campaignCode, + expire: options.expiryDate, + }; } await this.makeAuthenticatedRequest(endpoint, request); this.logger.log(`Successfully topped up SIM ${account}`, { account, + endpoint, quotaMb, quotaKb, + units: isScheduled ? 'KB (PA05-22)' : 'MB (PA04-04)', campaignCode: options.campaignCode, - scheduled: !!options.scheduledAt, + scheduled: isScheduled, }); } catch (error: any) { this.logger.error(`Failed to top up SIM ${account}`, { @@ -511,6 +528,7 @@ export class FreebititService { try { const request: Omit = { account, + kind: 'MVNO', }; if (typeof features.voiceMailEnabled === 'boolean') { diff --git a/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts b/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts index 1dc1052d..01958b21 100644 --- a/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts +++ b/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts @@ -102,9 +102,16 @@ export interface FreebititTrafficInfoResponse { export interface FreebititTopUpRequest { authKey: string; account: string; - quota: number; // KB units (e.g., 102400 for 100MB) + // NOTE: quota units vary by endpoint + // - PA04-04 (/master/addSpec/): MB units (string recommended by spec) + // - PA05-22 (/mvno/eachQuota/): KB units (string recommended by spec) + quota: number | string; quotaCode?: string; // Campaign code expire?: string; // YYYYMMDD format + // For PA04-04 addSpec + kind?: string; // e.g. 'MVNO' (required by /master/addSpec/) + // For PA05-22 eachQuota + runTime?: string; // YYYYMMDD or YYYYMMDDhhmmss } export interface FreebititTopUpResponse { @@ -119,6 +126,7 @@ export interface FreebititTopUpResponse { export interface FreebititAddSpecRequest { authKey: string; account: string; + kind?: string; // Required by PA04-04 (/master/addSpec/), e.g. 'MVNO' // Feature flags: 10 = enabled, 20 = disabled voiceMail?: '10' | '20'; voicemail?: '10' | '20'; diff --git a/apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx b/apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx index 8efa8fea..6c7c0876 100644 --- a/apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx +++ b/apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx @@ -21,7 +21,7 @@ export default function SimTopUpPage() { const isValidAmount = () => { const gb = Number(gbAmount); - return Number.isInteger(gb) && gb >= 1 && gb <= 100; // 1-100 GB in whole numbers + return Number.isInteger(gb) && gb >= 1 && gb <= 50; // 1-50 GB in whole numbers (Freebit API limit) }; const calculateCost = () => { @@ -89,7 +89,7 @@ export default function SimTopUpPage() { onChange={(e) => setGbAmount(e.target.value)} placeholder="Enter amount in GB" min="1" - max="100" + max="50" step="1" className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 pr-12" /> @@ -97,9 +97,9 @@ export default function SimTopUpPage() { GB -

- Enter the amount of data you want to add (1 - 100 GB, whole numbers) -

+

+ Enter the amount of data you want to add (1 - 50 GB, whole numbers) +

{/* Cost Display */} @@ -118,7 +118,7 @@ export default function SimTopUpPage() { ¥{calculateCost().toLocaleString()}
- (500 JPY per GB) + (1GB = ¥500)
@@ -131,9 +131,9 @@ export default function SimTopUpPage() { -

- Amount must be a whole number between 1 GB and 100 GB -

+

+ Amount must be a whole number between 1 GB and 50 GB +

)} diff --git a/apps/portal/src/features/sim-management/components/TopUpModal.tsx b/apps/portal/src/features/sim-management/components/TopUpModal.tsx index 42e78edf..6f380b23 100644 --- a/apps/portal/src/features/sim-management/components/TopUpModal.tsx +++ b/apps/portal/src/features/sim-management/components/TopUpModal.tsx @@ -26,7 +26,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU const isValidAmount = () => { const gb = Number(gbAmount); - return Number.isInteger(gb) && gb >= 1 && gb <= 100; // 1-100 GB, whole numbers only + return Number.isInteger(gb) && gb >= 1 && gb <= 50; // 1-50 GB, whole numbers only (Freebit API limit) }; const calculateCost = () => { @@ -104,7 +104,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU onChange={(e) => setGbAmount(e.target.value)} placeholder="Enter amount in GB" min="1" - max="100" + max="50" step="1" className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 pr-12" /> @@ -113,7 +113,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU

- Enter the amount of data you want to add (1 - 100 GB, whole numbers) + Enter the amount of data you want to add (1 - 50 GB, whole numbers)

@@ -133,7 +133,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU ¥{calculateCost().toLocaleString()}
- (500 JPY per GB) + (1GB = ¥500)
@@ -145,7 +145,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU

- Amount must be a whole number between 1 GB and 100 GB + Amount must be a whole number between 1 GB and 50 GB

diff --git a/docs/FREEBIT-SIM-MANAGEMENT.md b/docs/FREEBIT-SIM-MANAGEMENT.md index 9748ddc4..d6149a36 100644 --- a/docs/FREEBIT-SIM-MANAGEMENT.md +++ b/docs/FREEBIT-SIM-MANAGEMENT.md @@ -344,7 +344,7 @@ This section provides a detailed breakdown of every element on the SIM managemen #### Top Up Data (Complete Payment Flow): 1. User clicks "Top Up Data" → Opens `TopUpModal` 2. User selects amount (1GB = 500 JPY) → `POST /api/subscriptions/29951/sim/top-up` -3. Backend: Calculate cost (quotaGb * 500 JPY) +3. Backend: Calculate cost (ceil(GB) × ¥500) 4. Backend: WHMCS `CreateInvoice` → Generate invoice for payment 5. Backend: WHMCS `CapturePayment` → Process payment with invoice 6. Backend: If payment successful → Freebit `PA04-04: Add Specs & Quota` @@ -813,13 +813,11 @@ The Freebit SIM management system is now fully implemented and ready for deploym User Action → Cost Calculation → Invoice Creation → Payment Capture → Data Addition ``` -### 📊 **Pricing Structure**: -- **1 GB = 500 JPY** -- **2 GB = 1,000 JPY** -- **5 GB = 2,500 JPY** -- **10 GB = 5,000 JPY** -- **20 GB = 10,000 JPY** -- **50 GB = 25,000 JPY** +### 📊 **Pricing Structure** +- **1 GB = ¥500** +- **2 GB = ¥1,000** +- **5 GB = ¥2,500** +- **10 GB = ¥5,000** ### ⚠️ **Error Handling**: - **Payment Failed**: No data added, user notified diff --git a/docs/SIM-MANAGEMENT-API-DATA-FLOW.md b/docs/SIM-MANAGEMENT-API-DATA-FLOW.md index 82d49eab..fff1fa1b 100644 --- a/docs/SIM-MANAGEMENT-API-DATA-FLOW.md +++ b/docs/SIM-MANAGEMENT-API-DATA-FLOW.md @@ -295,7 +295,7 @@ TopUpModal ───────▶ POST /sim/top-up ───────▶ capturePayment ───────────────▶ │ │ paid (or failed) ├── on success ─────────────────────────────▶ /master/addSpec/ - │ (quota in KB) + │ (quota in MB) └── on failure ──┐ └──── return error (no Freebit call) ``` @@ -308,12 +308,12 @@ BFF responsibilities - Validate `quotaMb` (1–100000) - Price computation and invoice line creation (description includes quota) - Attempt payment capture (stored method or SSO handoff) -- On success, call Freebit AddSpec with `quota=quotaMb*1000` and optional `expire` +- On success, call Freebit AddSpec with `quota` in MB (string) and optional `expire` - Return success to UI and refresh SIM info Freebit PA04-04 (Add Spec & Quota) request fields - `account`: MSISDN (phone number) -- `quota`: integer KB (100MB–51200MB in screenshot spec; environment-dependent) +- `quota`: integer MB (string) (100MB–51200MB) - `quotaCode` (optional): campaign code - `expire` (optional): YYYYMMDD