Refactor import paths for AlertBanner, AsyncBlock, and other components in the portal to enhance module structure and maintainability. Remove unused components and streamline imports across various views and features, ensuring consistent file organization.
This commit is contained in:
parent
3da96d0c84
commit
065e2f9acf
@ -5,6 +5,7 @@ import type {
|
||||
SimUsage,
|
||||
SimTopUpHistory,
|
||||
} from "./interfaces/freebit.types";
|
||||
import type { MnpData } from "@customer-portal/domain";
|
||||
|
||||
@Injectable()
|
||||
export class FreebitService {
|
||||
@ -116,7 +117,7 @@ export class FreebitService {
|
||||
simType: "eSIM" | "Physical SIM";
|
||||
activationType: "Immediate" | "Scheduled";
|
||||
scheduledAt?: string;
|
||||
mnp?: any;
|
||||
mnp?: MnpData;
|
||||
}): Promise<void> {
|
||||
// For eSIM, use the enhanced reissue method
|
||||
if (params.simType === "eSIM") {
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
// Export all Freebit services
|
||||
export { FreebitAuthService } from "./freebit-auth.service";
|
||||
export { FreebitClientService } from "./freebit-client.service";
|
||||
export { FreebitMapperService } from "./freebit-mapper.service";
|
||||
export { FreebitOperationsService } from "./freebit-operations.service";
|
||||
export { FreebitOrchestratorService } from "./freebit-orchestrator.service";
|
||||
export { FreebitError } from "./freebit-error.service";
|
||||
export { FreebitOrchestratorService } from './freebit-orchestrator.service';
|
||||
export { FreebitMapperService } from './freebit-mapper.service';
|
||||
export { FreebitOperationsService } from './freebit-operations.service';
|
||||
|
||||
@ -30,6 +30,13 @@ export class WhmcsConfigService {
|
||||
return this.accessKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for WHMCS API
|
||||
*/
|
||||
getBaseUrl(): string {
|
||||
return this.config.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if admin authentication is available
|
||||
*/
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
// Main orchestrator service
|
||||
export { WhmcsConnectionOrchestratorService } from "./services/whmcs-connection-orchestrator.service";
|
||||
|
||||
// Individual services
|
||||
export { WhmcsConfigService } from "./config/whmcs-config.service";
|
||||
export { WhmcsHttpClientService } from "./services/whmcs-http-client.service";
|
||||
export { WhmcsErrorHandlerService } from "./services/whmcs-error-handler.service";
|
||||
export { WhmcsApiMethodsService } from "./services/whmcs-api-methods.service";
|
||||
|
||||
// Types
|
||||
export type {
|
||||
WhmcsApiConfig,
|
||||
WhmcsRequestOptions,
|
||||
WhmcsRetryConfig,
|
||||
WhmcsConnectionStats,
|
||||
} from "./types/connection.types";
|
||||
@ -9,25 +9,13 @@ import type {
|
||||
WhmcsAddClientResponse,
|
||||
WhmcsCatalogProductsResponse,
|
||||
WhmcsPayMethodsResponse,
|
||||
WhmcsAddPayMethodResponse,
|
||||
WhmcsPaymentGatewaysResponse,
|
||||
WhmcsCreateInvoiceResponse,
|
||||
WhmcsUpdateInvoiceResponse,
|
||||
WhmcsCapturePaymentResponse,
|
||||
WhmcsAddCreditResponse,
|
||||
WhmcsAddInvoicePaymentResponse,
|
||||
WhmcsGetInvoicesParams,
|
||||
WhmcsGetClientsProductsParams,
|
||||
WhmcsCreateSsoTokenParams,
|
||||
WhmcsValidateLoginParams,
|
||||
WhmcsAddClientParams,
|
||||
WhmcsGetPayMethodsParams,
|
||||
WhmcsAddPayMethodParams,
|
||||
WhmcsCreateInvoiceParams,
|
||||
WhmcsUpdateInvoiceParams,
|
||||
WhmcsCapturePaymentParams,
|
||||
WhmcsAddCreditParams,
|
||||
WhmcsAddInvoicePaymentParams,
|
||||
} from "../../types/whmcs-api.types";
|
||||
import { WhmcsHttpClientService } from "./whmcs-http-client.service";
|
||||
import { WhmcsConfigService } from "../config/whmcs-config.service";
|
||||
@ -122,13 +110,6 @@ export class WhmcsApiMethodsService {
|
||||
return this.makeRequest("GetInvoice", { invoiceid: invoiceId });
|
||||
}
|
||||
|
||||
async createInvoice(params: WhmcsCreateInvoiceParams): Promise<WhmcsCreateInvoiceResponse> {
|
||||
return this.makeRequest("CreateInvoice", params);
|
||||
}
|
||||
|
||||
async updateInvoice(params: WhmcsUpdateInvoiceParams): Promise<WhmcsUpdateInvoiceResponse> {
|
||||
return this.makeRequest("UpdateInvoice", params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PRODUCT/SUBSCRIPTION API METHODS
|
||||
@ -150,27 +131,11 @@ export class WhmcsApiMethodsService {
|
||||
return this.makeRequest("GetPayMethods", params);
|
||||
}
|
||||
|
||||
async addPaymentMethod(params: WhmcsAddPayMethodParams): Promise<WhmcsAddPayMethodResponse> {
|
||||
return this.makeRequest("AddPayMethod", params);
|
||||
}
|
||||
|
||||
async getPaymentGateways(): Promise<WhmcsPaymentGatewaysResponse> {
|
||||
return this.makeRequest("GetPaymentMethods", {});
|
||||
}
|
||||
|
||||
async capturePayment(params: WhmcsCapturePaymentParams): Promise<WhmcsCapturePaymentResponse> {
|
||||
return this.makeRequest("CapturePayment", params);
|
||||
}
|
||||
|
||||
async addCredit(params: WhmcsAddCreditParams): Promise<WhmcsAddCreditResponse> {
|
||||
return this.makeRequest("AddCredit", params);
|
||||
}
|
||||
|
||||
async addInvoicePayment(
|
||||
params: WhmcsAddInvoicePaymentParams
|
||||
): Promise<WhmcsAddInvoicePaymentResponse> {
|
||||
return this.makeRequest("AddInvoicePayment", params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SSO API METHODS
|
||||
@ -180,32 +145,9 @@ export class WhmcsApiMethodsService {
|
||||
return this.makeRequest("CreateSsoToken", params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ADMIN API METHODS (require admin auth)
|
||||
// ==========================================
|
||||
|
||||
async acceptOrder(orderId: number): Promise<{ result: string }> {
|
||||
if (!this.configService.hasAdminAuth()) {
|
||||
throw new Error("Admin authentication required for AcceptOrder");
|
||||
}
|
||||
|
||||
return this.makeRequest<{ result: string }>(
|
||||
"AcceptOrder",
|
||||
{ orderid: orderId },
|
||||
{ useAdminAuth: true }
|
||||
);
|
||||
}
|
||||
|
||||
async cancelOrder(orderId: number): Promise<{ result: string }> {
|
||||
if (!this.configService.hasAdminAuth()) {
|
||||
throw new Error("Admin authentication required for CancelOrder");
|
||||
}
|
||||
|
||||
return this.makeRequest<{ result: string }>(
|
||||
"CancelOrder",
|
||||
{ orderid: orderId },
|
||||
{ useAdminAuth: true }
|
||||
);
|
||||
async getProducts() {
|
||||
return this.makeRequest("GetProducts", {});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
||||
@ -7,7 +7,13 @@ import { WhmcsErrorHandlerService } from "./whmcs-error-handler.service";
|
||||
import { WhmcsApiMethodsService } from "./whmcs-api-methods.service";
|
||||
import type {
|
||||
WhmcsApiResponse,
|
||||
WhmcsErrorResponse
|
||||
WhmcsErrorResponse,
|
||||
WhmcsAddClientParams,
|
||||
WhmcsValidateLoginParams,
|
||||
WhmcsGetInvoicesParams,
|
||||
WhmcsGetClientsProductsParams,
|
||||
WhmcsGetPayMethodsParams,
|
||||
WhmcsCreateSsoTokenParams,
|
||||
} from "../../types/whmcs-api.types";
|
||||
import type {
|
||||
WhmcsRequestOptions,
|
||||
@ -36,7 +42,7 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
||||
// Test connection
|
||||
const isAvailable = await this.apiMethods.isAvailable();
|
||||
if (isAvailable) {
|
||||
this.logger.info("WHMCS connection established successfully");
|
||||
this.logger.log("WHMCS connection established successfully");
|
||||
} else {
|
||||
this.logger.warn("WHMCS connection test failed - service may be unavailable");
|
||||
}
|
||||
@ -108,15 +114,15 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
||||
return this.apiMethods.getClientDetailsByEmail(email);
|
||||
}
|
||||
|
||||
async updateClient(clientId: number, updateData: any) {
|
||||
async updateClient(clientId: number, updateData: Record<string, unknown>) {
|
||||
return this.apiMethods.updateClient(clientId, updateData);
|
||||
}
|
||||
|
||||
async addClient(params: any) {
|
||||
async addClient(params: WhmcsAddClientParams) {
|
||||
return this.apiMethods.addClient(params);
|
||||
}
|
||||
|
||||
async validateLogin(params: any) {
|
||||
async validateLogin(params: WhmcsValidateLoginParams) {
|
||||
return this.apiMethods.validateLogin(params);
|
||||
}
|
||||
|
||||
@ -124,7 +130,7 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
||||
// INVOICE API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getInvoices(params: any = {}) {
|
||||
async getInvoices(params: WhmcsGetInvoicesParams = {}) {
|
||||
return this.apiMethods.getInvoices(params);
|
||||
}
|
||||
|
||||
@ -132,19 +138,12 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
||||
return this.apiMethods.getInvoice(invoiceId);
|
||||
}
|
||||
|
||||
async createInvoice(params: any) {
|
||||
return this.apiMethods.createInvoice(params);
|
||||
}
|
||||
|
||||
async updateInvoice(params: any) {
|
||||
return this.apiMethods.updateInvoice(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PRODUCT/SUBSCRIPTION API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getClientsProducts(params: any) {
|
||||
async getClientsProducts(params: WhmcsGetClientsProductsParams) {
|
||||
return this.apiMethods.getClientsProducts(params);
|
||||
}
|
||||
|
||||
@ -152,53 +151,44 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
||||
return this.apiMethods.getCatalogProducts();
|
||||
}
|
||||
|
||||
|
||||
async getProducts() {
|
||||
return this.apiMethods.getProducts();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PAYMENT API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getPaymentMethods(params: any) {
|
||||
async getPaymentMethods(params: WhmcsGetPayMethodsParams) {
|
||||
return this.apiMethods.getPaymentMethods(params);
|
||||
}
|
||||
|
||||
async addPaymentMethod(params: any) {
|
||||
return this.apiMethods.addPaymentMethod(params);
|
||||
// Legacy method name for backward compatibility
|
||||
async getPayMethods(params: WhmcsGetPayMethodsParams) {
|
||||
return this.getPaymentMethods(params);
|
||||
}
|
||||
|
||||
async getPaymentGateways() {
|
||||
return this.apiMethods.getPaymentGateways();
|
||||
}
|
||||
|
||||
async capturePayment(params: any) {
|
||||
return this.apiMethods.capturePayment(params);
|
||||
}
|
||||
|
||||
async addCredit(params: any) {
|
||||
return this.apiMethods.addCredit(params);
|
||||
}
|
||||
|
||||
async addInvoicePayment(params: any) {
|
||||
return this.apiMethods.addInvoicePayment(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SSO API METHODS
|
||||
// ==========================================
|
||||
|
||||
async createSsoToken(params: any) {
|
||||
async createSsoToken(params: WhmcsCreateSsoTokenParams) {
|
||||
return this.apiMethods.createSsoToken(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ADMIN API METHODS
|
||||
// UTILITY METHODS
|
||||
// ==========================================
|
||||
|
||||
async acceptOrder(orderId: number) {
|
||||
return this.apiMethods.acceptOrder(orderId);
|
||||
getBaseUrl(): string {
|
||||
return this.configService.getBaseUrl();
|
||||
}
|
||||
|
||||
async cancelOrder(orderId: number) {
|
||||
return this.apiMethods.cancelOrder(orderId);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// UTILITY METHODS
|
||||
|
||||
@ -7,9 +7,6 @@ import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer";
|
||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
||||
import {
|
||||
WhmcsGetInvoicesParams,
|
||||
WhmcsCreateInvoiceParams,
|
||||
WhmcsUpdateInvoiceParams,
|
||||
WhmcsCapturePaymentParams,
|
||||
WhmcsInvoicesResponse,
|
||||
} from "../types/whmcs-api.types";
|
||||
|
||||
@ -203,224 +200,6 @@ export class WhmcsInvoiceService {
|
||||
this.logger.log(`Invalidated invoice cache for user ${userId}, invoice ${invoiceId}`);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NEW: Invoice Creation Methods
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Create a new invoice for a client
|
||||
*/
|
||||
async createInvoice(params: {
|
||||
clientId: number;
|
||||
description: string;
|
||||
amount: number;
|
||||
currency?: string;
|
||||
dueDate?: Date;
|
||||
notes?: string;
|
||||
}): Promise<{ id: number; number: string; total: number; status: string }> {
|
||||
try {
|
||||
const dueDateStr = params.dueDate
|
||||
? params.dueDate.toISOString().split("T")[0]
|
||||
: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]; // 7 days from now
|
||||
|
||||
const whmcsParams: WhmcsCreateInvoiceParams = {
|
||||
userid: params.clientId,
|
||||
status: "Unpaid",
|
||||
sendnotification: false, // Don't send email notification automatically
|
||||
duedate: dueDateStr,
|
||||
notes: params.notes,
|
||||
itemdescription1: params.description,
|
||||
itemamount1: params.amount,
|
||||
itemtaxed1: false, // No tax for data top-ups for now
|
||||
};
|
||||
|
||||
const response = await this.connectionService.createInvoice(whmcsParams);
|
||||
|
||||
if (response.result !== "success") {
|
||||
throw new Error(`WHMCS invoice creation failed: ${response.message}`);
|
||||
}
|
||||
|
||||
this.logger.log(`Created WHMCS invoice ${response.invoiceid} for client ${params.clientId}`, {
|
||||
invoiceId: response.invoiceid,
|
||||
amount: params.amount,
|
||||
description: params.description,
|
||||
});
|
||||
|
||||
return {
|
||||
id: response.invoiceid,
|
||||
number: `INV-${response.invoiceid}`,
|
||||
total: params.amount,
|
||||
status: response.status,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to create invoice for client ${params.clientId}`, {
|
||||
error: getErrorMessage(error),
|
||||
params,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing invoice
|
||||
*/
|
||||
async updateInvoice(params: {
|
||||
invoiceId: number;
|
||||
status?:
|
||||
| "Draft"
|
||||
| "Unpaid"
|
||||
| "Paid"
|
||||
| "Cancelled"
|
||||
| "Refunded"
|
||||
| "Collections"
|
||||
| "Payment Pending";
|
||||
dueDate?: Date;
|
||||
notes?: string;
|
||||
}): Promise<{ success: boolean; message?: string }> {
|
||||
try {
|
||||
const whmcsParams: WhmcsUpdateInvoiceParams = {
|
||||
invoiceid: params.invoiceId,
|
||||
status: params.status,
|
||||
duedate: params.dueDate ? params.dueDate.toISOString().split("T")[0] : undefined,
|
||||
notes: params.notes,
|
||||
};
|
||||
|
||||
const response = await this.connectionService.updateInvoice(whmcsParams);
|
||||
|
||||
if (response.result !== "success") {
|
||||
throw new Error(`WHMCS invoice update failed: ${response.message}`);
|
||||
}
|
||||
|
||||
this.logger.log(`Updated WHMCS invoice ${params.invoiceId}`, {
|
||||
invoiceId: params.invoiceId,
|
||||
status: params.status,
|
||||
notes: params.notes,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: response.message,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to update invoice ${params.invoiceId}`, {
|
||||
error: getErrorMessage(error),
|
||||
params,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture payment for an invoice using the client's default payment method
|
||||
*/
|
||||
async capturePayment(params: {
|
||||
invoiceId: number;
|
||||
amount: number;
|
||||
currency?: string;
|
||||
}): Promise<{ success: boolean; transactionId?: string; error?: string }> {
|
||||
try {
|
||||
const whmcsParams: WhmcsCapturePaymentParams = {
|
||||
invoiceid: params.invoiceId,
|
||||
};
|
||||
|
||||
const response = await this.connectionService.capturePayment(whmcsParams);
|
||||
|
||||
if (response.result === "success") {
|
||||
this.logger.log(`Successfully captured payment for invoice ${params.invoiceId}`, {
|
||||
invoiceId: params.invoiceId,
|
||||
transactionId: response.transactionid,
|
||||
amount: response.amount,
|
||||
});
|
||||
|
||||
// Invalidate invoice cache since status changed
|
||||
await this.cacheService.invalidateInvoice(`invoice-${params.invoiceId}`, params.invoiceId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionId: response.transactionid,
|
||||
};
|
||||
} else {
|
||||
this.logger.warn(`Payment capture failed for invoice ${params.invoiceId}`, {
|
||||
invoiceId: params.invoiceId,
|
||||
error: response.message || response.error,
|
||||
});
|
||||
|
||||
// Return user-friendly error message instead of technical API error
|
||||
const userFriendlyError = this.getUserFriendlyPaymentError(
|
||||
response.message || response.error || "Unknown payment error"
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: userFriendlyError,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to capture payment for invoice ${params.invoiceId}`, {
|
||||
error: getErrorMessage(error),
|
||||
params,
|
||||
});
|
||||
|
||||
// Return user-friendly error message for exceptions
|
||||
const userFriendlyError = this.getUserFriendlyPaymentError(getErrorMessage(error));
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: userFriendlyError,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert technical payment errors to user-friendly messages
|
||||
*/
|
||||
private getUserFriendlyPaymentError(technicalError: string): string {
|
||||
if (!technicalError) {
|
||||
return "Unable to process payment. Please try again or contact support.";
|
||||
}
|
||||
|
||||
const errorLower = technicalError.toLowerCase();
|
||||
|
||||
// WHMCS API permission errors
|
||||
if (errorLower.includes("invalid permissions") || errorLower.includes("not allowed")) {
|
||||
return "Payment processing is temporarily unavailable. Please contact support for assistance.";
|
||||
}
|
||||
|
||||
// Authentication/authorization errors
|
||||
if (
|
||||
errorLower.includes("unauthorized") ||
|
||||
errorLower.includes("forbidden") ||
|
||||
errorLower.includes("403")
|
||||
) {
|
||||
return "Payment processing is temporarily unavailable. Please contact support for assistance.";
|
||||
}
|
||||
|
||||
// Network/timeout errors
|
||||
if (
|
||||
errorLower.includes("timeout") ||
|
||||
errorLower.includes("network") ||
|
||||
errorLower.includes("connection")
|
||||
) {
|
||||
return "Payment processing timed out. Please try again.";
|
||||
}
|
||||
|
||||
// Payment method errors
|
||||
if (
|
||||
errorLower.includes("payment method") ||
|
||||
errorLower.includes("card") ||
|
||||
errorLower.includes("insufficient funds")
|
||||
) {
|
||||
return "Unable to process payment with your current payment method. Please check your payment details or try a different method.";
|
||||
}
|
||||
|
||||
// Generic API errors
|
||||
if (errorLower.includes("api") || errorLower.includes("http") || errorLower.includes("error")) {
|
||||
return "Payment processing failed. Please try again or contact support if the issue persists.";
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return "Unable to process payment. Please try again or contact support.";
|
||||
}
|
||||
|
||||
private transformInvoicesResponse(
|
||||
response: WhmcsInvoicesResponse,
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
// Main orchestrator service
|
||||
export { WhmcsTransformerOrchestratorService } from "./services/whmcs-transformer-orchestrator.service";
|
||||
|
||||
// Individual transformer services
|
||||
export { InvoiceTransformerService } from "./services/invoice-transformer.service";
|
||||
export { SubscriptionTransformerService } from "./services/subscription-transformer.service";
|
||||
export { PaymentTransformerService } from "./services/payment-transformer.service";
|
||||
|
||||
// Utilities
|
||||
export { DataUtils } from "./utils/data-utils";
|
||||
export { StatusNormalizer } from "./utils/status-normalizer";
|
||||
|
||||
// Validators
|
||||
export { TransformationValidator } from "./validators/transformation-validator";
|
||||
@ -75,7 +75,7 @@ export class InvoiceTransformerService {
|
||||
const message = DataUtils.toErrorMessage(error);
|
||||
this.logger.error(`Failed to transform invoice ${invoiceId}`, {
|
||||
error: message,
|
||||
whmcsData: DataUtils.sanitizeForLog(whmcsInvoice as Record<string, unknown>),
|
||||
whmcsData: DataUtils.sanitizeForLog(whmcsInvoice as unknown as Record<string, unknown>),
|
||||
});
|
||||
throw new Error(`Failed to transform invoice: ${message}`);
|
||||
}
|
||||
@ -97,7 +97,7 @@ export class InvoiceTransformerService {
|
||||
} catch (error) {
|
||||
this.logger.warn("Failed to transform invoice items", {
|
||||
error: DataUtils.toErrorMessage(error),
|
||||
itemsData: DataUtils.sanitizeForLog(items as Record<string, unknown>),
|
||||
itemsData: DataUtils.sanitizeForLog(items as unknown as Record<string, unknown>),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
@ -109,9 +109,11 @@ export class InvoiceTransformerService {
|
||||
private transformSingleInvoiceItem(item: Record<string, unknown>): InvoiceItem | null {
|
||||
try {
|
||||
const transformedItem: InvoiceItem = {
|
||||
id: DataUtils.safeNumber(item.id, 0),
|
||||
description: DataUtils.safeString(item.description, "Unknown Item"),
|
||||
amount: DataUtils.parseAmount(item.amount),
|
||||
quantity: DataUtils.safeNumber(item.qty, 1),
|
||||
type: DataUtils.safeString(item.type, "Unknown"),
|
||||
};
|
||||
|
||||
// Add service ID if available
|
||||
@ -119,10 +121,8 @@ export class InvoiceTransformerService {
|
||||
transformedItem.serviceId = DataUtils.safeNumber(item.relid);
|
||||
}
|
||||
|
||||
// Add tax information if available
|
||||
if (item.taxed === "1" || item.taxed === true) {
|
||||
transformedItem.taxable = true;
|
||||
}
|
||||
// Note: taxable property is not part of the InvoiceItem schema
|
||||
// Tax information is handled at the invoice level
|
||||
|
||||
return transformedItem;
|
||||
} catch (error) {
|
||||
|
||||
@ -26,7 +26,7 @@ export class PaymentTransformerService {
|
||||
whmcsGateway.display_name || whmcsGateway.name,
|
||||
whmcsGateway.name
|
||||
),
|
||||
type: DataUtils.safeString(whmcsGateway.type, "unknown"),
|
||||
type: this.normalizeGatewayType(DataUtils.safeString(whmcsGateway.type, "manual")),
|
||||
isActive: DataUtils.safeBoolean(whmcsGateway.active),
|
||||
acceptsCreditCards: DataUtils.safeBoolean(whmcsGateway.accepts_credit_cards),
|
||||
acceptsBankAccount: DataUtils.safeBoolean(whmcsGateway.accepts_bank_account),
|
||||
@ -64,12 +64,11 @@ export class PaymentTransformerService {
|
||||
}
|
||||
|
||||
const transformed: PaymentMethod = {
|
||||
id: DataUtils.safeString(payMethodId),
|
||||
id: DataUtils.safeNumber(payMethodId, 0),
|
||||
type: this.normalizePaymentType(gatewayName),
|
||||
gateway: DataUtils.safeString(gatewayName),
|
||||
description: this.buildPaymentDescription(whmcsPayMethod),
|
||||
gatewayName: DataUtils.safeString(gatewayName),
|
||||
isDefault: DataUtils.safeBoolean(whmcsPayMethod.is_default || whmcsPayMethod.default),
|
||||
isActive: DataUtils.safeBoolean(whmcsPayMethod.is_active ?? true), // Default to active if not specified
|
||||
};
|
||||
|
||||
// Add credit card specific fields
|
||||
@ -78,7 +77,7 @@ export class PaymentTransformerService {
|
||||
}
|
||||
|
||||
if (cardType) {
|
||||
transformed.cardType = DataUtils.safeString(cardType);
|
||||
transformed.ccType = DataUtils.safeString(cardType);
|
||||
}
|
||||
|
||||
if (expiryDate) {
|
||||
@ -90,9 +89,8 @@ export class PaymentTransformerService {
|
||||
transformed.accountType = DataUtils.safeString(whmcsPayMethod.account_type);
|
||||
}
|
||||
|
||||
if (whmcsPayMethod.routing_number) {
|
||||
transformed.routingNumber = DataUtils.safeString(whmcsPayMethod.routing_number);
|
||||
}
|
||||
// Note: routingNumber is not part of the PaymentMethod interface
|
||||
// This would need to be added to the interface if needed
|
||||
|
||||
if (!this.validator.validatePaymentMethod(transformed)) {
|
||||
throw new Error("Transformed payment method failed validation");
|
||||
@ -257,4 +255,49 @@ export class PaymentTransformerService {
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PRIVATE HELPER METHODS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Normalize gateway type to match our enum
|
||||
*/
|
||||
private normalizeGatewayType(type: string): "merchant" | "thirdparty" | "tokenization" | "manual" {
|
||||
const normalizedType = type.toLowerCase();
|
||||
switch (normalizedType) {
|
||||
case "merchant":
|
||||
case "credit_card":
|
||||
case "creditcard":
|
||||
return "merchant";
|
||||
case "thirdparty":
|
||||
case "third_party":
|
||||
case "external":
|
||||
return "thirdparty";
|
||||
case "tokenization":
|
||||
case "token":
|
||||
return "tokenization";
|
||||
default:
|
||||
return "manual";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize payment method type to match our enum
|
||||
*/
|
||||
private normalizePaymentType(gatewayName?: string): "CreditCard" | "BankAccount" | "RemoteCreditCard" | "RemoteBankAccount" | "Manual" {
|
||||
if (!gatewayName) return "Manual";
|
||||
|
||||
const normalized = gatewayName.toLowerCase();
|
||||
if (normalized.includes("credit") || normalized.includes("card") || normalized.includes("visa") || normalized.includes("mastercard")) {
|
||||
return "CreditCard";
|
||||
}
|
||||
if (normalized.includes("bank") || normalized.includes("ach") || normalized.includes("account")) {
|
||||
return "BankAccount";
|
||||
}
|
||||
if (normalized.includes("remote") || normalized.includes("token")) {
|
||||
return "RemoteCreditCard";
|
||||
}
|
||||
return "Manual";
|
||||
}
|
||||
}
|
||||
|
||||
@ -314,32 +314,6 @@ export interface WhmcsGetPayMethodsParams {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsAddPayMethodParams {
|
||||
clientid: number;
|
||||
type: "CreditCard" | "BankAccount" | "RemoteCreditCard";
|
||||
description: string;
|
||||
gateway_name?: string;
|
||||
// Credit Card specific
|
||||
card_number?: string;
|
||||
card_expiry?: string;
|
||||
cvv?: string;
|
||||
card_holder_name?: string;
|
||||
// Bank Account specific
|
||||
bank_name?: string;
|
||||
account_type?: string;
|
||||
routing_number?: string;
|
||||
account_number?: string;
|
||||
account_holder_name?: string;
|
||||
// Remote/Tokenized
|
||||
remote_token?: string;
|
||||
// Billing info
|
||||
billing_contact_id?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsAddPayMethodResponse {
|
||||
paymethodid: number;
|
||||
}
|
||||
|
||||
// Payment Gateway Types
|
||||
export interface WhmcsPaymentGateway {
|
||||
@ -359,123 +333,7 @@ export interface WhmcsPaymentGatewaysResponse {
|
||||
totalresults: number;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NEW: Invoice Creation and Payment Capture Types
|
||||
// ========================================
|
||||
|
||||
// CreateInvoice API Types
|
||||
export interface WhmcsCreateInvoiceParams {
|
||||
userid: number;
|
||||
status?:
|
||||
| "Draft"
|
||||
| "Unpaid"
|
||||
| "Paid"
|
||||
| "Cancelled"
|
||||
| "Refunded"
|
||||
| "Collections"
|
||||
| "Payment Pending";
|
||||
sendnotification?: boolean;
|
||||
paymentmethod?: string;
|
||||
taxrate?: number;
|
||||
taxrate2?: number;
|
||||
date?: string; // YYYY-MM-DD format
|
||||
duedate?: string; // YYYY-MM-DD format
|
||||
notes?: string;
|
||||
itemdescription1?: string;
|
||||
itemamount1?: number;
|
||||
itemtaxed1?: boolean;
|
||||
itemdescription2?: string;
|
||||
itemamount2?: number;
|
||||
itemtaxed2?: boolean;
|
||||
// Can have up to 24 line items (itemdescription1-24, itemamount1-24, itemtaxed1-24)
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsCreateInvoiceResponse {
|
||||
result: "success" | "error";
|
||||
invoiceid: number;
|
||||
status: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// UpdateInvoice API Types
|
||||
export interface WhmcsUpdateInvoiceParams {
|
||||
invoiceid: number;
|
||||
status?:
|
||||
| "Draft"
|
||||
| "Unpaid"
|
||||
| "Paid"
|
||||
| "Cancelled"
|
||||
| "Refunded"
|
||||
| "Collections"
|
||||
| "Payment Pending";
|
||||
duedate?: string; // YYYY-MM-DD format
|
||||
notes?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsUpdateInvoiceResponse {
|
||||
result: "success" | "error";
|
||||
invoiceid: number;
|
||||
status: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// CapturePayment API Types
|
||||
export interface WhmcsCapturePaymentParams {
|
||||
invoiceid: number;
|
||||
cvv?: string;
|
||||
cardnum?: string;
|
||||
cccvv?: string;
|
||||
cardtype?: string;
|
||||
cardexp?: string;
|
||||
// For existing payment methods
|
||||
paymentmethodid?: number;
|
||||
// Manual payment capture
|
||||
transid?: string;
|
||||
gateway?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsCapturePaymentResponse {
|
||||
result: "success" | "error";
|
||||
invoiceid: number;
|
||||
status: string;
|
||||
transactionid?: string;
|
||||
amount?: number;
|
||||
fees?: number;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// AddCredit API Types (for refunds if needed)
|
||||
export interface WhmcsAddCreditParams {
|
||||
clientid: number;
|
||||
description: string;
|
||||
amount: number;
|
||||
type?: "add" | "refund";
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsAddCreditResponse {
|
||||
result: "success" | "error";
|
||||
creditid: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// AddInvoicePayment API Types (for manual payment recording)
|
||||
export interface WhmcsAddInvoicePaymentParams {
|
||||
invoiceid: number;
|
||||
transid: string;
|
||||
amount?: number;
|
||||
fees?: number;
|
||||
gateway: string;
|
||||
date?: string; // YYYY-MM-DD HH:MM:SS format
|
||||
noemail?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WhmcsAddInvoicePaymentResponse {
|
||||
result: "success" | "error";
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@ -341,53 +341,6 @@ export class WhmcsService {
|
||||
return this.connectionService.getClientsProducts(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// INVOICE CREATION AND PAYMENT OPERATIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Create a new invoice for a client
|
||||
*/
|
||||
async createInvoice(params: {
|
||||
clientId: number;
|
||||
description: string;
|
||||
amount: number;
|
||||
currency?: string;
|
||||
dueDate?: Date;
|
||||
notes?: string;
|
||||
}): Promise<{ id: number; number: string; total: number; status: string }> {
|
||||
return this.invoiceService.createInvoice(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing invoice
|
||||
*/
|
||||
async updateInvoice(params: {
|
||||
invoiceId: number;
|
||||
status?:
|
||||
| "Draft"
|
||||
| "Unpaid"
|
||||
| "Paid"
|
||||
| "Cancelled"
|
||||
| "Refunded"
|
||||
| "Collections"
|
||||
| "Payment Pending";
|
||||
dueDate?: Date;
|
||||
notes?: string;
|
||||
}): Promise<{ success: boolean; message?: string }> {
|
||||
return this.invoiceService.updateInvoice(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture payment for an invoice
|
||||
*/
|
||||
async capturePayment(params: {
|
||||
invoiceId: number;
|
||||
amount: number;
|
||||
currency?: string;
|
||||
}): Promise<{ success: boolean; transactionId?: string; error?: string }> {
|
||||
return this.invoiceService.capturePayment(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ORDER OPERATIONS (delegate to OrderService)
|
||||
|
||||
22
apps/bff/src/modules/invoices/index.ts
Normal file
22
apps/bff/src/modules/invoices/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// Main orchestrator service
|
||||
export { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service";
|
||||
|
||||
// Individual services
|
||||
export { InvoiceRetrievalService } from "./services/invoice-retrieval.service";
|
||||
export { InvoiceOperationsService } from "./services/invoice-operations.service";
|
||||
export { PaymentMethodsService } from "./services/payment-methods.service";
|
||||
export { InvoiceHealthService } from "./services/invoice-health.service";
|
||||
|
||||
// Validators
|
||||
export { InvoiceValidatorService } from "./validators/invoice-validator.service";
|
||||
|
||||
// Types
|
||||
export type {
|
||||
GetInvoicesOptions,
|
||||
InvoiceValidationResult,
|
||||
InvoiceServiceStats,
|
||||
InvoiceHealthStatus,
|
||||
InvoiceStatus,
|
||||
PaginationOptions,
|
||||
UserMappingInfo,
|
||||
} from "./types/invoice-service.types";
|
||||
@ -3,10 +3,28 @@ import { InvoicesController } from "./invoices.controller";
|
||||
import { InvoicesService } from "./invoices.service";
|
||||
import { WhmcsModule } from "@bff/integrations/whmcs/whmcs.module";
|
||||
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module";
|
||||
// New modular invoice services
|
||||
import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service";
|
||||
import { InvoiceRetrievalService } from "./services/invoice-retrieval.service";
|
||||
import { InvoiceOperationsService } from "./services/invoice-operations.service";
|
||||
import { PaymentMethodsService } from "./services/payment-methods.service";
|
||||
import { InvoiceHealthService } from "./services/invoice-health.service";
|
||||
import { InvoiceValidatorService } from "./validators/invoice-validator.service";
|
||||
|
||||
@Module({
|
||||
imports: [WhmcsModule, MappingsModule],
|
||||
controllers: [InvoicesController],
|
||||
providers: [InvoicesService],
|
||||
providers: [
|
||||
// Legacy service (now facade)
|
||||
InvoicesService,
|
||||
// New modular services
|
||||
InvoicesOrchestratorService,
|
||||
InvoiceRetrievalService,
|
||||
InvoiceOperationsService,
|
||||
PaymentMethodsService,
|
||||
InvoiceHealthService,
|
||||
InvoiceValidatorService,
|
||||
],
|
||||
exports: [InvoicesService, InvoicesOrchestratorService],
|
||||
})
|
||||
export class InvoicesModule {}
|
||||
|
||||
@ -1,205 +1,67 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
Inject,
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
Invoice,
|
||||
InvoiceItem,
|
||||
InvoiceList,
|
||||
InvoiceSsoLink,
|
||||
Subscription,
|
||||
PaymentMethodList,
|
||||
PaymentGatewayList,
|
||||
InvoicePaymentLink,
|
||||
} from "@customer-portal/domain";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service";
|
||||
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import type {
|
||||
Invoice,
|
||||
InvoiceList,
|
||||
InvoiceSsoLink,
|
||||
InvoicePaymentLink,
|
||||
PaymentMethodList,
|
||||
PaymentGatewayList,
|
||||
} from "@customer-portal/domain";
|
||||
|
||||
// Re-export the interface for backward compatibility
|
||||
export interface GetInvoicesOptions {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoices Service - now acts as a facade to the orchestrator service
|
||||
* Maintains backward compatibility while delegating to modular services
|
||||
*/
|
||||
@Injectable()
|
||||
export class InvoicesService {
|
||||
constructor(
|
||||
private readonly orchestrator: InvoicesOrchestratorService,
|
||||
private readonly whmcsService: WhmcsService,
|
||||
private readonly mappingsService: MappingsService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
private readonly mappingsService: MappingsService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get paginated invoices for a user
|
||||
*/
|
||||
async getInvoices(userId: string, options: GetInvoicesOptions = {}): Promise<InvoiceList> {
|
||||
const { page = 1, limit = 10, status } = options;
|
||||
|
||||
try {
|
||||
// Get WHMCS client ID from user mapping
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
// Validate pagination parameters
|
||||
if (page < 1) {
|
||||
throw new BadRequestException("Page must be greater than 0");
|
||||
}
|
||||
if (limit < 1 || limit > 100) {
|
||||
throw new BadRequestException("Limit must be between 1 and 100");
|
||||
}
|
||||
|
||||
// Fetch invoices from WHMCS
|
||||
const invoiceList = await this.whmcsService.getInvoices(mapping.whmcsClientId, userId, {
|
||||
page,
|
||||
limit,
|
||||
status,
|
||||
});
|
||||
|
||||
this.logger.log(`Retrieved ${invoiceList.invoices.length} invoices for user ${userId}`, {
|
||||
page,
|
||||
limit,
|
||||
status,
|
||||
totalItems: invoiceList.pagination?.totalItems,
|
||||
});
|
||||
|
||||
return invoiceList;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get invoices for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
options,
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to retrieve invoices");
|
||||
}
|
||||
return this.orchestrator.getInvoices(userId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get individual invoice by ID
|
||||
*/
|
||||
async getInvoiceById(userId: string, invoiceId: number): Promise<Invoice> {
|
||||
try {
|
||||
// Validate invoice ID
|
||||
if (!invoiceId || invoiceId < 1) {
|
||||
throw new BadRequestException("Invalid invoice ID");
|
||||
}
|
||||
|
||||
// Get WHMCS client ID from user mapping
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
// Fetch invoice from WHMCS
|
||||
const invoice = await this.whmcsService.getInvoiceById(
|
||||
mapping.whmcsClientId,
|
||||
userId,
|
||||
invoiceId
|
||||
);
|
||||
|
||||
this.logger.log(`Retrieved invoice ${invoiceId} for user ${userId}`, {
|
||||
invoiceNumber: invoice.number,
|
||||
status: invoice.status,
|
||||
total: invoice.total,
|
||||
currency: invoice.currency,
|
||||
});
|
||||
|
||||
return invoice;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get invoice ${invoiceId} for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to retrieve invoice");
|
||||
}
|
||||
return this.orchestrator.getInvoiceById(userId, invoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SSO link for invoice viewing, PDF download, or direct payment
|
||||
* Create SSO link for invoice management
|
||||
*/
|
||||
async createSsoLink(
|
||||
userId: string,
|
||||
invoiceId: number,
|
||||
target: "view" | "download" | "pay" = "view"
|
||||
): Promise<InvoiceSsoLink> {
|
||||
try {
|
||||
// Validate invoice ID
|
||||
if (!invoiceId || invoiceId < 1) {
|
||||
throw new BadRequestException("Invalid invoice ID");
|
||||
}
|
||||
|
||||
// Get WHMCS client ID from user mapping
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
// Verify the invoice exists and belongs to this user
|
||||
await this.getInvoiceById(userId, invoiceId);
|
||||
|
||||
// Determine the target path based on the requested action
|
||||
let path: string;
|
||||
switch (target) {
|
||||
case "pay":
|
||||
// Direct payment page using WHMCS Friendly URLs
|
||||
path = `index.php?rp=/invoice/${invoiceId}/pay`;
|
||||
break;
|
||||
case "download":
|
||||
// PDF download
|
||||
path = `dl.php?type=i&id=${invoiceId}`;
|
||||
break;
|
||||
case "view":
|
||||
default:
|
||||
// Invoice view page
|
||||
path = `viewinvoice.php?id=${invoiceId}`;
|
||||
break;
|
||||
}
|
||||
|
||||
// Create SSO token for the specific invoice
|
||||
const ssoResult = await this.whmcsService.createSsoToken(
|
||||
mapping.whmcsClientId,
|
||||
"sso:custom_redirect",
|
||||
path
|
||||
);
|
||||
|
||||
const result: InvoiceSsoLink = {
|
||||
url: ssoResult.url,
|
||||
expiresAt: ssoResult.expiresAt,
|
||||
};
|
||||
|
||||
this.logger.log(`Created SSO link for invoice ${invoiceId}, user ${userId}`, {
|
||||
target,
|
||||
path,
|
||||
expiresAt: result.expiresAt,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to create SSO link for invoice ${invoiceId}, user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
target,
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to create SSO link");
|
||||
async createInvoiceSsoLink(userId: string, invoiceId?: number): Promise<InvoiceSsoLink> {
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new Error("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
const ssoResult = await this.whmcsService.createSsoToken(
|
||||
mapping.whmcsClientId,
|
||||
invoiceId ? `index.php?rp=/invoice/${invoiceId}` : undefined
|
||||
);
|
||||
|
||||
return {
|
||||
url: ssoResult.url,
|
||||
expiresAt: ssoResult.expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -210,29 +72,7 @@ export class InvoicesService {
|
||||
status: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections",
|
||||
options: Pick<GetInvoicesOptions, "page" | "limit"> = {}
|
||||
): Promise<InvoiceList> {
|
||||
const { page = 1, limit = 10 } = options;
|
||||
|
||||
try {
|
||||
// Validate status
|
||||
const validStatuses = ["Paid", "Unpaid", "Cancelled", "Overdue", "Collections"] as const;
|
||||
if (!validStatuses.includes(status)) {
|
||||
throw new BadRequestException(
|
||||
`Invalid status. Must be one of: ${validStatuses.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
return await this.getInvoices(userId, { page, limit, status });
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get ${status} invoices for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
options,
|
||||
});
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to retrieve invoices");
|
||||
}
|
||||
return this.orchestrator.getInvoicesByStatus(userId, status, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -242,7 +82,7 @@ export class InvoicesService {
|
||||
userId: string,
|
||||
options: Pick<GetInvoicesOptions, "page" | "limit"> = {}
|
||||
): Promise<InvoiceList> {
|
||||
return this.getInvoicesByStatus(userId, "Unpaid", options);
|
||||
return this.orchestrator.getUnpaidInvoices(userId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -252,325 +92,124 @@ export class InvoicesService {
|
||||
userId: string,
|
||||
options: Pick<GetInvoicesOptions, "page" | "limit"> = {}
|
||||
): Promise<InvoiceList> {
|
||||
return this.getInvoicesByStatus(userId, "Overdue", options);
|
||||
return this.orchestrator.getOverdueInvoices(userId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice statistics for a user
|
||||
* Get paid invoices for a user
|
||||
*/
|
||||
async getInvoiceStats(userId: string): Promise<{
|
||||
total: number;
|
||||
paid: number;
|
||||
unpaid: number;
|
||||
overdue: number;
|
||||
totalAmount: number;
|
||||
unpaidAmount: number;
|
||||
currency: string;
|
||||
}> {
|
||||
try {
|
||||
// Get all invoices (first 1000, should be enough for stats)
|
||||
const invoiceList = await this.getInvoices(userId, {
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
});
|
||||
const invoices = invoiceList.invoices;
|
||||
|
||||
if (invoices.length === 0) {
|
||||
return {
|
||||
total: 0,
|
||||
paid: 0,
|
||||
unpaid: 0,
|
||||
overdue: 0,
|
||||
totalAmount: 0,
|
||||
unpaidAmount: 0,
|
||||
currency: "JPY",
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
const stats = {
|
||||
total: invoices.length,
|
||||
paid: invoices.filter((i: Invoice) => i.status === "Paid").length,
|
||||
unpaid: invoices.filter((i: Invoice) => i.status === "Unpaid").length,
|
||||
overdue: invoices.filter((i: Invoice) => i.status === "Overdue").length,
|
||||
totalAmount: invoices.reduce((sum: number, i: Invoice) => sum + i.total, 0),
|
||||
unpaidAmount: invoices
|
||||
.filter((i: Invoice) => ["Unpaid", "Overdue"].includes(i.status))
|
||||
.reduce((sum: number, i: Invoice) => sum + i.total, 0),
|
||||
currency: invoices[0]?.currency || "JPY",
|
||||
};
|
||||
|
||||
this.logger.log(`Generated invoice stats for user ${userId}`, stats);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to generate invoice stats for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
throw new InternalServerErrorException("Failed to generate invoice statistics");
|
||||
}
|
||||
async getPaidInvoices(
|
||||
userId: string,
|
||||
options: Pick<GetInvoicesOptions, "page" | "limit"> = {}
|
||||
): Promise<InvoiceList> {
|
||||
return this.orchestrator.getPaidInvoices(userId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscriptions related to an invoice
|
||||
* Get cancelled invoices for a user
|
||||
*/
|
||||
async getInvoiceSubscriptions(userId: string, invoiceId: number): Promise<Subscription[]> {
|
||||
try {
|
||||
// Get the invoice with items
|
||||
const invoice = await this.getInvoiceById(userId, invoiceId);
|
||||
|
||||
if (!invoice.items || invoice.items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get WHMCS client ID from user mapping
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
// Get subscription IDs from invoice items
|
||||
const subscriptionIds = invoice.items
|
||||
.filter((item: InvoiceItem) => item.serviceId && item.serviceId > 0)
|
||||
.map((item: InvoiceItem) => item.serviceId!);
|
||||
|
||||
if (subscriptionIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all subscriptions for the user
|
||||
const allSubscriptions = await this.whmcsService.getSubscriptions(
|
||||
mapping.whmcsClientId,
|
||||
userId
|
||||
);
|
||||
|
||||
// Filter subscriptions that are referenced in the invoice
|
||||
const relatedSubscriptions = allSubscriptions.subscriptions.filter(
|
||||
(subscription: Subscription) => subscriptionIds.includes(subscription.serviceId)
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${relatedSubscriptions.length} subscriptions for invoice ${invoiceId}`,
|
||||
{
|
||||
userId,
|
||||
invoiceId,
|
||||
subscriptionIds,
|
||||
}
|
||||
);
|
||||
|
||||
return relatedSubscriptions;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get subscriptions for invoice ${invoiceId}`, {
|
||||
error: getErrorMessage(error),
|
||||
userId,
|
||||
invoiceId,
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to retrieve invoice subscriptions");
|
||||
}
|
||||
async getCancelledInvoices(
|
||||
userId: string,
|
||||
options: Pick<GetInvoicesOptions, "page" | "limit"> = {}
|
||||
): Promise<InvoiceList> {
|
||||
return this.orchestrator.getCancelledInvoices(userId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate invoice cache for a user
|
||||
* Get invoices in collections for a user
|
||||
*/
|
||||
async invalidateCache(userId: string, invoiceId?: number): Promise<void> {
|
||||
try {
|
||||
if (invoiceId) {
|
||||
await this.whmcsService.invalidateInvoiceCache(userId, invoiceId);
|
||||
} else {
|
||||
await this.whmcsService.invalidateUserCache(userId);
|
||||
}
|
||||
async getCollectionsInvoices(
|
||||
userId: string,
|
||||
options: Pick<GetInvoicesOptions, "page" | "limit"> = {}
|
||||
): Promise<InvoiceList> {
|
||||
return this.orchestrator.getCollectionsInvoices(userId, options);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Invalidated invoice cache for user ${userId}${invoiceId ? `, invoice ${invoiceId}` : ""}`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to invalidate invoice cache for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
invoiceId,
|
||||
});
|
||||
/**
|
||||
* Create payment link for a specific invoice
|
||||
*/
|
||||
async createInvoicePaymentLink(
|
||||
userId: string,
|
||||
invoiceId: number,
|
||||
gatewayName: string,
|
||||
returnUrl: string
|
||||
): Promise<InvoicePaymentLink> {
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new Error("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
const ssoResult = await this.whmcsService.createPaymentSsoToken(
|
||||
mapping.whmcsClientId,
|
||||
invoiceId,
|
||||
undefined,
|
||||
gatewayName
|
||||
);
|
||||
|
||||
return {
|
||||
url: ssoResult.url,
|
||||
expiresAt: ssoResult.expiresAt,
|
||||
gatewayName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payment methods for a user
|
||||
*/
|
||||
async getPaymentMethods(userId: string): Promise<PaymentMethodList> {
|
||||
try {
|
||||
this.logger.log(`Starting payment methods retrieval for user ${userId}`);
|
||||
|
||||
// Get WHMCS client ID from user mapping
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
this.logger.error(`No WHMCS client mapping found for user ${userId}`);
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
this.logger.log(`Found WHMCS client ID ${mapping.whmcsClientId} for user ${userId}`);
|
||||
|
||||
// Fetch payment methods from WHMCS
|
||||
const paymentMethods = await this.whmcsService.getPaymentMethods(
|
||||
mapping.whmcsClientId,
|
||||
userId
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Retrieved ${paymentMethods.paymentMethods.length} payment methods for user ${userId} (client ${mapping.whmcsClientId})`
|
||||
);
|
||||
return paymentMethods;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get payment methods for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to retrieve payment methods");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate payment methods cache for a user
|
||||
*/
|
||||
async invalidatePaymentMethodsCache(userId: string): Promise<void> {
|
||||
try {
|
||||
// Get WHMCS client ID from user mapping
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
// Invalidate WHMCS payment methods cache
|
||||
await this.whmcsService.invalidatePaymentMethodsCache(userId);
|
||||
|
||||
this.logger.log(`Invalidated payment methods cache for user ${userId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to invalidate payment methods cache for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
throw new InternalServerErrorException("Failed to invalidate payment methods cache");
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new Error("WHMCS client mapping not found");
|
||||
}
|
||||
return this.whmcsService.getPaymentMethods(mapping.whmcsClientId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available payment gateways
|
||||
*/
|
||||
async getPaymentGateways(): Promise<PaymentGatewayList> {
|
||||
try {
|
||||
// Fetch payment gateways from WHMCS
|
||||
const paymentGateways = await this.whmcsService.getPaymentGateways();
|
||||
|
||||
this.logger.log(`Retrieved ${paymentGateways.gateways.length} payment gateways`);
|
||||
return paymentGateways;
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to get payment gateways", {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
|
||||
throw new InternalServerErrorException("Failed to retrieve payment gateways");
|
||||
}
|
||||
return this.whmcsService.getPaymentGateways();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create payment SSO link for invoice with specific payment method or gateway
|
||||
* Invalidate payment methods cache for a user
|
||||
*/
|
||||
async createPaymentSsoLink(
|
||||
userId: string,
|
||||
invoiceId: number,
|
||||
paymentMethodId?: number,
|
||||
gatewayName?: string
|
||||
): Promise<InvoicePaymentLink> {
|
||||
try {
|
||||
// Validate invoice ID
|
||||
if (!invoiceId || invoiceId < 1) {
|
||||
throw new BadRequestException("Invalid invoice ID");
|
||||
}
|
||||
|
||||
// Get WHMCS client ID from user mapping
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
// Verify the invoice exists and belongs to this user
|
||||
await this.getInvoiceById(userId, invoiceId);
|
||||
|
||||
// Create payment SSO token with specific payment method or gateway
|
||||
const ssoResult = await this.whmcsService.createPaymentSsoToken(
|
||||
mapping.whmcsClientId,
|
||||
invoiceId,
|
||||
paymentMethodId,
|
||||
gatewayName
|
||||
);
|
||||
|
||||
const result: InvoicePaymentLink = {
|
||||
url: ssoResult.url,
|
||||
expiresAt: ssoResult.expiresAt,
|
||||
paymentMethodId,
|
||||
gatewayName,
|
||||
};
|
||||
|
||||
this.logger.log(`Created payment SSO link for invoice ${invoiceId}, user ${userId}`, {
|
||||
paymentMethodId,
|
||||
gatewayName,
|
||||
expiresAt: result.expiresAt,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create payment SSO link for invoice ${invoiceId}, user ${userId}`,
|
||||
{
|
||||
error: getErrorMessage(error),
|
||||
paymentMethodId,
|
||||
gatewayName,
|
||||
}
|
||||
);
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to create payment SSO link");
|
||||
}
|
||||
async invalidatePaymentMethodsCache(userId: string): Promise<void> {
|
||||
return this.whmcsService.invalidatePaymentMethodsCache(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check for invoice service
|
||||
*/
|
||||
async healthCheck(): Promise<{ status: string; details: unknown }> {
|
||||
try {
|
||||
const whmcsHealthy = await this.whmcsService.healthCheck();
|
||||
const health = await this.orchestrator.healthCheck();
|
||||
return {
|
||||
status: health.status,
|
||||
details: health.details,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: whmcsHealthy ? "healthy" : "unhealthy",
|
||||
details: {
|
||||
whmcsApi: whmcsHealthy ? "connected" : "disconnected",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error("Invoice service health check failed", {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
return {
|
||||
status: "unhealthy",
|
||||
details: {
|
||||
error: getErrorMessage(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get service statistics
|
||||
*/
|
||||
getServiceStats() {
|
||||
return this.orchestrator.getServiceStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has any invoices
|
||||
*/
|
||||
async hasInvoices(userId: string): Promise<boolean> {
|
||||
return this.orchestrator.hasInvoices(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice count by status
|
||||
*/
|
||||
async getInvoiceCountByStatus(
|
||||
userId: string,
|
||||
status: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections"
|
||||
): Promise<number> {
|
||||
return this.orchestrator.getInvoiceCountByStatus(userId, status);
|
||||
}
|
||||
}
|
||||
193
apps/bff/src/modules/invoices/services/invoice-health.service.ts
Normal file
193
apps/bff/src/modules/invoices/services/invoice-health.service.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import type { InvoiceHealthStatus, InvoiceServiceStats } from "../types/invoice-service.types";
|
||||
|
||||
/**
|
||||
* Service responsible for health checks and monitoring of invoice services
|
||||
*/
|
||||
@Injectable()
|
||||
export class InvoiceHealthService {
|
||||
private stats: InvoiceServiceStats = {
|
||||
totalInvoicesRetrieved: 0,
|
||||
totalPaymentLinksCreated: 0,
|
||||
totalSsoLinksCreated: 0,
|
||||
averageResponseTime: 0,
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly whmcsService: WhmcsService,
|
||||
private readonly mappingsService: MappingsService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Perform comprehensive health check
|
||||
*/
|
||||
async healthCheck(): Promise<InvoiceHealthStatus> {
|
||||
try {
|
||||
const checks = await Promise.allSettled([
|
||||
this.checkWhmcsHealth(),
|
||||
this.checkMappingsHealth(),
|
||||
]);
|
||||
|
||||
const whmcsResult = checks[0];
|
||||
const mappingsResult = checks[1];
|
||||
|
||||
const isHealthy =
|
||||
whmcsResult.status === "fulfilled" && whmcsResult.value &&
|
||||
mappingsResult.status === "fulfilled" && mappingsResult.value;
|
||||
|
||||
return {
|
||||
status: isHealthy ? "healthy" : "unhealthy",
|
||||
details: {
|
||||
whmcsApi: whmcsResult.status === "fulfilled" && whmcsResult.value ? "connected" : "disconnected",
|
||||
mappingsService: mappingsResult.status === "fulfilled" && mappingsResult.value ? "available" : "unavailable",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error("Invoice service health check failed", {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
|
||||
return {
|
||||
status: "unhealthy",
|
||||
details: {
|
||||
error: getErrorMessage(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service statistics
|
||||
*/
|
||||
getStats(): InvoiceServiceStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset service statistics
|
||||
*/
|
||||
resetStats(): void {
|
||||
this.stats = {
|
||||
totalInvoicesRetrieved: 0,
|
||||
totalPaymentLinksCreated: 0,
|
||||
totalSsoLinksCreated: 0,
|
||||
averageResponseTime: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Record invoice retrieval
|
||||
*/
|
||||
recordInvoiceRetrieval(responseTime: number): void {
|
||||
this.stats.totalInvoicesRetrieved++;
|
||||
this.updateAverageResponseTime(responseTime);
|
||||
this.stats.lastRequestTime = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record payment link creation
|
||||
*/
|
||||
recordPaymentLinkCreation(responseTime: number): void {
|
||||
this.stats.totalPaymentLinksCreated++;
|
||||
this.updateAverageResponseTime(responseTime);
|
||||
this.stats.lastRequestTime = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record SSO link creation
|
||||
*/
|
||||
recordSsoLinkCreation(responseTime: number): void {
|
||||
this.stats.totalSsoLinksCreated++;
|
||||
this.updateAverageResponseTime(responseTime);
|
||||
this.stats.lastRequestTime = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record error
|
||||
*/
|
||||
recordError(): void {
|
||||
this.stats.lastErrorTime = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check WHMCS service health
|
||||
*/
|
||||
private async checkWhmcsHealth(): Promise<boolean> {
|
||||
try {
|
||||
return await this.whmcsService.healthCheck();
|
||||
} catch (error) {
|
||||
this.logger.warn("WHMCS health check failed", {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check mappings service health
|
||||
*/
|
||||
private async checkMappingsHealth(): Promise<boolean> {
|
||||
try {
|
||||
// Simple check to see if mappings service is responsive
|
||||
// We don't want to create test data, so we'll just check if the service responds
|
||||
await this.mappingsService.findByUserId("health-check-test");
|
||||
return true;
|
||||
} catch (error) {
|
||||
// We expect this to fail for a non-existent user, but if the service responds, it's healthy
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
// If it's a "not found" error, the service is working
|
||||
if (errorMessage.toLowerCase().includes("not found")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.logger.warn("Mappings service health check failed", {
|
||||
error: errorMessage,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update average response time
|
||||
*/
|
||||
private updateAverageResponseTime(responseTime: number): void {
|
||||
const totalRequests =
|
||||
this.stats.totalInvoicesRetrieved +
|
||||
this.stats.totalPaymentLinksCreated +
|
||||
this.stats.totalSsoLinksCreated;
|
||||
|
||||
if (totalRequests === 1) {
|
||||
this.stats.averageResponseTime = responseTime;
|
||||
} else {
|
||||
this.stats.averageResponseTime =
|
||||
(this.stats.averageResponseTime * (totalRequests - 1) + responseTime) / totalRequests;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health summary
|
||||
*/
|
||||
async getHealthSummary(): Promise<{
|
||||
status: string;
|
||||
uptime: number;
|
||||
stats: InvoiceServiceStats;
|
||||
lastCheck: string;
|
||||
}> {
|
||||
const health = await this.healthCheck();
|
||||
|
||||
return {
|
||||
status: health.status,
|
||||
uptime: process.uptime(),
|
||||
stats: this.getStats(),
|
||||
lastCheck: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,221 @@
|
||||
import { Injectable, NotFoundException, InternalServerErrorException, Inject } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { Invoice, InvoiceList } from "@customer-portal/domain";
|
||||
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import { InvoiceValidatorService } from "../validators/invoice-validator.service";
|
||||
import type {
|
||||
GetInvoicesOptions,
|
||||
InvoiceStatus,
|
||||
PaginationOptions,
|
||||
UserMappingInfo
|
||||
} from "../types/invoice-service.types";
|
||||
|
||||
/**
|
||||
* Service responsible for retrieving invoices from WHMCS
|
||||
*/
|
||||
@Injectable()
|
||||
export class InvoiceRetrievalService {
|
||||
constructor(
|
||||
private readonly whmcsService: WhmcsService,
|
||||
private readonly mappingsService: MappingsService,
|
||||
private readonly validator: InvoiceValidatorService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get paginated invoices for a user
|
||||
*/
|
||||
async getInvoices(userId: string, options: GetInvoicesOptions = {}): Promise<InvoiceList> {
|
||||
const { page = 1, limit = 10, status } = options;
|
||||
|
||||
try {
|
||||
// Validate inputs
|
||||
this.validator.validateUserId(userId);
|
||||
this.validator.validatePagination({ page, limit });
|
||||
|
||||
if (status) {
|
||||
this.validator.validateInvoiceStatus(status);
|
||||
}
|
||||
|
||||
// Get user mapping
|
||||
const mapping = await this.getUserMapping(userId);
|
||||
|
||||
// Fetch invoices from WHMCS
|
||||
const invoiceList = await this.whmcsService.getInvoices(mapping.whmcsClientId, userId, {
|
||||
page,
|
||||
limit,
|
||||
status,
|
||||
});
|
||||
|
||||
this.logger.log(`Retrieved ${invoiceList.invoices.length} invoices for user ${userId}`, {
|
||||
page,
|
||||
limit,
|
||||
status,
|
||||
totalItems: invoiceList.pagination?.totalItems,
|
||||
});
|
||||
|
||||
return invoiceList;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get invoices for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
options,
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to retrieve invoices");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get individual invoice by ID
|
||||
*/
|
||||
async getInvoiceById(userId: string, invoiceId: number): Promise<Invoice> {
|
||||
try {
|
||||
// Validate inputs
|
||||
this.validator.validateUserId(userId);
|
||||
this.validator.validateInvoiceId(invoiceId);
|
||||
|
||||
// Get user mapping
|
||||
const mapping = await this.getUserMapping(userId);
|
||||
|
||||
// Fetch invoice from WHMCS
|
||||
const invoice = await this.whmcsService.getInvoiceById(
|
||||
mapping.whmcsClientId,
|
||||
userId,
|
||||
invoiceId
|
||||
);
|
||||
|
||||
this.logger.log(`Retrieved invoice ${invoiceId} for user ${userId}`);
|
||||
return invoice;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get invoice ${invoiceId} for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to retrieve invoice");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoices by status
|
||||
*/
|
||||
async getInvoicesByStatus(
|
||||
userId: string,
|
||||
status: InvoiceStatus,
|
||||
options: PaginationOptions = {}
|
||||
): Promise<InvoiceList> {
|
||||
const { page = 1, limit = 10 } = options;
|
||||
|
||||
try {
|
||||
// Validate inputs
|
||||
this.validator.validateUserId(userId);
|
||||
this.validator.validateInvoiceStatus(status);
|
||||
this.validator.validatePagination({ page, limit });
|
||||
|
||||
return await this.getInvoices(userId, { page, limit, status });
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get ${status} invoices for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
options,
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("Failed to retrieve invoices");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unpaid invoices for a user
|
||||
*/
|
||||
async getUnpaidInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.getInvoicesByStatus(userId, "Unpaid", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overdue invoices for a user
|
||||
*/
|
||||
async getOverdueInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.getInvoicesByStatus(userId, "Overdue", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paid invoices for a user
|
||||
*/
|
||||
async getPaidInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.getInvoicesByStatus(userId, "Paid", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cancelled invoices for a user
|
||||
*/
|
||||
async getCancelledInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.getInvoicesByStatus(userId, "Cancelled", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoices in collections for a user
|
||||
*/
|
||||
async getCollectionsInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.getInvoicesByStatus(userId, "Collections", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user mapping with validation
|
||||
*/
|
||||
private async getUserMapping(userId: string): Promise<UserMappingInfo> {
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
this.validator.validateWhmcsClientId(mapping.whmcsClientId);
|
||||
|
||||
return {
|
||||
userId,
|
||||
whmcsClientId: mapping.whmcsClientId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has any invoices
|
||||
*/
|
||||
async hasInvoices(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const invoices = await this.getInvoices(userId, { page: 1, limit: 1 });
|
||||
return invoices.invoices.length > 0;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to check if user ${userId} has invoices`, {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice count by status
|
||||
*/
|
||||
async getInvoiceCountByStatus(userId: string, status: InvoiceStatus): Promise<number> {
|
||||
try {
|
||||
const invoices = await this.getInvoicesByStatus(userId, status, { page: 1, limit: 1 });
|
||||
return invoices.pagination?.totalItems || 0;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get ${status} invoice count for user ${userId}`, {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,206 @@
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import {
|
||||
Invoice,
|
||||
InvoiceList,
|
||||
InvoiceSsoLink,
|
||||
InvoicePaymentLink,
|
||||
PaymentMethodList,
|
||||
PaymentGatewayList
|
||||
} from "@customer-portal/domain";
|
||||
import { InvoiceRetrievalService } from "./invoice-retrieval.service";
|
||||
import { InvoiceHealthService } from "./invoice-health.service";
|
||||
import { InvoiceValidatorService } from "../validators/invoice-validator.service";
|
||||
import type {
|
||||
GetInvoicesOptions,
|
||||
InvoiceStatus,
|
||||
PaginationOptions,
|
||||
InvoiceHealthStatus,
|
||||
InvoiceServiceStats
|
||||
} from "../types/invoice-service.types";
|
||||
|
||||
/**
|
||||
* Main orchestrator service for invoice operations
|
||||
* Coordinates all invoice-related services and provides a unified interface
|
||||
*/
|
||||
@Injectable()
|
||||
export class InvoicesOrchestratorService {
|
||||
constructor(
|
||||
private readonly retrievalService: InvoiceRetrievalService,
|
||||
private readonly healthService: InvoiceHealthService,
|
||||
private readonly validator: InvoiceValidatorService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
// ==========================================
|
||||
// INVOICE RETRIEVAL METHODS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get paginated invoices for a user
|
||||
*/
|
||||
async getInvoices(userId: string, options: GetInvoicesOptions = {}): Promise<InvoiceList> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const result = await this.retrievalService.getInvoices(userId, options);
|
||||
this.healthService.recordInvoiceRetrieval(Date.now() - startTime);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.healthService.recordError();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get individual invoice by ID
|
||||
*/
|
||||
async getInvoiceById(userId: string, invoiceId: number): Promise<Invoice> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const result = await this.retrievalService.getInvoiceById(userId, invoiceId);
|
||||
this.healthService.recordInvoiceRetrieval(Date.now() - startTime);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.healthService.recordError();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoices by status
|
||||
*/
|
||||
async getInvoicesByStatus(
|
||||
userId: string,
|
||||
status: InvoiceStatus,
|
||||
options: PaginationOptions = {}
|
||||
): Promise<InvoiceList> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const result = await this.retrievalService.getInvoicesByStatus(userId, status, options);
|
||||
this.healthService.recordInvoiceRetrieval(Date.now() - startTime);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.healthService.recordError();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unpaid invoices for a user
|
||||
*/
|
||||
async getUnpaidInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.retrievalService.getUnpaidInvoices(userId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overdue invoices for a user
|
||||
*/
|
||||
async getOverdueInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.retrievalService.getOverdueInvoices(userId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paid invoices for a user
|
||||
*/
|
||||
async getPaidInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.retrievalService.getPaidInvoices(userId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cancelled invoices for a user
|
||||
*/
|
||||
async getCancelledInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.retrievalService.getCancelledInvoices(userId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoices in collections for a user
|
||||
*/
|
||||
async getCollectionsInvoices(userId: string, options: PaginationOptions = {}): Promise<InvoiceList> {
|
||||
return this.retrievalService.getCollectionsInvoices(userId, options);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// INVOICE OPERATIONS METHODS
|
||||
// ==========================================
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==========================================
|
||||
// UTILITY METHODS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Check if user has any invoices
|
||||
*/
|
||||
async hasInvoices(userId: string): Promise<boolean> {
|
||||
return this.retrievalService.hasInvoices(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice count by status
|
||||
*/
|
||||
async getInvoiceCountByStatus(userId: string, status: InvoiceStatus): Promise<number> {
|
||||
return this.retrievalService.getInvoiceCountByStatus(userId, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check for invoice service
|
||||
*/
|
||||
async healthCheck(): Promise<InvoiceHealthStatus> {
|
||||
return this.healthService.healthCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service statistics
|
||||
*/
|
||||
getServiceStats(): InvoiceServiceStats {
|
||||
return this.healthService.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset service statistics
|
||||
*/
|
||||
resetServiceStats(): void {
|
||||
this.healthService.resetStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health summary
|
||||
*/
|
||||
async getHealthSummary(): Promise<{
|
||||
status: string;
|
||||
uptime: number;
|
||||
stats: InvoiceServiceStats;
|
||||
lastCheck: string;
|
||||
}> {
|
||||
return this.healthService.getHealthSummary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate get invoices options
|
||||
*/
|
||||
validateGetInvoicesOptions(options: GetInvoicesOptions) {
|
||||
return this.validator.validateGetInvoicesOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid invoice statuses
|
||||
*/
|
||||
getValidStatuses() {
|
||||
return this.validator.getValidStatuses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pagination limits
|
||||
*/
|
||||
getPaginationLimits() {
|
||||
return this.validator.getPaginationLimits();
|
||||
}
|
||||
}
|
||||
41
apps/bff/src/modules/invoices/types/invoice-service.types.ts
Normal file
41
apps/bff/src/modules/invoices/types/invoice-service.types.ts
Normal file
@ -0,0 +1,41 @@
|
||||
export interface GetInvoicesOptions {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";
|
||||
}
|
||||
|
||||
export interface InvoiceValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface InvoiceServiceStats {
|
||||
totalInvoicesRetrieved: number;
|
||||
totalPaymentLinksCreated: number;
|
||||
totalSsoLinksCreated: number;
|
||||
averageResponseTime: number;
|
||||
lastRequestTime?: Date;
|
||||
lastErrorTime?: Date;
|
||||
}
|
||||
|
||||
export interface InvoiceHealthStatus {
|
||||
status: "healthy" | "unhealthy";
|
||||
details: {
|
||||
whmcsApi?: string;
|
||||
mappingsService?: string;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type InvoiceStatus = "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";
|
||||
|
||||
export interface PaginationOptions {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface UserMappingInfo {
|
||||
userId: string;
|
||||
whmcsClientId: number;
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
import type {
|
||||
GetInvoicesOptions,
|
||||
InvoiceValidationResult,
|
||||
InvoiceStatus,
|
||||
PaginationOptions
|
||||
} from "../types/invoice-service.types";
|
||||
|
||||
/**
|
||||
* Service for validating invoice-related inputs and business rules
|
||||
*/
|
||||
@Injectable()
|
||||
export class InvoiceValidatorService {
|
||||
private readonly validStatuses: readonly InvoiceStatus[] = [
|
||||
"Paid", "Unpaid", "Cancelled", "Overdue", "Collections"
|
||||
] as const;
|
||||
|
||||
private readonly maxLimit = 100;
|
||||
private readonly minLimit = 1;
|
||||
|
||||
/**
|
||||
* Validate invoice ID
|
||||
*/
|
||||
validateInvoiceId(invoiceId: number): void {
|
||||
if (!invoiceId || invoiceId < 1) {
|
||||
throw new BadRequestException("Invalid invoice ID");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user ID
|
||||
*/
|
||||
validateUserId(userId: string): void {
|
||||
if (!userId || typeof userId !== "string" || userId.trim().length === 0) {
|
||||
throw new BadRequestException("Invalid user ID");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate pagination parameters
|
||||
*/
|
||||
validatePagination(options: PaginationOptions): void {
|
||||
const { page = 1, limit = 10 } = options;
|
||||
|
||||
if (page < 1) {
|
||||
throw new BadRequestException("Page must be greater than 0");
|
||||
}
|
||||
|
||||
if (limit < this.minLimit || limit > this.maxLimit) {
|
||||
throw new BadRequestException(`Limit must be between ${this.minLimit} and ${this.maxLimit}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate invoice status
|
||||
*/
|
||||
validateInvoiceStatus(status: string): InvoiceStatus {
|
||||
if (!this.validStatuses.includes(status as InvoiceStatus)) {
|
||||
throw new BadRequestException(
|
||||
`Invalid status. Must be one of: ${this.validStatuses.join(", ")}`
|
||||
);
|
||||
}
|
||||
return status as InvoiceStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate get invoices options
|
||||
*/
|
||||
validateGetInvoicesOptions(options: GetInvoicesOptions): InvoiceValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
try {
|
||||
this.validatePagination(options);
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException) {
|
||||
errors.push(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.status) {
|
||||
try {
|
||||
this.validateInvoiceStatus(options.status);
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException) {
|
||||
errors.push(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate WHMCS client ID
|
||||
*/
|
||||
validateWhmcsClientId(clientId: number | undefined): void {
|
||||
if (!clientId || clientId < 1) {
|
||||
throw new BadRequestException("Invalid WHMCS client ID");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate payment gateway name
|
||||
*/
|
||||
validatePaymentGateway(gatewayName: string): void {
|
||||
if (!gatewayName || typeof gatewayName !== "string" || gatewayName.trim().length === 0) {
|
||||
throw new BadRequestException("Invalid payment gateway name");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate return URL for payment links
|
||||
*/
|
||||
validateReturnUrl(returnUrl: string): void {
|
||||
if (!returnUrl || typeof returnUrl !== "string") {
|
||||
throw new BadRequestException("Return URL is required");
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(returnUrl);
|
||||
} catch {
|
||||
throw new BadRequestException("Invalid return URL format");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid invoice statuses
|
||||
*/
|
||||
getValidStatuses(): readonly InvoiceStatus[] {
|
||||
return this.validStatuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pagination limits
|
||||
*/
|
||||
getPaginationLimits(): { min: number; max: number } {
|
||||
return {
|
||||
min: this.minLimit,
|
||||
max: this.maxLimit,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize pagination options with defaults
|
||||
*/
|
||||
sanitizePaginationOptions(options: PaginationOptions): Required<PaginationOptions> {
|
||||
const { page = 1, limit = 10 } = options;
|
||||
|
||||
return {
|
||||
page: Math.max(1, Math.floor(page)),
|
||||
limit: Math.max(this.minLimit, Math.min(this.maxLimit, Math.floor(limit))),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if status is a valid invoice status
|
||||
*/
|
||||
isValidStatus(status: string): status is InvoiceStatus {
|
||||
return this.validStatuses.includes(status as InvoiceStatus);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { AuthLayout } from "@/components/templates/AuthLayout";
|
||||
import { AuthLayout } from "@/components/templates/AuthLayout/AuthLayout";
|
||||
import { Skeleton } from "@/components/atoms/loading-skeleton";
|
||||
|
||||
export default function AuthSegmentLoading() {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./AlertBanner";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./AsyncBlock";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./DetailHeader";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./PaginationBar";
|
||||
@ -1,5 +1,5 @@
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard";
|
||||
|
||||
interface Step {
|
||||
number: number;
|
||||
|
||||
@ -13,10 +13,10 @@ export type { FormFieldProps } from "./FormField/FormField";
|
||||
|
||||
export { SearchFilterBar } from "./SearchFilterBar/SearchFilterBar";
|
||||
export type { SearchFilterBarProps, FilterOption } from "./SearchFilterBar/SearchFilterBar";
|
||||
export * from "./PaginationBar";
|
||||
export * from "./DetailHeader";
|
||||
export * from "./AlertBanner";
|
||||
export * from "./AsyncBlock";
|
||||
export * from "./PaginationBar/PaginationBar";
|
||||
export * from "./DetailHeader/DetailHeader";
|
||||
export * from "./AlertBanner/AlertBanner";
|
||||
export * from "./AsyncBlock/AsyncBlock";
|
||||
export * from "./SectionHeader/SectionHeader";
|
||||
export * from "./ProgressSteps/ProgressSteps";
|
||||
export * from "./SubCard/SubCard";
|
||||
|
||||
@ -2,15 +2,15 @@
|
||||
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { UserIcon, PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import type { ProfileEditFormData } from "@customer-portal/domain";
|
||||
import type { ProfileDisplayData } from "@customer-portal/domain";
|
||||
|
||||
interface PersonalInfoCardProps {
|
||||
data: ProfileEditFormData;
|
||||
data: ProfileDisplayData;
|
||||
isEditing: boolean;
|
||||
isSaving: boolean;
|
||||
onEdit: () => void;
|
||||
onCancel: () => void;
|
||||
onChange: (field: keyof ProfileEditFormData, value: string) => void;
|
||||
onChange: (field: keyof ProfileDisplayData, value: string) => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { MapPinIcon, PencilIcon, CheckIcon, XMarkIcon, UserIcon } from "@heroicons/react/24/outline";
|
||||
import { useAuthStore } from "@/features/auth/services/auth.store";
|
||||
import { accountService } from "@/features/account/services/account.service";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export { LinkWhmcsForm } from "./LinkWhmcsForm";
|
||||
@ -1 +0,0 @@
|
||||
export { LoginForm } from "./LoginForm";
|
||||
@ -1 +0,0 @@
|
||||
export { PasswordResetForm } from "./PasswordResetForm";
|
||||
@ -1 +0,0 @@
|
||||
export { SetPasswordForm } from "./SetPasswordForm";
|
||||
@ -3,9 +3,9 @@
|
||||
* Centralized exports for authentication components
|
||||
*/
|
||||
|
||||
export { LoginForm } from "./LoginForm";
|
||||
export { SignupForm } from "./SignupForm";
|
||||
export { PasswordResetForm } from "./PasswordResetForm";
|
||||
export { SetPasswordForm } from "./SetPasswordForm";
|
||||
export { LinkWhmcsForm } from "./LinkWhmcsForm";
|
||||
export { LoginForm } from "./LoginForm/LoginForm";
|
||||
export { SignupForm } from "./SignupForm/SignupForm";
|
||||
export { PasswordResetForm } from "./PasswordResetForm/PasswordResetForm";
|
||||
export { SetPasswordForm } from "./SetPasswordForm/SetPasswordForm";
|
||||
export { LinkWhmcsForm } from "./LinkWhmcsForm/LinkWhmcsForm";
|
||||
export { AuthLayout } from "@/components/templates/AuthLayout";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./BillingStatusBadge";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./BillingSummary";
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { DetailHeader } from "@/components/molecules/DetailHeader";
|
||||
import { DetailHeader } from "@/components/molecules/DetailHeader/DetailHeader";
|
||||
import { Skeleton } from "@/components/atoms/loading-skeleton";
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { LoadingTable } from "@/components/atoms/loading-skeleton";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
||||
import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar";
|
||||
import { PaginationBar } from "@/components/molecules/PaginationBar";
|
||||
import { PaginationBar } from "@/components/molecules/PaginationBar/PaginationBar";
|
||||
import { InvoiceTable } from "@/features/billing/components/InvoiceTable/InvoiceTable";
|
||||
import { useInvoices } from "@/features/billing/hooks/useBilling";
|
||||
import { useSubscriptionInvoices } from "@/features/subscriptions/hooks/useSubscriptions";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./InvoiceList";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./InvoiceTable";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./PaymentMethodCard";
|
||||
@ -18,7 +18,7 @@ import {
|
||||
InvoiceItems,
|
||||
InvoiceTotals,
|
||||
} from "@/features/billing/components/InvoiceDetail";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { InvoicePaymentActions } from "@/features/billing/components/InvoiceDetail/InvoicePaymentActions";
|
||||
|
||||
export function InvoiceDetailContainer() {
|
||||
|
||||
@ -15,7 +15,7 @@ import { CreditCardIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { InlineToast } from "@/components/atoms/inline-toast";
|
||||
import { SectionHeader } from "@/components/molecules";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
||||
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
|
||||
import { logger } from "@customer-portal/logging";
|
||||
import { EmptyState } from "@/components/atoms/empty-state";
|
||||
|
||||
@ -5,7 +5,7 @@ import type { CatalogProductBase } from "@customer-portal/domain";
|
||||
import { getMonthlyPrice, getOneTimePrice } from "../../utils/pricing";
|
||||
|
||||
interface AddonGroupProps {
|
||||
addons: Array<CatalogProductBase & { bundledAddonId?: string; isBundledAddon?: boolean; raw?: any }>;
|
||||
addons: Array<CatalogProductBase & { bundledAddonId?: string; isBundledAddon?: boolean; raw?: unknown }>;
|
||||
selectedAddonSkus: string[];
|
||||
onAddonToggle: (skus: string[]) => void;
|
||||
showSkus?: boolean;
|
||||
@ -23,7 +23,7 @@ type BundledAddonGroup = {
|
||||
};
|
||||
|
||||
function buildGroupedAddons(
|
||||
addons: Array<CatalogProductBase & { bundledAddonId?: string; isBundledAddon?: boolean; raw?: any }>
|
||||
addons: Array<CatalogProductBase & { bundledAddonId?: string; isBundledAddon?: boolean; raw?: unknown }>
|
||||
): BundledAddonGroup[] {
|
||||
const groups: BundledAddonGroup[] = [];
|
||||
const processedSkus = new Set<string>();
|
||||
@ -34,7 +34,12 @@ function buildGroupedAddons(
|
||||
if (processedSkus.has(addon.sku)) return;
|
||||
|
||||
if (addon.isBundledAddon && addon.bundledAddonId) {
|
||||
const partner = sorted.find(candidate => candidate.raw.Id === addon.bundledAddonId);
|
||||
const partner = sorted.find(candidate =>
|
||||
candidate.raw &&
|
||||
typeof candidate.raw === 'object' &&
|
||||
'Id' in candidate.raw &&
|
||||
candidate.raw.Id === addon.bundledAddonId
|
||||
);
|
||||
|
||||
if (partner) {
|
||||
const monthlyAddon = addon.billingCycle === "Monthly" ? addon : partner;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Skeleton } from "@/components/atoms";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
|
||||
export interface StepValidation {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Skeleton } from "@/components/atoms/loading-skeleton";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { CreditCardIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
|
||||
import type { PaymentMethod } from "@customer-portal/domain";
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { CheckIcon, XMarkIcon, CurrencyYenIcon } from "@heroicons/react/24/outline";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
|
||||
export interface ComparisonProduct {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard";
|
||||
|
||||
export function FeatureCard({
|
||||
icon,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./ServiceHeroCard";
|
||||
export * from "./FeatureCard";
|
||||
@ -3,7 +3,7 @@
|
||||
import { AnimatedCard } from "@/components/molecules";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { StepHeader } from "@/components/atoms";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||
import type { InternetPlanCatalogItem } from "@customer-portal/domain";
|
||||
import type { AccessMode } from "../../../hooks/useConfigureParams";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./InternetPlanCard";
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { DevicePhoneMobileIcon, UsersIcon, CurrencyYenIcon } from "@heroicons/react/24/outline";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard";
|
||||
import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import type { SimCatalogProduct } from "@customer-portal/domain";
|
||||
import { getMonthlyPrice } from "../../utils/pricing";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./VpnPlanCard";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./catalog.utils";
|
||||
@ -21,10 +21,10 @@ import type {
|
||||
import { getMonthlyPrice } from "../utils/pricing";
|
||||
import { LoadingCard, Skeleton, LoadingTable } from "@/components/atoms/loading-skeleton";
|
||||
import { AnimatedCard } from "@/components/molecules";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { InternetPlanCard } from "@/features/catalog/components/internet/InternetPlanCard";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
|
||||
export function InternetPlansContainer() {
|
||||
const { data, isLoading, error } = useInternetCatalog();
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { useSimCatalog } from "@/features/catalog/hooks";
|
||||
import type { SimCatalogProduct } from "@customer-portal/domain";
|
||||
import { SimPlanTypeSection } from "@/features/catalog/components/sim/SimPlanTypeSection";
|
||||
|
||||
@ -4,9 +4,9 @@ import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { ShieldCheckIcon, ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { useVpnCatalog } from "@/features/catalog/hooks";
|
||||
import { LoadingCard } from "@/components/atoms";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { VpnPlanCard } from "@/features/catalog/components/vpn/VpnPlanCard";
|
||||
|
||||
export function VpnPlansView() {
|
||||
|
||||
@ -3,8 +3,8 @@ import { useCheckout } from "@/features/checkout/hooks/useCheckout";
|
||||
import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { PageAsync } from "@/components/molecules/AsyncBlock";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { PageAsync } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
||||
import { InlineToast } from "@/components/atoms/inline-toast";
|
||||
import { StatusPill } from "@/components/atoms/status-pill";
|
||||
import { AddressConfirmation } from "@/features/catalog/components/base/AddressConfirmation";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
|
||||
interface PaymentErrorBannerProps {
|
||||
message: string;
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
export { OrderCard } from "./OrderCard";
|
||||
export { OrderCardSkeleton } from "./OrderCardSkeleton";
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { ClipboardDocumentListIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { AnimatedCard } from "@/components/molecules";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { ordersService } from "@/features/orders/services/orders.service";
|
||||
import { OrderCard } from "@/features/orders/components/OrderCard";
|
||||
import { OrderCardSkeleton } from "@/features/orders/components/OrderCardSkeleton";
|
||||
|
||||
@ -7,7 +7,7 @@ import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { DevicePhoneMobileIcon } from "@heroicons/react/24/outline";
|
||||
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
|
||||
const PLAN_CODES = ["PASI_5G", "PASI_10G", "PASI_25G", "PASI_50G"] as const;
|
||||
type PlanCode = (typeof PLAN_CODES)[number];
|
||||
|
||||
@ -6,7 +6,7 @@ import { useParams } from "next/navigation";
|
||||
import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { DevicePhoneMobileIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export function SimTopUpContainer() {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { DetailHeader } from "@/components/molecules/DetailHeader";
|
||||
import { DetailHeader } from "@/components/molecules/DetailHeader/DetailHeader";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
@ -12,7 +12,7 @@ import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar";
|
||||
import { LoadingTable } from "@/components/atoms/loading-skeleton";
|
||||
import { ErrorState } from "@/components/atoms/error-state";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock";
|
||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
||||
import {
|
||||
ServerIcon,
|
||||
CheckCircleIcon,
|
||||
|
||||
@ -17,6 +17,11 @@ export const profileEditFormSchema = updateProfileRequestSchema.extend({
|
||||
lastName: nameSchema,
|
||||
});
|
||||
|
||||
// Profile display schema includes non-editable fields like email
|
||||
export const profileDisplaySchema = profileEditFormSchema.extend({
|
||||
email: emailSchema, // Read-only field for display
|
||||
});
|
||||
|
||||
// Use required address schema for forms where address is mandatory
|
||||
export const addressFormSchema = requiredAddressSchema;
|
||||
|
||||
@ -65,8 +70,12 @@ import type {
|
||||
UpdateAddressRequest as UpdateAddressRequestData,
|
||||
} from "../api/requests";
|
||||
|
||||
// Import email schema for display type
|
||||
import { emailSchema } from "../shared/primitives";
|
||||
|
||||
// Export form types and API request types
|
||||
export type ProfileEditFormData = z.infer<typeof profileEditFormSchema>;
|
||||
export type ProfileDisplayData = z.infer<typeof profileDisplaySchema>;
|
||||
export type AddressFormData = z.infer<typeof addressFormSchema>;
|
||||
export type ContactFormData = z.infer<typeof contactFormSchema>;
|
||||
|
||||
|
||||
@ -16,6 +16,9 @@
|
||||
// Shared validation modules (modular architecture)
|
||||
export * from "./shared";
|
||||
|
||||
// Export specific billing cycle types for convenience
|
||||
export type { SubscriptionBillingCycleSchema as SubscriptionBillingCycle } from "./shared/primitives";
|
||||
|
||||
// API request schemas (backend) - explicit exports for better tree shaking
|
||||
export {
|
||||
// Auth API schemas
|
||||
@ -109,11 +112,13 @@ export {
|
||||
export {
|
||||
// Profile form schemas
|
||||
profileEditFormSchema,
|
||||
profileDisplaySchema,
|
||||
addressFormSchema,
|
||||
contactFormSchema,
|
||||
|
||||
// Profile form types
|
||||
type ProfileEditFormData,
|
||||
type ProfileDisplayData,
|
||||
type AddressFormData,
|
||||
type ContactFormData,
|
||||
|
||||
@ -152,5 +157,19 @@ export {
|
||||
type PaymentMethodValidation,
|
||||
} from "./business";
|
||||
|
||||
// Order validation schemas and types
|
||||
export {
|
||||
orderItemProductSchema,
|
||||
orderDetailItemSchema,
|
||||
orderSummaryItemSchema,
|
||||
orderDetailsSchema,
|
||||
orderSummarySchema,
|
||||
type OrderItemProduct,
|
||||
type OrderDetailItem,
|
||||
type OrderItemSummary,
|
||||
type OrderDetailsResponse,
|
||||
type OrderSummaryResponse,
|
||||
} from "./shared/order";
|
||||
|
||||
// Simple validation utilities (direct Zod usage)
|
||||
export { z, parseOrThrow, safeParse } from "./shared/utilities";
|
||||
|
||||
@ -96,6 +96,10 @@ export const statusEnum = z.enum(["active", "inactive", "pending", "suspended"])
|
||||
export const priorityEnum = z.enum(["low", "medium", "high", "urgent"]);
|
||||
export const categoryEnum = z.enum(["technical", "billing", "account", "general"]);
|
||||
|
||||
// Billing cycle enums
|
||||
export const billingCycleEnum = z.enum(["Monthly", "Quarterly", "Annually", "Onetime", "Free"]);
|
||||
export const subscriptionBillingCycleEnum = z.enum(["Monthly", "Quarterly", "Annually", "Biennially", "Triennially"]);
|
||||
|
||||
// =====================================================
|
||||
// TYPE EXPORTS
|
||||
// =====================================================
|
||||
@ -115,3 +119,5 @@ export type GenderSchema = z.infer<typeof genderEnum>;
|
||||
export type StatusSchema = z.infer<typeof statusEnum>;
|
||||
export type PrioritySchema = z.infer<typeof priorityEnum>;
|
||||
export type CategorySchema = z.infer<typeof categoryEnum>;
|
||||
export type BillingCycleSchema = z.infer<typeof billingCycleEnum>;
|
||||
export type SubscriptionBillingCycleSchema = z.infer<typeof subscriptionBillingCycleEnum>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user