fixed lintel errors,

This commit is contained in:
T. Narantuya 2025-08-23 18:02:05 +09:00
parent 111bbc8c91
commit 855fe211f7
32 changed files with 416 additions and 206 deletions

View File

@ -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),

View File

@ -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}`;

View File

@ -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();

View File

@ -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 {

View File

@ -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,
});

View File

@ -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")

View File

@ -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,
});

View File

@ -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();

View File

@ -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");
}

View File

@ -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),
});
});

View File

@ -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);
}

View File

@ -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),

View File

@ -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)) {

View File

@ -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);
}

View File

@ -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}`]);
}

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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}`;
}

View File

@ -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";
}
/**

View File

@ -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 {

View File

@ -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();
}
}

View File

@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

View File

@ -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);

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -0,0 +1,2 @@
export * from "./components";
export * from "./hooks";

View File

@ -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 {

View File

@ -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;

View File

@ -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 = {

View File

@ -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;