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.
This commit is contained in:
tema 2025-09-09 15:40:13 +09:00
parent 5c6057bf2e
commit 425ef83dba
7 changed files with 78 additions and 44 deletions

View File

@ -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,

View File

@ -380,30 +380,47 @@ export class FreebititService {
scheduledAt?: string;
} = {}): Promise<void> {
try {
const quotaKb = quotaMb * 1000;
const request: Omit<FreebititTopUpRequest, 'authKey'> = {
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<FreebititTopUpRequest, 'authKey'>;
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<FreebititTopUpResponse>(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<FreebititAddSpecRequest, 'authKey'> = {
account,
kind: 'MVNO',
};
if (typeof features.voiceMailEnabled === 'boolean') {

View File

@ -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';

View File

@ -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() {
<span className="text-gray-500 text-sm">GB</span>
</div>
</div>
<p className="text-xs text-gray-500 mt-1">
Enter the amount of data you want to add (1 - 100 GB, whole numbers)
</p>
<p className="text-xs text-gray-500 mt-1">
Enter the amount of data you want to add (1 - 50 GB, whole numbers)
</p>
</div>
{/* Cost Display */}
@ -118,7 +118,7 @@ export default function SimTopUpPage() {
¥{calculateCost().toLocaleString()}
</div>
<div className="text-xs text-blue-700">
(500 JPY per GB)
(1GB = ¥500)
</div>
</div>
</div>
@ -131,9 +131,9 @@ export default function SimTopUpPage() {
<svg className="h-4 w-4 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<p className="text-sm text-red-800">
Amount must be a whole number between 1 GB and 100 GB
</p>
<p className="text-sm text-red-800">
Amount must be a whole number between 1 GB and 50 GB
</p>
</div>
</div>
)}

View File

@ -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
</div>
</div>
<p className="text-xs text-gray-500 mt-1">
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)
</p>
</div>
@ -133,7 +133,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
¥{calculateCost().toLocaleString()}
</div>
<div className="text-xs text-blue-700">
(500 JPY per GB)
(1GB = ¥500)
</div>
</div>
</div>
@ -145,7 +145,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
<div className="flex items-center">
<ExclamationTriangleIcon className="h-4 w-4 text-red-500 mr-2" />
<p className="text-sm text-red-800">
Amount must be a whole number between 1 GB and 100 GB
Amount must be a whole number between 1 GB and 50 GB
</p>
</div>
</div>

View File

@ -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

View File

@ -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` (1100000)
- 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 (100MB51200MB in screenshot spec; environment-dependent)
- `quota`: integer MB (string) (100MB51200MB)
- `quotaCode` (optional): campaign code
- `expire` (optional): YYYYMMDD