Enhance SIM management and Freebit service with improved scheduling and error handling

- Updated SimManagementService to schedule contract line changes 30 minutes after applying voice options, improving user experience.
- Refactored FreebititService to include a new method for authenticated JSON POST requests, enhancing error handling and logging for API responses.
- Introduced new interfaces for voice option and contract line change requests and responses, improving type safety and clarity in API interactions.
- Enhanced error handling in FreebititService to provide more specific error messages based on API response status codes.
This commit is contained in:
tema 2025-09-09 17:13:10 +09:00
parent 340ff94d07
commit 74e27e3ca2
3 changed files with 176 additions and 32 deletions

View File

@ -640,7 +640,52 @@ export class SimManagementService {
throw new BadRequestException('networkType must be either "4G" or "5G"');
}
await this.freebititService.updateSimFeatures(account, request);
const doVoice =
typeof request.voiceMailEnabled === 'boolean' ||
typeof request.callWaitingEnabled === 'boolean' ||
typeof request.internationalRoamingEnabled === 'boolean';
const doContract = typeof request.networkType === 'string';
if (doVoice && doContract) {
// First apply voice options immediately (PA05-06)
await this.freebititService.updateSimFeatures(account, {
voiceMailEnabled: request.voiceMailEnabled,
callWaitingEnabled: request.callWaitingEnabled,
internationalRoamingEnabled: request.internationalRoamingEnabled,
});
// Then schedule contract line change after 30 minutes (PA05-38)
const delayMs = 30 * 60 * 1000;
setTimeout(() => {
this.freebititService
.updateSimFeatures(account, { networkType: request.networkType })
.then(() =>
this.logger.log('Deferred contract line change executed after 30 minutes', {
userId,
subscriptionId,
account,
networkType: request.networkType,
})
)
.catch(err =>
this.logger.error('Deferred contract line change failed', {
error: getErrorMessage(err),
userId,
subscriptionId,
account,
})
);
}, delayMs);
this.logger.log('Scheduled contract line change 30 minutes after voice option change', {
userId,
subscriptionId,
account,
networkType: request.networkType,
});
} else {
await this.freebititService.updateSimFeatures(account, request);
}
this.logger.log(`Updated SIM features for subscription ${subscriptionId}`, {
userId,

View File

@ -182,10 +182,16 @@ export class FreebititService {
const responseData = (await response.json()) as T;
// Check for API-level errors
const rc = String(responseData?.resultCode ?? "");
if (rc !== "100") {
const errorMessage = String(responseData.status?.message ?? "Unknown error");
// Check for API-level errors (some endpoints return resultCode '101' with message 'OK')
const rc = String((responseData as any)?.resultCode ?? "");
const statusObj: any = (responseData as any)?.status ?? {};
const errorMessage = String((statusObj?.message ?? (responseData as any)?.message ?? "Unknown error"));
const statusCodeStr = String(statusObj?.statusCode ?? (responseData as any)?.statusCode ?? "");
const msgUpper = errorMessage.toUpperCase();
const isOkByRc = rc === "100" || rc === "101";
const isOkByMessage = msgUpper === "OK" || msgUpper === "SUCCESS";
const isOkByStatus = statusCodeStr === "200";
if (!(isOkByRc || isOkByMessage || isOkByStatus)) {
// Provide more specific error messages for common cases
let userFriendlyMessage = `API Error: ${errorMessage}`;
@ -200,7 +206,7 @@ export class FreebititService {
this.logger.error("Freebit API error response", {
endpoint,
resultCode: rc,
statusCode: responseData.status?.statusCode,
statusCode: statusCodeStr,
message: errorMessage,
userFriendlyMessage,
});
@ -208,7 +214,7 @@ export class FreebititService {
throw new FreebititErrorImpl(
userFriendlyMessage,
rc,
String(responseData.status?.statusCode ?? ""),
statusCodeStr,
errorMessage
);
}
@ -230,6 +236,42 @@ export class FreebititService {
}
}
// Make authenticated JSON POST request (for endpoints that require JSON body)
private async makeAuthenticatedJsonRequest<T>(endpoint: string, body: Record<string, unknown>): Promise<T> {
const authKey = await this.getAuthKey();
const url = `${this.config.baseUrl}${endpoint}`;
const payload = { ...body, authKey };
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text().catch(() => null);
this.logger.error('Freebit JSON API non-OK', {
endpoint,
status: response.status,
statusText: response.statusText,
body: text?.slice(0, 500),
});
throw new InternalServerErrorException(`HTTP ${response.status}: ${response.statusText}`);
}
const data = (await response.json()) as T;
const rc = String((data as any)?.resultCode ?? '');
if (rc !== '100') {
const message = (data as any)?.message || (data as any)?.status?.message || 'Unknown error';
this.logger.error('Freebit JSON API error response', {
endpoint,
resultCode: rc,
statusCode: (data as any)?.statusCode || (data as any)?.status?.statusCode,
message,
});
throw new FreebititErrorImpl(`API Error: ${message}`, rc, String((data as any)?.statusCode || ''), message);
}
this.logger.debug('Freebit JSON API Request Success', { endpoint, resultCode: rc });
return data;
}
/**
* Get detailed SIM account information
*/
@ -602,30 +644,48 @@ export class FreebititService {
}
): Promise<void> {
try {
const request: Omit<FreebititAddSpecRequest, "authKey"> = {
account,
kind: "MVNO",
};
const doVoice =
typeof features.voiceMailEnabled === 'boolean' ||
typeof features.callWaitingEnabled === 'boolean' ||
typeof features.internationalRoamingEnabled === 'boolean';
const doContract = typeof features.networkType === 'string';
if (typeof features.voiceMailEnabled === "boolean") {
request.voiceMail = features.voiceMailEnabled ? ("10" as const) : ("20" as const);
request.voicemail = request.voiceMail; // include alternate casing for compatibility
}
if (typeof features.callWaitingEnabled === "boolean") {
request.callWaiting = features.callWaitingEnabled ? ("10" as const) : ("20" as const);
request.callwaiting = request.callWaiting;
}
if (typeof features.internationalRoamingEnabled === "boolean") {
request.worldWing = features.internationalRoamingEnabled
? ("10" as const)
: ("20" as const);
request.worldwing = request.worldWing;
}
if (features.networkType) {
request.contractLine = features.networkType;
if (doVoice) {
const talkOption: any = {};
if (typeof features.voiceMailEnabled === 'boolean') {
talkOption.voiceMail = features.voiceMailEnabled ? '10' : '20';
}
if (typeof features.callWaitingEnabled === 'boolean') {
talkOption.callWaiting = features.callWaitingEnabled ? '10' : '20';
}
if (typeof features.internationalRoamingEnabled === 'boolean') {
talkOption.worldWing = features.internationalRoamingEnabled ? '10' : '20';
}
await this.makeAuthenticatedRequest<import('./interfaces/freebit.types').FreebititVoiceOptionChangeResponse>(
'/mvno/talkoption/changeOrder/',
{
account,
userConfirmed: '10',
aladinOperated: '10',
talkOption,
}
);
this.logger.log('Applied voice option change (PA05-06)', { account, talkOption });
}
await this.makeAuthenticatedRequest<FreebititAddSpecResponse>("/master/addSpec/", request);
if (doContract && features.networkType) {
await this.makeAuthenticatedJsonRequest<import('./interfaces/freebit.types').FreebititContractLineChangeResponse>(
'/mvno/contractline/change/',
{
account,
contractLine: features.networkType,
}
);
this.logger.log('Applied contract line change (PA05-38)', {
account,
contractLine: features.networkType,
});
}
this.logger.log(`Updated SIM features for account ${account}`, {
account,
@ -636,10 +696,7 @@ export class FreebititService {
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to update SIM features for account ${account}`, {
error: message,
account,
});
this.logger.error(`Failed to update SIM features for account ${account}`, { error: message, account });
throw error as Error;
}
}

View File

@ -187,6 +187,48 @@ export interface FreebititPlanChangeResponse {
ipv6?: string;
}
// PA05-06: MVNO Voice Option Change
export interface FreebititVoiceOptionChangeRequest {
authKey: string;
account: string;
userConfirmed: '10' | '20';
aladinOperated: '10' | '20';
talkOption: {
voiceMail?: '10' | '20';
callWaiting?: '10' | '20';
worldWing?: '10' | '20';
worldCall?: '10' | '20';
callTransfer?: '10' | '20';
callTransferNoId?: '10' | '20';
worldCallCreditLimit?: string;
worldWingCreditLimit?: string;
};
}
export interface FreebititVoiceOptionChangeResponse {
resultCode: string;
status: {
message: string;
statusCode: string;
};
}
// PA05-38: MVNO Contract Change (4G/5G)
export interface FreebititContractLineChangeRequest {
authKey: string;
account: string;
contractLine: '4G' | '5G';
productNumber?: string;
eid?: string;
}
export interface FreebititContractLineChangeResponse {
resultCode: string | number;
status?: unknown;
statusCode?: string;
message?: string;
}
export interface FreebititCancelPlanRequest {
authKey: string;
account: string;