fixed lintel errors,
This commit is contained in:
parent
111bbc8c91
commit
855fe211f7
@ -20,6 +20,10 @@ import { SetPasswordDto } from "./dto/set-password.dto";
|
||||
import { getErrorMessage } from "../common/utils/error.util";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { EmailService } from "../common/email/email.service";
|
||||
import { User as SharedUser } from "@customer-portal/shared";
|
||||
import type { User as PrismaUser } from "@prisma/client";
|
||||
import type { Request } from "express";
|
||||
import { WhmcsClientResponse } from "../vendors/whmcs/types/whmcs-api.types";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -39,7 +43,7 @@ export class AuthService {
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
async signup(signupData: SignupDto, request?: unknown) {
|
||||
async signup(signupData: SignupDto, request?: Request) {
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
@ -58,7 +62,7 @@ export class AuthService {
|
||||
this.validateSignupData(signupData);
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await this.usersService.findByEmailInternal(email);
|
||||
const existingUser: PrismaUser | null = await this.usersService.findByEmailInternal(email);
|
||||
if (existingUser) {
|
||||
await this.auditService.logAuthEvent(
|
||||
AuditAction.SIGNUP,
|
||||
@ -77,7 +81,8 @@ export class AuthService {
|
||||
|
||||
try {
|
||||
// 0. Lookup Salesforce Account by Customer Number (SF Number)
|
||||
const sfAccount = await this.salesforceService.findAccountByCustomerNumber(sfNumber);
|
||||
const sfAccount: { id: string } | null =
|
||||
await this.salesforceService.findAccountByCustomerNumber(sfNumber);
|
||||
if (!sfAccount) {
|
||||
throw new BadRequestException(
|
||||
`Salesforce account not found for Customer Number: ${sfNumber}`
|
||||
@ -85,7 +90,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// 1. Create user in portal
|
||||
const user = await this.usersService.create({
|
||||
const user: SharedUser = await this.usersService.create({
|
||||
email,
|
||||
passwordHash,
|
||||
firstName,
|
||||
@ -114,7 +119,7 @@ export class AuthService {
|
||||
if (genderFieldId && gender) customfields[genderFieldId] = gender;
|
||||
if (nationalityFieldId && nationality) customfields[nationalityFieldId] = nationality;
|
||||
|
||||
const whmcsClient = await this.whmcsService.addClient({
|
||||
const whmcsClient: { clientId: number } = await this.whmcsService.addClient({
|
||||
firstname: firstName,
|
||||
lastname: lastName,
|
||||
email,
|
||||
@ -203,11 +208,11 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async linkWhmcsUser(linkData: LinkWhmcsDto, _request?: unknown) {
|
||||
async linkWhmcsUser(linkData: LinkWhmcsDto, _request?: Request) {
|
||||
const { email, password } = linkData;
|
||||
|
||||
// Check if user already exists in portal
|
||||
const existingUser = await this.usersService.findByEmailInternal(email);
|
||||
const existingUser: PrismaUser | null = await this.usersService.findByEmailInternal(email);
|
||||
if (existingUser) {
|
||||
// If user exists but has no password (abandoned during setup), allow them to continue
|
||||
if (!existingUser.passwordHash) {
|
||||
@ -227,7 +232,7 @@ export class AuthService {
|
||||
|
||||
try {
|
||||
// 1. First, find the client by email using GetClientsDetails directly
|
||||
let clientDetails;
|
||||
let clientDetails: WhmcsClientResponse["client"];
|
||||
try {
|
||||
clientDetails = await this.whmcsService.getClientDetailsByEmail(email);
|
||||
} catch (error) {
|
||||
@ -251,11 +256,11 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// 3. Extract Customer Number from field ID 198
|
||||
const customerNumberField = clientDetails.customfields?.find(
|
||||
(field: { id: number | string; value?: unknown }) => field.id == 198
|
||||
const customerNumberField = clientDetails.customfields?.customfield.find(
|
||||
field => field.id == 198
|
||||
);
|
||||
|
||||
const customerNumber = customerNumberField?.value;
|
||||
const customerNumber = customerNumberField?.value as string;
|
||||
|
||||
if (!customerNumber || customerNumber.toString().trim() === "") {
|
||||
throw new BadRequestException(
|
||||
@ -271,7 +276,8 @@ export class AuthService {
|
||||
);
|
||||
|
||||
// 3. Find existing Salesforce account using Customer Number
|
||||
const sfAccount = await this.salesforceService.findAccountByCustomerNumber(customerNumber);
|
||||
const sfAccount: { id: string } | null =
|
||||
await this.salesforceService.findAccountByCustomerNumber(customerNumber);
|
||||
if (!sfAccount) {
|
||||
throw new BadRequestException(
|
||||
`Salesforce account not found for Customer Number: ${customerNumber}. Please contact support.`
|
||||
@ -279,7 +285,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// 4. Create portal user (without password initially)
|
||||
const user = await this.usersService.create({
|
||||
const user: SharedUser = await this.usersService.create({
|
||||
email,
|
||||
passwordHash: null, // No password hash - will be set when user sets password
|
||||
firstName: clientDetails.firstname || "",
|
||||
@ -292,7 +298,7 @@ export class AuthService {
|
||||
// 5. Store ID mappings
|
||||
await this.mappingsService.createMapping({
|
||||
userId: user.id,
|
||||
whmcsClientId: parseInt(clientDetails.id),
|
||||
whmcsClientId: clientDetails.id,
|
||||
sfAccountId: sfAccount.id,
|
||||
});
|
||||
|
||||
@ -310,7 +316,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async checkPasswordNeeded(email: string) {
|
||||
const user = await this.usersService.findByEmailInternal(email);
|
||||
const user: PrismaUser | null = await this.usersService.findByEmailInternal(email);
|
||||
if (!user) {
|
||||
return { needsPasswordSet: false, userExists: false };
|
||||
}
|
||||
@ -322,10 +328,10 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async setPassword(setPasswordData: SetPasswordDto, _request?: unknown) {
|
||||
async setPassword(setPasswordData: SetPasswordDto, _request?: Request) {
|
||||
const { email, password } = setPasswordData;
|
||||
|
||||
const user = await this.usersService.findByEmailInternal(email);
|
||||
const user: PrismaUser | null = await this.usersService.findByEmailInternal(email);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User not found");
|
||||
}
|
||||
@ -340,7 +346,7 @@ export class AuthService {
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Update user with new password
|
||||
const updatedUser = await this.usersService.update(user.id, {
|
||||
const updatedUser: SharedUser = await this.usersService.update(user.id, {
|
||||
passwordHash,
|
||||
});
|
||||
|
||||
@ -356,16 +362,9 @@ export class AuthService {
|
||||
async validateUser(
|
||||
email: string,
|
||||
password: string,
|
||||
_request?: unknown
|
||||
): Promise<{
|
||||
id: string;
|
||||
email: string;
|
||||
role?: string;
|
||||
passwordHash: string | null;
|
||||
failedLoginAttempts?: number | null;
|
||||
lockedUntil?: Date | null;
|
||||
} | null> {
|
||||
const user = await this.usersService.findByEmailInternal(email);
|
||||
_request?: Request
|
||||
): Promise<PrismaUser | null> {
|
||||
const user: PrismaUser | null = await this.usersService.findByEmailInternal(email);
|
||||
|
||||
if (!user) {
|
||||
await this.auditService.logAuthEvent(
|
||||
@ -428,10 +427,7 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleFailedLogin(
|
||||
user: { id: string; email: string; failedLoginAttempts?: number | null },
|
||||
_request?: unknown
|
||||
): Promise<void> {
|
||||
private async handleFailedLogin(user: PrismaUser, _request?: unknown): Promise<void> {
|
||||
const newFailedAttempts = (user.failedLoginAttempts || 0) + 1;
|
||||
let lockedUntil = null;
|
||||
let isAccountLocked = false;
|
||||
@ -479,7 +475,7 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async logout(userId: string, token: string, _request?: unknown): Promise<void> {
|
||||
async logout(userId: string, token: string, _request?: Request): Promise<void> {
|
||||
// Blacklist the token
|
||||
await this.tokenBlacklistService.blacklistToken(token);
|
||||
|
||||
@ -487,7 +483,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private async generateTokens(user: { id: string; email: string; role?: string }) {
|
||||
private generateTokens(user: { id: string; email: string; role?: string }) {
|
||||
const payload = { email: user.email, sub: user.id, role: user.role };
|
||||
return {
|
||||
access_token: this.jwtService.sign(payload),
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ThrottlerGuard } from "@nestjs/throttler";
|
||||
import type { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class AuthThrottleGuard extends ThrottlerGuard {
|
||||
protected async getTracker(req: Record<string, any>): Promise<string> {
|
||||
protected async getTracker(req: Request): Promise<string> {
|
||||
// Track by IP address for failed login attempts
|
||||
const forwarded = req.headers["x-forwarded-for"];
|
||||
const forwardedIp = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
||||
const ip =
|
||||
forwardedIp?.split(",")[0]?.trim() ||
|
||||
(typeof forwardedIp === "string" ? forwardedIp.split(",")[0]?.trim() : undefined) ||
|
||||
(req.headers["x-real-ip"] as string | undefined) ||
|
||||
req.socket?.remoteAddress ||
|
||||
req.ip ||
|
||||
(req.socket as any)?.remoteAddress ||
|
||||
(req as any).ip ||
|
||||
"unknown";
|
||||
|
||||
return `auth_${ip}`;
|
||||
|
||||
@ -502,7 +502,7 @@ export class InvoicesService {
|
||||
/**
|
||||
* Health check for invoice service
|
||||
*/
|
||||
async healthCheck(): Promise<{ status: string; details: any }> {
|
||||
async healthCheck(): Promise<{ status: string; details: unknown }> {
|
||||
try {
|
||||
const whmcsHealthy = await this.whmcsService.healthCheck();
|
||||
|
||||
|
||||
@ -123,7 +123,7 @@ export class MappingCacheService {
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
async getStats(): Promise<{ totalKeys: number; memoryUsage: number }> {
|
||||
getStats(): { totalKeys: number; memoryUsage: number } {
|
||||
let result = { totalKeys: 0, memoryUsage: 0 };
|
||||
|
||||
try {
|
||||
|
||||
@ -281,14 +281,18 @@ export class MappingValidatorService {
|
||||
/**
|
||||
* Log validation result
|
||||
*/
|
||||
logValidationResult(operation: string, validation: MappingValidationResult, context?: any): void {
|
||||
logValidationResult(
|
||||
operation: string,
|
||||
validation: MappingValidationResult,
|
||||
context?: unknown
|
||||
): void {
|
||||
const summary = this.getValidationSummary(validation);
|
||||
|
||||
if (validation.isValid) {
|
||||
this.logger.debug(`${operation} validation: ${summary}`, context);
|
||||
} else {
|
||||
this.logger.warn(`${operation} validation failed: ${summary}`, {
|
||||
...context,
|
||||
...(context && typeof context === "object" ? context : {}),
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
});
|
||||
|
||||
@ -6,7 +6,7 @@ import { RequestWithUser } from "../auth/auth.types";
|
||||
|
||||
interface CreateOrderBody {
|
||||
orderType: "Internet" | "eSIM" | "SIM" | "VPN" | "Other";
|
||||
selections: Record<string, any>;
|
||||
selections: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@ApiTags("orders")
|
||||
|
||||
@ -7,7 +7,7 @@ import { WhmcsConnectionService } from "../vendors/whmcs/services/whmcs-connecti
|
||||
|
||||
interface CreateOrderBody {
|
||||
orderType: "Internet" | "eSIM" | "SIM" | "VPN" | "Other";
|
||||
selections: Record<string, any>;
|
||||
selections: Record<string, unknown>;
|
||||
opportunityId?: string;
|
||||
}
|
||||
|
||||
@ -260,7 +260,7 @@ export class OrdersService {
|
||||
items.push({
|
||||
itemType: "Service",
|
||||
productHint: svcHint,
|
||||
sku: body.selections.skuService,
|
||||
sku: body.selections.skuService as string | undefined,
|
||||
billingCycle: "monthly",
|
||||
quantity: 1,
|
||||
});
|
||||
@ -270,7 +270,7 @@ export class OrdersService {
|
||||
items.push({
|
||||
itemType: "Installation",
|
||||
productHint: "Installation Fee (Single)",
|
||||
sku: body.selections.skuInstall,
|
||||
sku: body.selections.skuInstall as string | undefined,
|
||||
billingCycle: "onetime",
|
||||
quantity: 1,
|
||||
});
|
||||
@ -278,7 +278,7 @@ export class OrdersService {
|
||||
items.push({
|
||||
itemType: "Installation",
|
||||
productHint: "Installation Fee (12-Month)",
|
||||
sku: body.selections.skuInstall,
|
||||
sku: body.selections.skuInstall as string | undefined,
|
||||
billingCycle: "monthly",
|
||||
quantity: 1,
|
||||
});
|
||||
@ -286,7 +286,7 @@ export class OrdersService {
|
||||
items.push({
|
||||
itemType: "Installation",
|
||||
productHint: "Installation Fee (24-Month)",
|
||||
sku: body.selections.skuInstall,
|
||||
sku: body.selections.skuInstall as string | undefined,
|
||||
billingCycle: "monthly",
|
||||
quantity: 1,
|
||||
});
|
||||
@ -295,7 +295,7 @@ export class OrdersService {
|
||||
items.push({
|
||||
itemType: "Service",
|
||||
productHint: `${body.orderType} Plan`,
|
||||
sku: body.selections.skuService,
|
||||
sku: body.selections.skuService as string | undefined,
|
||||
billingCycle: "monthly",
|
||||
quantity: 1,
|
||||
});
|
||||
@ -303,14 +303,14 @@ export class OrdersService {
|
||||
items.push({
|
||||
itemType: "Service",
|
||||
productHint: `VPN ${body.selections.region || ""}`,
|
||||
sku: body.selections.skuService,
|
||||
sku: body.selections.skuService as string | undefined,
|
||||
billingCycle: "monthly",
|
||||
quantity: 1,
|
||||
});
|
||||
items.push({
|
||||
itemType: "Installation",
|
||||
productHint: "VPN Activation Fee",
|
||||
sku: body.selections.skuInstall,
|
||||
sku: body.selections.skuInstall as string | undefined,
|
||||
billingCycle: "onetime",
|
||||
quantity: 1,
|
||||
});
|
||||
|
||||
@ -461,7 +461,7 @@ export class SubscriptionsService {
|
||||
/**
|
||||
* Health check for subscription service
|
||||
*/
|
||||
async healthCheck(): Promise<{ status: string; details: any }> {
|
||||
async healthCheck(): Promise<{ status: string; details: unknown }> {
|
||||
try {
|
||||
const whmcsHealthy = await this.whmcsService.healthCheck();
|
||||
|
||||
|
||||
@ -53,7 +53,7 @@ export class UsersController {
|
||||
@ApiResponse({ status: 200, description: "Billing information updated successfully" })
|
||||
@ApiResponse({ status: 400, description: "Invalid input data" })
|
||||
@ApiResponse({ status: 401, description: "Unauthorized" })
|
||||
async updateBilling(@Req() _req: RequestWithUser, @Body() _billingData: UpdateBillingDto) {
|
||||
updateBilling(@Req() _req: RequestWithUser, @Body() _billingData: UpdateBillingDto) {
|
||||
// TODO: Sync to WHMCS custom fields
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { Injectable, Inject } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { PrismaService } from "../common/prisma/prisma.service";
|
||||
import { User, Activity } from "@customer-portal/shared";
|
||||
import { User as PrismaUser } from "@prisma/client";
|
||||
import { WhmcsService } from "../vendors/whmcs/whmcs.service";
|
||||
import { SalesforceService } from "../vendors/salesforce/salesforce.service";
|
||||
import { MappingsService } from "../mappings/mappings.service";
|
||||
@ -22,6 +23,28 @@ export interface EnhancedUser extends Omit<User, "createdAt" | "updatedAt"> {
|
||||
};
|
||||
}
|
||||
|
||||
// Salesforce Account interface based on the data model
|
||||
interface SalesforceAccount {
|
||||
Id: string;
|
||||
Name?: string;
|
||||
PersonMailingStreet?: string;
|
||||
PersonMailingCity?: string;
|
||||
PersonMailingState?: string;
|
||||
PersonMailingPostalCode?: string;
|
||||
PersonMailingCountry?: string;
|
||||
BillingStreet?: string;
|
||||
BillingCity?: string;
|
||||
BillingState?: string;
|
||||
BillingPostalCode?: string;
|
||||
BillingCountry?: string;
|
||||
BuildingName__pc?: string;
|
||||
BuildingName__c?: string;
|
||||
RoomNumber__pc?: string;
|
||||
RoomNumber__c?: string;
|
||||
PersonMobilePhone?: string;
|
||||
Mobile?: string;
|
||||
}
|
||||
|
||||
interface UserUpdateData {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
@ -53,14 +76,14 @@ export class UsersService {
|
||||
) {}
|
||||
|
||||
// Helper function to convert Prisma user to EnhancedUser type
|
||||
private toEnhancedUser(user: any, extras: Partial<EnhancedUser> = {}): EnhancedUser {
|
||||
private toEnhancedUser(user: PrismaUser, extras: Partial<EnhancedUser> = {}): EnhancedUser {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
company: user.company,
|
||||
phone: user.phone,
|
||||
firstName: user.firstName || undefined,
|
||||
lastName: user.lastName || undefined,
|
||||
company: user.company || undefined,
|
||||
phone: user.phone || undefined,
|
||||
mfaEnabled: !!user.mfaSecret, // Derive from mfaSecret existence
|
||||
emailVerified: user.emailVerified,
|
||||
createdAt: user.createdAt,
|
||||
@ -70,14 +93,14 @@ export class UsersService {
|
||||
}
|
||||
|
||||
// Helper function to convert Prisma user to shared User type
|
||||
private toUser(user: any): User {
|
||||
private toUser(user: PrismaUser): User {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
company: user.company,
|
||||
phone: user.phone,
|
||||
firstName: user.firstName || undefined,
|
||||
lastName: user.lastName || undefined,
|
||||
company: user.company || undefined,
|
||||
phone: user.phone || undefined,
|
||||
mfaEnabled: !!user.mfaSecret, // Derive from mfaSecret existence
|
||||
emailVerified: user.emailVerified,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
@ -120,7 +143,7 @@ export class UsersService {
|
||||
}
|
||||
|
||||
// Internal method for auth service - returns raw user with sensitive fields
|
||||
async findByEmailInternal(email: string): Promise<any | null> {
|
||||
async findByEmailInternal(email: string): Promise<PrismaUser | null> {
|
||||
const validEmail = this.validateEmail(email);
|
||||
|
||||
try {
|
||||
@ -170,11 +193,13 @@ export class UsersService {
|
||||
if (!mapping?.sfAccountId) return this.toEnhancedUser(user);
|
||||
|
||||
try {
|
||||
const account = await this.salesforceService.getAccount(mapping.sfAccountId);
|
||||
const account = (await this.salesforceService.getAccount(
|
||||
mapping.sfAccountId
|
||||
)) as SalesforceAccount | null;
|
||||
if (!account) return this.toEnhancedUser(user);
|
||||
|
||||
return this.toEnhancedUser(user, {
|
||||
company: account.Name?.trim() || user.company,
|
||||
company: account.Name?.trim() || user.company || undefined,
|
||||
email: user.email, // Keep original email for now
|
||||
phone: user.phone || undefined, // Keep original phone for now
|
||||
// Address temporarily disabled until field issues resolved
|
||||
@ -187,12 +212,20 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
private hasAddress(_account: any): boolean {
|
||||
private hasAddress(_account: SalesforceAccount): boolean {
|
||||
// Temporarily disabled until field mapping is resolved
|
||||
return false;
|
||||
}
|
||||
|
||||
private extractAddress(account: any): any {
|
||||
private extractAddress(account: SalesforceAccount): {
|
||||
street: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
postalCode: string | null;
|
||||
country: string | null;
|
||||
buildingName: string | null;
|
||||
roomNumber: string | null;
|
||||
} {
|
||||
// Prefer Person Account fields (Contact), fallback to Business Account fields
|
||||
return {
|
||||
street: account.PersonMailingStreet || account.BillingStreet || null,
|
||||
@ -205,8 +238,8 @@ export class UsersService {
|
||||
};
|
||||
}
|
||||
|
||||
async create(userData: any): Promise<User> {
|
||||
const validEmail = this.validateEmail(userData.email);
|
||||
async create(userData: Partial<PrismaUser>): Promise<User> {
|
||||
const validEmail = this.validateEmail(userData.email!);
|
||||
|
||||
try {
|
||||
const normalizedData = { ...userData, email: validEmail };
|
||||
@ -248,8 +281,8 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeUserData(userData: UserUpdateData): any {
|
||||
const sanitized: any = {};
|
||||
private sanitizeUserData(userData: UserUpdateData): Partial<PrismaUser> {
|
||||
const sanitized: Partial<PrismaUser> = {};
|
||||
if (userData.firstName !== undefined)
|
||||
sanitized.firstName = userData.firstName?.trim().substring(0, 50) || null;
|
||||
if (userData.lastName !== undefined)
|
||||
@ -289,8 +322,8 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
private buildSalesforceUpdate(userData: UserUpdateData): any {
|
||||
const update: any = {};
|
||||
private buildSalesforceUpdate(userData: UserUpdateData): Partial<SalesforceAccount> {
|
||||
const update: Partial<SalesforceAccount> = {};
|
||||
|
||||
if (userData.company !== undefined) update.Name = userData.company;
|
||||
if (userData.phone !== undefined) {
|
||||
@ -369,17 +402,28 @@ export class UsersService {
|
||||
|
||||
// Process subscriptions
|
||||
let activeSubscriptions = 0;
|
||||
let recentSubscriptions: any[] = [];
|
||||
let recentSubscriptions: Array<{
|
||||
id: string;
|
||||
status: string;
|
||||
registrationDate: string;
|
||||
productName: string;
|
||||
}> = [];
|
||||
if (subscriptionsData.status === "fulfilled") {
|
||||
const subscriptions = subscriptionsData.value.subscriptions;
|
||||
activeSubscriptions = subscriptions.filter((sub: any) => sub.status === "Active").length;
|
||||
activeSubscriptions = subscriptions.filter(sub => sub.status === "Active").length;
|
||||
recentSubscriptions = subscriptions
|
||||
.filter((sub: any) => sub.status === "Active")
|
||||
.filter(sub => sub.status === "Active")
|
||||
.sort(
|
||||
(a: any, b: any) =>
|
||||
(a, b) =>
|
||||
new Date(b.registrationDate).getTime() - new Date(a.registrationDate).getTime()
|
||||
)
|
||||
.slice(0, 3);
|
||||
.slice(0, 3)
|
||||
.map(sub => ({
|
||||
id: sub.id.toString(),
|
||||
status: sub.status,
|
||||
registrationDate: sub.registrationDate,
|
||||
productName: sub.productName,
|
||||
}));
|
||||
} else {
|
||||
this.logger.error(
|
||||
`Failed to fetch subscriptions for user ${userId}:`,
|
||||
@ -390,26 +434,32 @@ export class UsersService {
|
||||
// Process invoices
|
||||
let unpaidInvoices = 0;
|
||||
let nextInvoice = null;
|
||||
let recentInvoices: any[] = [];
|
||||
let recentInvoices: Array<{
|
||||
id: string;
|
||||
status: string;
|
||||
dueDate?: string;
|
||||
total: number;
|
||||
number: string;
|
||||
issuedAt?: string;
|
||||
paidDate?: string;
|
||||
}> = [];
|
||||
if (invoicesData.status === "fulfilled") {
|
||||
const invoices = invoicesData.value.invoices;
|
||||
|
||||
// Count unpaid invoices
|
||||
unpaidInvoices = invoices.filter(
|
||||
(inv: any) => inv.status === "Unpaid" || inv.status === "Overdue"
|
||||
inv => inv.status === "Unpaid" || inv.status === "Overdue"
|
||||
).length;
|
||||
|
||||
// Find next due invoice
|
||||
const upcomingInvoices = invoices
|
||||
.filter(
|
||||
(inv: any) => (inv.status === "Unpaid" || inv.status === "Overdue") && inv.dueDate
|
||||
)
|
||||
.sort((a: any, b: any) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime());
|
||||
.filter(inv => (inv.status === "Unpaid" || inv.status === "Overdue") && inv.dueDate)
|
||||
.sort((a, b) => new Date(a.dueDate!).getTime() - new Date(b.dueDate!).getTime());
|
||||
|
||||
if (upcomingInvoices.length > 0) {
|
||||
const invoice = upcomingInvoices[0];
|
||||
nextInvoice = {
|
||||
id: invoice.id,
|
||||
id: invoice.id.toString(),
|
||||
dueDate: invoice.dueDate,
|
||||
amount: invoice.total,
|
||||
currency: "JPY",
|
||||
@ -419,10 +469,17 @@ export class UsersService {
|
||||
// Recent invoices for activity
|
||||
recentInvoices = invoices
|
||||
.sort(
|
||||
(a: any, b: any) =>
|
||||
new Date(b.issuedAt || "").getTime() - new Date(a.issuedAt || "").getTime()
|
||||
(a, b) => new Date(b.issuedAt || "").getTime() - new Date(a.issuedAt || "").getTime()
|
||||
)
|
||||
.slice(0, 5);
|
||||
.slice(0, 5)
|
||||
.map(inv => ({
|
||||
id: inv.id.toString(),
|
||||
status: inv.status,
|
||||
dueDate: inv.dueDate,
|
||||
total: inv.total,
|
||||
number: inv.number,
|
||||
issuedAt: inv.issuedAt,
|
||||
}));
|
||||
} else {
|
||||
this.logger.error(`Failed to fetch invoices for user ${userId}`, {
|
||||
reason: getErrorMessage(invoicesData.reason),
|
||||
@ -441,7 +498,7 @@ export class UsersService {
|
||||
title: `Invoice #${invoice.number} paid`,
|
||||
description: `Payment of ¥${invoice.total.toLocaleString()} processed`,
|
||||
date: invoice.paidDate || invoice.issuedAt || new Date().toISOString(),
|
||||
relatedId: invoice.id,
|
||||
relatedId: Number(invoice.id),
|
||||
});
|
||||
} else if (invoice.status === "Unpaid" || invoice.status === "Overdue") {
|
||||
activities.push({
|
||||
@ -449,8 +506,8 @@ export class UsersService {
|
||||
type: "invoice_created",
|
||||
title: `Invoice #${invoice.number} created`,
|
||||
description: `Amount: ¥${invoice.total.toLocaleString()}`,
|
||||
date: invoice.issuedAt || invoice.updatedAt || new Date().toISOString(),
|
||||
relatedId: invoice.id,
|
||||
date: invoice.issuedAt || new Date().toISOString(),
|
||||
relatedId: Number(invoice.id),
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -463,7 +520,7 @@ export class UsersService {
|
||||
title: `${subscription.productName} activated`,
|
||||
description: "Service successfully provisioned",
|
||||
date: subscription.registrationDate,
|
||||
relatedId: subscription.id,
|
||||
relatedId: Number(subscription.id),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -66,11 +66,11 @@ export class SalesforceService implements OnModuleInit {
|
||||
return this.accountService.upsert(accountData);
|
||||
}
|
||||
|
||||
async getAccount(accountId: string): Promise<any | null> {
|
||||
return this.accountService.getById(accountId);
|
||||
async getAccount(accountId: string): Promise<Record<string, unknown> | null> {
|
||||
return this.accountService.getById(accountId) as Promise<Record<string, unknown> | null>;
|
||||
}
|
||||
|
||||
async updateAccount(accountId: string, updates: any): Promise<void> {
|
||||
async updateAccount(accountId: string, updates: Record<string, unknown>): Promise<void> {
|
||||
return this.accountService.update(accountId, updates);
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@ export class SalesforceService implements OnModuleInit {
|
||||
return this.caseService.createCase(userData, caseRequest);
|
||||
}
|
||||
|
||||
async updateCase(caseId: string, updates: any): Promise<void> {
|
||||
async updateCase(caseId: string, updates: Record<string, unknown>): Promise<void> {
|
||||
return this.caseService.updateCase(caseId, updates);
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,21 @@ export interface UpsertResult {
|
||||
created: boolean;
|
||||
}
|
||||
|
||||
interface SalesforceQueryResult {
|
||||
records: SalesforceAccount[];
|
||||
totalSize: number;
|
||||
}
|
||||
|
||||
interface SalesforceAccount {
|
||||
Id: string;
|
||||
Name: string;
|
||||
}
|
||||
|
||||
interface SalesforceCreateResult {
|
||||
id: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SalesforceAccountService {
|
||||
constructor(
|
||||
@ -33,7 +48,7 @@ export class SalesforceAccountService {
|
||||
try {
|
||||
const result = await this.connection.query(
|
||||
`SELECT Id FROM Account WHERE SF_Account_No__c = '${this.safeSoql(customerNumber.trim())}'`
|
||||
);
|
||||
) as SalesforceQueryResult;
|
||||
return result.totalSize > 0 ? { id: result.records[0].Id } : null;
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to find account by customer number", {
|
||||
@ -49,7 +64,7 @@ export class SalesforceAccountService {
|
||||
try {
|
||||
const existingAccount = await this.connection.query(
|
||||
`SELECT Id FROM Account WHERE Name = '${this.safeSoql(accountData.name.trim())}'`
|
||||
);
|
||||
) as SalesforceQueryResult;
|
||||
|
||||
const sfData = {
|
||||
Name: accountData.name.trim(),
|
||||
@ -73,10 +88,10 @@ export class SalesforceAccountService {
|
||||
|
||||
if (existingAccount.totalSize > 0) {
|
||||
const accountId = existingAccount.records[0].Id;
|
||||
await this.connection.sobject("Account").update({ Id: accountId, ...sfData });
|
||||
await (this.connection.sobject("Account") as any).update({ Id: accountId, ...sfData });
|
||||
return { id: accountId, created: false };
|
||||
} else {
|
||||
const result = await this.connection.sobject("Account").create(sfData);
|
||||
const result = await (this.connection.sobject("Account") as any).create(sfData) as SalesforceCreateResult;
|
||||
return { id: result.id, created: true };
|
||||
}
|
||||
} catch (error) {
|
||||
@ -87,7 +102,7 @@ export class SalesforceAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
async getById(accountId: string): Promise<any | null> {
|
||||
async getById(accountId: string): Promise<SalesforceAccount | null> {
|
||||
if (!accountId?.trim()) throw new Error("Account ID is required");
|
||||
|
||||
try {
|
||||
@ -95,7 +110,7 @@ export class SalesforceAccountService {
|
||||
SELECT Id, Name
|
||||
FROM Account
|
||||
WHERE Id = '${this.validateId(accountId)}'
|
||||
`);
|
||||
`) as SalesforceQueryResult;
|
||||
|
||||
return result.totalSize > 0 ? result.records[0] : null;
|
||||
} catch (error) {
|
||||
@ -106,11 +121,11 @@ export class SalesforceAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
async update(accountId: string, updates: any): Promise<void> {
|
||||
async update(accountId: string, updates: Record<string, unknown>): Promise<void> {
|
||||
const validAccountId = this.validateId(accountId);
|
||||
|
||||
try {
|
||||
await this.connection.sobject("Account").update({ Id: validAccountId, ...updates });
|
||||
await (this.connection.sobject("Account") as any).update({ Id: validAccountId, ...updates });
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to update account", {
|
||||
error: getErrorMessage(error),
|
||||
|
||||
@ -2,7 +2,8 @@ import { Injectable, Inject } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { getErrorMessage } from "../../../common/utils/error.util";
|
||||
import { SalesforceConnection } from "./salesforce-connection.service";
|
||||
import { SupportCase, CreateCaseRequest } from "@customer-portal/shared";
|
||||
import { SupportCase, CreateCaseRequest, CaseType } from "@customer-portal/shared";
|
||||
import { CaseStatus, CasePriority, CASE_STATUS, CASE_PRIORITY } from "@customer-portal/shared";
|
||||
|
||||
export interface CaseQueryParams {
|
||||
status?: string;
|
||||
@ -26,6 +27,36 @@ interface CaseData {
|
||||
origin?: string;
|
||||
}
|
||||
|
||||
interface SalesforceQueryResult {
|
||||
records: SalesforceCase[];
|
||||
totalSize: number;
|
||||
}
|
||||
|
||||
interface SalesforceCase {
|
||||
Id: string;
|
||||
CaseNumber: string;
|
||||
Subject: string;
|
||||
Description: string;
|
||||
Status: string;
|
||||
Priority: string;
|
||||
Type: string;
|
||||
Origin: string;
|
||||
CreatedDate: string;
|
||||
LastModifiedDate?: string;
|
||||
ClosedDate?: string;
|
||||
ContactId: string;
|
||||
AccountId: string;
|
||||
OwnerId: string;
|
||||
Owner?: {
|
||||
Name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SalesforceCreateResult {
|
||||
id: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SalesforceCaseService {
|
||||
constructor(
|
||||
@ -61,9 +92,9 @@ export class SalesforceCaseService {
|
||||
query += ` OFFSET ${params.offset}`;
|
||||
}
|
||||
|
||||
const result = await this.connection.query(query);
|
||||
const result = (await this.connection.query(query)) as SalesforceQueryResult;
|
||||
|
||||
const cases = result.records.map(this.transformCase);
|
||||
const cases = result.records.map(record => this.transformCase(record));
|
||||
|
||||
return { cases, totalSize: result.totalSize };
|
||||
} catch (error) {
|
||||
@ -102,11 +133,11 @@ export class SalesforceCaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateCase(caseId: string, updates: any): Promise<void> {
|
||||
async updateCase(caseId: string, updates: Record<string, unknown>): Promise<void> {
|
||||
const validCaseId = this.validateId(caseId);
|
||||
|
||||
try {
|
||||
await this.connection.sobject("Case").update({ Id: validCaseId, ...updates });
|
||||
await (this.connection.sobject("Case") as any).update({ Id: validCaseId, ...updates });
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to update case", {
|
||||
error: getErrorMessage(error),
|
||||
@ -118,12 +149,12 @@ export class SalesforceCaseService {
|
||||
private async findOrCreateContact(userData: CreateCaseUserData): Promise<string> {
|
||||
try {
|
||||
// Try to find existing contact
|
||||
const existingContact = await this.connection.query(`
|
||||
const existingContact = (await this.connection.query(`
|
||||
SELECT Id FROM Contact
|
||||
WHERE Email = '${this.safeSoql(userData.email)}'
|
||||
AND AccountId = '${userData.accountId}'
|
||||
LIMIT 1
|
||||
`);
|
||||
`)) as SalesforceQueryResult;
|
||||
|
||||
if (existingContact.totalSize > 0) {
|
||||
return existingContact.records[0].Id;
|
||||
@ -137,7 +168,7 @@ export class SalesforceCaseService {
|
||||
AccountId: userData.accountId,
|
||||
};
|
||||
|
||||
const result = await this.connection.sobject("Contact").create(contactData);
|
||||
const result = await (this.connection.sobject("Contact") as any).create(contactData) as SalesforceCreateResult;
|
||||
return result.id;
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to find or create contact for case", {
|
||||
@ -147,7 +178,9 @@ export class SalesforceCaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async createSalesforceCase(caseData: CaseData & { contactId: string }): Promise<any> {
|
||||
private async createSalesforceCase(
|
||||
caseData: CaseData & { contactId: string }
|
||||
): Promise<SalesforceCase> {
|
||||
const validTypes = ["Question", "Problem", "Feature Request"];
|
||||
const validPriorities = ["Low", "Medium", "High", "Critical"];
|
||||
|
||||
@ -161,7 +194,7 @@ export class SalesforceCaseService {
|
||||
Origin: caseData.origin || "Web",
|
||||
};
|
||||
|
||||
const result = await this.connection.sobject("Case").create(sfData);
|
||||
const result = await (this.connection.sobject("Case") as any).create(sfData) as SalesforceCreateResult;
|
||||
|
||||
// Fetch the created case with all fields
|
||||
const createdCase = await this.connection.query(`
|
||||
@ -169,20 +202,20 @@ export class SalesforceCaseService {
|
||||
CreatedDate, LastModifiedDate, ClosedDate, ContactId, AccountId, OwnerId, Owner.Name
|
||||
FROM Case
|
||||
WHERE Id = '${result.id}'
|
||||
`);
|
||||
`) as SalesforceQueryResult;
|
||||
|
||||
return createdCase.records[0];
|
||||
}
|
||||
|
||||
private transformCase(sfCase: any): SupportCase {
|
||||
private transformCase(sfCase: SalesforceCase): SupportCase {
|
||||
return {
|
||||
id: sfCase.Id,
|
||||
number: sfCase.CaseNumber, // Use 'number' instead of 'caseNumber'
|
||||
subject: sfCase.Subject,
|
||||
description: sfCase.Description,
|
||||
status: sfCase.Status,
|
||||
priority: sfCase.Priority,
|
||||
type: sfCase.Type,
|
||||
status: this.mapSalesforceStatus(sfCase.Status),
|
||||
priority: this.mapSalesforcePriority(sfCase.Priority),
|
||||
type: this.mapSalesforceType(sfCase.Type),
|
||||
createdDate: sfCase.CreatedDate,
|
||||
lastModifiedDate: sfCase.LastModifiedDate || sfCase.CreatedDate,
|
||||
closedDate: sfCase.ClosedDate,
|
||||
@ -193,6 +226,53 @@ export class SalesforceCaseService {
|
||||
};
|
||||
}
|
||||
|
||||
private mapSalesforceStatus(status: string): CaseStatus {
|
||||
// Map Salesforce status values to our enum
|
||||
switch (status) {
|
||||
case "New":
|
||||
return CASE_STATUS.NEW;
|
||||
case "Working":
|
||||
case "In Progress":
|
||||
return CASE_STATUS.WORKING;
|
||||
case "Escalated":
|
||||
return CASE_STATUS.ESCALATED;
|
||||
case "Closed":
|
||||
return CASE_STATUS.CLOSED;
|
||||
default:
|
||||
return CASE_STATUS.NEW; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
private mapSalesforcePriority(priority: string): CasePriority {
|
||||
// Map Salesforce priority values to our enum
|
||||
switch (priority) {
|
||||
case "Low":
|
||||
return CASE_PRIORITY.LOW;
|
||||
case "Medium":
|
||||
return CASE_PRIORITY.MEDIUM;
|
||||
case "High":
|
||||
return CASE_PRIORITY.HIGH;
|
||||
case "Critical":
|
||||
return CASE_PRIORITY.CRITICAL;
|
||||
default:
|
||||
return CASE_PRIORITY.MEDIUM; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
private mapSalesforceType(type: string): CaseType {
|
||||
// Map Salesforce type values to our enum
|
||||
switch (type) {
|
||||
case "Question":
|
||||
return "Question";
|
||||
case "Problem":
|
||||
return "Problem";
|
||||
case "Feature Request":
|
||||
return "Feature Request";
|
||||
default:
|
||||
return "Question"; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
private validateId(id: string): string {
|
||||
const trimmed = id?.trim();
|
||||
if (!trimmed || trimmed.length !== 18 || !/^[a-zA-Z0-9]{18}$/.test(trimmed)) {
|
||||
|
||||
@ -102,10 +102,10 @@ export class SalesforceConnection {
|
||||
);
|
||||
}
|
||||
|
||||
const { access_token, instance_url } = await res.json();
|
||||
const tokenResponse = await res.json() as { access_token: string; instance_url: string };
|
||||
|
||||
this.connection.accessToken = access_token;
|
||||
this.connection.instanceUrl = instance_url;
|
||||
this.connection.accessToken = tokenResponse.access_token;
|
||||
this.connection.instanceUrl = tokenResponse.instance_url;
|
||||
|
||||
this.logger.log("✅ Salesforce connection established");
|
||||
} catch (error) {
|
||||
@ -120,11 +120,11 @@ export class SalesforceConnection {
|
||||
}
|
||||
|
||||
// Expose connection methods
|
||||
async query(soql: string): Promise<any> {
|
||||
async query(soql: string): Promise<unknown> {
|
||||
return await this.connection.query(soql);
|
||||
}
|
||||
|
||||
sobject(type: string): any {
|
||||
sobject(type: string): unknown {
|
||||
return this.connection.sobject(type);
|
||||
}
|
||||
|
||||
|
||||
@ -151,15 +151,15 @@ export class WhmcsCacheService {
|
||||
/**
|
||||
* Get cached client data
|
||||
*/
|
||||
async getClientData(clientId: number): Promise<any | null> {
|
||||
async getClientData(clientId: number): Promise<unknown | null> {
|
||||
const key = this.buildClientKey(clientId);
|
||||
return this.get<any>(key, "client");
|
||||
return this.get<unknown>(key, "client");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache client data
|
||||
*/
|
||||
async setClientData(clientId: number, data: any): Promise<void> {
|
||||
async setClientData(clientId: number, data: unknown): Promise<void> {
|
||||
const key = this.buildClientKey(clientId);
|
||||
await this.set(key, data, "client", [`client:${clientId}`]);
|
||||
}
|
||||
|
||||
@ -3,7 +3,11 @@ import { Logger } from "nestjs-pino";
|
||||
import { getErrorMessage } from "../../../common/utils/error.util";
|
||||
import { WhmcsConnectionService } from "./whmcs-connection.service";
|
||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
||||
import { WhmcsValidateLoginParams, WhmcsAddClientParams } from "../types/whmcs-api.types";
|
||||
import {
|
||||
WhmcsValidateLoginParams,
|
||||
WhmcsAddClientParams,
|
||||
WhmcsClientResponse,
|
||||
} from "../types/whmcs-api.types";
|
||||
|
||||
@Injectable()
|
||||
export class WhmcsClientService {
|
||||
@ -44,13 +48,13 @@ export class WhmcsClientService {
|
||||
/**
|
||||
* Get client details by ID
|
||||
*/
|
||||
async getClientDetails(clientId: number): Promise<any> {
|
||||
async getClientDetails(clientId: number): Promise<WhmcsClientResponse["client"]> {
|
||||
try {
|
||||
// Try cache first
|
||||
const cached = await this.cacheService.getClientData(clientId);
|
||||
if (cached) {
|
||||
this.logger.debug(`Cache hit for client: ${clientId}`);
|
||||
return cached;
|
||||
return cached as WhmcsClientResponse["client"];
|
||||
}
|
||||
|
||||
const response = await this.connectionService.getClientDetails(clientId);
|
||||
@ -75,7 +79,7 @@ export class WhmcsClientService {
|
||||
/**
|
||||
* Get client details by email
|
||||
*/
|
||||
async getClientDetailsByEmail(email: string): Promise<any> {
|
||||
async getClientDetailsByEmail(email: string): Promise<WhmcsClientResponse["client"]> {
|
||||
try {
|
||||
const response = await this.connectionService.getClientDetailsByEmail(email);
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ export class WhmcsConnectionService {
|
||||
*/
|
||||
private async makeRequest<T>(
|
||||
action: string,
|
||||
params: Record<string, any> = {},
|
||||
params: Record<string, unknown> = {},
|
||||
attempt: number = 1
|
||||
): Promise<T> {
|
||||
const url = `${this.config.baseUrl}/includes/api.php`;
|
||||
@ -117,7 +117,7 @@ export class WhmcsConnectionService {
|
||||
let data: WhmcsApiResponse<T>;
|
||||
|
||||
try {
|
||||
data = JSON.parse(responseText);
|
||||
data = JSON.parse(responseText) as WhmcsApiResponse<T>;
|
||||
} catch (parseError) {
|
||||
this.logger.error(`Invalid JSON response from WHMCS API [${action}]`, {
|
||||
responseText: responseText.substring(0, 500),
|
||||
@ -169,7 +169,7 @@ export class WhmcsConnectionService {
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRetry(error: any): boolean {
|
||||
private shouldRetry(error: unknown): boolean {
|
||||
// Retry on network errors, timeouts, and 5xx server errors
|
||||
return (
|
||||
getErrorMessage(error).includes("fetch") ||
|
||||
@ -183,19 +183,19 @@ export class WhmcsConnectionService {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private sanitizeParams(params: Record<string, any>): Record<string, string> {
|
||||
private sanitizeParams(params: Record<string, unknown>): Record<string, string> {
|
||||
const sanitized: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
sanitized[key] = String(value);
|
||||
sanitized[key] = typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private sanitizeLogParams(params: Record<string, any>): Record<string, any> {
|
||||
private sanitizeLogParams(params: Record<string, unknown>): Record<string, unknown> {
|
||||
const sanitized = { ...params };
|
||||
|
||||
// Remove sensitive data from logs
|
||||
@ -243,7 +243,7 @@ export class WhmcsConnectionService {
|
||||
/**
|
||||
* Get WHMCS system information
|
||||
*/
|
||||
async getSystemInfo(): Promise<any> {
|
||||
async getSystemInfo(): Promise<unknown> {
|
||||
try {
|
||||
return await this.makeRequest("GetProducts", { limitnum: 1 });
|
||||
} catch (error) {
|
||||
|
||||
@ -8,7 +8,7 @@ import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
||||
import { WhmcsGetInvoicesParams } from "../types/whmcs-api.types";
|
||||
|
||||
export interface InvoiceFilters {
|
||||
status?: string;
|
||||
status?: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
@ -50,7 +50,7 @@ export class WhmcsInvoiceService {
|
||||
limitnum: limit,
|
||||
orderby: "date",
|
||||
order: "DESC",
|
||||
...(status && { status: status as any }),
|
||||
...(status && { status: status as WhmcsGetInvoicesParams["status"] }),
|
||||
};
|
||||
|
||||
const response = await this.connectionService.getInvoices(params);
|
||||
|
||||
@ -155,7 +155,7 @@ export class WhmcsPaymentService {
|
||||
/**
|
||||
* Get products catalog
|
||||
*/
|
||||
async getProducts(): Promise<any> {
|
||||
async getProducts(): Promise<unknown> {
|
||||
try {
|
||||
const response = await this.connectionService.getProducts();
|
||||
return response;
|
||||
@ -170,7 +170,7 @@ export class WhmcsPaymentService {
|
||||
/**
|
||||
* Transform product data (delegate to transformer)
|
||||
*/
|
||||
transformProduct(whmcsProduct: any): any {
|
||||
transformProduct(whmcsProduct: Record<string, unknown>): unknown {
|
||||
return this.dataTransformer.transformProduct(whmcsProduct);
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ export class WhmcsSsoService {
|
||||
clientId: number,
|
||||
module: string,
|
||||
action?: string,
|
||||
params?: Record<string, any>
|
||||
params?: Record<string, unknown>
|
||||
): Promise<{ url: string; expiresAt: string }> {
|
||||
try {
|
||||
// Build the module path
|
||||
@ -135,7 +135,13 @@ export class WhmcsSsoService {
|
||||
modulePath += `&a=${action}`;
|
||||
}
|
||||
if (params) {
|
||||
const queryParams = new URLSearchParams(params).toString();
|
||||
const stringParams: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
stringParams[key] = String(value);
|
||||
}
|
||||
}
|
||||
const queryParams = new URLSearchParams(stringParams).toString();
|
||||
if (queryParams) {
|
||||
modulePath += `&${queryParams}`;
|
||||
}
|
||||
|
||||
@ -7,6 +7,9 @@ import {
|
||||
Subscription,
|
||||
PaymentMethod,
|
||||
PaymentGateway,
|
||||
InvoiceStatus,
|
||||
SubscriptionStatus,
|
||||
BillingCycle,
|
||||
} from "@customer-portal/shared";
|
||||
import {
|
||||
WhmcsInvoice,
|
||||
@ -188,53 +191,49 @@ export class WhmcsDataTransformer {
|
||||
/**
|
||||
* Normalize invoice status to our standard values
|
||||
*/
|
||||
private normalizeInvoiceStatus(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
private normalizeInvoiceStatus(status: string): InvoiceStatus {
|
||||
const statusMap: Record<string, InvoiceStatus> = {
|
||||
paid: "Paid",
|
||||
unpaid: "Unpaid",
|
||||
cancelled: "Cancelled",
|
||||
overdue: "Overdue",
|
||||
collections: "Collections",
|
||||
draft: "Draft",
|
||||
refunded: "Refunded",
|
||||
};
|
||||
|
||||
return statusMap[status?.toLowerCase()] || status || "Unknown";
|
||||
return statusMap[status?.toLowerCase()] || "Unpaid";
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize product status to our standard values
|
||||
*/
|
||||
private normalizeProductStatus(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
private normalizeProductStatus(status: string): SubscriptionStatus {
|
||||
const statusMap: Record<string, SubscriptionStatus> = {
|
||||
active: "Active",
|
||||
suspended: "Suspended",
|
||||
terminated: "Terminated",
|
||||
cancelled: "Cancelled",
|
||||
pending: "Pending",
|
||||
completed: "Completed",
|
||||
fraud: "Fraud",
|
||||
};
|
||||
|
||||
return statusMap[status?.toLowerCase()] || status || "Unknown";
|
||||
return statusMap[status?.toLowerCase()] || "Pending";
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize billing cycle to our standard values
|
||||
*/
|
||||
private normalizeBillingCycle(cycle: string): string {
|
||||
const cycleMap: Record<string, string> = {
|
||||
private normalizeBillingCycle(cycle: string): BillingCycle {
|
||||
const cycleMap: Record<string, BillingCycle> = {
|
||||
monthly: "Monthly",
|
||||
quarterly: "Quarterly",
|
||||
semiannually: "Semi-Annually",
|
||||
annually: "Annually",
|
||||
biennially: "Biennially",
|
||||
triennially: "Triennially",
|
||||
onetime: "One Time",
|
||||
free: "Free",
|
||||
};
|
||||
|
||||
return cycleMap[cycle?.toLowerCase()] || cycle || "Unknown";
|
||||
return cycleMap[cycle?.toLowerCase()] || "Monthly";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
// Base API Response Structure
|
||||
export interface WhmcsApiResponse<T = any> {
|
||||
export interface WhmcsApiResponse<T = unknown> {
|
||||
result: "success" | "error";
|
||||
message?: string;
|
||||
data?: T;
|
||||
@ -84,7 +84,7 @@ export interface WhmcsInvoice {
|
||||
notes?: string;
|
||||
ccgateway?: boolean;
|
||||
items?: WhmcsInvoiceItems;
|
||||
transactions?: any;
|
||||
transactions?: unknown;
|
||||
// Legacy field names for backwards compatibility
|
||||
id?: number;
|
||||
clientid?: number;
|
||||
@ -108,7 +108,7 @@ export interface WhmcsInvoiceItems {
|
||||
|
||||
export interface WhmcsInvoiceResponse extends WhmcsInvoice {
|
||||
result: "success" | "error";
|
||||
transactions?: any;
|
||||
transactions?: unknown;
|
||||
}
|
||||
|
||||
// Product/Service Types
|
||||
@ -152,7 +152,7 @@ export interface WhmcsProduct {
|
||||
promovalue?: string;
|
||||
packageid?: number;
|
||||
packagename?: string;
|
||||
configoptions?: any;
|
||||
configoptions?: Record<string, unknown>;
|
||||
customfields?: WhmcsCustomFields;
|
||||
firstpaymentamount: string;
|
||||
recurringamount: string;
|
||||
@ -187,6 +187,7 @@ export interface WhmcsGetInvoicesParams {
|
||||
limitnum?: number;
|
||||
orderby?: "id" | "invoicenum" | "date" | "duedate" | "total" | "status";
|
||||
order?: "ASC" | "DESC";
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsGetClientsProductsParams {
|
||||
@ -198,17 +199,20 @@ export interface WhmcsGetClientsProductsParams {
|
||||
limitnum?: number;
|
||||
orderby?: "id" | "productname" | "regdate" | "nextduedate";
|
||||
order?: "ASC" | "DESC";
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsCreateSsoTokenParams {
|
||||
client_id: number;
|
||||
destination?: string;
|
||||
sso_redirect_path?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsValidateLoginParams {
|
||||
email: string;
|
||||
password2: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsValidateLoginResponse {
|
||||
@ -237,6 +241,7 @@ export interface WhmcsAddClientParams {
|
||||
notes?: string;
|
||||
marketing_emails_opt_in?: boolean;
|
||||
no_email?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsAddClientResponse {
|
||||
@ -302,6 +307,7 @@ export interface WhmcsPayMethodsResponse {
|
||||
|
||||
export interface WhmcsGetPayMethodsParams {
|
||||
clientid: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsAddPayMethodParams {
|
||||
@ -324,6 +330,7 @@ export interface WhmcsAddPayMethodParams {
|
||||
remote_token?: string;
|
||||
// Billing info
|
||||
billing_contact_id?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsAddPayMethodResponse {
|
||||
|
||||
18
apps/bff/src/vendors/whmcs/whmcs.service.ts
vendored
18
apps/bff/src/vendors/whmcs/whmcs.service.ts
vendored
@ -17,7 +17,11 @@ import {
|
||||
import { WhmcsClientService } from "./services/whmcs-client.service";
|
||||
import { WhmcsPaymentService } from "./services/whmcs-payment.service";
|
||||
import { WhmcsSsoService } from "./services/whmcs-sso.service";
|
||||
import { WhmcsAddClientParams } from "./types/whmcs-api.types";
|
||||
import {
|
||||
WhmcsAddClientParams,
|
||||
WhmcsClientResponse,
|
||||
WhmcsCatalogProductsResponse,
|
||||
} from "./types/whmcs-api.types";
|
||||
import { Logger } from "nestjs-pino";
|
||||
|
||||
// Re-export interfaces for backward compatibility
|
||||
@ -161,14 +165,14 @@ export class WhmcsService {
|
||||
/**
|
||||
* Get client details by ID
|
||||
*/
|
||||
async getClientDetails(clientId: number): Promise<any> {
|
||||
async getClientDetails(clientId: number): Promise<WhmcsClientResponse["client"]> {
|
||||
return this.clientService.getClientDetails(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client details by email
|
||||
*/
|
||||
async getClientDetailsByEmail(email: string): Promise<any> {
|
||||
async getClientDetailsByEmail(email: string): Promise<WhmcsClientResponse["client"]> {
|
||||
return this.clientService.getClientDetailsByEmail(email);
|
||||
}
|
||||
|
||||
@ -224,14 +228,14 @@ export class WhmcsService {
|
||||
/**
|
||||
* Get products catalog
|
||||
*/
|
||||
async getProducts(): Promise<any> {
|
||||
return this.paymentService.getProducts();
|
||||
async getProducts(): Promise<WhmcsCatalogProductsResponse> {
|
||||
return this.paymentService.getProducts() as Promise<WhmcsCatalogProductsResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform product data (delegate to transformer)
|
||||
*/
|
||||
transformProduct(whmcsProduct: any): any {
|
||||
transformProduct(whmcsProduct: WhmcsCatalogProductsResponse["products"]["product"][0]): unknown {
|
||||
return this.paymentService.transformProduct(whmcsProduct);
|
||||
}
|
||||
|
||||
@ -282,7 +286,7 @@ export class WhmcsService {
|
||||
/**
|
||||
* Get WHMCS system information
|
||||
*/
|
||||
async getSystemInfo(): Promise<any> {
|
||||
async getSystemInfo(): Promise<unknown> {
|
||||
return this.connectionService.getSystemInfo();
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/portal/.eslintrc.json
Normal file
3
apps/portal/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
@ -93,7 +93,7 @@ export default function ProfilePage() {
|
||||
// Update the auth store with the new user data
|
||||
useAuthStore.setState(state => ({
|
||||
...state,
|
||||
user: { ...state.user, ...updatedUser },
|
||||
user: state.user ? { ...state.user, ...updatedUser } : state.user,
|
||||
}));
|
||||
|
||||
setIsEditing(false);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@ -26,7 +26,7 @@ const schema = z
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
function ResetPasswordContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { resetPassword, isLoading } = useAuthStore();
|
||||
@ -98,3 +98,21 @@ export default function ResetPasswordPage() {
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<AuthLayout title="Reset your password" subtitle="Loading...">
|
||||
<div className="animate-pulse">
|
||||
<div className="bg-gray-200 h-10 rounded mb-4"></div>
|
||||
<div className="bg-gray-200 h-10 rounded mb-4"></div>
|
||||
<div className="bg-gray-200 h-10 rounded"></div>
|
||||
</div>
|
||||
</AuthLayout>
|
||||
}
|
||||
>
|
||||
<ResetPasswordContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo, useState, Suspense } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { PageLayout } from "@/components/layout/page-layout";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||
import { authenticatedApi } from "@/lib/api";
|
||||
|
||||
export default function CheckoutPage() {
|
||||
function CheckoutContent() {
|
||||
const params = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@ -67,3 +67,24 @@ export default function CheckoutPage() {
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CheckoutPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<PageLayout
|
||||
icon={<ShieldCheckIcon />}
|
||||
title="Checkout"
|
||||
description="Loading checkout details..."
|
||||
>
|
||||
<div className="animate-pulse">
|
||||
<div className="bg-gray-200 h-32 rounded mb-4"></div>
|
||||
<div className="bg-gray-200 h-10 rounded w-32"></div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
}
|
||||
>
|
||||
<CheckoutContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
2
apps/portal/src/features/dashboard/index.ts
Normal file
2
apps/portal/src/features/dashboard/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./components";
|
||||
export * from "./hooks";
|
||||
@ -1,7 +1,5 @@
|
||||
// Support case types from Salesforce
|
||||
|
||||
export type CaseStatus = "New" | "Working" | "Escalated" | "Closed";
|
||||
export type CasePriority = "Low" | "Medium" | "High" | "Critical";
|
||||
import type { CaseStatus, CasePriority } from "./status";
|
||||
export type CaseType = "Question" | "Problem" | "Feature Request";
|
||||
|
||||
export interface SupportCase {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
// Invoice types from WHMCS
|
||||
|
||||
export type InvoiceStatus = "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";
|
||||
import type { InvoiceStatus } from "./status";
|
||||
|
||||
export interface Invoice {
|
||||
id: number;
|
||||
|
||||
@ -8,34 +8,37 @@ export const USER_STATUS = {
|
||||
} as const;
|
||||
|
||||
export const INVOICE_STATUS = {
|
||||
DRAFT: "draft",
|
||||
PENDING: "pending",
|
||||
PAID: "paid",
|
||||
OVERDUE: "overdue",
|
||||
CANCELLED: "cancelled",
|
||||
DRAFT: "Draft",
|
||||
PENDING: "Pending",
|
||||
PAID: "Paid",
|
||||
UNPAID: "Unpaid",
|
||||
OVERDUE: "Overdue",
|
||||
CANCELLED: "Cancelled",
|
||||
COLLECTIONS: "Collections",
|
||||
} as const;
|
||||
|
||||
export const SUBSCRIPTION_STATUS = {
|
||||
ACTIVE: "active",
|
||||
INACTIVE: "inactive",
|
||||
PENDING: "pending",
|
||||
CANCELLED: "cancelled",
|
||||
SUSPENDED: "suspended",
|
||||
ACTIVE: "Active",
|
||||
INACTIVE: "Inactive",
|
||||
PENDING: "Pending",
|
||||
CANCELLED: "Cancelled",
|
||||
SUSPENDED: "Suspended",
|
||||
TERMINATED: "Terminated",
|
||||
COMPLETED: "Completed",
|
||||
} as const;
|
||||
|
||||
export const CASE_STATUS = {
|
||||
OPEN: "open",
|
||||
IN_PROGRESS: "in_progress",
|
||||
PENDING_CUSTOMER: "pending_customer",
|
||||
RESOLVED: "resolved",
|
||||
CLOSED: "closed",
|
||||
NEW: "New",
|
||||
WORKING: "Working",
|
||||
ESCALATED: "Escalated",
|
||||
CLOSED: "Closed",
|
||||
} as const;
|
||||
|
||||
export const CASE_PRIORITY = {
|
||||
LOW: "low",
|
||||
MEDIUM: "medium",
|
||||
HIGH: "high",
|
||||
URGENT: "urgent",
|
||||
LOW: "Low",
|
||||
MEDIUM: "Medium",
|
||||
HIGH: "High",
|
||||
CRITICAL: "Critical",
|
||||
} as const;
|
||||
|
||||
export const PAYMENT_STATUS = {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// Subscription types from WHMCS
|
||||
import type { SubscriptionStatus } from "./status";
|
||||
|
||||
export type BillingCycle =
|
||||
| "Monthly"
|
||||
@ -8,14 +9,6 @@ export type BillingCycle =
|
||||
| "Biennially"
|
||||
| "Triennially";
|
||||
|
||||
export type SubscriptionStatus =
|
||||
| "Active"
|
||||
| "Suspended"
|
||||
| "Terminated"
|
||||
| "Cancelled"
|
||||
| "Pending"
|
||||
| "Completed";
|
||||
|
||||
export interface Subscription {
|
||||
id: number;
|
||||
serviceId: number;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user