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:
tema 2025-09-09 15:59:30 +09:00
parent 05817e8c67
commit de35397cf9
19 changed files with 141 additions and 110 deletions

View File

@ -261,7 +261,10 @@ export class OrderOrchestrator {
quantity: item.Quantity, quantity: item.Quantity,
unitPrice: item.UnitPrice, unitPrice: item.UnitPrice,
totalPrice: item.TotalPrice, 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; return acc;
}, },

View File

@ -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 { Logger } from "nestjs-pino";
import { FreebititService } from "../vendors/freebit/freebit.service"; import { FreebititService } from "../vendors/freebit/freebit.service";
import { WhmcsService } from "../vendors/whmcs/whmcs.service"; import { WhmcsService } from "../vendors/whmcs/whmcs.service";
@ -46,7 +46,10 @@ export class SimManagementService {
/** /**
* Debug method to check subscription data for SIM services * 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 { try {
const subscription = await this.subscriptionsService.getSubscriptionById( const subscription = await this.subscriptionsService.getSubscriptionById(
userId, userId,
@ -58,11 +61,11 @@ export class SimManagementService {
const expectedEid = "89049032000001000000043598005455"; const expectedEid = "89049032000001000000043598005455";
const simNumberField = Object.entries(subscription.customFields || {}).find( 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( const eidField = Object.entries(subscription.customFields || {}).find(
([key, value]) => value && value.toString().includes(expectedEid) ([_key, value]) => value && value.toString().includes(expectedEid)
); );
return { return {
@ -199,7 +202,7 @@ export class SimManagementService {
// Check if any field contains the expected SIM number // Check if any field contains the expected SIM number
const expectedSimNumber = "02000331144508"; const expectedSimNumber = "02000331144508";
const foundSimNumber = Object.entries(subscription.customFields || {}).find( const foundSimNumber = Object.entries(subscription.customFields || {}).find(
([key, value]) => value && value.toString().includes(expectedSimNumber) ([_key, value]) => value && value.toString().includes(expectedSimNumber)
); );
if (foundSimNumber) { if (foundSimNumber) {

View File

@ -19,13 +19,16 @@ export class SimUsageStoreService {
async upsertToday(account: string, usageMb: number, date?: Date): Promise<void> { async upsertToday(account: string, usageMb: number, date?: Date): Promise<void> {
const day = this.normalizeDate(date); const day = this.normalizeDate(date);
try { try {
await (this.prisma as any).simUsageDaily.upsert({ await this.prisma.simUsageDaily.upsert({
where: { account_date: { account, date: day } as any }, // 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 }, update: { usageMb },
create: { account, date: day, usageMb }, create: { account, date: day, usageMb },
}); });
} catch (e: any) { } catch (e: unknown) {
this.logger.error("Failed to upsert daily usage", { account, error: e?.message }); 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 end = this.normalizeDate();
const start = new Date(end); const start = new Date(end);
start.setUTCDate(end.getUTCDate() - (days - 1)); 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 } }, where: { account, date: { gte: start, lte: end } },
orderBy: { date: "desc" }, orderBy: { date: "desc" },
})) as Array<{ date: Date; usageMb: number }>; })) as Array<{ date: Date; usageMb: number }>;
@ -46,7 +49,7 @@ export class SimUsageStoreService {
async cleanupPreviousMonths(): Promise<number> { async cleanupPreviousMonths(): Promise<number> {
const now = new Date(); const now = new Date();
const firstOfMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); 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 } }, where: { date: { lt: firstOfMonth } },
}); });
return result.count; return result.count;

View File

@ -204,7 +204,7 @@ export class SubscriptionsController {
async debugSimSubscription( async debugSimSubscription(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number @Param("id", ParseIntPipe) subscriptionId: number
) { ): Promise<Record<string, unknown>> {
return this.simManagementService.debugSimSubscription(req.user.id, subscriptionId); return this.simManagementService.debugSimSubscription(req.user.id, subscriptionId);
} }

View File

@ -6,7 +6,7 @@ import {
} from "@nestjs/common"; } from "@nestjs/common";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino"; import { Logger } from "nestjs-pino";
import { import type {
FreebititConfig, FreebititConfig,
FreebititAuthRequest, FreebititAuthRequest,
FreebititAuthResponse, FreebititAuthResponse,
@ -22,14 +22,13 @@ import {
FreebititPlanChangeResponse, FreebititPlanChangeResponse,
FreebititCancelPlanRequest, FreebititCancelPlanRequest,
FreebititCancelPlanResponse, FreebititCancelPlanResponse,
FreebititEsimReissueRequest,
FreebititEsimReissueResponse,
FreebititEsimAddAccountRequest, FreebititEsimAddAccountRequest,
FreebititEsimAddAccountResponse, FreebititEsimAddAccountResponse,
FreebititEsimAccountActivationRequest,
FreebititEsimAccountActivationResponse,
SimDetails, SimDetails,
SimUsage, SimUsage,
SimTopUpHistory, SimTopUpHistory,
FreebititError,
FreebititAddSpecRequest, FreebititAddSpecRequest,
FreebititAddSpecResponse, FreebititAddSpecResponse,
} from "./interfaces/freebit.types"; } from "./interfaces/freebit.types";
@ -148,9 +147,12 @@ export class FreebititService {
/** /**
* Make authenticated API request with error handling * 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 authKey = await this.getAuthKey();
const requestData = { ...data, authKey }; const requestData = { ...(data as Record<string, unknown>), authKey };
try { try {
const url = `${this.config.baseUrl}${endpoint}`; const url = `${this.config.baseUrl}${endpoint}`;
@ -164,10 +166,13 @@ export class FreebititService {
if (!response.ok) { if (!response.ok) {
let bodySnippet: string | undefined; let bodySnippet: string | undefined;
let text: string | null = null;
try { try {
const text = await response.text(); text = await response.text();
bodySnippet = text ? text.slice(0, 500) : undefined; } catch (_e) {
} catch {} text = null;
}
bodySnippet = text ? text.slice(0, 500) : undefined;
this.logger.error("Freebit API non-OK response", { this.logger.error("Freebit API non-OK response", {
endpoint, endpoint,
url, url,
@ -181,39 +186,39 @@ export class FreebititService {
const responseData = (await response.json()) as T; const responseData = (await response.json()) as T;
// Check for API-level errors // Check for API-level errors
if (responseData && (responseData as any).resultCode !== "100") { const rc = String(responseData?.resultCode ?? "");
const errorData = responseData as any; if (rc !== "100") {
const errorMessage = errorData.status?.message || "Unknown error"; const errorMessage = String(responseData.status?.message ?? "Unknown error");
// Provide more specific error messages for common cases // Provide more specific error messages for common cases
let userFriendlyMessage = `API Error: ${errorMessage}`; let userFriendlyMessage = `API Error: ${errorMessage}`;
if (errorMessage === "NG") { if (errorMessage === "NG") {
userFriendlyMessage = `Account not found or invalid in Freebit system. Please verify the account number exists and is properly configured.`; 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.`; 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.`; userFriendlyMessage = `Request timeout to Freebit API. Please try again later.`;
} }
this.logger.error("Freebit API error response", { this.logger.error("Freebit API error response", {
endpoint, endpoint,
resultCode: errorData.resultCode, resultCode: rc,
statusCode: errorData.status?.statusCode, statusCode: responseData.status?.statusCode,
message: errorMessage, message: errorMessage,
userFriendlyMessage, userFriendlyMessage,
}); });
throw new FreebititErrorImpl( throw new FreebititErrorImpl(
userFriendlyMessage, userFriendlyMessage,
errorData.resultCode, rc,
errorData.status?.statusCode, String(responseData.status?.statusCode ?? ""),
errorMessage errorMessage
); );
} }
this.logger.debug("Freebit API Request Success", { this.logger.debug("Freebit API Request Success", {
endpoint, endpoint,
resultCode: (responseData as any).resultCode, resultCode: rc,
}); });
return responseData; return responseData;
@ -222,12 +227,9 @@ export class FreebititService {
throw error; throw error;
} }
this.logger.error(`Freebit API request failed: ${endpoint}`, { const message = error instanceof Error ? error.message : String(error);
error: (error as any).message, this.logger.error(`Freebit API request failed: ${endpoint}`, { error: message });
}); throw new InternalServerErrorException(`Freebit API request failed: ${message}`);
throw new InternalServerErrorException(
`Freebit API request failed: ${(error as any).message}`
);
} }
} }
@ -264,7 +266,7 @@ export class FreebititService {
); );
let response: FreebititAccountDetailsResponse | undefined; let response: FreebititAccountDetailsResponse | undefined;
let lastError: any; let lastError: unknown;
for (const ep of candidates) { for (const ep of candidates) {
try { try {
if (ep !== candidates[0]) { if (ep !== candidates[0]) {
@ -275,9 +277,9 @@ export class FreebititService {
request request
); );
break; // success break; // success
} catch (err: any) { } catch (err: unknown) {
lastError = err; lastError = err;
if (typeof err?.message === "string" && err.message.includes("HTTP 404")) { if (err instanceof Error && err.message.includes("HTTP 404")) {
// try next candidate // try next candidate
continue; continue;
} }
@ -293,14 +295,40 @@ export class FreebititService {
); );
} }
const datas = (response as any).responseDatas; type AcctDetailItem = {
const list = Array.isArray(datas) ? datas : datas ? [datas] : []; 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) { if (!list.length) {
throw new BadRequestException("No SIM details found for this account"); throw new BadRequestException("No SIM details found for this account");
} }
// Prefer the MVNO entry if present // Prefer the MVNO entry if present
const mvno = const mvno = list.find(d => String(d.kind ?? "").toUpperCase() === "MVNO") || list[0];
list.find((d: any) => (d.kind || "").toString().toUpperCase() === "MVNO") || list[0];
const simData = mvno; const simData = mvno;
const startDateRaw = simData.startDate ? String(simData.startDate) : undefined; const startDateRaw = simData.startDate ? String(simData.startDate) : undefined;
@ -348,11 +376,10 @@ export class FreebititService {
}); });
return simDetails; return simDetails;
} catch (error: any) { } catch (error: unknown) {
this.logger.error(`Failed to get SIM details for account ${account}`, { const message = error instanceof Error ? error.message : String(error);
error: error.message, this.logger.error(`Failed to get SIM details for account ${account}`, { error: message });
}); throw error as Error;
throw error;
} }
} }
@ -655,7 +682,7 @@ export class FreebititService {
throw new BadRequestException("eSIM EID not found for this account"); throw new BadRequestException("eSIM EID not found for this account");
} }
const payload: import("./interfaces/freebit.types").FreebititEsimAccountActivationRequest = { const payload: FreebititEsimAccountActivationRequest = {
authKey, authKey,
aladinOperated: "20", aladinOperated: "20",
createType: "reissue", createType: "reissue",
@ -686,8 +713,7 @@ export class FreebititService {
throw new InternalServerErrorException(`HTTP ${response.status}: ${response.statusText}`); throw new InternalServerErrorException(`HTTP ${response.status}: ${response.statusText}`);
} }
const data = const data = (await response.json()) as FreebititEsimAccountActivationResponse;
(await response.json()) as import("./interfaces/freebit.types").FreebititEsimAccountActivationResponse;
const rc = const rc =
typeof data.resultCode === "number" ? String(data.resultCode) : data.resultCode || ""; typeof data.resultCode === "number" ? String(data.resultCode) : data.resultCode || "";
if (rc !== "100") { if (rc !== "100") {

View File

@ -18,7 +18,7 @@ export interface FreebititAccountDetailsRequest {
authKey: string; authKey: string;
version?: string | number; // Docs recommend "2" version?: string | number; // Docs recommend "2"
requestDatas: Array<{ requestDatas: Array<{
kind: "MASTER" | "MVNO" | string; kind: string;
account?: string | number; 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 // Docs show this can be an array (MASTER + MVNO) or a single object for MVNO
responseDatas: responseDatas:
| { | {
kind: "MASTER" | "MVNO" | string; kind: string;
account: string | number; account: string | number;
state: "active" | "suspended" | "temporary" | "waiting" | "obsolete" | string; state: string;
startDate?: string | number; startDate?: string | number;
relationCode?: string; relationCode?: string;
resultCode?: string | number; resultCode?: string | number;
@ -44,21 +44,21 @@ export interface FreebititAccountDetailsResponse {
imsi?: string | number; imsi?: string | number;
eid?: string; eid?: string;
contractLine?: string; contractLine?: string;
size?: "standard" | "nano" | "micro" | "esim" | string; size?: string;
sms?: number; // 10=active, 20=inactive sms?: number; // 10=active, 20=inactive
talk?: number; // 10=active, 20=inactive talk?: number; // 10=active, 20=inactive
ipv4?: string; ipv4?: string;
ipv6?: string; ipv6?: string;
quota?: number; // Remaining quota (units vary by env) quota?: number; // Remaining quota (units vary by env)
async?: { async?: {
func: "regist" | "stop" | "resume" | "cancel" | "pinset" | "pinunset" | string; func: string;
date: string | number; date: string | number;
}; };
} }
| Array<{ | Array<{
kind: "MASTER" | "MVNO" | string; kind: string;
account: string | number; account: string | number;
state: "active" | "suspended" | "temporary" | "waiting" | "obsolete" | string; state: string;
startDate?: string | number; startDate?: string | number;
relationCode?: string; relationCode?: string;
resultCode?: string | number; resultCode?: string | number;
@ -67,14 +67,14 @@ export interface FreebititAccountDetailsResponse {
imsi?: string | number; imsi?: string | number;
eid?: string; eid?: string;
contractLine?: string; contractLine?: string;
size?: "standard" | "nano" | "micro" | "esim" | string; size?: string;
sms?: number; sms?: number;
talk?: number; talk?: number;
ipv4?: string; ipv4?: string;
ipv6?: string; ipv6?: string;
quota?: number; quota?: number;
async?: { async?: {
func: "regist" | "stop" | "resume" | "cancel" | "pinset" | "pinunset" | string; func: string;
date: string | number; date: string | number;
}; };
}>; }>;
@ -244,13 +244,13 @@ export interface FreebititEsimAccountActivationRequest {
aladinOperated: string; // '10' issue, '20' no-issue aladinOperated: string; // '10' issue, '20' no-issue
masterAccount?: string; masterAccount?: string;
masterPassword?: string; masterPassword?: string;
createType: "new" | "reissue" | "exchange" | string; createType: string;
eid?: string; // required for reissue/exchange per business rules eid?: string; // required for reissue/exchange per business rules
account: string; // MSISDN account: string; // MSISDN
simkind: "esim" | string; simkind: string;
repAccount?: string; repAccount?: string;
size?: string; size?: string;
addKind?: "N" | "R" | string; // e.g., 'R' for reissue addKind?: string; // e.g., 'R' for reissue
oldEid?: string; oldEid?: string;
oldProductNumber?: string; oldProductNumber?: string;
mnp?: { mnp?: {
@ -272,7 +272,7 @@ export interface FreebititEsimAccountActivationRequest {
export interface FreebititEsimAccountActivationResponse { export interface FreebititEsimAccountActivationResponse {
resultCode: number | string; resultCode: number | string;
status?: any; status?: unknown;
statusCode?: string; statusCode?: string;
message?: string; message?: string;
} }

View File

@ -5,15 +5,7 @@ import { Invoice, InvoiceList } from "@customer-portal/shared";
import { WhmcsConnectionService } from "./whmcs-connection.service"; import { WhmcsConnectionService } from "./whmcs-connection.service";
import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer"; import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer";
import { WhmcsCacheService } from "../cache/whmcs-cache.service"; import { WhmcsCacheService } from "../cache/whmcs-cache.service";
import { import { WhmcsGetInvoicesParams } from "../types/whmcs-api.types";
WhmcsGetInvoicesParams,
WhmcsCreateInvoiceParams,
WhmcsCreateInvoiceResponse,
WhmcsUpdateInvoiceParams,
WhmcsUpdateInvoiceResponse,
WhmcsCapturePaymentParams,
WhmcsCapturePaymentResponse,
} from "../types/whmcs-api.types";
export interface InvoiceFilters { export interface InvoiceFilters {
status?: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections"; status?: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";

View File

@ -1,4 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/* eslint-env node */
/* eslint-disable no-console */
// Ensure dev-time Next.js manifests exist to avoid noisy ENOENT errors // Ensure dev-time Next.js manifests exist to avoid noisy ENOENT errors
import { mkdirSync, existsSync, writeFileSync } from "fs"; import { mkdirSync, existsSync, writeFileSync } from "fs";
import { join } from "path"; import { join } from "path";

View File

@ -457,7 +457,7 @@ export default function SimPlansPage() {
<div className="font-medium text-blue-900">Contract Period</div> <div className="font-medium text-blue-900">Contract Period</div>
<p className="text-blue-800"> <p className="text-blue-800">
Minimum 3 full billing months required. First month (sign-up to end of month) is 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&apos;t count toward contract.
</p> </p>
</div> </div>
<div> <div>

View File

@ -12,8 +12,6 @@ import {
CubeIcon, CubeIcon,
StarIcon, StarIcon,
WrenchScrewdriverIcon, WrenchScrewdriverIcon,
PlusIcon,
BoltIcon,
ExclamationTriangleIcon, ExclamationTriangleIcon,
EnvelopeIcon, EnvelopeIcon,
PhoneIcon, PhoneIcon,

View File

@ -20,7 +20,7 @@ export default function SimCancelPage() {
try { try {
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/cancel`, {}); await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/cancel`, {});
setMessage("SIM service cancelled successfully"); setMessage("SIM service cancelled successfully");
} catch (e: any) { } catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to cancel SIM service"); setError(e instanceof Error ? e.message : "Failed to cancel SIM service");
} finally { } finally {
setLoading(false); setLoading(false);
@ -62,7 +62,7 @@ export default function SimCancelPage() {
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={submit} onClick={() => void submit()}
disabled={loading} disabled={loading}
className="px-4 py-2 rounded-md bg-red-600 text-white text-sm disabled:opacity-50" className="px-4 py-2 rounded-md bg-red-600 text-white text-sm disabled:opacity-50"
> >

View File

@ -43,7 +43,7 @@ export default function SimChangePlanPage() {
newPlanCode, newPlanCode,
}); });
setMessage("Plan change submitted successfully"); setMessage("Plan change submitted successfully");
} catch (e: any) { } catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to change plan"); setError(e instanceof Error ? e.message : "Failed to change plan");
} finally { } finally {
setLoading(false); setLoading(false);
@ -79,7 +79,7 @@ export default function SimChangePlanPage() {
</div> </div>
)} )}
<form onSubmit={submit} className="space-y-6"> <form onSubmit={(e) => void submit(e)} className="space-y-6">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">New Plan</label> <label className="block text-sm font-medium text-gray-700 mb-2">New Plan</label>
<select <select

View File

@ -46,7 +46,7 @@ export default function SimTopUpPage() {
quotaMb: getCurrentAmountMb(), quotaMb: getCurrentAmountMb(),
}); });
setMessage(`Successfully topped up ${gbAmount} GB for ¥${calculateCost().toLocaleString()}`); 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"); setError(e instanceof Error ? e.message : "Failed to submit top-up");
} finally { } finally {
setLoading(false); setLoading(false);
@ -83,7 +83,7 @@ export default function SimTopUpPage() {
</div> </div>
)} )}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={(e) => void handleSubmit(e)} className="space-y-6">
{/* Amount Input */} {/* Amount Input */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Amount (GB)</label> <label className="block text-sm font-medium text-gray-700 mb-2">Amount (GB)</label>

View File

@ -46,7 +46,7 @@ export function ChangePlanModal({
newPlanCode: newPlanCode, newPlanCode: newPlanCode,
}); });
onSuccess(); onSuccess();
} catch (e: any) { } catch (e: unknown) {
onError(e instanceof Error ? e.message : "Failed to change plan"); onError(e instanceof Error ? e.message : "Failed to change plan");
} finally { } finally {
setLoading(false); 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"> <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
type="button" type="button"
onClick={submit} onClick={() => void submit()}
disabled={loading} 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" 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"
> >

View File

@ -242,8 +242,8 @@ export function DataUsageChart({
<div> <div>
<h4 className="text-sm font-medium text-red-800">High Usage Warning</h4> <h4 className="text-sm font-medium text-red-800">High Usage Warning</h4>
<p className="text-sm text-red-700 mt-1"> <p className="text-sm text-red-700 mt-1">
You've used {usagePercentage.toFixed(1)}% of your data quota. Consider topping up You have used {usagePercentage.toFixed(1)}% of your data quota. Consider topping
to avoid service interruption. up to avoid service interruption.
</p> </p>
</div> </div>
</div> </div>
@ -257,8 +257,8 @@ export function DataUsageChart({
<div> <div>
<h4 className="text-sm font-medium text-yellow-800">Usage Notice</h4> <h4 className="text-sm font-medium text-yellow-800">Usage Notice</h4>
<p className="text-sm text-yellow-700 mt-1"> <p className="text-sm text-yellow-700 mt-1">
You've used {usagePercentage.toFixed(1)}% of your data quota. Consider monitoring You have used {usagePercentage.toFixed(1)}% of your data quota. Consider
your usage. monitoring your usage.
</p> </p>
</div> </div>
</div> </div>

View File

@ -63,7 +63,7 @@ export function SimActions({
setSuccess("eSIM profile reissued successfully"); setSuccess("eSIM profile reissued successfully");
setShowReissueConfirm(false); setShowReissueConfirm(false);
onReissueSuccess?.(); onReissueSuccess?.();
} catch (error: any) { } catch (error: unknown) {
setError(error instanceof Error ? error.message : "Failed to reissue eSIM profile"); setError(error instanceof Error ? error.message : "Failed to reissue eSIM profile");
} finally { } finally {
setLoading(null); setLoading(null);
@ -80,7 +80,7 @@ export function SimActions({
setSuccess("SIM service cancelled successfully"); setSuccess("SIM service cancelled successfully");
setShowCancelConfirm(false); setShowCancelConfirm(false);
onCancelSuccess?.(); onCancelSuccess?.();
} catch (error: any) { } catch (error: unknown) {
setError(error instanceof Error ? error.message : "Failed to cancel SIM service"); setError(error instanceof Error ? error.message : "Failed to cancel SIM service");
} finally { } finally {
setLoading(null); 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"> <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
type="button" type="button"
onClick={handleReissueEsim} onClick={() => void handleReissueEsim()}
disabled={loading === "reissue"} 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" 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"> <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
type="button" type="button"
onClick={handleCancelSim} onClick={() => void handleCancelSim()}
disabled={loading === "cancel"} 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" 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"
> >

View File

@ -63,22 +63,24 @@ export function SimFeatureToggles({
setError(null); setError(null);
setSuccess(null); setSuccess(null);
try { try {
const featurePayload: any = {}; const featurePayload: {
voiceMailEnabled?: boolean;
callWaitingEnabled?: boolean;
internationalRoamingEnabled?: boolean;
networkType?: "4G" | "5G";
} = {};
if (vm !== initial.vm) featurePayload.voiceMailEnabled = vm; if (vm !== initial.vm) featurePayload.voiceMailEnabled = vm;
if (cw !== initial.cw) featurePayload.callWaitingEnabled = cw; if (cw !== initial.cw) featurePayload.callWaitingEnabled = cw;
if (ir !== initial.ir) featurePayload.internationalRoamingEnabled = ir; if (ir !== initial.ir) featurePayload.internationalRoamingEnabled = ir;
if (nt !== initial.nt) featurePayload.networkType = nt; if (nt !== initial.nt) featurePayload.networkType = nt;
if (Object.keys(featurePayload).length > 0) { if (Object.keys(featurePayload).length > 0) {
await authenticatedApi.post( await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/features`, featurePayload);
`/subscriptions/${subscriptionId}/sim/features`,
featurePayload
);
} }
setSuccess("Changes submitted successfully"); setSuccess("Changes submitted successfully");
onChanged?.(); onChanged?.();
} catch (e: any) { } catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to submit changes"); setError(e instanceof Error ? e.message : "Failed to submit changes");
} finally { } finally {
setLoading(false); setLoading(false);
@ -347,7 +349,7 @@ export function SimFeatureToggles({
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-3">
<button <button
onClick={applyChanges} onClick={() => void applyChanges()}
disabled={loading} 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" 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>
<button <button
onClick={reset} onClick={() => reset()}
disabled={loading} 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" 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"
> >

View File

@ -36,12 +36,14 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
}>(`/subscriptions/${subscriptionId}/sim`); }>(`/subscriptions/${subscriptionId}/sim`);
setSimInfo(data); setSimInfo(data);
} catch (error: any) { } catch (err: unknown) {
if (error.status === 400) { 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 // Not a SIM subscription - this component shouldn't be shown
setError("This subscription is not a SIM service"); setError("This subscription is not a SIM service");
} else { } else {
setError(error instanceof Error ? error.message : "Failed to load SIM information"); setError(err instanceof Error ? err.message : "Failed to load SIM information");
} }
} finally { } finally {
setLoading(false); setLoading(false);
@ -49,17 +51,17 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
}; };
useEffect(() => { useEffect(() => {
fetchSimInfo(); void fetchSimInfo();
}, [subscriptionId]); }, [subscriptionId]);
const handleRefresh = () => { const handleRefresh = () => {
setLoading(true); setLoading(true);
fetchSimInfo(); void fetchSimInfo();
}; };
const handleActionSuccess = () => { const handleActionSuccess = () => {
// Refresh SIM info after any successful action // Refresh SIM info after any successful action
fetchSimInfo(); void fetchSimInfo();
}; };
if (loading) { if (loading) {

View File

@ -48,7 +48,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/top-up`, requestBody); await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/top-up`, requestBody);
onSuccess(); onSuccess();
} catch (error: any) { } catch (error: unknown) {
onError(error instanceof Error ? error.message : "Failed to top up SIM"); onError(error instanceof Error ? error.message : "Failed to top up SIM");
} finally { } finally {
setLoading(false); setLoading(false);
@ -87,7 +87,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
</button> </button>
</div> </div>
<form onSubmit={handleSubmit}> <form onSubmit={(e) => void handleSubmit(e)}>
{/* Amount Input */} {/* Amount Input */}
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">Amount (GB)</label> <label className="block text-sm font-medium text-gray-700 mb-2">Amount (GB)</label>