tema 675f7d5cfd Remove cached profile fields migration and update CSRF middleware for new public auth endpoints
- 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.
2025-11-21 17:12:34 +09:00

172 lines
5.5 KiB
TypeScript

import { Injectable, Inject } 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 { SimUsageStoreService } from "../../sim-usage-store.service";
import { getErrorMessage } from "@bff/core/utils/error.util";
import type { SimUsage, SimTopUpHistory } from "@bff/integrations/freebit/interfaces/freebit.types";
import type { SimTopUpHistoryRequest } from "../types/sim-requests.types";
import { BadRequestException } from "@nestjs/common";
@Injectable()
export class SimUsageService {
constructor(
private readonly freebitService: FreebitOrchestratorService,
private readonly simValidation: SimValidationService,
private readonly usageStore: SimUsageStoreService,
@Inject(Logger) private readonly logger: Logger
) {}
/**
* Get SIM data usage for a subscription
*/
async getSimUsage(userId: string, subscriptionId: number): Promise<SimUsage> {
let account = "";
try {
const validation = await this.simValidation.validateSimSubscription(userId, subscriptionId);
account = validation.account;
const simUsage = await this.freebitService.getSimUsage(account);
// Persist today's usage for monthly charts and cleanup previous months
try {
await this.usageStore.upsertToday(account, simUsage.todayUsageMb);
await this.usageStore.cleanupPreviousMonths();
const stored = await this.usageStore.getLastNDays(account, 30);
if (stored.length > 0) {
simUsage.recentDaysUsage = stored.map(d => ({
date: d.date,
usageKb: Math.round(d.usageMb * 1000),
usageMb: d.usageMb,
}));
}
} catch (e) {
const sanitizedError = getErrorMessage(e);
this.logger.warn("SIM usage persistence failed (non-fatal)", {
account,
error: sanitizedError,
});
}
this.logger.log(`Retrieved SIM usage for subscription ${subscriptionId}`, {
userId,
subscriptionId,
account,
todayUsageMb: simUsage.todayUsageMb,
});
return simUsage;
} catch (error) {
const sanitizedError = getErrorMessage(error);
this.logger.error(`Failed to get SIM usage for subscription ${subscriptionId}`, {
error: sanitizedError,
userId,
subscriptionId,
account,
});
if (account && sanitizedError.toLowerCase().includes("failed to get sim usage")) {
try {
const fallback = await this.buildFallbackUsage(account);
this.logger.warn("Serving cached SIM usage after Freebit failure", {
userId,
subscriptionId,
account,
fallbackSource: fallback.recentDaysUsage.length > 0 ? "cache" : "default",
});
return fallback;
} catch (fallbackError) {
this.logger.warn("Unable to build fallback SIM usage", {
account,
error: getErrorMessage(fallbackError),
});
}
}
throw error;
}
}
private async buildFallbackUsage(account: string): Promise<SimUsage> {
try {
const records = await this.usageStore.getLastNDays(account, 30);
if (records.length > 0) {
const todayIso = new Date().toISOString().slice(0, 10);
const todayRecord = records.find(r => r.date === todayIso) ?? records[records.length - 1];
const todayUsageMb = todayRecord?.usageMb ?? 0;
const mostRecentDate = records[0]?.date;
return {
account,
todayUsageMb,
todayUsageKb: Math.round(todayUsageMb * 1000),
recentDaysUsage: records.map(r => ({
date: r.date,
usageMb: r.usageMb,
usageKb: Math.round(r.usageMb * 1000),
})),
isBlacklisted: false,
lastUpdated: mostRecentDate ? `${mostRecentDate}T00:00:00.000Z` : new Date().toISOString(),
};
}
} catch (error) {
this.logger.warn("Failed to load cached SIM usage", {
account,
error: getErrorMessage(error),
});
}
return {
account,
todayUsageMb: 0,
todayUsageKb: 0,
recentDaysUsage: [],
isBlacklisted: false,
lastUpdated: new Date().toISOString(),
};
}
/**
* Get SIM top-up history
*/
async getSimTopUpHistory(
userId: string,
subscriptionId: number,
request: SimTopUpHistoryRequest
): Promise<SimTopUpHistory> {
try {
const { account } = await this.simValidation.validateSimSubscription(userId, subscriptionId);
// Validate date format
if (!/^\d{8}$/.test(request.fromDate) || !/^\d{8}$/.test(request.toDate)) {
throw new BadRequestException("Dates must be in YYYYMMDD format");
}
const history = await this.freebitService.getSimTopUpHistory(
account,
request.fromDate,
request.toDate
);
this.logger.log(`Retrieved SIM top-up history for subscription ${subscriptionId}`, {
userId,
subscriptionId,
account,
totalAdditions: history.totalAdditions,
});
return history;
} catch (error) {
const sanitizedError = getErrorMessage(error);
this.logger.error(`Failed to get SIM top-up history for subscription ${subscriptionId}`, {
error: sanitizedError,
userId,
subscriptionId,
});
throw error;
}
}
}