From 855fe211f7b885e73239e21db4cc86cc84904be9 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Sat, 23 Aug 2025 18:02:05 +0900 Subject: [PATCH] fixed lintel errors, --- apps/bff/src/auth/auth.service.ts | 64 ++++---- .../src/auth/guards/auth-throttle.guard.ts | 9 +- apps/bff/src/invoices/invoices.service.ts | 2 +- .../mappings/cache/mapping-cache.service.ts | 2 +- .../validation/mapping-validator.service.ts | 8 +- apps/bff/src/orders/orders.controller.ts | 2 +- apps/bff/src/orders/orders.service.ts | 16 +- .../subscriptions/subscriptions.service.ts | 2 +- apps/bff/src/users/users.controller.ts | 2 +- apps/bff/src/users/users.service.ts | 137 +++++++++++++----- .../vendors/salesforce/salesforce.service.ts | 8 +- .../services/salesforce-account.service.ts | 31 +++- .../services/salesforce-case.service.ts | 110 ++++++++++++-- .../services/salesforce-connection.service.ts | 10 +- .../whmcs/cache/whmcs-cache.service.ts | 6 +- .../whmcs/services/whmcs-client.service.ts | 12 +- .../services/whmcs-connection.service.ts | 14 +- .../whmcs/services/whmcs-invoice.service.ts | 4 +- .../whmcs/services/whmcs-payment.service.ts | 4 +- .../whmcs/services/whmcs-sso.service.ts | 10 +- .../transformers/whmcs-data.transformer.ts | 25 ++-- .../vendors/whmcs/types/whmcs-api.types.ts | 15 +- apps/bff/src/vendors/whmcs/whmcs.service.ts | 18 ++- apps/portal/.eslintrc.json | 3 + apps/portal/src/app/account/profile/page.tsx | 2 +- .../src/app/auth/reset-password/page.tsx | 22 ++- apps/portal/src/app/checkout/page.tsx | 25 +++- apps/portal/src/features/dashboard/index.ts | 2 + packages/shared/src/case.ts | 4 +- packages/shared/src/invoice.ts | 3 +- packages/shared/src/status.ts | 41 +++--- packages/shared/src/subscription.ts | 9 +- 32 files changed, 416 insertions(+), 206 deletions(-) create mode 100644 apps/portal/.eslintrc.json create mode 100644 apps/portal/src/features/dashboard/index.ts diff --git a/apps/bff/src/auth/auth.service.ts b/apps/bff/src/auth/auth.service.ts index 2bbe77e7..3c6d63bb 100644 --- a/apps/bff/src/auth/auth.service.ts +++ b/apps/bff/src/auth/auth.service.ts @@ -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 { + 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 { + private async handleFailedLogin(user: PrismaUser, _request?: unknown): Promise { 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 { + async logout(userId: string, token: string, _request?: Request): Promise { // 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), diff --git a/apps/bff/src/auth/guards/auth-throttle.guard.ts b/apps/bff/src/auth/guards/auth-throttle.guard.ts index db38d694..360fbe8f 100644 --- a/apps/bff/src/auth/guards/auth-throttle.guard.ts +++ b/apps/bff/src/auth/guards/auth-throttle.guard.ts @@ -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): Promise { + protected async getTracker(req: Request): Promise { // 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}`; diff --git a/apps/bff/src/invoices/invoices.service.ts b/apps/bff/src/invoices/invoices.service.ts index 0d979d7b..963c4dca 100644 --- a/apps/bff/src/invoices/invoices.service.ts +++ b/apps/bff/src/invoices/invoices.service.ts @@ -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(); diff --git a/apps/bff/src/mappings/cache/mapping-cache.service.ts b/apps/bff/src/mappings/cache/mapping-cache.service.ts index 0305c519..8da1098a 100644 --- a/apps/bff/src/mappings/cache/mapping-cache.service.ts +++ b/apps/bff/src/mappings/cache/mapping-cache.service.ts @@ -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 { diff --git a/apps/bff/src/mappings/validation/mapping-validator.service.ts b/apps/bff/src/mappings/validation/mapping-validator.service.ts index 3dc0b535..8ca6aec9 100644 --- a/apps/bff/src/mappings/validation/mapping-validator.service.ts +++ b/apps/bff/src/mappings/validation/mapping-validator.service.ts @@ -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, }); diff --git a/apps/bff/src/orders/orders.controller.ts b/apps/bff/src/orders/orders.controller.ts index ba7a6c7d..c8eddaac 100644 --- a/apps/bff/src/orders/orders.controller.ts +++ b/apps/bff/src/orders/orders.controller.ts @@ -6,7 +6,7 @@ import { RequestWithUser } from "../auth/auth.types"; interface CreateOrderBody { orderType: "Internet" | "eSIM" | "SIM" | "VPN" | "Other"; - selections: Record; + selections: Record; } @ApiTags("orders") diff --git a/apps/bff/src/orders/orders.service.ts b/apps/bff/src/orders/orders.service.ts index 325ddaac..1de49a40 100644 --- a/apps/bff/src/orders/orders.service.ts +++ b/apps/bff/src/orders/orders.service.ts @@ -7,7 +7,7 @@ import { WhmcsConnectionService } from "../vendors/whmcs/services/whmcs-connecti interface CreateOrderBody { orderType: "Internet" | "eSIM" | "SIM" | "VPN" | "Other"; - selections: Record; + selections: Record; 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, }); diff --git a/apps/bff/src/subscriptions/subscriptions.service.ts b/apps/bff/src/subscriptions/subscriptions.service.ts index 59606d1b..c2137572 100644 --- a/apps/bff/src/subscriptions/subscriptions.service.ts +++ b/apps/bff/src/subscriptions/subscriptions.service.ts @@ -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(); diff --git a/apps/bff/src/users/users.controller.ts b/apps/bff/src/users/users.controller.ts index 606de875..c74f8c78 100644 --- a/apps/bff/src/users/users.controller.ts +++ b/apps/bff/src/users/users.controller.ts @@ -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"); } diff --git a/apps/bff/src/users/users.service.ts b/apps/bff/src/users/users.service.ts index c338cf3a..9c8186bd 100644 --- a/apps/bff/src/users/users.service.ts +++ b/apps/bff/src/users/users.service.ts @@ -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 { }; } +// 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 { + private toEnhancedUser(user: PrismaUser, extras: Partial = {}): 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 { + async findByEmailInternal(email: string): Promise { 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 { - const validEmail = this.validateEmail(userData.email); + async create(userData: Partial): Promise { + 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 { + const sanitized: Partial = {}; 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 { + const update: Partial = {}; 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), }); }); diff --git a/apps/bff/src/vendors/salesforce/salesforce.service.ts b/apps/bff/src/vendors/salesforce/salesforce.service.ts index 92c8594a..8ae30b42 100644 --- a/apps/bff/src/vendors/salesforce/salesforce.service.ts +++ b/apps/bff/src/vendors/salesforce/salesforce.service.ts @@ -66,11 +66,11 @@ export class SalesforceService implements OnModuleInit { return this.accountService.upsert(accountData); } - async getAccount(accountId: string): Promise { - return this.accountService.getById(accountId); + async getAccount(accountId: string): Promise | null> { + return this.accountService.getById(accountId) as Promise | null>; } - async updateAccount(accountId: string, updates: any): Promise { + async updateAccount(accountId: string, updates: Record): Promise { 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 { + async updateCase(caseId: string, updates: Record): Promise { return this.caseService.updateCase(caseId, updates); } diff --git a/apps/bff/src/vendors/salesforce/services/salesforce-account.service.ts b/apps/bff/src/vendors/salesforce/services/salesforce-account.service.ts index 78a8bdd9..fb4d65fe 100644 --- a/apps/bff/src/vendors/salesforce/services/salesforce-account.service.ts +++ b/apps/bff/src/vendors/salesforce/services/salesforce-account.service.ts @@ -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 { + async getById(accountId: string): Promise { 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 { + async update(accountId: string, updates: Record): Promise { 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), diff --git a/apps/bff/src/vendors/salesforce/services/salesforce-case.service.ts b/apps/bff/src/vendors/salesforce/services/salesforce-case.service.ts index d18050be..24f3d74a 100644 --- a/apps/bff/src/vendors/salesforce/services/salesforce-case.service.ts +++ b/apps/bff/src/vendors/salesforce/services/salesforce-case.service.ts @@ -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 { + async updateCase(caseId: string, updates: Record): Promise { 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 { 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 { + private async createSalesforceCase( + caseData: CaseData & { contactId: string } + ): Promise { 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)) { diff --git a/apps/bff/src/vendors/salesforce/services/salesforce-connection.service.ts b/apps/bff/src/vendors/salesforce/services/salesforce-connection.service.ts index 22db33a2..d24caa94 100644 --- a/apps/bff/src/vendors/salesforce/services/salesforce-connection.service.ts +++ b/apps/bff/src/vendors/salesforce/services/salesforce-connection.service.ts @@ -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 { + async query(soql: string): Promise { return await this.connection.query(soql); } - sobject(type: string): any { + sobject(type: string): unknown { return this.connection.sobject(type); } diff --git a/apps/bff/src/vendors/whmcs/cache/whmcs-cache.service.ts b/apps/bff/src/vendors/whmcs/cache/whmcs-cache.service.ts index 44bdea39..86d02be0 100644 --- a/apps/bff/src/vendors/whmcs/cache/whmcs-cache.service.ts +++ b/apps/bff/src/vendors/whmcs/cache/whmcs-cache.service.ts @@ -151,15 +151,15 @@ export class WhmcsCacheService { /** * Get cached client data */ - async getClientData(clientId: number): Promise { + async getClientData(clientId: number): Promise { const key = this.buildClientKey(clientId); - return this.get(key, "client"); + return this.get(key, "client"); } /** * Cache client data */ - async setClientData(clientId: number, data: any): Promise { + async setClientData(clientId: number, data: unknown): Promise { const key = this.buildClientKey(clientId); await this.set(key, data, "client", [`client:${clientId}`]); } diff --git a/apps/bff/src/vendors/whmcs/services/whmcs-client.service.ts b/apps/bff/src/vendors/whmcs/services/whmcs-client.service.ts index 70381983..2e35a85d 100644 --- a/apps/bff/src/vendors/whmcs/services/whmcs-client.service.ts +++ b/apps/bff/src/vendors/whmcs/services/whmcs-client.service.ts @@ -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 { + async getClientDetails(clientId: number): Promise { 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 { + async getClientDetailsByEmail(email: string): Promise { try { const response = await this.connectionService.getClientDetailsByEmail(email); diff --git a/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts b/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts index e2d23dd7..bdfe78e2 100644 --- a/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts +++ b/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts @@ -74,7 +74,7 @@ export class WhmcsConnectionService { */ private async makeRequest( action: string, - params: Record = {}, + params: Record = {}, attempt: number = 1 ): Promise { const url = `${this.config.baseUrl}/includes/api.php`; @@ -117,7 +117,7 @@ export class WhmcsConnectionService { let data: WhmcsApiResponse; try { - data = JSON.parse(responseText); + data = JSON.parse(responseText) as WhmcsApiResponse; } 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): Record { + private sanitizeParams(params: Record): Record { const sanitized: Record = {}; 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): Record { + private sanitizeLogParams(params: Record): Record { const sanitized = { ...params }; // Remove sensitive data from logs @@ -243,7 +243,7 @@ export class WhmcsConnectionService { /** * Get WHMCS system information */ - async getSystemInfo(): Promise { + async getSystemInfo(): Promise { try { return await this.makeRequest("GetProducts", { limitnum: 1 }); } catch (error) { diff --git a/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts b/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts index 8faa7b30..874f5de0 100644 --- a/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts +++ b/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts @@ -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); diff --git a/apps/bff/src/vendors/whmcs/services/whmcs-payment.service.ts b/apps/bff/src/vendors/whmcs/services/whmcs-payment.service.ts index 33a7a897..dae345a6 100644 --- a/apps/bff/src/vendors/whmcs/services/whmcs-payment.service.ts +++ b/apps/bff/src/vendors/whmcs/services/whmcs-payment.service.ts @@ -155,7 +155,7 @@ export class WhmcsPaymentService { /** * Get products catalog */ - async getProducts(): Promise { + async getProducts(): Promise { 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): unknown { return this.dataTransformer.transformProduct(whmcsProduct); } } diff --git a/apps/bff/src/vendors/whmcs/services/whmcs-sso.service.ts b/apps/bff/src/vendors/whmcs/services/whmcs-sso.service.ts index 072f1d9f..bb3c512d 100644 --- a/apps/bff/src/vendors/whmcs/services/whmcs-sso.service.ts +++ b/apps/bff/src/vendors/whmcs/services/whmcs-sso.service.ts @@ -126,7 +126,7 @@ export class WhmcsSsoService { clientId: number, module: string, action?: string, - params?: Record + params?: Record ): 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 = {}; + 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}`; } diff --git a/apps/bff/src/vendors/whmcs/transformers/whmcs-data.transformer.ts b/apps/bff/src/vendors/whmcs/transformers/whmcs-data.transformer.ts index 672fdc1f..ef8fd517 100644 --- a/apps/bff/src/vendors/whmcs/transformers/whmcs-data.transformer.ts +++ b/apps/bff/src/vendors/whmcs/transformers/whmcs-data.transformer.ts @@ -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 = { + private normalizeInvoiceStatus(status: string): InvoiceStatus { + const statusMap: Record = { 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 = { + private normalizeProductStatus(status: string): SubscriptionStatus { + const statusMap: Record = { 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 = { + private normalizeBillingCycle(cycle: string): BillingCycle { + const cycleMap: Record = { 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"; } /** diff --git a/apps/bff/src/vendors/whmcs/types/whmcs-api.types.ts b/apps/bff/src/vendors/whmcs/types/whmcs-api.types.ts index a13b3c85..33cc4492 100644 --- a/apps/bff/src/vendors/whmcs/types/whmcs-api.types.ts +++ b/apps/bff/src/vendors/whmcs/types/whmcs-api.types.ts @@ -4,7 +4,7 @@ */ // Base API Response Structure -export interface WhmcsApiResponse { +export interface WhmcsApiResponse { 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; 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 { diff --git a/apps/bff/src/vendors/whmcs/whmcs.service.ts b/apps/bff/src/vendors/whmcs/whmcs.service.ts index ab9c02b5..da18ecef 100644 --- a/apps/bff/src/vendors/whmcs/whmcs.service.ts +++ b/apps/bff/src/vendors/whmcs/whmcs.service.ts @@ -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 { + async getClientDetails(clientId: number): Promise { return this.clientService.getClientDetails(clientId); } /** * Get client details by email */ - async getClientDetailsByEmail(email: string): Promise { + async getClientDetailsByEmail(email: string): Promise { return this.clientService.getClientDetailsByEmail(email); } @@ -224,14 +228,14 @@ export class WhmcsService { /** * Get products catalog */ - async getProducts(): Promise { - return this.paymentService.getProducts(); + async getProducts(): Promise { + return this.paymentService.getProducts() as Promise; } /** * 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 { + async getSystemInfo(): Promise { return this.connectionService.getSystemInfo(); } } diff --git a/apps/portal/.eslintrc.json b/apps/portal/.eslintrc.json new file mode 100644 index 00000000..37224185 --- /dev/null +++ b/apps/portal/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/apps/portal/src/app/account/profile/page.tsx b/apps/portal/src/app/account/profile/page.tsx index 7a7229b0..fbf4f4da 100644 --- a/apps/portal/src/app/account/profile/page.tsx +++ b/apps/portal/src/app/account/profile/page.tsx @@ -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); diff --git a/apps/portal/src/app/auth/reset-password/page.tsx b/apps/portal/src/app/auth/reset-password/page.tsx index b81da80c..a054dfc8 100644 --- a/apps/portal/src/app/auth/reset-password/page.tsx +++ b/apps/portal/src/app/auth/reset-password/page.tsx @@ -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; -export default function ResetPasswordPage() { +function ResetPasswordContent() { const router = useRouter(); const searchParams = useSearchParams(); const { resetPassword, isLoading } = useAuthStore(); @@ -98,3 +98,21 @@ export default function ResetPasswordPage() { ); } + +export default function ResetPasswordPage() { + return ( + +
+
+
+
+
+ + } + > + +
+ ); +} diff --git a/apps/portal/src/app/checkout/page.tsx b/apps/portal/src/app/checkout/page.tsx index 3f439ed2..995ffd5f 100644 --- a/apps/portal/src/app/checkout/page.tsx +++ b/apps/portal/src/app/checkout/page.tsx @@ -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() { ); } + +export default function CheckoutPage() { + return ( + } + title="Checkout" + description="Loading checkout details..." + > +
+
+
+
+ + } + > + +
+ ); +} diff --git a/apps/portal/src/features/dashboard/index.ts b/apps/portal/src/features/dashboard/index.ts new file mode 100644 index 00000000..a234113b --- /dev/null +++ b/apps/portal/src/features/dashboard/index.ts @@ -0,0 +1,2 @@ +export * from "./components"; +export * from "./hooks"; diff --git a/packages/shared/src/case.ts b/packages/shared/src/case.ts index bc56092c..e9f25692 100644 --- a/packages/shared/src/case.ts +++ b/packages/shared/src/case.ts @@ -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 { diff --git a/packages/shared/src/invoice.ts b/packages/shared/src/invoice.ts index 0768953c..006e52dc 100644 --- a/packages/shared/src/invoice.ts +++ b/packages/shared/src/invoice.ts @@ -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; diff --git a/packages/shared/src/status.ts b/packages/shared/src/status.ts index 6460b13c..47637951 100644 --- a/packages/shared/src/status.ts +++ b/packages/shared/src/status.ts @@ -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 = { diff --git a/packages/shared/src/subscription.ts b/packages/shared/src/subscription.ts index 602629a0..345e0e2a 100644 --- a/packages/shared/src/subscription.ts +++ b/packages/shared/src/subscription.ts @@ -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;