From 5a8e9624ae52ac3a79473292335d8d38b4cf4f58 Mon Sep 17 00:00:00 2001 From: barsa Date: Thu, 25 Sep 2025 15:14:36 +0900 Subject: [PATCH] Update import paths for SubCard and FormField components across multiple files to improve module structure and maintainability. --- .../services/whmcs-api-methods.service.ts | 227 +++++++++++++ .../services/whmcs-error-handler.service.ts | 209 ++++++++++++ .../services/whmcs-http-client.service.ts | 308 ++++++++++++++++++ .../account/components/PasswordChangeCard.tsx | 2 +- .../account/components/PersonalInfoCard.tsx | 2 +- .../LinkWhmcsForm/LinkWhmcsForm.tsx | 2 +- .../auth/components/LoginForm/LoginForm.tsx | 2 +- .../PasswordResetForm/PasswordResetForm.tsx | 2 +- .../SetPasswordForm/SetPasswordForm.tsx | 2 +- .../components/SignupForm/AccountStep.tsx | 2 +- .../components/InvoiceDetail/InvoiceItems.tsx | 2 +- .../InvoiceDetail/InvoiceTotals.tsx | 2 +- .../components/InvoiceList/InvoiceList.tsx | 4 +- .../features/billing/views/InvoiceDetail.tsx | 2 +- .../features/billing/views/PaymentMethods.tsx | 2 +- .../components/base/AddressConfirmation.tsx | 2 +- .../checkout/views/CheckoutContainer.tsx | 2 +- .../src/features/orders/views/OrderDetail.tsx | 2 +- .../components/SubscriptionCard.tsx | 2 +- .../components/SubscriptionDetails.tsx | 2 +- .../subscriptions/views/SimChangePlan.tsx | 2 +- .../features/subscriptions/views/SimTopUp.tsx | 2 +- .../views/SubscriptionDetail.tsx | 2 +- .../subscriptions/views/SubscriptionsList.tsx | 4 +- 24 files changed, 767 insertions(+), 23 deletions(-) create mode 100644 apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts create mode 100644 apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts create mode 100644 apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts 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 new file mode 100644 index 00000000..56864b99 --- /dev/null +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts @@ -0,0 +1,227 @@ +import { Injectable } from "@nestjs/common"; +import type { + WhmcsInvoicesResponse, + WhmcsInvoiceResponse, + WhmcsProductsResponse, + WhmcsClientResponse, + WhmcsSsoResponse, + WhmcsValidateLoginResponse, + 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"; +import type { WhmcsRequestOptions } from "../types/connection.types"; + +/** + * Service containing all WHMCS API method implementations + * Organized by functional area (clients, invoices, payments, etc.) + */ +@Injectable() +export class WhmcsApiMethodsService { + constructor( + private readonly httpClient: WhmcsHttpClientService, + private readonly configService: WhmcsConfigService + ) {} + + // ========================================== + // HEALTH CHECK METHODS + // ========================================== + + /** + * Perform health check on WHMCS API + */ + async healthCheck(): Promise { + try { + await this.makeRequest("GetProducts", { limitnum: 1 }); + return true; + } catch { + return false; + } + } + + /** + * Check if WHMCS service is available + */ + async isAvailable(): Promise { + return this.healthCheck(); + } + + /** + * Get WHMCS system information + */ + async getSystemInfo(): Promise { + return this.makeRequest("GetProducts", { limitnum: 1 }); + } + + // ========================================== + // CLIENT API METHODS + // ========================================== + + async getClientDetails(clientId: number): Promise { + return this.makeRequest("GetClientsDetails", { + clientid: clientId, + stats: true, // Required by some WHMCS versions to include defaultpaymethodid + }); + } + + async getClientDetailsByEmail(email: string): Promise { + return this.makeRequest("GetClientsDetails", { + email, + stats: true, + }); + } + + async updateClient( + clientId: number, + updateData: Partial + ): Promise<{ result: string }> { + return this.makeRequest<{ result: string }>("UpdateClient", { + clientid: clientId, + ...updateData, + }); + } + + async addClient(params: WhmcsAddClientParams): Promise { + return this.makeRequest("AddClient", params); + } + + async validateLogin(params: WhmcsValidateLoginParams): Promise { + return this.makeRequest("ValidateLogin", params); + } + + // ========================================== + // INVOICE API METHODS + // ========================================== + + async getInvoices(params: WhmcsGetInvoicesParams = {}): Promise { + return this.makeRequest("GetInvoices", params); + } + + async getInvoice(invoiceId: number): Promise { + 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 + // ========================================== + + async getClientsProducts(params: WhmcsGetClientsProductsParams): Promise { + return this.makeRequest("GetClientsProducts", params); + } + + async getCatalogProducts(): Promise { + return this.makeRequest("GetProducts", {}); + } + + // ========================================== + // PAYMENT API METHODS + // ========================================== + + async getPaymentMethods(params: WhmcsGetPayMethodsParams): Promise { + 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 + // ========================================== + + async createSsoToken(params: WhmcsCreateSsoTokenParams): Promise { + 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 } + ); + } + + // ========================================== + // PRIVATE HELPER METHODS + // ========================================== + + /** + * Make a request using the HTTP client + */ + private async makeRequest( + action: string, + params: Record, + options: WhmcsRequestOptions = {} + ): Promise { + const config = this.configService.getConfig(); + const response = await this.httpClient.makeRequest(config, action, params, options); + return response.data as T; + } +} diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts new file mode 100644 index 00000000..386567ae --- /dev/null +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts @@ -0,0 +1,209 @@ +import { Injectable, NotFoundException, BadRequestException, UnauthorizedException } from "@nestjs/common"; +import { getErrorMessage } from "@bff/core/utils/error.util"; +import type { WhmcsErrorResponse } from "../../types/whmcs-api.types"; + +/** + * Service for handling and normalizing WHMCS API errors + * Maps WHMCS errors to appropriate NestJS exceptions + */ +@Injectable() +export class WhmcsErrorHandlerService { + /** + * Handle WHMCS API error response + */ + handleApiError( + errorResponse: WhmcsErrorResponse, + action: string, + params: Record + ): never { + const message = errorResponse.message; + const errorCode = errorResponse.errorcode; + + // Normalize common, expected error responses to domain exceptions + if (this.isNotFoundError(action, message)) { + throw this.createNotFoundException(action, message, params); + } + + if (this.isAuthenticationError(message, errorCode)) { + throw new UnauthorizedException(`WHMCS Authentication Error: ${message}`); + } + + if (this.isValidationError(message, errorCode)) { + throw new BadRequestException(`WHMCS Validation Error: ${message}`); + } + + // Generic WHMCS API error + throw new Error(`WHMCS API Error [${action}]: ${message} (${errorCode || 'unknown'})`); + } + + /** + * Handle general request errors (network, timeout, etc.) + */ + handleRequestError(error: unknown, action: string, params: Record): never { + const message = getErrorMessage(error); + + if (this.isTimeoutError(error)) { + throw new Error(`WHMCS API timeout [${action}]: Request timed out`); + } + + if (this.isNetworkError(error)) { + throw new Error(`WHMCS API network error [${action}]: ${message}`); + } + + // Re-throw the original error if it's already a known exception type + if (this.isKnownException(error)) { + throw error; + } + + // Generic request error + throw new Error(`WHMCS API request failed [${action}]: ${message}`); + } + + /** + * Check if error indicates a not found condition + */ + private isNotFoundError(action: string, message: string): boolean { + const lowerMessage = message.toLowerCase(); + + // Client not found errors + if (action === "GetClientsDetails" && lowerMessage.includes("client not found")) { + return true; + } + + // Invoice not found errors + if ((action === "GetInvoice" || action === "UpdateInvoice") && + lowerMessage.includes("invoice not found")) { + return true; + } + + // Product not found errors + if (action === "GetClientsProducts" && lowerMessage.includes("no products found")) { + return true; + } + + return false; + } + + /** + * Check if error indicates authentication failure + */ + private isAuthenticationError(message: string, errorCode?: string): boolean { + const lowerMessage = message.toLowerCase(); + + return lowerMessage.includes("authentication") || + lowerMessage.includes("unauthorized") || + lowerMessage.includes("invalid credentials") || + lowerMessage.includes("access denied") || + errorCode === "AUTHENTICATION_FAILED"; + } + + /** + * Check if error indicates validation failure + */ + private isValidationError(message: string, errorCode?: string): boolean { + const lowerMessage = message.toLowerCase(); + + return lowerMessage.includes("required") || + lowerMessage.includes("invalid") || + lowerMessage.includes("missing") || + lowerMessage.includes("validation") || + errorCode === "VALIDATION_ERROR"; + } + + /** + * Create appropriate NotFoundException based on action and params + */ + private createNotFoundException( + action: string, + message: string, + params: Record + ): NotFoundException { + if (action === "GetClientsDetails") { + const emailParam = params["email"]; + if (typeof emailParam === "string") { + return new NotFoundException(`Client with email ${emailParam} not found`); + } + + const clientIdParam = params["clientid"]; + const identifier = + typeof clientIdParam === "string" || typeof clientIdParam === "number" + ? clientIdParam + : "unknown"; + + return new NotFoundException(`Client with ID ${identifier} not found`); + } + + if (action === "GetInvoice" || action === "UpdateInvoice") { + const invoiceIdParam = params["invoiceid"]; + const identifier = + typeof invoiceIdParam === "string" || typeof invoiceIdParam === "number" + ? invoiceIdParam + : "unknown"; + + return new NotFoundException(`Invoice with ID ${identifier} not found`); + } + + // Generic not found error + return new NotFoundException(message); + } + + /** + * Check if error is a timeout error + */ + private isTimeoutError(error: unknown): boolean { + const message = getErrorMessage(error).toLowerCase(); + return message.includes("timeout") || + message.includes("aborted") || + (error instanceof Error && error.name === "AbortError"); + } + + /** + * Check if error is a network error + */ + private isNetworkError(error: unknown): boolean { + const message = getErrorMessage(error).toLowerCase(); + return message.includes("network") || + message.includes("connection") || + message.includes("econnrefused") || + message.includes("enotfound") || + message.includes("fetch"); + } + + /** + * Check if error is already a known NestJS exception + */ + private isKnownException(error: unknown): boolean { + return error instanceof NotFoundException || + error instanceof BadRequestException || + error instanceof UnauthorizedException; + } + + /** + * Get user-friendly error message for client consumption + */ + getUserFriendlyMessage(error: unknown): string { + if (error instanceof NotFoundException) { + return "The requested resource was not found."; + } + + if (error instanceof BadRequestException) { + return "The request contains invalid data."; + } + + if (error instanceof UnauthorizedException) { + return "Authentication failed. Please check your credentials."; + } + + const message = getErrorMessage(error).toLowerCase(); + + if (message.includes("timeout")) { + return "The request timed out. Please try again."; + } + + if (message.includes("network") || message.includes("connection")) { + return "Network error. Please check your connection and try again."; + } + + return "An unexpected error occurred. Please try again later."; + } +} diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts new file mode 100644 index 00000000..aff71983 --- /dev/null +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts @@ -0,0 +1,308 @@ +import { Injectable, Inject } from "@nestjs/common"; +import { Logger } from "nestjs-pino"; +import { getErrorMessage } from "@bff/core/utils/error.util"; +import type { + WhmcsApiResponse, + WhmcsErrorResponse +} from "../../types/whmcs-api.types"; +import type { + WhmcsApiConfig, + WhmcsRequestOptions, + WhmcsConnectionStats +} from "../types/connection.types"; + +/** + * Service for handling HTTP requests to WHMCS API + * Manages timeouts, retries, and response parsing + */ +@Injectable() +export class WhmcsHttpClientService { + private stats: WhmcsConnectionStats = { + totalRequests: 0, + successfulRequests: 0, + failedRequests: 0, + averageResponseTime: 0, + }; + + constructor(@Inject(Logger) private readonly logger: Logger) {} + + /** + * Make HTTP request to WHMCS API + */ + async makeRequest( + config: WhmcsApiConfig, + action: string, + params: Record, + options: WhmcsRequestOptions = {} + ): Promise> { + const startTime = Date.now(); + this.stats.totalRequests++; + this.stats.lastRequestTime = new Date(); + + try { + const response = await this.executeRequest(config, action, params, options); + + const responseTime = Date.now() - startTime; + this.updateSuccessStats(responseTime); + + return response; + } catch (error) { + this.stats.failedRequests++; + this.stats.lastErrorTime = new Date(); + + this.logger.error(`WHMCS HTTP request failed [${action}]`, { + error: getErrorMessage(error), + action, + params: this.sanitizeLogParams(params), + responseTime: Date.now() - startTime, + }); + + throw error; + } + } + + /** + * Get connection statistics + */ + getStats(): WhmcsConnectionStats { + return { ...this.stats }; + } + + /** + * Reset connection statistics + */ + resetStats(): void { + this.stats = { + totalRequests: 0, + successfulRequests: 0, + failedRequests: 0, + averageResponseTime: 0, + }; + } + + /** + * Execute the actual HTTP request with retry logic + */ + private async executeRequest( + config: WhmcsApiConfig, + action: string, + params: Record, + options: WhmcsRequestOptions + ): Promise> { + const maxAttempts = options.retryAttempts ?? config.retryAttempts ?? 3; + let lastError: Error; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await this.performSingleRequest(config, action, params, options); + } catch (error) { + lastError = error as Error; + + if (attempt === maxAttempts) { + break; + } + + // Don't retry on certain error types + if (this.shouldNotRetry(error)) { + break; + } + + const delay = this.calculateRetryDelay(attempt, config.retryDelay ?? 1000); + this.logger.warn(`WHMCS request failed, retrying in ${delay}ms`, { + action, + attempt, + maxAttempts, + error: getErrorMessage(error), + }); + + await this.sleep(delay); + } + } + + throw lastError!; + } + + /** + * Perform a single HTTP request + */ + private async performSingleRequest( + config: WhmcsApiConfig, + action: string, + params: Record, + options: WhmcsRequestOptions + ): Promise> { + const timeout = options.timeout ?? config.timeout ?? 30000; + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const requestBody = this.buildRequestBody(config, action, params, options); + const url = `${config.baseUrl}/includes/api.php`; + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "CustomerPortal/1.0", + }, + body: requestBody, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + const responseText = await response.text(); + + if (!response.ok) { + const snippet = responseText?.slice(0, 300); + throw new Error( + `HTTP ${response.status}: ${response.statusText}${snippet ? ` | Body: ${snippet}` : ""}` + ); + } + + return this.parseResponse(responseText, action, params); + } finally { + clearTimeout(timeoutId); + } + } + + /** + * Build request body for WHMCS API + */ + private buildRequestBody( + config: WhmcsApiConfig, + action: string, + params: Record, + options: WhmcsRequestOptions + ): string { + const formData = new URLSearchParams(); + + // Add authentication + if (options.useAdminAuth && config.adminUsername && config.adminPasswordHash) { + formData.append("username", config.adminUsername); + formData.append("password", config.adminPasswordHash); + } else { + formData.append("identifier", config.identifier); + formData.append("secret", config.secret); + } + + // Add action and response format + formData.append("action", action); + formData.append("responsetype", "json"); + + // Add parameters + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + formData.append(key, String(value)); + } + } + + return formData.toString(); + } + + /** + * Parse WHMCS API response + */ + private parseResponse( + responseText: string, + action: string, + params: Record + ): WhmcsApiResponse { + let data: WhmcsApiResponse; + + try { + data = JSON.parse(responseText) as WhmcsApiResponse; + } catch (parseError) { + this.logger.error(`Invalid JSON response from WHMCS API [${action}]`, { + responseText: responseText.substring(0, 500), + parseError: getErrorMessage(parseError), + params: this.sanitizeLogParams(params), + }); + throw new Error("Invalid JSON response from WHMCS API"); + } + + if (data.result === "error") { + const errorResponse = data as WhmcsErrorResponse; + throw new Error(`WHMCS API Error: ${errorResponse.message} (${errorResponse.errorcode || 'unknown'})`); + } + + return data; + } + + /** + * Check if error should not be retried + */ + private shouldNotRetry(error: unknown): boolean { + const message = getErrorMessage(error).toLowerCase(); + + // Don't retry authentication errors + if (message.includes('authentication') || message.includes('unauthorized')) { + return true; + } + + // Don't retry validation errors + if (message.includes('invalid') || message.includes('required')) { + return true; + } + + // Don't retry not found errors + if (message.includes('not found')) { + return true; + } + + return false; + } + + /** + * Calculate retry delay with exponential backoff + */ + private calculateRetryDelay(attempt: number, baseDelay: number): number { + const exponentialDelay = baseDelay * Math.pow(2, attempt - 1); + const jitter = Math.random() * 0.1 * exponentialDelay; + return Math.min(exponentialDelay + jitter, 10000); // Max 10 seconds + } + + /** + * Sleep for specified milliseconds + */ + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Update success statistics + */ + private updateSuccessStats(responseTime: number): void { + this.stats.successfulRequests++; + + // Update average response time + const totalSuccessful = this.stats.successfulRequests; + this.stats.averageResponseTime = + (this.stats.averageResponseTime * (totalSuccessful - 1) + responseTime) / totalSuccessful; + } + + /** + * Sanitize parameters for logging (remove sensitive data) + */ + private sanitizeLogParams(params: Record): Record { + const sensitiveKeys = [ + 'password', 'secret', 'token', 'key', 'auth', + 'credit_card', 'cvv', 'ssn', 'social_security' + ]; + + const sanitized: Record = {}; + + for (const [key, value] of Object.entries(params)) { + const keyLower = key.toLowerCase(); + const isSensitive = sensitiveKeys.some(sensitive => keyLower.includes(sensitive)); + + if (isSensitive) { + sanitized[key] = '[REDACTED]'; + } else { + sanitized[key] = value; + } + } + + return sanitized; + } +} diff --git a/apps/portal/src/features/account/components/PasswordChangeCard.tsx b/apps/portal/src/features/account/components/PasswordChangeCard.tsx index 5c4df64d..f7d94a78 100644 --- a/apps/portal/src/features/account/components/PasswordChangeCard.tsx +++ b/apps/portal/src/features/account/components/PasswordChangeCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; interface PasswordChangeCardProps { isChanging: boolean; diff --git a/apps/portal/src/features/account/components/PersonalInfoCard.tsx b/apps/portal/src/features/account/components/PersonalInfoCard.tsx index 144346d9..5691b660 100644 --- a/apps/portal/src/features/account/components/PersonalInfoCard.tsx +++ b/apps/portal/src/features/account/components/PersonalInfoCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { UserIcon, PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline"; import type { ProfileEditFormData } from "@customer-portal/domain"; diff --git a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx b/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx index f40b38c3..d01e3a26 100644 --- a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx +++ b/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx @@ -2,7 +2,7 @@ import { useCallback } from "react"; import { Button, Input, ErrorMessage } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField"; +import { FormField } from "@/components/molecules/FormField/FormField"; import { useWhmcsLink } from "@/features/auth/hooks"; import { linkWhmcsRequestSchema, diff --git a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx index 00d57a2a..fae66e7d 100644 --- a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx +++ b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx @@ -8,7 +8,7 @@ import { useCallback } from "react"; import Link from "next/link"; import { Button, Input, ErrorMessage } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField"; +import { FormField } from "@/components/molecules/FormField/FormField"; import { useLogin } from "../../hooks/use-auth"; import { loginFormSchema, diff --git a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx index 07a71b21..8b1ea414 100644 --- a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx +++ b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx @@ -8,7 +8,7 @@ import { useEffect } from "react"; import Link from "next/link"; import { Button, Input, ErrorMessage } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField"; +import { FormField } from "@/components/molecules/FormField/FormField"; import { usePasswordReset } from "../../hooks/use-auth"; import { useZodForm } from "@customer-portal/validation"; import { diff --git a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx index 5352f9f7..21fc14ed 100644 --- a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx +++ b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx @@ -8,7 +8,7 @@ import { useEffect } from "react"; import Link from "next/link"; import { Button, Input, ErrorMessage } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField"; +import { FormField } from "@/components/molecules/FormField/FormField"; import { useWhmcsLink } from "../../hooks/use-auth"; import { useZodForm } from "@customer-portal/validation"; import { diff --git a/apps/portal/src/features/auth/components/SignupForm/AccountStep.tsx b/apps/portal/src/features/auth/components/SignupForm/AccountStep.tsx index 896deb97..9243a35a 100644 --- a/apps/portal/src/features/auth/components/SignupForm/AccountStep.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/AccountStep.tsx @@ -6,7 +6,7 @@ "use client"; import { Input } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField"; +import { FormField } from "@/components/molecules/FormField/FormField"; interface AccountStepProps { formData: { diff --git a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx index c192ae08..9805be07 100644 --- a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx +++ b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { formatCurrency } from "@customer-portal/domain"; import type { InvoiceItem } from "@customer-portal/domain"; diff --git a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx index f007b5c4..84f9d39d 100644 --- a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx +++ b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { formatCurrency } from "@customer-portal/domain"; interface InvoiceTotalsProps { diff --git a/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx b/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx index 0ceade59..c43e343d 100644 --- a/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx +++ b/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx @@ -1,10 +1,10 @@ "use client"; import React, { useMemo, useState } from "react"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { LoadingTable } from "@/components/atoms/loading-skeleton"; import { AsyncBlock } from "@/components/molecules/AsyncBlock"; -import { SearchFilterBar } from "@/components/molecules/SearchFilterBar"; +import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar"; import { PaginationBar } from "@/components/molecules/PaginationBar"; import { InvoiceTable } from "@/features/billing/components/InvoiceTable/InvoiceTable"; import { useInvoices } from "@/features/billing/hooks/useBilling"; diff --git a/apps/portal/src/features/billing/views/InvoiceDetail.tsx b/apps/portal/src/features/billing/views/InvoiceDetail.tsx index ac0ca23c..b9cb4d00 100644 --- a/apps/portal/src/features/billing/views/InvoiceDetail.tsx +++ b/apps/portal/src/features/billing/views/InvoiceDetail.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import Link from "next/link"; import { useParams } from "next/navigation"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton"; import { ErrorState } from "@/components/atoms/error-state"; import { CheckCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline"; diff --git a/apps/portal/src/features/billing/views/PaymentMethods.tsx b/apps/portal/src/features/billing/views/PaymentMethods.tsx index c2a73052..30e6b4b1 100644 --- a/apps/portal/src/features/billing/views/PaymentMethods.tsx +++ b/apps/portal/src/features/billing/views/PaymentMethods.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { PageLayout } from "@/components/templates/PageLayout"; import { ErrorBoundary } from "@/components/molecules"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { useSession } from "@/features/auth/hooks"; import { useAuthStore } from "@/features/auth/services/auth.store"; diff --git a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx index c67f707d..a8fdfbdc 100644 --- a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx +++ b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx @@ -3,7 +3,7 @@ import { Skeleton } from "@/components/atoms"; import { AlertBanner } from "@/components/molecules/AlertBanner"; import { Button } from "@/components/atoms/button"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { useState, useEffect, useCallback } from "react"; import { accountService } from "@/features/account/services/account.service"; diff --git a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx b/apps/portal/src/features/checkout/views/CheckoutContainer.tsx index cc17e5f9..a56981d3 100644 --- a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx +++ b/apps/portal/src/features/checkout/views/CheckoutContainer.tsx @@ -1,7 +1,7 @@ "use client"; import { useCheckout } from "@/features/checkout/hooks/useCheckout"; import { PageLayout } from "@/components/templates/PageLayout"; -import { SubCard } from "@/components/molecules/SubCard"; +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"; diff --git a/apps/portal/src/features/orders/views/OrderDetail.tsx b/apps/portal/src/features/orders/views/OrderDetail.tsx index ebb7a7b7..5eefe169 100644 --- a/apps/portal/src/features/orders/views/OrderDetail.tsx +++ b/apps/portal/src/features/orders/views/OrderDetail.tsx @@ -11,7 +11,7 @@ import { LockClosedIcon, CubeIcon, } from "@heroicons/react/24/outline"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { StatusPill } from "@/components/atoms/status-pill"; import { ordersService } from "@/features/orders/services/orders.service"; import { calculateOrderTotals, deriveOrderStatusDescriptor, getServiceCategory } from "@/features/orders/utils/order-presenters"; diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx index fdd70ae4..000b31b0 100644 --- a/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx +++ b/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx @@ -14,7 +14,7 @@ import { } from "@heroicons/react/24/outline"; import { StatusPill } from "@/components/atoms/status-pill"; import { Button } from "@/components/atoms/button"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; import type { Subscription } from "@customer-portal/domain"; import { cn } from "@/lib/utils"; diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx index ad1a291c..a7def57c 100644 --- a/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx +++ b/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx @@ -14,7 +14,7 @@ import { TagIcon, } from "@heroicons/react/24/outline"; import { StatusPill } from "@/components/atoms/status-pill"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; import type { Subscription } from "@customer-portal/domain"; import { cn } from "@/lib/utils"; diff --git a/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx b/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx index 050ca46f..31ae0db1 100644 --- a/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx +++ b/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { PageLayout } from "@/components/templates/PageLayout"; -import { SubCard } from "@/components/molecules/SubCard"; +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"; diff --git a/apps/portal/src/features/subscriptions/views/SimTopUp.tsx b/apps/portal/src/features/subscriptions/views/SimTopUp.tsx index 7f4def9e..7e23cd2d 100644 --- a/apps/portal/src/features/subscriptions/views/SimTopUp.tsx +++ b/apps/portal/src/features/subscriptions/views/SimTopUp.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { PageLayout } from "@/components/templates/PageLayout"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { simActionsService } from "@/features/subscriptions/services/sim-actions.service"; import { AlertBanner } from "@/components/molecules/AlertBanner"; import { DevicePhoneMobileIcon } from "@heroicons/react/24/outline"; diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx index 918fc534..46a9f006 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx @@ -1,7 +1,7 @@ "use client"; import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton"; -import { SubCard } from "@/components/molecules/SubCard"; +import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { DetailHeader } from "@/components/molecules/DetailHeader"; import { useEffect, useState } from "react"; import { useParams, useSearchParams } from "next/navigation"; diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx index 5984fb8c..ea52a0aa 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx @@ -8,8 +8,8 @@ import { ErrorBoundary } from "@/components/molecules"; import { PageLayout } from "@/components/templates/PageLayout"; import { DataTable } from "@/components/molecules/DataTable/DataTable"; import { StatusPill } from "@/components/atoms/status-pill"; -import { SubCard } from "@/components/molecules/SubCard"; -import { SearchFilterBar } from "@/components/molecules/SearchFilterBar"; +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";