diff --git a/apps/bff/src/integrations/freebit/freebit.service.ts b/apps/bff/src/integrations/freebit/freebit.service.ts index cb051fa5..e8bdc53d 100644 --- a/apps/bff/src/integrations/freebit/freebit.service.ts +++ b/apps/bff/src/integrations/freebit/freebit.service.ts @@ -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 { // For eSIM, use the enhanced reissue method if (params.simType === "eSIM") { diff --git a/apps/bff/src/integrations/freebit/services/index.ts b/apps/bff/src/integrations/freebit/services/index.ts index f2ee4f61..4e6098c1 100644 --- a/apps/bff/src/integrations/freebit/services/index.ts +++ b/apps/bff/src/integrations/freebit/services/index.ts @@ -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'; diff --git a/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts b/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts index 2989ec78..091ea31f 100644 --- a/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts @@ -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 */ diff --git a/apps/bff/src/integrations/whmcs/connection/index.ts b/apps/bff/src/integrations/whmcs/connection/index.ts deleted file mode 100644 index 170152d8..00000000 --- a/apps/bff/src/integrations/whmcs/connection/index.ts +++ /dev/null @@ -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"; diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts index 56864b99..537f556a 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts @@ -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 { - return this.makeRequest("CreateInvoice", params); - } - - async updateInvoice(params: WhmcsUpdateInvoiceParams): Promise { - 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 { - return this.makeRequest("AddPayMethod", params); - } async getPaymentGateways(): Promise { return this.makeRequest("GetPaymentMethods", {}); } - async capturePayment(params: WhmcsCapturePaymentParams): Promise { - return this.makeRequest("CapturePayment", params); - } - - async addCredit(params: WhmcsAddCreditParams): Promise { - return this.makeRequest("AddCredit", params); - } - - async addInvoicePayment( - params: WhmcsAddInvoicePaymentParams - ): Promise { - 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", {}); } // ========================================== diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts index 3b334b04..574f7f99 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts @@ -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) { 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 diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts index 181fb962..29062553 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts @@ -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, diff --git a/apps/bff/src/integrations/whmcs/transformers/index.ts b/apps/bff/src/integrations/whmcs/transformers/index.ts deleted file mode 100644 index b1afc30b..00000000 --- a/apps/bff/src/integrations/whmcs/transformers/index.ts +++ /dev/null @@ -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"; diff --git a/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts b/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts index 30bc52b5..3e0dd596 100644 --- a/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts +++ b/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts @@ -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), + whmcsData: DataUtils.sanitizeForLog(whmcsInvoice as unknown as Record), }); 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), + itemsData: DataUtils.sanitizeForLog(items as unknown as Record), }); return []; } @@ -109,9 +109,11 @@ export class InvoiceTransformerService { private transformSingleInvoiceItem(item: Record): 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) { diff --git a/apps/bff/src/integrations/whmcs/transformers/services/payment-transformer.service.ts b/apps/bff/src/integrations/whmcs/transformers/services/payment-transformer.service.ts index 31f941fa..128cf236 100644 --- a/apps/bff/src/integrations/whmcs/transformers/services/payment-transformer.service.ts +++ b/apps/bff/src/integrations/whmcs/transformers/services/payment-transformer.service.ts @@ -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"; + } } diff --git a/apps/bff/src/integrations/whmcs/types/whmcs-api.types.ts b/apps/bff/src/integrations/whmcs/types/whmcs-api.types.ts index 29eaf608..8ba3372c 100644 --- a/apps/bff/src/integrations/whmcs/types/whmcs-api.types.ts +++ b/apps/bff/src/integrations/whmcs/types/whmcs-api.types.ts @@ -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; -} diff --git a/apps/bff/src/integrations/whmcs/whmcs.service.ts b/apps/bff/src/integrations/whmcs/whmcs.service.ts index b106e93d..19345ce7 100644 --- a/apps/bff/src/integrations/whmcs/whmcs.service.ts +++ b/apps/bff/src/integrations/whmcs/whmcs.service.ts @@ -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) diff --git a/apps/bff/src/modules/invoices/index.ts b/apps/bff/src/modules/invoices/index.ts new file mode 100644 index 00000000..f2b251a5 --- /dev/null +++ b/apps/bff/src/modules/invoices/index.ts @@ -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"; diff --git a/apps/bff/src/modules/invoices/invoices.module.ts b/apps/bff/src/modules/invoices/invoices.module.ts index ce7e2c96..43bf6555 100644 --- a/apps/bff/src/modules/invoices/invoices.module.ts +++ b/apps/bff/src/modules/invoices/invoices.module.ts @@ -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 {} diff --git a/apps/bff/src/modules/invoices/invoices.service.ts b/apps/bff/src/modules/invoices/invoices.service.ts index ee65afb7..4af0f296 100644 --- a/apps/bff/src/modules/invoices/invoices.service.ts +++ b/apps/bff/src/modules/invoices/invoices.service.ts @@ -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 { - 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 { - 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 { - 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 { + 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 = {} ): Promise { - 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 = {} ): Promise { - return this.getInvoicesByStatus(userId, "Unpaid", options); + return this.orchestrator.getUnpaidInvoices(userId, options); } /** @@ -252,325 +92,124 @@ export class InvoicesService { userId: string, options: Pick = {} ): Promise { - 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 = {} + ): Promise { + 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 { - 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 = {} + ): Promise { + 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 { - try { - if (invoiceId) { - await this.whmcsService.invalidateInvoiceCache(userId, invoiceId); - } else { - await this.whmcsService.invalidateUserCache(userId); - } + async getCollectionsInvoices( + userId: string, + options: Pick = {} + ): Promise { + 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 { + 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 { - 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 { - 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 { - 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 { - 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 { + return this.whmcsService.invalidatePaymentMethodsCache(userId); } /** * Health check for invoice service */ async healthCheck(): Promise<{ status: string; details: unknown }> { - try { - const whmcsHealthy = await this.whmcsService.healthCheck(); - - 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(), - }, - }; - } + const health = await this.orchestrator.healthCheck(); + return { + status: health.status, + details: health.details, + }; } -} + + /** + * Get service statistics + */ + getServiceStats() { + return this.orchestrator.getServiceStats(); + } + + /** + * Check if user has any invoices + */ + async hasInvoices(userId: string): Promise { + return this.orchestrator.hasInvoices(userId); + } + + /** + * Get invoice count by status + */ + async getInvoiceCountByStatus( + userId: string, + status: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections" + ): Promise { + return this.orchestrator.getInvoiceCountByStatus(userId, status); + } +} \ No newline at end of file diff --git a/apps/bff/src/modules/invoices/services/invoice-health.service.ts b/apps/bff/src/modules/invoices/services/invoice-health.service.ts new file mode 100644 index 00000000..4843d619 --- /dev/null +++ b/apps/bff/src/modules/invoices/services/invoice-health.service.ts @@ -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 { + 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 { + 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 { + 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(), + }; + } +} diff --git a/apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts b/apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts new file mode 100644 index 00000000..cae6dc67 --- /dev/null +++ b/apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts @@ -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 { + 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 { + 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 { + 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 { + return this.getInvoicesByStatus(userId, "Unpaid", options); + } + + /** + * Get overdue invoices for a user + */ + async getOverdueInvoices(userId: string, options: PaginationOptions = {}): Promise { + return this.getInvoicesByStatus(userId, "Overdue", options); + } + + /** + * Get paid invoices for a user + */ + async getPaidInvoices(userId: string, options: PaginationOptions = {}): Promise { + return this.getInvoicesByStatus(userId, "Paid", options); + } + + /** + * Get cancelled invoices for a user + */ + async getCancelledInvoices(userId: string, options: PaginationOptions = {}): Promise { + return this.getInvoicesByStatus(userId, "Cancelled", options); + } + + /** + * Get invoices in collections for a user + */ + async getCollectionsInvoices(userId: string, options: PaginationOptions = {}): Promise { + return this.getInvoicesByStatus(userId, "Collections", options); + } + + /** + * Get user mapping with validation + */ + private async getUserMapping(userId: string): Promise { + 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 { + 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 { + 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; + } + } +} diff --git a/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts b/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts new file mode 100644 index 00000000..a616eeb3 --- /dev/null +++ b/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts @@ -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 { + 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 { + 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 { + 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 { + return this.retrievalService.getUnpaidInvoices(userId, options); + } + + /** + * Get overdue invoices for a user + */ + async getOverdueInvoices(userId: string, options: PaginationOptions = {}): Promise { + return this.retrievalService.getOverdueInvoices(userId, options); + } + + /** + * Get paid invoices for a user + */ + async getPaidInvoices(userId: string, options: PaginationOptions = {}): Promise { + return this.retrievalService.getPaidInvoices(userId, options); + } + + /** + * Get cancelled invoices for a user + */ + async getCancelledInvoices(userId: string, options: PaginationOptions = {}): Promise { + return this.retrievalService.getCancelledInvoices(userId, options); + } + + /** + * Get invoices in collections for a user + */ + async getCollectionsInvoices(userId: string, options: PaginationOptions = {}): Promise { + return this.retrievalService.getCollectionsInvoices(userId, options); + } + + // ========================================== + // INVOICE OPERATIONS METHODS + // ========================================== + + + + + + + // ========================================== + // UTILITY METHODS + // ========================================== + + /** + * Check if user has any invoices + */ + async hasInvoices(userId: string): Promise { + return this.retrievalService.hasInvoices(userId); + } + + /** + * Get invoice count by status + */ + async getInvoiceCountByStatus(userId: string, status: InvoiceStatus): Promise { + return this.retrievalService.getInvoiceCountByStatus(userId, status); + } + + /** + * Health check for invoice service + */ + async healthCheck(): Promise { + 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(); + } +} diff --git a/apps/bff/src/modules/invoices/types/invoice-service.types.ts b/apps/bff/src/modules/invoices/types/invoice-service.types.ts new file mode 100644 index 00000000..b109f988 --- /dev/null +++ b/apps/bff/src/modules/invoices/types/invoice-service.types.ts @@ -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; +} diff --git a/apps/bff/src/modules/invoices/validators/invoice-validator.service.ts b/apps/bff/src/modules/invoices/validators/invoice-validator.service.ts new file mode 100644 index 00000000..a520d646 --- /dev/null +++ b/apps/bff/src/modules/invoices/validators/invoice-validator.service.ts @@ -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 { + 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); + } +} diff --git a/apps/portal/src/app/(public)/auth/loading.tsx b/apps/portal/src/app/(public)/auth/loading.tsx index bcf7152f..4fffdb60 100644 --- a/apps/portal/src/app/(public)/auth/loading.tsx +++ b/apps/portal/src/app/(public)/auth/loading.tsx @@ -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() { diff --git a/apps/portal/src/components/molecules/AlertBanner/index.ts b/apps/portal/src/components/molecules/AlertBanner/index.ts deleted file mode 100644 index 4a7b45cb..00000000 --- a/apps/portal/src/components/molecules/AlertBanner/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./AlertBanner"; diff --git a/apps/portal/src/components/molecules/AsyncBlock/index.ts b/apps/portal/src/components/molecules/AsyncBlock/index.ts deleted file mode 100644 index fbdba92e..00000000 --- a/apps/portal/src/components/molecules/AsyncBlock/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./AsyncBlock"; diff --git a/apps/portal/src/components/molecules/DetailHeader/index.ts b/apps/portal/src/components/molecules/DetailHeader/index.ts deleted file mode 100644 index 0226dfb7..00000000 --- a/apps/portal/src/components/molecules/DetailHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./DetailHeader"; diff --git a/apps/portal/src/components/molecules/PaginationBar/index.ts b/apps/portal/src/components/molecules/PaginationBar/index.ts deleted file mode 100644 index 65ca19cb..00000000 --- a/apps/portal/src/components/molecules/PaginationBar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./PaginationBar"; diff --git a/apps/portal/src/components/molecules/ProgressSteps/ProgressSteps.tsx b/apps/portal/src/components/molecules/ProgressSteps/ProgressSteps.tsx index 4311ce18..8c08d7d1 100644 --- a/apps/portal/src/components/molecules/ProgressSteps/ProgressSteps.tsx +++ b/apps/portal/src/components/molecules/ProgressSteps/ProgressSteps.tsx @@ -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; diff --git a/apps/portal/src/components/molecules/index.ts b/apps/portal/src/components/molecules/index.ts index 9f28c0c4..d6166f33 100644 --- a/apps/portal/src/components/molecules/index.ts +++ b/apps/portal/src/components/molecules/index.ts @@ -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"; diff --git a/apps/portal/src/features/account/components/PersonalInfoCard.tsx b/apps/portal/src/features/account/components/PersonalInfoCard.tsx index 5691b660..2561e5d2 100644 --- a/apps/portal/src/features/account/components/PersonalInfoCard.tsx +++ b/apps/portal/src/features/account/components/PersonalInfoCard.tsx @@ -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; } diff --git a/apps/portal/src/features/account/views/ProfileContainer.tsx b/apps/portal/src/features/account/views/ProfileContainer.tsx index 455500f2..6d8ca5da 100644 --- a/apps/portal/src/features/account/views/ProfileContainer.tsx +++ b/apps/portal/src/features/account/views/ProfileContainer.tsx @@ -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"; diff --git a/apps/portal/src/features/auth/components/LinkWhmcsForm/index.ts b/apps/portal/src/features/auth/components/LinkWhmcsForm/index.ts deleted file mode 100644 index 65d52ecc..00000000 --- a/apps/portal/src/features/auth/components/LinkWhmcsForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LinkWhmcsForm } from "./LinkWhmcsForm"; diff --git a/apps/portal/src/features/auth/components/LoginForm/index.ts b/apps/portal/src/features/auth/components/LoginForm/index.ts deleted file mode 100644 index 7262daaa..00000000 --- a/apps/portal/src/features/auth/components/LoginForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoginForm } from "./LoginForm"; diff --git a/apps/portal/src/features/auth/components/PasswordResetForm/index.ts b/apps/portal/src/features/auth/components/PasswordResetForm/index.ts deleted file mode 100644 index ea0848bd..00000000 --- a/apps/portal/src/features/auth/components/PasswordResetForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PasswordResetForm } from "./PasswordResetForm"; diff --git a/apps/portal/src/features/auth/components/SetPasswordForm/index.ts b/apps/portal/src/features/auth/components/SetPasswordForm/index.ts deleted file mode 100644 index e9e92ae1..00000000 --- a/apps/portal/src/features/auth/components/SetPasswordForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SetPasswordForm } from "./SetPasswordForm"; diff --git a/apps/portal/src/features/auth/components/index.ts b/apps/portal/src/features/auth/components/index.ts index 96f4b9ac..5742e6ad 100644 --- a/apps/portal/src/features/auth/components/index.ts +++ b/apps/portal/src/features/auth/components/index.ts @@ -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"; diff --git a/apps/portal/src/features/billing/components/BillingStatusBadge/index.ts b/apps/portal/src/features/billing/components/BillingStatusBadge/index.ts deleted file mode 100644 index 921684f1..00000000 --- a/apps/portal/src/features/billing/components/BillingStatusBadge/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./BillingStatusBadge"; diff --git a/apps/portal/src/features/billing/components/BillingSummary/index.ts b/apps/portal/src/features/billing/components/BillingSummary/index.ts deleted file mode 100644 index 5db6ee3e..00000000 --- a/apps/portal/src/features/billing/components/BillingSummary/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./BillingSummary"; diff --git a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceHeader.tsx b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceHeader.tsx index 5af0e839..98070144 100644 --- a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceHeader.tsx +++ b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceHeader.tsx @@ -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, diff --git a/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx b/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx index c43e343d..98a14856 100644 --- a/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx +++ b/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx @@ -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"; diff --git a/apps/portal/src/features/billing/components/InvoiceList/index.ts b/apps/portal/src/features/billing/components/InvoiceList/index.ts deleted file mode 100644 index 62e7de19..00000000 --- a/apps/portal/src/features/billing/components/InvoiceList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./InvoiceList"; diff --git a/apps/portal/src/features/billing/components/InvoiceTable/index.ts b/apps/portal/src/features/billing/components/InvoiceTable/index.ts deleted file mode 100644 index 65aec23d..00000000 --- a/apps/portal/src/features/billing/components/InvoiceTable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./InvoiceTable"; diff --git a/apps/portal/src/features/billing/components/PaymentMethodCard/index.ts b/apps/portal/src/features/billing/components/PaymentMethodCard/index.ts deleted file mode 100644 index f5e765a4..00000000 --- a/apps/portal/src/features/billing/components/PaymentMethodCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./PaymentMethodCard"; diff --git a/apps/portal/src/features/billing/views/InvoiceDetail.tsx b/apps/portal/src/features/billing/views/InvoiceDetail.tsx index b9cb4d00..058efe63 100644 --- a/apps/portal/src/features/billing/views/InvoiceDetail.tsx +++ b/apps/portal/src/features/billing/views/InvoiceDetail.tsx @@ -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() { diff --git a/apps/portal/src/features/billing/views/PaymentMethods.tsx b/apps/portal/src/features/billing/views/PaymentMethods.tsx index 30e6b4b1..ac6b4232 100644 --- a/apps/portal/src/features/billing/views/PaymentMethods.tsx +++ b/apps/portal/src/features/billing/views/PaymentMethods.tsx @@ -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"; diff --git a/apps/portal/src/features/catalog/components/base/AddonGroup.tsx b/apps/portal/src/features/catalog/components/base/AddonGroup.tsx index ee93e522..ea0f366f 100644 --- a/apps/portal/src/features/catalog/components/base/AddonGroup.tsx +++ b/apps/portal/src/features/catalog/components/base/AddonGroup.tsx @@ -5,7 +5,7 @@ import type { CatalogProductBase } from "@customer-portal/domain"; import { getMonthlyPrice, getOneTimePrice } from "../../utils/pricing"; interface AddonGroupProps { - addons: Array; + addons: Array; selectedAddonSkus: string[]; onAddonToggle: (skus: string[]) => void; showSkus?: boolean; @@ -23,7 +23,7 @@ type BundledAddonGroup = { }; function buildGroupedAddons( - addons: Array + addons: Array ): BundledAddonGroup[] { const groups: BundledAddonGroup[] = []; const processedSkus = new Set(); @@ -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; diff --git a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx index a8fdfbdc..d8397848 100644 --- a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx +++ b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx @@ -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"; diff --git a/apps/portal/src/features/catalog/components/base/ConfigurationStep.tsx b/apps/portal/src/features/catalog/components/base/ConfigurationStep.tsx index 8c1c5907..7548c266 100644 --- a/apps/portal/src/features/catalog/components/base/ConfigurationStep.tsx +++ b/apps/portal/src/features/catalog/components/base/ConfigurationStep.tsx @@ -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 { diff --git a/apps/portal/src/features/catalog/components/base/PaymentForm.tsx b/apps/portal/src/features/catalog/components/base/PaymentForm.tsx index 0d15fb64..1e71db90 100644 --- a/apps/portal/src/features/catalog/components/base/PaymentForm.tsx +++ b/apps/portal/src/features/catalog/components/base/PaymentForm.tsx @@ -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"; diff --git a/apps/portal/src/features/catalog/components/base/ProductComparison.tsx b/apps/portal/src/features/catalog/components/base/ProductComparison.tsx index f74ff076..3c030a35 100644 --- a/apps/portal/src/features/catalog/components/base/ProductComparison.tsx +++ b/apps/portal/src/features/catalog/components/base/ProductComparison.tsx @@ -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 { diff --git a/apps/portal/src/features/catalog/components/common/FeatureCard.tsx b/apps/portal/src/features/catalog/components/common/FeatureCard.tsx index 6ad38e04..98bcbae5 100644 --- a/apps/portal/src/features/catalog/components/common/FeatureCard.tsx +++ b/apps/portal/src/features/catalog/components/common/FeatureCard.tsx @@ -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, diff --git a/apps/portal/src/features/catalog/components/common/ServiceHeroCard.tsx b/apps/portal/src/features/catalog/components/common/ServiceHeroCard.tsx index 556d5f5d..0574e4f3 100644 --- a/apps/portal/src/features/catalog/components/common/ServiceHeroCard.tsx +++ b/apps/portal/src/features/catalog/components/common/ServiceHeroCard.tsx @@ -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"; diff --git a/apps/portal/src/features/catalog/components/common/index.ts b/apps/portal/src/features/catalog/components/common/index.ts deleted file mode 100644 index 1cb6f628..00000000 --- a/apps/portal/src/features/catalog/components/common/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ServiceHeroCard"; -export * from "./FeatureCard"; diff --git a/apps/portal/src/features/catalog/components/internet/configure/steps/ServiceConfigurationStep.tsx b/apps/portal/src/features/catalog/components/internet/configure/steps/ServiceConfigurationStep.tsx index a8d31207..8901692c 100644 --- a/apps/portal/src/features/catalog/components/internet/configure/steps/ServiceConfigurationStep.tsx +++ b/apps/portal/src/features/catalog/components/internet/configure/steps/ServiceConfigurationStep.tsx @@ -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"; diff --git a/apps/portal/src/features/catalog/components/internet/index.ts b/apps/portal/src/features/catalog/components/internet/index.ts deleted file mode 100644 index f7361c9c..00000000 --- a/apps/portal/src/features/catalog/components/internet/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./InternetPlanCard"; diff --git a/apps/portal/src/features/catalog/components/sim/SimPlanCard.tsx b/apps/portal/src/features/catalog/components/sim/SimPlanCard.tsx index 759170c8..f0d6f0c7 100644 --- a/apps/portal/src/features/catalog/components/sim/SimPlanCard.tsx +++ b/apps/portal/src/features/catalog/components/sim/SimPlanCard.tsx @@ -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"; diff --git a/apps/portal/src/features/catalog/components/vpn/index.ts b/apps/portal/src/features/catalog/components/vpn/index.ts deleted file mode 100644 index 6fa68cf8..00000000 --- a/apps/portal/src/features/catalog/components/vpn/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./VpnPlanCard"; diff --git a/apps/portal/src/features/catalog/utils/index.ts b/apps/portal/src/features/catalog/utils/index.ts deleted file mode 100644 index 543bc8f8..00000000 --- a/apps/portal/src/features/catalog/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./catalog.utils"; diff --git a/apps/portal/src/features/catalog/views/InternetPlans.tsx b/apps/portal/src/features/catalog/views/InternetPlans.tsx index 7361919f..6bb6b809 100644 --- a/apps/portal/src/features/catalog/views/InternetPlans.tsx +++ b/apps/portal/src/features/catalog/views/InternetPlans.tsx @@ -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(); diff --git a/apps/portal/src/features/catalog/views/SimPlans.tsx b/apps/portal/src/features/catalog/views/SimPlans.tsx index c60f5c76..ec8090d8 100644 --- a/apps/portal/src/features/catalog/views/SimPlans.tsx +++ b/apps/portal/src/features/catalog/views/SimPlans.tsx @@ -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"; diff --git a/apps/portal/src/features/catalog/views/VpnPlans.tsx b/apps/portal/src/features/catalog/views/VpnPlans.tsx index d265e247..4e6402c4 100644 --- a/apps/portal/src/features/catalog/views/VpnPlans.tsx +++ b/apps/portal/src/features/catalog/views/VpnPlans.tsx @@ -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() { diff --git a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx b/apps/portal/src/features/checkout/views/CheckoutContainer.tsx index a56981d3..d1adb1c3 100644 --- a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx +++ b/apps/portal/src/features/checkout/views/CheckoutContainer.tsx @@ -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"; diff --git a/apps/portal/src/features/dashboard/components/PaymentErrorBanner.tsx b/apps/portal/src/features/dashboard/components/PaymentErrorBanner.tsx index a139537d..c6ca8cce 100644 --- a/apps/portal/src/features/dashboard/components/PaymentErrorBanner.tsx +++ b/apps/portal/src/features/dashboard/components/PaymentErrorBanner.tsx @@ -1,6 +1,6 @@ "use client"; -import { AlertBanner } from "@/components/molecules/AlertBanner"; +import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; interface PaymentErrorBannerProps { message: string; diff --git a/apps/portal/src/features/orders/components/index.ts b/apps/portal/src/features/orders/components/index.ts deleted file mode 100644 index 5f0f10d9..00000000 --- a/apps/portal/src/features/orders/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { OrderCard } from "./OrderCard"; -export { OrderCardSkeleton } from "./OrderCardSkeleton"; - - diff --git a/apps/portal/src/features/orders/views/OrdersList.tsx b/apps/portal/src/features/orders/views/OrdersList.tsx index 593c4535..ffac8583 100644 --- a/apps/portal/src/features/orders/views/OrdersList.tsx +++ b/apps/portal/src/features/orders/views/OrdersList.tsx @@ -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"; diff --git a/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx b/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx index 31ae0db1..9fefaf0c 100644 --- a/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx +++ b/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx @@ -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]; diff --git a/apps/portal/src/features/subscriptions/views/SimTopUp.tsx b/apps/portal/src/features/subscriptions/views/SimTopUp.tsx index 7e23cd2d..8aeafd6c 100644 --- a/apps/portal/src/features/subscriptions/views/SimTopUp.tsx +++ b/apps/portal/src/features/subscriptions/views/SimTopUp.tsx @@ -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() { diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx index 46a9f006..f92dc9a1 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx @@ -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"; diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx index ea52a0aa..2cf044a1 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx @@ -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, diff --git a/packages/domain/src/validation/forms/profile.ts b/packages/domain/src/validation/forms/profile.ts index 66942971..62be5f98 100644 --- a/packages/domain/src/validation/forms/profile.ts +++ b/packages/domain/src/validation/forms/profile.ts @@ -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; +export type ProfileDisplayData = z.infer; export type AddressFormData = z.infer; export type ContactFormData = z.infer; diff --git a/packages/domain/src/validation/index.ts b/packages/domain/src/validation/index.ts index 80bf1084..22e483f0 100644 --- a/packages/domain/src/validation/index.ts +++ b/packages/domain/src/validation/index.ts @@ -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"; diff --git a/packages/domain/src/validation/shared/primitives.ts b/packages/domain/src/validation/shared/primitives.ts index 209f8f2d..0dfe3c69 100644 --- a/packages/domain/src/validation/shared/primitives.ts +++ b/packages/domain/src/validation/shared/primitives.ts @@ -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; export type StatusSchema = z.infer; export type PrioritySchema = z.infer; export type CategorySchema = z.infer; +export type BillingCycleSchema = z.infer; +export type SubscriptionBillingCycleSchema = z.infer;