Refactor order and subscription services for improved type safety and error handling
- Updated billing cycle assignment in OrderOrchestrator to ensure proper type handling. - Enhanced error handling in SimManagementService and related components to use more specific types for exceptions. - Standardized error handling across various components to improve consistency and clarity. - Adjusted function signatures in multiple services and controllers to return more precise types, enhancing type safety.
This commit is contained in:
parent
05817e8c67
commit
de35397cf9
@ -261,7 +261,10 @@ export class OrderOrchestrator {
|
||||
quantity: item.Quantity,
|
||||
unitPrice: item.UnitPrice,
|
||||
totalPrice: item.TotalPrice,
|
||||
billingCycle: String(item.PricebookEntry?.Product2?.Billing_Cycle__c || ""),
|
||||
billingCycle: ((): string | undefined => {
|
||||
const v = item.PricebookEntry?.Product2?.Billing_Cycle__c;
|
||||
return typeof v === "string" ? v : undefined;
|
||||
})(),
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Injectable, Inject, BadRequestException, NotFoundException } from "@nestjs/common";
|
||||
import { Injectable, Inject, BadRequestException } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { FreebititService } from "../vendors/freebit/freebit.service";
|
||||
import { WhmcsService } from "../vendors/whmcs/whmcs.service";
|
||||
@ -46,7 +46,10 @@ export class SimManagementService {
|
||||
/**
|
||||
* Debug method to check subscription data for SIM services
|
||||
*/
|
||||
async debugSimSubscription(userId: string, subscriptionId: number): Promise<any> {
|
||||
async debugSimSubscription(
|
||||
userId: string,
|
||||
subscriptionId: number
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const subscription = await this.subscriptionsService.getSubscriptionById(
|
||||
userId,
|
||||
@ -58,11 +61,11 @@ export class SimManagementService {
|
||||
const expectedEid = "89049032000001000000043598005455";
|
||||
|
||||
const simNumberField = Object.entries(subscription.customFields || {}).find(
|
||||
([key, value]) => value && value.toString().includes(expectedSimNumber)
|
||||
([_key, value]) => value && value.toString().includes(expectedSimNumber)
|
||||
);
|
||||
|
||||
const eidField = Object.entries(subscription.customFields || {}).find(
|
||||
([key, value]) => value && value.toString().includes(expectedEid)
|
||||
([_key, value]) => value && value.toString().includes(expectedEid)
|
||||
);
|
||||
|
||||
return {
|
||||
@ -199,7 +202,7 @@ export class SimManagementService {
|
||||
// Check if any field contains the expected SIM number
|
||||
const expectedSimNumber = "02000331144508";
|
||||
const foundSimNumber = Object.entries(subscription.customFields || {}).find(
|
||||
([key, value]) => value && value.toString().includes(expectedSimNumber)
|
||||
([_key, value]) => value && value.toString().includes(expectedSimNumber)
|
||||
);
|
||||
|
||||
if (foundSimNumber) {
|
||||
|
||||
@ -19,13 +19,16 @@ export class SimUsageStoreService {
|
||||
async upsertToday(account: string, usageMb: number, date?: Date): Promise<void> {
|
||||
const day = this.normalizeDate(date);
|
||||
try {
|
||||
await (this.prisma as any).simUsageDaily.upsert({
|
||||
where: { account_date: { account, date: day } as any },
|
||||
await this.prisma.simUsageDaily.upsert({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error composite unique input type depends on Prisma schema
|
||||
where: { account_date: { account, date: day } as unknown },
|
||||
update: { usageMb },
|
||||
create: { account, date: day, usageMb },
|
||||
});
|
||||
} catch (e: any) {
|
||||
this.logger.error("Failed to upsert daily usage", { account, error: e?.message });
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
this.logger.error("Failed to upsert daily usage", { account, error: message });
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +39,7 @@ export class SimUsageStoreService {
|
||||
const end = this.normalizeDate();
|
||||
const start = new Date(end);
|
||||
start.setUTCDate(end.getUTCDate() - (days - 1));
|
||||
const rows = (await (this.prisma as any).simUsageDaily.findMany({
|
||||
const rows = (await this.prisma.simUsageDaily.findMany({
|
||||
where: { account, date: { gte: start, lte: end } },
|
||||
orderBy: { date: "desc" },
|
||||
})) as Array<{ date: Date; usageMb: number }>;
|
||||
@ -46,7 +49,7 @@ export class SimUsageStoreService {
|
||||
async cleanupPreviousMonths(): Promise<number> {
|
||||
const now = new Date();
|
||||
const firstOfMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
||||
const result = await (this.prisma as any).simUsageDaily.deleteMany({
|
||||
const result = await this.prisma.simUsageDaily.deleteMany({
|
||||
where: { date: { lt: firstOfMonth } },
|
||||
});
|
||||
return result.count;
|
||||
|
||||
@ -204,7 +204,7 @@ export class SubscriptionsController {
|
||||
async debugSimSubscription(
|
||||
@Request() req: RequestWithUser,
|
||||
@Param("id", ParseIntPipe) subscriptionId: number
|
||||
) {
|
||||
): Promise<Record<string, unknown>> {
|
||||
return this.simManagementService.debugSimSubscription(req.user.id, subscriptionId);
|
||||
}
|
||||
|
||||
|
||||
106
apps/bff/src/vendors/freebit/freebit.service.ts
vendored
106
apps/bff/src/vendors/freebit/freebit.service.ts
vendored
@ -6,7 +6,7 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import {
|
||||
import type {
|
||||
FreebititConfig,
|
||||
FreebititAuthRequest,
|
||||
FreebititAuthResponse,
|
||||
@ -22,14 +22,13 @@ import {
|
||||
FreebititPlanChangeResponse,
|
||||
FreebititCancelPlanRequest,
|
||||
FreebititCancelPlanResponse,
|
||||
FreebititEsimReissueRequest,
|
||||
FreebititEsimReissueResponse,
|
||||
FreebititEsimAddAccountRequest,
|
||||
FreebititEsimAddAccountResponse,
|
||||
FreebititEsimAccountActivationRequest,
|
||||
FreebititEsimAccountActivationResponse,
|
||||
SimDetails,
|
||||
SimUsage,
|
||||
SimTopUpHistory,
|
||||
FreebititError,
|
||||
FreebititAddSpecRequest,
|
||||
FreebititAddSpecResponse,
|
||||
} from "./interfaces/freebit.types";
|
||||
@ -148,9 +147,12 @@ export class FreebititService {
|
||||
/**
|
||||
* Make authenticated API request with error handling
|
||||
*/
|
||||
private async makeAuthenticatedRequest<T>(endpoint: string, data: any): Promise<T> {
|
||||
private async makeAuthenticatedRequest<T extends { resultCode: string | number; status?: { message?: string; statusCode?: string | number } }>(
|
||||
endpoint: string,
|
||||
data: unknown
|
||||
): Promise<T> {
|
||||
const authKey = await this.getAuthKey();
|
||||
const requestData = { ...data, authKey };
|
||||
const requestData = { ...(data as Record<string, unknown>), authKey };
|
||||
|
||||
try {
|
||||
const url = `${this.config.baseUrl}${endpoint}`;
|
||||
@ -164,10 +166,13 @@ export class FreebititService {
|
||||
|
||||
if (!response.ok) {
|
||||
let bodySnippet: string | undefined;
|
||||
let text: string | null = null;
|
||||
try {
|
||||
const text = await response.text();
|
||||
bodySnippet = text ? text.slice(0, 500) : undefined;
|
||||
} catch {}
|
||||
text = await response.text();
|
||||
} catch (_e) {
|
||||
text = null;
|
||||
}
|
||||
bodySnippet = text ? text.slice(0, 500) : undefined;
|
||||
this.logger.error("Freebit API non-OK response", {
|
||||
endpoint,
|
||||
url,
|
||||
@ -181,39 +186,39 @@ export class FreebititService {
|
||||
const responseData = (await response.json()) as T;
|
||||
|
||||
// Check for API-level errors
|
||||
if (responseData && (responseData as any).resultCode !== "100") {
|
||||
const errorData = responseData as any;
|
||||
const errorMessage = errorData.status?.message || "Unknown error";
|
||||
const rc = String(responseData?.resultCode ?? "");
|
||||
if (rc !== "100") {
|
||||
const errorMessage = String(responseData.status?.message ?? "Unknown error");
|
||||
|
||||
// Provide more specific error messages for common cases
|
||||
let userFriendlyMessage = `API Error: ${errorMessage}`;
|
||||
if (errorMessage === "NG") {
|
||||
userFriendlyMessage = `Account not found or invalid in Freebit system. Please verify the account number exists and is properly configured.`;
|
||||
} else if (errorMessage.includes("auth") || errorMessage.includes("Auth")) {
|
||||
} else if (errorMessage.toLowerCase().includes("auth")) {
|
||||
userFriendlyMessage = `Authentication failed with Freebit API. Please check API credentials.`;
|
||||
} else if (errorMessage.includes("timeout") || errorMessage.includes("Timeout")) {
|
||||
} else if (errorMessage.toLowerCase().includes("timeout")) {
|
||||
userFriendlyMessage = `Request timeout to Freebit API. Please try again later.`;
|
||||
}
|
||||
|
||||
this.logger.error("Freebit API error response", {
|
||||
endpoint,
|
||||
resultCode: errorData.resultCode,
|
||||
statusCode: errorData.status?.statusCode,
|
||||
resultCode: rc,
|
||||
statusCode: responseData.status?.statusCode,
|
||||
message: errorMessage,
|
||||
userFriendlyMessage,
|
||||
});
|
||||
|
||||
throw new FreebititErrorImpl(
|
||||
userFriendlyMessage,
|
||||
errorData.resultCode,
|
||||
errorData.status?.statusCode,
|
||||
rc,
|
||||
String(responseData.status?.statusCode ?? ""),
|
||||
errorMessage
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.debug("Freebit API Request Success", {
|
||||
endpoint,
|
||||
resultCode: (responseData as any).resultCode,
|
||||
resultCode: rc,
|
||||
});
|
||||
|
||||
return responseData;
|
||||
@ -222,12 +227,9 @@ export class FreebititService {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error(`Freebit API request failed: ${endpoint}`, {
|
||||
error: (error as any).message,
|
||||
});
|
||||
throw new InternalServerErrorException(
|
||||
`Freebit API request failed: ${(error as any).message}`
|
||||
);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Freebit API request failed: ${endpoint}`, { error: message });
|
||||
throw new InternalServerErrorException(`Freebit API request failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,7 +266,7 @@ export class FreebititService {
|
||||
);
|
||||
|
||||
let response: FreebititAccountDetailsResponse | undefined;
|
||||
let lastError: any;
|
||||
let lastError: unknown;
|
||||
for (const ep of candidates) {
|
||||
try {
|
||||
if (ep !== candidates[0]) {
|
||||
@ -275,9 +277,9 @@ export class FreebititService {
|
||||
request
|
||||
);
|
||||
break; // success
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
lastError = err;
|
||||
if (typeof err?.message === "string" && err.message.includes("HTTP 404")) {
|
||||
if (err instanceof Error && err.message.includes("HTTP 404")) {
|
||||
// try next candidate
|
||||
continue;
|
||||
}
|
||||
@ -293,14 +295,40 @@ export class FreebititService {
|
||||
);
|
||||
}
|
||||
|
||||
const datas = (response as any).responseDatas;
|
||||
const list = Array.isArray(datas) ? datas : datas ? [datas] : [];
|
||||
type AcctDetailItem = {
|
||||
kind?: string;
|
||||
account?: string | number;
|
||||
state?: string;
|
||||
startDate?: string | number;
|
||||
relationCode?: string;
|
||||
resultCode?: string | number;
|
||||
planCode?: string;
|
||||
iccid?: string | number;
|
||||
imsi?: string | number;
|
||||
eid?: string;
|
||||
contractLine?: string;
|
||||
size?: "standard" | "nano" | "micro" | "esim" | string;
|
||||
sms?: number;
|
||||
talk?: number;
|
||||
ipv4?: string;
|
||||
ipv6?: string;
|
||||
quota?: number;
|
||||
async?: { func: string; date: string | number };
|
||||
voicemail?: number;
|
||||
voiceMail?: number;
|
||||
callwaiting?: number;
|
||||
callWaiting?: number;
|
||||
worldwing?: number;
|
||||
worldWing?: number;
|
||||
};
|
||||
|
||||
const datas = response.responseDatas as unknown;
|
||||
const list = Array.isArray(datas) ? (datas as AcctDetailItem[]) : datas ? [datas as AcctDetailItem] : [];
|
||||
if (!list.length) {
|
||||
throw new BadRequestException("No SIM details found for this account");
|
||||
}
|
||||
// Prefer the MVNO entry if present
|
||||
const mvno =
|
||||
list.find((d: any) => (d.kind || "").toString().toUpperCase() === "MVNO") || list[0];
|
||||
const mvno = list.find(d => String(d.kind ?? "").toUpperCase() === "MVNO") || list[0];
|
||||
const simData = mvno;
|
||||
|
||||
const startDateRaw = simData.startDate ? String(simData.startDate) : undefined;
|
||||
@ -348,11 +376,10 @@ export class FreebititService {
|
||||
});
|
||||
|
||||
return simDetails;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Failed to get SIM details for account ${account}`, {
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Failed to get SIM details for account ${account}`, { error: message });
|
||||
throw error as Error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -655,7 +682,7 @@ export class FreebititService {
|
||||
throw new BadRequestException("eSIM EID not found for this account");
|
||||
}
|
||||
|
||||
const payload: import("./interfaces/freebit.types").FreebititEsimAccountActivationRequest = {
|
||||
const payload: FreebititEsimAccountActivationRequest = {
|
||||
authKey,
|
||||
aladinOperated: "20",
|
||||
createType: "reissue",
|
||||
@ -686,8 +713,7 @@ export class FreebititService {
|
||||
throw new InternalServerErrorException(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data =
|
||||
(await response.json()) as import("./interfaces/freebit.types").FreebititEsimAccountActivationResponse;
|
||||
const data = (await response.json()) as FreebititEsimAccountActivationResponse;
|
||||
const rc =
|
||||
typeof data.resultCode === "number" ? String(data.resultCode) : data.resultCode || "";
|
||||
if (rc !== "100") {
|
||||
|
||||
@ -18,7 +18,7 @@ export interface FreebititAccountDetailsRequest {
|
||||
authKey: string;
|
||||
version?: string | number; // Docs recommend "2"
|
||||
requestDatas: Array<{
|
||||
kind: "MASTER" | "MVNO" | string;
|
||||
kind: string;
|
||||
account?: string | number;
|
||||
}>;
|
||||
}
|
||||
@ -33,9 +33,9 @@ export interface FreebititAccountDetailsResponse {
|
||||
// Docs show this can be an array (MASTER + MVNO) or a single object for MVNO
|
||||
responseDatas:
|
||||
| {
|
||||
kind: "MASTER" | "MVNO" | string;
|
||||
kind: string;
|
||||
account: string | number;
|
||||
state: "active" | "suspended" | "temporary" | "waiting" | "obsolete" | string;
|
||||
state: string;
|
||||
startDate?: string | number;
|
||||
relationCode?: string;
|
||||
resultCode?: string | number;
|
||||
@ -44,21 +44,21 @@ export interface FreebititAccountDetailsResponse {
|
||||
imsi?: string | number;
|
||||
eid?: string;
|
||||
contractLine?: string;
|
||||
size?: "standard" | "nano" | "micro" | "esim" | string;
|
||||
size?: string;
|
||||
sms?: number; // 10=active, 20=inactive
|
||||
talk?: number; // 10=active, 20=inactive
|
||||
ipv4?: string;
|
||||
ipv6?: string;
|
||||
quota?: number; // Remaining quota (units vary by env)
|
||||
async?: {
|
||||
func: "regist" | "stop" | "resume" | "cancel" | "pinset" | "pinunset" | string;
|
||||
func: string;
|
||||
date: string | number;
|
||||
};
|
||||
}
|
||||
| Array<{
|
||||
kind: "MASTER" | "MVNO" | string;
|
||||
kind: string;
|
||||
account: string | number;
|
||||
state: "active" | "suspended" | "temporary" | "waiting" | "obsolete" | string;
|
||||
state: string;
|
||||
startDate?: string | number;
|
||||
relationCode?: string;
|
||||
resultCode?: string | number;
|
||||
@ -67,14 +67,14 @@ export interface FreebititAccountDetailsResponse {
|
||||
imsi?: string | number;
|
||||
eid?: string;
|
||||
contractLine?: string;
|
||||
size?: "standard" | "nano" | "micro" | "esim" | string;
|
||||
size?: string;
|
||||
sms?: number;
|
||||
talk?: number;
|
||||
ipv4?: string;
|
||||
ipv6?: string;
|
||||
quota?: number;
|
||||
async?: {
|
||||
func: "regist" | "stop" | "resume" | "cancel" | "pinset" | "pinunset" | string;
|
||||
func: string;
|
||||
date: string | number;
|
||||
};
|
||||
}>;
|
||||
@ -244,13 +244,13 @@ export interface FreebititEsimAccountActivationRequest {
|
||||
aladinOperated: string; // '10' issue, '20' no-issue
|
||||
masterAccount?: string;
|
||||
masterPassword?: string;
|
||||
createType: "new" | "reissue" | "exchange" | string;
|
||||
createType: string;
|
||||
eid?: string; // required for reissue/exchange per business rules
|
||||
account: string; // MSISDN
|
||||
simkind: "esim" | string;
|
||||
simkind: string;
|
||||
repAccount?: string;
|
||||
size?: string;
|
||||
addKind?: "N" | "R" | string; // e.g., 'R' for reissue
|
||||
addKind?: string; // e.g., 'R' for reissue
|
||||
oldEid?: string;
|
||||
oldProductNumber?: string;
|
||||
mnp?: {
|
||||
@ -272,7 +272,7 @@ export interface FreebititEsimAccountActivationRequest {
|
||||
|
||||
export interface FreebititEsimAccountActivationResponse {
|
||||
resultCode: number | string;
|
||||
status?: any;
|
||||
status?: unknown;
|
||||
statusCode?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@ -5,15 +5,7 @@ import { Invoice, InvoiceList } from "@customer-portal/shared";
|
||||
import { WhmcsConnectionService } from "./whmcs-connection.service";
|
||||
import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer";
|
||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
||||
import {
|
||||
WhmcsGetInvoicesParams,
|
||||
WhmcsCreateInvoiceParams,
|
||||
WhmcsCreateInvoiceResponse,
|
||||
WhmcsUpdateInvoiceParams,
|
||||
WhmcsUpdateInvoiceResponse,
|
||||
WhmcsCapturePaymentParams,
|
||||
WhmcsCapturePaymentResponse,
|
||||
} from "../types/whmcs-api.types";
|
||||
import { WhmcsGetInvoicesParams } from "../types/whmcs-api.types";
|
||||
|
||||
export interface InvoiceFilters {
|
||||
status?: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-env node */
|
||||
/* eslint-disable no-console */
|
||||
// Ensure dev-time Next.js manifests exist to avoid noisy ENOENT errors
|
||||
import { mkdirSync, existsSync, writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
@ -457,7 +457,7 @@ export default function SimPlansPage() {
|
||||
<div className="font-medium text-blue-900">Contract Period</div>
|
||||
<p className="text-blue-800">
|
||||
Minimum 3 full billing months required. First month (sign-up to end of month) is
|
||||
free and doesn't count toward contract.
|
||||
free and doesn't count toward contract.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -12,8 +12,6 @@ import {
|
||||
CubeIcon,
|
||||
StarIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
PlusIcon,
|
||||
BoltIcon,
|
||||
ExclamationTriangleIcon,
|
||||
EnvelopeIcon,
|
||||
PhoneIcon,
|
||||
|
||||
@ -20,7 +20,7 @@ export default function SimCancelPage() {
|
||||
try {
|
||||
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/cancel`, {});
|
||||
setMessage("SIM service cancelled successfully");
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : "Failed to cancel SIM service");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -62,7 +62,7 @@ export default function SimCancelPage() {
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={submit}
|
||||
onClick={() => void submit()}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 rounded-md bg-red-600 text-white text-sm disabled:opacity-50"
|
||||
>
|
||||
|
||||
@ -43,7 +43,7 @@ export default function SimChangePlanPage() {
|
||||
newPlanCode,
|
||||
});
|
||||
setMessage("Plan change submitted successfully");
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : "Failed to change plan");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -79,7 +79,7 @@ export default function SimChangePlanPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={submit} className="space-y-6">
|
||||
<form onSubmit={(e) => void submit(e)} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">New Plan</label>
|
||||
<select
|
||||
|
||||
@ -46,7 +46,7 @@ export default function SimTopUpPage() {
|
||||
quotaMb: getCurrentAmountMb(),
|
||||
});
|
||||
setMessage(`Successfully topped up ${gbAmount} GB for ¥${calculateCost().toLocaleString()}`);
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : "Failed to submit top-up");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -83,7 +83,7 @@ export default function SimTopUpPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<form onSubmit={(e) => void handleSubmit(e)} className="space-y-6">
|
||||
{/* Amount Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Amount (GB)</label>
|
||||
|
||||
@ -46,7 +46,7 @@ export function ChangePlanModal({
|
||||
newPlanCode: newPlanCode,
|
||||
});
|
||||
onSuccess();
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
onError(e instanceof Error ? e.message : "Failed to change plan");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -103,7 +103,7 @@ export function ChangePlanModal({
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
onClick={() => void submit()}
|
||||
disabled={loading}
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
||||
>
|
||||
|
||||
@ -242,8 +242,8 @@ export function DataUsageChart({
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-red-800">High Usage Warning</h4>
|
||||
<p className="text-sm text-red-700 mt-1">
|
||||
You've used {usagePercentage.toFixed(1)}% of your data quota. Consider topping up
|
||||
to avoid service interruption.
|
||||
You have used {usagePercentage.toFixed(1)}% of your data quota. Consider topping
|
||||
up to avoid service interruption.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -257,8 +257,8 @@ export function DataUsageChart({
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-yellow-800">Usage Notice</h4>
|
||||
<p className="text-sm text-yellow-700 mt-1">
|
||||
You've used {usagePercentage.toFixed(1)}% of your data quota. Consider monitoring
|
||||
your usage.
|
||||
You have used {usagePercentage.toFixed(1)}% of your data quota. Consider
|
||||
monitoring your usage.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -63,7 +63,7 @@ export function SimActions({
|
||||
setSuccess("eSIM profile reissued successfully");
|
||||
setShowReissueConfirm(false);
|
||||
onReissueSuccess?.();
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : "Failed to reissue eSIM profile");
|
||||
} finally {
|
||||
setLoading(null);
|
||||
@ -80,7 +80,7 @@ export function SimActions({
|
||||
setSuccess("SIM service cancelled successfully");
|
||||
setShowCancelConfirm(false);
|
||||
onCancelSuccess?.();
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : "Failed to cancel SIM service");
|
||||
} finally {
|
||||
setLoading(null);
|
||||
@ -399,7 +399,7 @@ export function SimActions({
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleReissueEsim}
|
||||
onClick={() => void handleReissueEsim()}
|
||||
disabled={loading === "reissue"}
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-600 text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
||||
>
|
||||
@ -449,7 +449,7 @@ export function SimActions({
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelSim}
|
||||
onClick={() => void handleCancelSim()}
|
||||
disabled={loading === "cancel"}
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
|
||||
>
|
||||
|
||||
@ -63,22 +63,24 @@ export function SimFeatureToggles({
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
try {
|
||||
const featurePayload: any = {};
|
||||
const featurePayload: {
|
||||
voiceMailEnabled?: boolean;
|
||||
callWaitingEnabled?: boolean;
|
||||
internationalRoamingEnabled?: boolean;
|
||||
networkType?: "4G" | "5G";
|
||||
} = {};
|
||||
if (vm !== initial.vm) featurePayload.voiceMailEnabled = vm;
|
||||
if (cw !== initial.cw) featurePayload.callWaitingEnabled = cw;
|
||||
if (ir !== initial.ir) featurePayload.internationalRoamingEnabled = ir;
|
||||
if (nt !== initial.nt) featurePayload.networkType = nt;
|
||||
|
||||
if (Object.keys(featurePayload).length > 0) {
|
||||
await authenticatedApi.post(
|
||||
`/subscriptions/${subscriptionId}/sim/features`,
|
||||
featurePayload
|
||||
);
|
||||
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/features`, featurePayload);
|
||||
}
|
||||
|
||||
setSuccess("Changes submitted successfully");
|
||||
onChanged?.();
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : "Failed to submit changes");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -347,7 +349,7 @@ export function SimFeatureToggles({
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<button
|
||||
onClick={applyChanges}
|
||||
onClick={() => void applyChanges()}
|
||||
disabled={loading}
|
||||
className="flex-1 inline-flex items-center justify-center px-6 py-3 border border-transparent rounded-lg text-sm font-semibold text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
||||
>
|
||||
@ -389,7 +391,7 @@ export function SimFeatureToggles({
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={reset}
|
||||
onClick={() => reset()}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 rounded-lg text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
||||
>
|
||||
|
||||
@ -36,12 +36,14 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
|
||||
}>(`/subscriptions/${subscriptionId}/sim`);
|
||||
|
||||
setSimInfo(data);
|
||||
} catch (error: any) {
|
||||
if (error.status === 400) {
|
||||
} catch (err: unknown) {
|
||||
const hasStatus = (v: unknown): v is { status: number } =>
|
||||
typeof v === 'object' && v !== null && 'status' in v && typeof (v as { status: unknown }).status === 'number';
|
||||
if (hasStatus(err) && err.status === 400) {
|
||||
// Not a SIM subscription - this component shouldn't be shown
|
||||
setError("This subscription is not a SIM service");
|
||||
} else {
|
||||
setError(error instanceof Error ? error.message : "Failed to load SIM information");
|
||||
setError(err instanceof Error ? err.message : "Failed to load SIM information");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -49,17 +51,17 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSimInfo();
|
||||
void fetchSimInfo();
|
||||
}, [subscriptionId]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setLoading(true);
|
||||
fetchSimInfo();
|
||||
void fetchSimInfo();
|
||||
};
|
||||
|
||||
const handleActionSuccess = () => {
|
||||
// Refresh SIM info after any successful action
|
||||
fetchSimInfo();
|
||||
void fetchSimInfo();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
||||
@ -48,7 +48,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
|
||||
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/top-up`, requestBody);
|
||||
|
||||
onSuccess();
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
onError(error instanceof Error ? error.message : "Failed to top up SIM");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -87,7 +87,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={(e) => void handleSubmit(e)}>
|
||||
{/* Amount Input */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Amount (GB)</label>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user