- Deleted migration file that removed cached profile fields from the users table, centralizing profile data retrieval from WHMCS. - Updated CsrfMiddleware to include new public authentication endpoints for password reset, setting password, and WHMCS account linking. - Enhanced error handling in password and WHMCS linking workflows to provide clearer feedback on missing mappings and improve user experience. - Adjusted user creation and update methods in UsersFacade to handle cases where WHMCS mappings are not yet available, ensuring smoother account setup.
208 lines
6.8 KiB
TypeScript
208 lines
6.8 KiB
TypeScript
import { Injectable, Inject, BadRequestException } from "@nestjs/common";
|
|
import { Logger } from "nestjs-pino";
|
|
import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.service";
|
|
import { SimValidationService } from "./sim-validation.service";
|
|
import { SimNotificationService } from "./sim-notification.service";
|
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
|
import type { SimPlanChangeRequest, SimFeaturesUpdateRequest } from "../types/sim-requests.types";
|
|
|
|
@Injectable()
|
|
export class SimPlanService {
|
|
constructor(
|
|
private readonly freebitService: FreebitOrchestratorService,
|
|
private readonly simValidation: SimValidationService,
|
|
private readonly simNotification: SimNotificationService,
|
|
@Inject(Logger) private readonly logger: Logger
|
|
) {}
|
|
|
|
/**
|
|
* Change SIM plan
|
|
*/
|
|
async changeSimPlan(
|
|
userId: string,
|
|
subscriptionId: number,
|
|
request: SimPlanChangeRequest
|
|
): Promise<{ ipv4?: string; ipv6?: string }> {
|
|
try {
|
|
const { account } = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
|
|
|
// Validate plan code format - simplified for 5GB, 10GB, 25GB, 50GB plans
|
|
if (!request.newPlanCode || !["5GB", "10GB", "25GB", "50GB"].includes(request.newPlanCode)) {
|
|
throw new BadRequestException("Invalid plan code. Must be one of: 5GB, 10GB, 25GB, 50GB");
|
|
}
|
|
|
|
// Map simplified plan codes to Freebit plan codes
|
|
const planCodeMapping: Record<string, string> = {
|
|
"5GB": "PASI_5G",
|
|
"10GB": "PASI_10G",
|
|
"25GB": "PASI_25G",
|
|
"50GB": "PASI_50G",
|
|
};
|
|
|
|
const freebitPlanCode = planCodeMapping[request.newPlanCode];
|
|
if (!freebitPlanCode) {
|
|
throw new BadRequestException(`Unable to map plan code ${request.newPlanCode} to Freebit format`);
|
|
}
|
|
|
|
// Automatically set to 1st of next month
|
|
const nextMonth = new Date();
|
|
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
|
nextMonth.setDate(1); // Set to 1st of the month
|
|
|
|
// Format as YYYYMMDD for Freebit API
|
|
const year = nextMonth.getFullYear();
|
|
const month = String(nextMonth.getMonth() + 1).padStart(2, "0");
|
|
const day = String(nextMonth.getDate()).padStart(2, "0");
|
|
const scheduledAt = `${year}${month}${day}`;
|
|
|
|
this.logger.log(`Auto-scheduled plan change to 1st of next month: ${scheduledAt}`, {
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
newPlanCode: request.newPlanCode,
|
|
freebitPlanCode,
|
|
});
|
|
|
|
// First try immediate change, fallback to scheduled if that fails
|
|
let result: { ipv4?: string; ipv6?: string };
|
|
|
|
try {
|
|
result = await this.freebitService.changeSimPlan(account, freebitPlanCode, {
|
|
assignGlobalIp: false, // No global IP assignment
|
|
// Try immediate first
|
|
});
|
|
|
|
this.logger.log(`Immediate plan change successful for account ${account}`, {
|
|
account,
|
|
newPlanCode: request.newPlanCode,
|
|
freebitPlanCode,
|
|
});
|
|
} catch (immediateError) {
|
|
this.logger.warn(`Immediate plan change failed, trying scheduled: ${getErrorMessage(immediateError)}`, {
|
|
account,
|
|
newPlanCode: request.newPlanCode,
|
|
freebitPlanCode,
|
|
error: getErrorMessage(immediateError),
|
|
});
|
|
|
|
// Fallback to scheduled change
|
|
result = await this.freebitService.changeSimPlan(account, freebitPlanCode, {
|
|
assignGlobalIp: false, // No global IP assignment
|
|
scheduledAt: scheduledAt,
|
|
});
|
|
|
|
this.logger.log(`Scheduled plan change successful for account ${account}`, {
|
|
account,
|
|
newPlanCode: request.newPlanCode,
|
|
freebitPlanCode,
|
|
scheduledAt,
|
|
});
|
|
}
|
|
|
|
this.logger.log(`Successfully changed SIM plan for subscription ${subscriptionId}`, {
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
newPlanCode: request.newPlanCode,
|
|
freebitPlanCode,
|
|
scheduledAt: scheduledAt,
|
|
assignGlobalIp: false,
|
|
});
|
|
|
|
await this.simNotification.notifySimAction("Change Plan", "SUCCESS", {
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
newPlanCode: request.newPlanCode,
|
|
freebitPlanCode,
|
|
scheduledAt,
|
|
});
|
|
|
|
return result;
|
|
} catch (error) {
|
|
const sanitizedError = getErrorMessage(error);
|
|
this.logger.error(`Failed to change SIM plan for subscription ${subscriptionId}`, {
|
|
error: sanitizedError,
|
|
userId,
|
|
subscriptionId,
|
|
newPlanCode: request.newPlanCode,
|
|
});
|
|
|
|
await this.simNotification.notifySimAction("Change Plan", "ERROR", {
|
|
userId,
|
|
subscriptionId,
|
|
account: "unknown",
|
|
newPlanCode: request.newPlanCode,
|
|
error: sanitizedError,
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update SIM features (voicemail, call waiting, roaming, network type)
|
|
*/
|
|
async updateSimFeatures(
|
|
userId: string,
|
|
subscriptionId: number,
|
|
request: SimFeaturesUpdateRequest
|
|
): Promise<void> {
|
|
try {
|
|
const { account } = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
|
|
|
// Validate network type if provided
|
|
if (request.networkType && !["4G", "5G"].includes(request.networkType)) {
|
|
throw new BadRequestException('networkType must be either "4G" or "5G"');
|
|
}
|
|
|
|
// Log the request for debugging
|
|
this.logger.log(`Updating SIM features for subscription ${subscriptionId}`, {
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
request,
|
|
});
|
|
|
|
// Update all features in one call - the FreebitOperationsService will handle the complexity
|
|
await this.freebitService.updateSimFeatures(account, {
|
|
voiceMailEnabled: request.voiceMailEnabled,
|
|
callWaitingEnabled: request.callWaitingEnabled,
|
|
internationalRoamingEnabled: request.internationalRoamingEnabled,
|
|
networkType: request.networkType,
|
|
});
|
|
|
|
this.logger.log(`Successfully updated SIM features for subscription ${subscriptionId}`, {
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
...request,
|
|
});
|
|
|
|
await this.simNotification.notifySimAction("Update Features", "SUCCESS", {
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
...request,
|
|
});
|
|
} catch (error) {
|
|
const sanitizedError = getErrorMessage(error);
|
|
this.logger.error(`Failed to update SIM features for subscription ${subscriptionId}`, {
|
|
error: sanitizedError,
|
|
userId,
|
|
subscriptionId,
|
|
account: "unknown",
|
|
...request,
|
|
errorStack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
await this.simNotification.notifySimAction("Update Features", "ERROR", {
|
|
userId,
|
|
subscriptionId,
|
|
...request,
|
|
error: sanitizedError,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
}
|