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:
parent
5c6057bf2e
commit
425ef83dba
@ -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,
|
||||
|
||||
46
apps/bff/src/vendors/freebit/freebit.service.ts
vendored
46
apps/bff/src/vendors/freebit/freebit.service.ts
vendored
@ -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') {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user