From a22b84f128fc0f91ee557f7262d93f5ff3a231d1 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Thu, 18 Sep 2025 14:52:26 +0900 Subject: [PATCH] Refactor and clean up BFF and portal components for improved maintainability - Removed deprecated files and components from the BFF application, including various auth and catalog services, enhancing code clarity. - Updated package.json scripts for better organization and streamlined development processes. - Refactored portal components to improve structure and maintainability, including the removal of unused files and components. - Enhanced type definitions and imports across the application for consistency and clarity. --- apps/bff/openapi/openapi.json | 6 +- apps/bff/package.json | 5 +- apps/bff/src/auth/auth.controller.ts | 193 -------- apps/bff/src/core/config/router.config.ts | 16 +- .../salesforce/events/events.module.ts | 2 +- .../salesforce/events/pubsub.subscriber.ts | 2 +- .../auth/auth-admin.controller.ts | 0 .../{ => modules}/auth/auth-zod.controller.ts | 2 +- .../bff/src/{ => modules}/auth/auth.module.ts | 3 +- .../src/{ => modules}/auth/auth.service.ts | 6 +- apps/bff/src/{ => modules}/auth/auth.types.ts | 0 .../auth/decorators/public.decorator.ts | 0 .../auth/dto/account-status.dto.ts | 0 .../auth/dto/change-password.dto.ts | 0 .../{ => modules}/auth/dto/link-whmcs.dto.ts | 0 .../src/{ => modules}/auth/dto/login.dto.ts | 0 .../auth/dto/request-password-reset.dto.ts | 0 .../auth/dto/reset-password.dto.ts | 0 .../auth/dto/set-password.dto.ts | 0 .../src/{ => modules}/auth/dto/signup.dto.ts | 4 +- .../{ => modules}/auth/dto/sso-link.dto.ts | 0 .../auth/dto/validate-signup.dto.ts | 0 .../{ => modules}/auth/guards/admin.guard.ts | 0 .../auth/guards/auth-throttle.guard.ts | 0 .../auth/guards/global-auth.guard.ts | 0 .../auth/guards/jwt-auth.guard.ts | 0 .../auth/guards/local-auth.guard.ts | 0 .../auth/services/token-blacklist.service.ts | 0 .../auth/strategies/jwt.strategy.ts | 0 .../auth/strategies/local.strategy.ts | 0 .../{ => modules}/cases/cases.controller.ts | 0 .../src/{ => modules}/cases/cases.module.ts | 0 .../src/{ => modules}/cases/cases.service.ts | 0 .../catalog/catalog.controller.ts | 0 .../{ => modules}/catalog/catalog.module.ts | 0 .../catalog/services/base-catalog.service.ts | 0 .../services/internet-catalog.service.ts | 2 +- .../catalog/services/sim-catalog.service.ts | 2 +- .../catalog/services/vpn-catalog.service.ts | 0 .../{ => modules}/health/health.controller.ts | 0 .../src/{ => modules}/health/health.module.ts | 0 .../cache/mapping-cache.service.ts | 2 +- .../id-mappings/mappings.module.ts | 0 .../id-mappings/mappings.service.ts | 0 .../id-mappings/types/mapping.types.ts | 0 .../validation/mapping-validator.service.ts | 0 .../{ => modules}/invoices/dto/invoice.dto.ts | 0 .../invoices/invoices.controller.ts | 0 .../{ => modules}/invoices/invoices.module.ts | 0 .../invoices/invoices.service.ts | 0 .../bff/src/{ => modules}/jobs/jobs.module.ts | 0 .../src/{ => modules}/jobs/jobs.service.ts | 0 .../{ => modules}/jobs/reconcile.processor.ts | 0 .../src/{ => modules}/orders/dto/order.dto.ts | 0 .../{ => modules}/orders/orders.controller.ts | 0 .../src/{ => modules}/orders/orders.module.ts | 0 .../orders/queue/provisioning.processor.ts | 0 .../orders/queue/provisioning.queue.ts | 0 .../orders/services/order-builder.service.ts | 2 +- .../order-fulfillment-error.service.ts | 0 .../order-fulfillment-orchestrator.service.ts | 0 .../order-fulfillment-validator.service.ts | 2 +- .../services/order-item-builder.service.ts | 0 .../services/order-orchestrator.service.ts | 0 .../services/order-validator.service.ts | 2 +- .../services/order-whmcs-mapper.service.ts | 0 .../services/sim-fulfillment.service.ts | 0 .../orders/types/order-details.dto.ts | 0 .../subscriptions/dto/sim-cancel.dto.ts | 0 .../subscriptions/dto/sim-change-plan.dto.ts | 0 .../subscriptions/dto/sim-features.dto.ts | 0 .../subscriptions/dto/sim-topup.dto.ts | 0 .../subscriptions/sim-management.service.ts | 0 .../sim-order-activation.service.ts | 0 .../subscriptions/sim-orders.controller.ts | 0 .../subscriptions/sim-usage-store.service.ts | 0 .../subscriptions/subscriptions.controller.ts | 0 .../subscriptions/subscriptions.module.ts | 0 .../subscriptions/subscriptions.service.ts | 0 .../users/dto/update-address.dto.ts | 0 .../users/dto/update-user.dto.ts | 0 .../{ => modules}/users/users.controller.ts | 0 .../src/{ => modules}/users/users.module.ts | 0 .../src/{ => modules}/users/users.service.ts | 0 apps/bff/tsconfig.base.json | 2 +- apps/bff/tsconfig.build.json | 2 +- apps/bff/tsconfig.memory.json | 31 -- apps/bff/tsconfig.ultra-light.json | 62 --- apps/portal/next.config.mjs | 2 +- apps/portal/package.json | 1 - .../src/app/(portal)/account/billing/page.tsx | 431 ----------------- .../src/app/(portal)/dashboard/page.tsx | 18 +- .../src/app/(portal)/support/new/page.tsx | 2 +- apps/portal/src/app/layout.tsx | 4 +- .../src/components/auth/auth-layout.tsx | 37 -- .../auth/session-timeout-warning.tsx | 134 ------ .../components/catalog/activation-form.tsx | 92 ---- .../src/components/catalog/addon-group.tsx | 196 -------- .../components/catalog/animated-button.tsx | 67 --- .../src/components/catalog/animated-card.tsx | 42 -- .../catalog/installation-options.tsx | 130 ----- .../components/catalog/loading-spinner.tsx | 26 - .../src/components/catalog/mnp-form.tsx | 284 ----------- .../src/components/catalog/order-summary.tsx | 285 ----------- .../src/components/catalog/progress-steps.tsx | 71 --- .../components/catalog/sim-type-selector.tsx | 97 ---- .../src/components/catalog/step-header.tsx | 20 - .../checkout/address-confirmation.tsx | 446 ------------------ .../common/AsyncBlock/AsyncBlock.tsx | 2 +- .../components/common/FormField/FormField.tsx | 2 +- .../guards/profile-completion-guard.tsx | 2 +- .../components/layout/dashboard-layout.tsx | 6 +- apps/portal/src/components/ui/badge.tsx | 2 +- apps/portal/src/components/ui/button.tsx | 2 +- apps/portal/src/components/ui/empty-state.tsx | 2 +- .../src/components/ui/error-message.tsx | 2 +- apps/portal/src/components/ui/error-state.tsx | 2 +- apps/portal/src/components/ui/input.tsx | 2 +- apps/portal/src/components/ui/label.tsx | 2 +- .../src/components/ui/loading-skeleton.tsx | 2 +- .../src/components/ui/loading-spinner.tsx | 2 +- apps/portal/src/core/api/client.ts | 13 + apps/portal/src/core/api/index.ts | 3 + apps/portal/src/core/api/query-keys.ts | 42 ++ apps/portal/src/{lib => core/config}/env.ts | 0 apps/portal/src/core/config/index.ts | 1 + apps/portal/src/core/index.ts | 8 + apps/portal/src/core/providers/index.ts | 1 + .../src/core/providers/query-provider.tsx | 50 ++ .../src/features/account/hooks/index.ts | 5 + .../account/hooks/useProfileCompletion.ts} | 7 +- .../account/services/account.service.ts | 34 -- apps/portal/src/features/auth/api.ts | 189 -------- .../features/auth/components/AuthLayout.tsx | 51 ++ .../components/SignupForm/AddressStep.tsx | 2 +- .../src/features/auth/components/index.ts | 1 + .../portal/src/features/auth/hooks/useAuth.ts | 127 +++++ apps/portal/src/features/auth/index.ts | 1 - .../features/auth/services/auth.service.ts | 350 -------------- .../src/features/auth/services/auth.store.ts | 71 +-- .../src/features/auth/services/index.ts | 1 - .../auth/views/ForgotPasswordView.tsx | 2 +- .../src/features/auth/views/LinkWhmcsView.tsx | 2 +- .../src/features/auth/views/LoginView.tsx | 2 +- .../features/auth/views/ResetPasswordView.tsx | 2 +- .../features/auth/views/SetPasswordView.tsx | 2 +- .../src/features/auth/views/SignupView.tsx | 2 +- .../BillingSummary/BillingSummary.tsx | 4 +- .../components/InvoiceDetail/InvoiceItems.tsx | 2 +- .../InvoiceDetail/InvoiceTotals.tsx | 2 +- .../billing/components/InvoiceItemRow.tsx | 2 +- .../components/InvoiceTable/InvoiceTable.tsx | 4 +- .../PaymentMethodCard/PaymentMethodCard.tsx | 2 +- .../src/features/billing/hooks/index.ts | 3 +- .../src/features/billing/hooks/useBilling.ts | 266 +++-------- .../billing/hooks/usePaymentRefresh.ts | 4 +- .../billing/services/billing.service.ts | 158 ------- .../src/features/billing/services/index.ts | 1 - .../features/billing/views/InvoiceDetail.tsx | 9 +- .../features/billing/views/PaymentMethods.tsx | 9 +- .../catalog/services/catalog.service.ts | 131 ----- .../src/features/catalog/services/index.ts | 1 - .../features/catalog/utils/catalog.utils.ts | 2 +- .../dashboard/components/ActivityFeed.tsx | 2 +- .../components/UpcomingPaymentBanner.tsx | 2 +- .../src/features/dashboard/hooks/index.ts | 2 +- .../features/dashboard/hooks/useDashboard.ts | 27 ++ .../dashboard/hooks/useDashboardSummary.ts | 21 +- .../dashboard/services/dashboard.service.ts | 113 ----- .../src/features/dashboard/services/index.ts | 6 - .../dashboard/stores/dashboard.store.ts | 94 ---- .../src/features/dashboard/stores/index.ts | 6 - .../orders/services/orders.service.ts | 26 - .../components/ChangePlanModal.tsx | 4 +- .../sim-management/components/SimActions.tsx | 6 +- .../components/SimDetailsCard.tsx | 2 +- .../components/SimFeatureToggles.tsx | 4 +- .../components/SimManagementSection.tsx | 4 +- .../sim-management/components/TopUpModal.tsx | 4 +- .../components/SubscriptionActions.tsx | 2 +- .../components/SubscriptionCard.tsx | 4 +- .../components/SubscriptionDetails.tsx | 4 +- .../src/features/subscriptions/hooks/index.ts | 2 +- .../subscriptions/hooks/useSubscriptions.ts | 11 +- .../services/sim-actions.service.ts | 59 --- .../views/SubscriptionDetail.tsx | 2 +- .../subscriptions/views/SubscriptionsList.tsx | 2 +- apps/portal/src/hooks/useDashboard.ts | 38 -- apps/portal/src/hooks/useInvoices.ts | 141 ------ apps/portal/src/hooks/usePaymentRefresh.ts | 70 --- apps/portal/src/hooks/useSubscriptions.ts | 126 ----- apps/portal/src/lib/api.ts | 139 ------ apps/portal/src/lib/api/base.service.ts | 177 ------- apps/portal/src/lib/api/index.ts | 8 - apps/portal/src/lib/api/openapi-client.ts | 188 -------- apps/portal/src/lib/api/unwrap.ts | 4 - apps/portal/src/lib/auth/api.ts | 191 -------- apps/portal/src/lib/auth/store.ts | 241 ---------- apps/portal/src/lib/index.ts | 6 - apps/portal/src/lib/logger.ts | 15 - apps/portal/src/lib/plan.ts | 20 - apps/portal/src/lib/query-client.ts | 25 - apps/portal/src/lib/query.ts | 139 ------ apps/portal/src/lib/utils.ts | 6 - apps/portal/src/lib/utils/currency.ts | 118 ----- apps/portal/src/lib/utils/error-display.ts | 10 - apps/portal/src/providers/query-provider.tsx | 25 - apps/portal/src/shared/hooks/index.ts | 3 + apps/portal/src/shared/hooks/useDebounce.ts | 22 + .../src/shared/hooks/useLocalStorage.ts | 69 +++ apps/portal/src/shared/hooks/useMediaQuery.ts | 35 ++ apps/portal/src/shared/index.ts | 2 + apps/portal/src/{lib => shared}/utils/cn.ts | 6 +- apps/portal/src/shared/utils/error-display.ts | 16 + apps/portal/src/shared/utils/index.ts | 3 + apps/portal/src/shared/utils/plan.ts | 12 + apps/portal/src/utils/currency.ts | 132 ------ apps/portal/tsconfig.json | 1 - package.json | 3 - .../api-client/src/__generated__/types.ts | 4 +- packages/domain/src/entities/catalog.ts | 6 +- packages/domain/src/entities/checkout.ts | 2 +- packages/domain/src/entities/product.ts | 5 +- packages/domain/src/utils/validation.ts | 14 - .../src/validation/address-migration.ts | 88 ---- packages/domain/src/validation/bff-schemas.ts | 8 +- packages/domain/src/validation/index.ts | 14 - scripts/quick-syntax-check.sh | 184 -------- scripts/type-check-incremental.sh | 165 ------- scripts/type-check-memory.sh | 153 ------ 230 files changed, 745 insertions(+), 6630 deletions(-) delete mode 100644 apps/bff/src/auth/auth.controller.ts rename apps/bff/src/{ => modules}/auth/auth-admin.controller.ts (100%) rename apps/bff/src/{ => modules}/auth/auth-zod.controller.ts (99%) rename apps/bff/src/{ => modules}/auth/auth.module.ts (94%) rename apps/bff/src/{ => modules}/auth/auth.service.ts (99%) rename apps/bff/src/{ => modules}/auth/auth.types.ts (100%) rename apps/bff/src/{ => modules}/auth/decorators/public.decorator.ts (100%) rename apps/bff/src/{ => modules}/auth/dto/account-status.dto.ts (100%) rename apps/bff/src/{ => modules}/auth/dto/change-password.dto.ts (100%) rename apps/bff/src/{ => modules}/auth/dto/link-whmcs.dto.ts (100%) rename apps/bff/src/{ => modules}/auth/dto/login.dto.ts (100%) rename apps/bff/src/{ => modules}/auth/dto/request-password-reset.dto.ts (100%) rename apps/bff/src/{ => modules}/auth/dto/reset-password.dto.ts (100%) rename apps/bff/src/{ => modules}/auth/dto/set-password.dto.ts (100%) rename apps/bff/src/{ => modules}/auth/dto/signup.dto.ts (98%) rename apps/bff/src/{ => modules}/auth/dto/sso-link.dto.ts (100%) rename apps/bff/src/{ => modules}/auth/dto/validate-signup.dto.ts (100%) rename apps/bff/src/{ => modules}/auth/guards/admin.guard.ts (100%) rename apps/bff/src/{ => modules}/auth/guards/auth-throttle.guard.ts (100%) rename apps/bff/src/{ => modules}/auth/guards/global-auth.guard.ts (100%) rename apps/bff/src/{ => modules}/auth/guards/jwt-auth.guard.ts (100%) rename apps/bff/src/{ => modules}/auth/guards/local-auth.guard.ts (100%) rename apps/bff/src/{ => modules}/auth/services/token-blacklist.service.ts (100%) rename apps/bff/src/{ => modules}/auth/strategies/jwt.strategy.ts (100%) rename apps/bff/src/{ => modules}/auth/strategies/local.strategy.ts (100%) rename apps/bff/src/{ => modules}/cases/cases.controller.ts (100%) rename apps/bff/src/{ => modules}/cases/cases.module.ts (100%) rename apps/bff/src/{ => modules}/cases/cases.service.ts (100%) rename apps/bff/src/{ => modules}/catalog/catalog.controller.ts (100%) rename apps/bff/src/{ => modules}/catalog/catalog.module.ts (100%) rename apps/bff/src/{ => modules}/catalog/services/base-catalog.service.ts (100%) rename apps/bff/src/{ => modules}/catalog/services/internet-catalog.service.ts (99%) rename apps/bff/src/{ => modules}/catalog/services/sim-catalog.service.ts (98%) rename apps/bff/src/{ => modules}/catalog/services/vpn-catalog.service.ts (100%) rename apps/bff/src/{ => modules}/health/health.controller.ts (100%) rename apps/bff/src/{ => modules}/health/health.module.ts (100%) rename apps/bff/src/{ => modules}/id-mappings/cache/mapping-cache.service.ts (98%) rename apps/bff/src/{ => modules}/id-mappings/mappings.module.ts (100%) rename apps/bff/src/{ => modules}/id-mappings/mappings.service.ts (100%) rename apps/bff/src/{ => modules}/id-mappings/types/mapping.types.ts (100%) rename apps/bff/src/{ => modules}/id-mappings/validation/mapping-validator.service.ts (100%) rename apps/bff/src/{ => modules}/invoices/dto/invoice.dto.ts (100%) rename apps/bff/src/{ => modules}/invoices/invoices.controller.ts (100%) rename apps/bff/src/{ => modules}/invoices/invoices.module.ts (100%) rename apps/bff/src/{ => modules}/invoices/invoices.service.ts (100%) rename apps/bff/src/{ => modules}/jobs/jobs.module.ts (100%) rename apps/bff/src/{ => modules}/jobs/jobs.service.ts (100%) rename apps/bff/src/{ => modules}/jobs/reconcile.processor.ts (100%) rename apps/bff/src/{ => modules}/orders/dto/order.dto.ts (100%) rename apps/bff/src/{ => modules}/orders/orders.controller.ts (100%) rename apps/bff/src/{ => modules}/orders/orders.module.ts (100%) rename apps/bff/src/{ => modules}/orders/queue/provisioning.processor.ts (100%) rename apps/bff/src/{ => modules}/orders/queue/provisioning.queue.ts (100%) rename apps/bff/src/{ => modules}/orders/services/order-builder.service.ts (99%) rename apps/bff/src/{ => modules}/orders/services/order-fulfillment-error.service.ts (100%) rename apps/bff/src/{ => modules}/orders/services/order-fulfillment-orchestrator.service.ts (100%) rename apps/bff/src/{ => modules}/orders/services/order-fulfillment-validator.service.ts (98%) rename apps/bff/src/{ => modules}/orders/services/order-item-builder.service.ts (100%) rename apps/bff/src/{ => modules}/orders/services/order-orchestrator.service.ts (100%) rename apps/bff/src/{ => modules}/orders/services/order-validator.service.ts (99%) rename apps/bff/src/{ => modules}/orders/services/order-whmcs-mapper.service.ts (100%) rename apps/bff/src/{ => modules}/orders/services/sim-fulfillment.service.ts (100%) rename apps/bff/src/{ => modules}/orders/types/order-details.dto.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/dto/sim-cancel.dto.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/dto/sim-change-plan.dto.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/dto/sim-features.dto.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/dto/sim-topup.dto.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/sim-management.service.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/sim-order-activation.service.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/sim-orders.controller.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/sim-usage-store.service.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/subscriptions.controller.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/subscriptions.module.ts (100%) rename apps/bff/src/{ => modules}/subscriptions/subscriptions.service.ts (100%) rename apps/bff/src/{ => modules}/users/dto/update-address.dto.ts (100%) rename apps/bff/src/{ => modules}/users/dto/update-user.dto.ts (100%) rename apps/bff/src/{ => modules}/users/users.controller.ts (100%) rename apps/bff/src/{ => modules}/users/users.module.ts (100%) rename apps/bff/src/{ => modules}/users/users.service.ts (100%) delete mode 100644 apps/bff/tsconfig.memory.json delete mode 100644 apps/bff/tsconfig.ultra-light.json delete mode 100644 apps/portal/src/app/(portal)/account/billing/page.tsx delete mode 100644 apps/portal/src/components/auth/auth-layout.tsx delete mode 100644 apps/portal/src/components/auth/session-timeout-warning.tsx delete mode 100644 apps/portal/src/components/catalog/activation-form.tsx delete mode 100644 apps/portal/src/components/catalog/addon-group.tsx delete mode 100644 apps/portal/src/components/catalog/animated-button.tsx delete mode 100644 apps/portal/src/components/catalog/animated-card.tsx delete mode 100644 apps/portal/src/components/catalog/installation-options.tsx delete mode 100644 apps/portal/src/components/catalog/loading-spinner.tsx delete mode 100644 apps/portal/src/components/catalog/mnp-form.tsx delete mode 100644 apps/portal/src/components/catalog/order-summary.tsx delete mode 100644 apps/portal/src/components/catalog/progress-steps.tsx delete mode 100644 apps/portal/src/components/catalog/sim-type-selector.tsx delete mode 100644 apps/portal/src/components/catalog/step-header.tsx delete mode 100644 apps/portal/src/components/checkout/address-confirmation.tsx create mode 100644 apps/portal/src/core/api/client.ts create mode 100644 apps/portal/src/core/api/index.ts create mode 100644 apps/portal/src/core/api/query-keys.ts rename apps/portal/src/{lib => core/config}/env.ts (100%) create mode 100644 apps/portal/src/core/config/index.ts create mode 100644 apps/portal/src/core/index.ts create mode 100644 apps/portal/src/core/providers/index.ts create mode 100644 apps/portal/src/core/providers/query-provider.tsx create mode 100644 apps/portal/src/features/account/hooks/index.ts rename apps/portal/src/{hooks/use-profile-completion.ts => features/account/hooks/useProfileCompletion.ts} (87%) delete mode 100644 apps/portal/src/features/account/services/account.service.ts delete mode 100644 apps/portal/src/features/auth/api.ts create mode 100644 apps/portal/src/features/auth/components/AuthLayout.tsx create mode 100644 apps/portal/src/features/auth/hooks/useAuth.ts delete mode 100644 apps/portal/src/features/auth/services/auth.service.ts delete mode 100644 apps/portal/src/features/billing/services/billing.service.ts delete mode 100644 apps/portal/src/features/billing/services/index.ts delete mode 100644 apps/portal/src/features/catalog/services/catalog.service.ts delete mode 100644 apps/portal/src/features/catalog/services/index.ts create mode 100644 apps/portal/src/features/dashboard/hooks/useDashboard.ts delete mode 100644 apps/portal/src/features/dashboard/services/dashboard.service.ts delete mode 100644 apps/portal/src/features/dashboard/services/index.ts delete mode 100644 apps/portal/src/features/dashboard/stores/dashboard.store.ts delete mode 100644 apps/portal/src/features/dashboard/stores/index.ts delete mode 100644 apps/portal/src/features/orders/services/orders.service.ts delete mode 100644 apps/portal/src/features/subscriptions/services/sim-actions.service.ts delete mode 100644 apps/portal/src/hooks/useDashboard.ts delete mode 100644 apps/portal/src/hooks/useInvoices.ts delete mode 100644 apps/portal/src/hooks/usePaymentRefresh.ts delete mode 100644 apps/portal/src/hooks/useSubscriptions.ts delete mode 100644 apps/portal/src/lib/api.ts delete mode 100644 apps/portal/src/lib/api/base.service.ts delete mode 100644 apps/portal/src/lib/api/index.ts delete mode 100644 apps/portal/src/lib/api/openapi-client.ts delete mode 100644 apps/portal/src/lib/api/unwrap.ts delete mode 100644 apps/portal/src/lib/auth/api.ts delete mode 100644 apps/portal/src/lib/auth/store.ts delete mode 100644 apps/portal/src/lib/index.ts delete mode 100644 apps/portal/src/lib/logger.ts delete mode 100644 apps/portal/src/lib/plan.ts delete mode 100644 apps/portal/src/lib/query-client.ts delete mode 100644 apps/portal/src/lib/query.ts delete mode 100644 apps/portal/src/lib/utils.ts delete mode 100644 apps/portal/src/lib/utils/currency.ts delete mode 100644 apps/portal/src/lib/utils/error-display.ts delete mode 100644 apps/portal/src/providers/query-provider.tsx create mode 100644 apps/portal/src/shared/hooks/index.ts create mode 100644 apps/portal/src/shared/hooks/useDebounce.ts create mode 100644 apps/portal/src/shared/hooks/useLocalStorage.ts create mode 100644 apps/portal/src/shared/hooks/useMediaQuery.ts create mode 100644 apps/portal/src/shared/index.ts rename apps/portal/src/{lib => shared}/utils/cn.ts (56%) create mode 100644 apps/portal/src/shared/utils/error-display.ts create mode 100644 apps/portal/src/shared/utils/index.ts create mode 100644 apps/portal/src/shared/utils/plan.ts delete mode 100644 apps/portal/src/utils/currency.ts delete mode 100644 packages/domain/src/validation/address-migration.ts delete mode 100644 scripts/quick-syntax-check.sh delete mode 100755 scripts/type-check-incremental.sh delete mode 100755 scripts/type-check-memory.sh diff --git a/apps/bff/openapi/openapi.json b/apps/bff/openapi/openapi.json index f9258fe4..ac01e582 100644 --- a/apps/bff/openapi/openapi.json +++ b/apps/bff/openapi/openapi.json @@ -1900,11 +1900,11 @@ "AddressDto": { "type": "object", "properties": { - "line1": { + "street": { "type": "string", "example": "123 Main Street" }, - "line2": { + "streetLine2": { "type": "string", "example": "Apt 4B" }, @@ -1927,7 +1927,7 @@ } }, "required": [ - "line1", + "street", "city", "state", "postalCode", diff --git a/apps/bff/package.json b/apps/bff/package.json index 29166963..ef86e509 100644 --- a/apps/bff/package.json +++ b/apps/bff/package.json @@ -12,15 +12,14 @@ "dev": "NODE_OPTIONS=\"--no-deprecation --max-old-space-size=4096\" nest start --watch --preserveWatchOutput -c tsconfig.build.json", "start:debug": "NODE_OPTIONS=\"--no-deprecation --max-old-space-size=4096\" nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint .", - "lint:fix": "eslint . --fix", + "lint": "NODE_OPTIONS=\"--max-old-space-size=4096\" eslint .", + "lint:fix": "NODE_OPTIONS=\"--max-old-space-size=4096\" eslint . --fix", "test": "NODE_OPTIONS=\"--max-old-space-size=4096\" jest", "test:watch": "NODE_OPTIONS=\"--max-old-space-size=4096\" jest --watch", "test:cov": "NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "NODE_OPTIONS=\"--max-old-space-size=4096\" jest --config ./test/jest-e2e.json", "type-check": "NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit", - "type-check:incremental": "NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit --incremental", "clean": "rm -rf dist", "db:migrate": "prisma migrate dev", "db:generate": "prisma generate", diff --git a/apps/bff/src/auth/auth.controller.ts b/apps/bff/src/auth/auth.controller.ts deleted file mode 100644 index 84cf0a3d..00000000 --- a/apps/bff/src/auth/auth.controller.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { Controller, Post, Body, UseGuards, Get, Req, HttpCode } from "@nestjs/common"; -import type { Request } from "express"; -import { Throttle } from "@nestjs/throttler"; -import { AuthService } from "./auth.service"; -import { LocalAuthGuard } from "./guards/local-auth.guard"; -import { AuthThrottleGuard } from "./guards/auth-throttle.guard"; -import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse } from "@nestjs/swagger"; -import { RequestPasswordResetDto } from "./dto/request-password-reset.dto"; -import { ResetPasswordDto } from "./dto/reset-password.dto"; -import { ChangePasswordDto } from "./dto/change-password.dto"; -import { LinkWhmcsDto } from "./dto/link-whmcs.dto"; -import { SetPasswordDto } from "./dto/set-password.dto"; -import { ValidateSignupDto } from "./dto/validate-signup.dto"; -import { AccountStatusRequestDto, AccountStatusResponseDto } from "./dto/account-status.dto"; -import { Public } from "./decorators/public.decorator"; -import { SsoLinkDto } from "./dto/sso-link.dto"; -import { SignupDto } from "./dto/signup.dto"; - -@ApiTags("auth") -@Controller("auth") -export class AuthController { - constructor(private authService: AuthService) {} - - @Public() - @Post("validate-signup") - @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 10, ttl: 900000 } }) // 10 validations per 15 minutes per IP - @ApiOperation({ summary: "Validate customer number for signup" }) - @ApiResponse({ status: 200, description: "Validation successful" }) - @ApiResponse({ status: 409, description: "Customer already has account" }) - @ApiResponse({ status: 400, description: "Customer number not found" }) - @ApiResponse({ status: 429, description: "Too many validation attempts" }) - async validateSignup(@Body() validateDto: ValidateSignupDto, @Req() req: Request) { - return this.authService.validateSignup(validateDto, req); - } - - @Public() - @Get("health-check") - @ApiOperation({ summary: "Check auth service health and integrations" }) - @ApiResponse({ status: 200, description: "Health check results" }) - async healthCheck() { - return this.authService.healthCheck(); - } - - @Public() - @Post("signup-preflight") - @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 10, ttl: 900000 } }) - @HttpCode(200) - @ApiOperation({ summary: "Validate full signup data without creating anything" }) - @ApiResponse({ status: 200, description: "Preflight results with next action guidance" }) - async signupPreflight(@Body() body: SignupDto) { - return this.authService.signupPreflight(body); - } - - @Public() - @Post("account-status") - @ApiOperation({ summary: "Get account status by email" }) - @ApiOkResponse({ description: "Account status" }) - async accountStatus(@Body() body: AccountStatusRequestDto): Promise { - return this.authService.getAccountStatus(body.email); - } - - @Public() - @Post("signup") - @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 3, ttl: 900000 } }) // 3 signups per 15 minutes per IP - @ApiOperation({ summary: "Create new user account" }) - @ApiResponse({ status: 201, description: "User created successfully" }) - @ApiResponse({ status: 409, description: "User already exists" }) - @ApiResponse({ status: 429, description: "Too many signup attempts" }) - async signup(@Body() signupDto: SignupDto, @Req() req: Request) { - return this.authService.signup(signupDto, req); - } - - @Public() - @UseGuards(LocalAuthGuard) - @Post("login") - @ApiOperation({ summary: "Authenticate user" }) - @ApiResponse({ status: 200, description: "Login successful" }) - @ApiResponse({ status: 401, description: "Invalid credentials" }) - async login(@Req() req: Request & { user: { id: string; email: string; role?: string } }) { - return this.authService.login(req.user, req); - } - - @Post("logout") - @ApiOperation({ summary: "Logout user" }) - @ApiResponse({ status: 200, description: "Logout successful" }) - async logout(@Req() req: Request & { user: { id: string } }) { - const authHeader = req.headers.authorization as string | string[] | undefined; - let bearer: string | undefined; - if (typeof authHeader === "string") { - bearer = authHeader; - } else if (Array.isArray(authHeader) && authHeader.length > 0) { - bearer = authHeader[0]; - } - const token = bearer?.startsWith("Bearer ") ? bearer.slice(7) : undefined; - await this.authService.logout(req.user.id, token ?? "", req); - return { message: "Logout successful" }; - } - - @Public() - @Post("link-whmcs") - @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 3, ttl: 900000 } }) // 3 attempts per 15 minutes per IP - @ApiOperation({ summary: "Link existing WHMCS user" }) - @ApiResponse({ - status: 200, - description: "WHMCS account linked successfully", - }) - @ApiResponse({ status: 401, description: "Invalid WHMCS credentials" }) - @ApiResponse({ status: 429, description: "Too many link attempts" }) - async linkWhmcs(@Body() linkDto: LinkWhmcsDto, @Req() req: Request) { - return this.authService.linkWhmcsUser(linkDto, req); - } - - @Public() - @Post("set-password") - @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 3, ttl: 300000 } }) // 3 attempts per 5 minutes per IP - @ApiOperation({ summary: "Set password for linked user" }) - @ApiResponse({ status: 200, description: "Password set successfully" }) - @ApiResponse({ status: 401, description: "User not found" }) - @ApiResponse({ status: 429, description: "Too many password attempts" }) - async setPassword(@Body() setPasswordDto: SetPasswordDto, @Req() req: Request) { - return this.authService.setPassword(setPasswordDto, req); - } - - @Public() - @Post("check-password-needed") - @HttpCode(200) - @ApiOperation({ summary: "Check if user needs to set password" }) - @ApiResponse({ status: 200, description: "Password status checked" }) - async checkPasswordNeeded(@Body() { email }: { email: string }) { - return this.authService.checkPasswordNeeded(email); - } - - @Public() - @Post("request-password-reset") - @Throttle({ default: { limit: 5, ttl: 900000 } }) - @ApiOperation({ summary: "Request password reset email" }) - @ApiResponse({ status: 200, description: "Reset email sent if account exists" }) - async requestPasswordReset(@Body() body: RequestPasswordResetDto) { - await this.authService.requestPasswordReset(body.email); - return { message: "If an account exists, a reset email has been sent" }; - } - - @Public() - @Post("reset-password") - @Throttle({ default: { limit: 5, ttl: 900000 } }) - @ApiOperation({ summary: "Reset password with token" }) - @ApiResponse({ status: 200, description: "Password reset successful" }) - async resetPassword(@Body() body: ResetPasswordDto) { - return this.authService.resetPassword(body.token, body.password); - } - - @Post("change-password") - @Throttle({ default: { limit: 5, ttl: 300000 } }) - @ApiOperation({ summary: "Change password (authenticated)" }) - @ApiResponse({ status: 200, description: "Password changed successfully" }) - async changePassword( - @Req() req: Request & { user: { id: string } }, - @Body() body: ChangePasswordDto - ) { - return this.authService.changePassword(req.user.id, body.currentPassword, body.newPassword); - } - - @Get("me") - @ApiOperation({ summary: "Get current authentication status" }) - getAuthStatus(@Req() req: Request & { user: { id: string; email: string; role?: string } }) { - // Return basic auth info only - full profile should use /api/me - return { - isAuthenticated: true, - user: { - id: req.user.id, - email: req.user.email, - role: req.user.role, - }, - }; - } - - @Post("sso-link") - @ApiOperation({ summary: "Create SSO link to WHMCS" }) - @ApiResponse({ status: 200, description: "SSO link created successfully" }) - @ApiResponse({ - status: 404, - description: "User not found or not linked to WHMCS", - }) - async createSsoLink(@Req() req: Request & { user: { id: string } }, @Body() body: SsoLinkDto) { - const destination = body?.destination; - return this.authService.createSsoLink(req.user.id, destination); - } -} diff --git a/apps/bff/src/core/config/router.config.ts b/apps/bff/src/core/config/router.config.ts index 97b6f4a2..3838d88e 100644 --- a/apps/bff/src/core/config/router.config.ts +++ b/apps/bff/src/core/config/router.config.ts @@ -1,12 +1,12 @@ import type { Routes } from "@nestjs/core"; -import { AuthModule } from "../../auth/auth.module"; -import { UsersModule } from "../../users/users.module"; -import { MappingsModule } from "../../id-mappings/mappings.module"; -import { CatalogModule } from "../../catalog/catalog.module"; -import { OrdersModule } from "../../orders/orders.module"; -import { InvoicesModule } from "../../invoices/invoices.module"; -import { SubscriptionsModule } from "../../subscriptions/subscriptions.module"; -import { CasesModule } from "../../cases/cases.module"; +import { AuthModule } from "@bff/modules/auth/auth.module"; +import { UsersModule } from "@bff/modules/users/users.module"; +import { MappingsModule } from "@bff/modules/id-mappings/mappings.module"; +import { CatalogModule } from "@bff/modules/catalog/catalog.module"; +import { OrdersModule } from "@bff/modules/orders/orders.module"; +import { InvoicesModule } from "@bff/modules/invoices/invoices.module"; +import { SubscriptionsModule } from "@bff/modules/subscriptions/subscriptions.module"; +import { CasesModule } from "@bff/modules/cases/cases.module"; export const apiRoutes: Routes = [ { diff --git a/apps/bff/src/integrations/salesforce/events/events.module.ts b/apps/bff/src/integrations/salesforce/events/events.module.ts index e2b5164c..5850a044 100644 --- a/apps/bff/src/integrations/salesforce/events/events.module.ts +++ b/apps/bff/src/integrations/salesforce/events/events.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { IntegrationsModule } from "@bff/integrations/integrations.module"; -import { OrdersModule } from "../../../orders/orders.module"; +import { OrdersModule } from "@bff/modules/orders/orders.module"; import { SalesforcePubSubSubscriber } from "./pubsub.subscriber"; @Module({ diff --git a/apps/bff/src/integrations/salesforce/events/pubsub.subscriber.ts b/apps/bff/src/integrations/salesforce/events/pubsub.subscriber.ts index eb4e4862..414ccd0f 100644 --- a/apps/bff/src/integrations/salesforce/events/pubsub.subscriber.ts +++ b/apps/bff/src/integrations/salesforce/events/pubsub.subscriber.ts @@ -3,7 +3,7 @@ import { ConfigService } from "@nestjs/config"; import { Logger } from "nestjs-pino"; import PubSubApiClientPkg from "salesforce-pubsub-api-client"; import { SalesforceConnection } from "../services/salesforce-connection.service"; -import { ProvisioningQueueService } from "../../../orders/queue/provisioning.queue"; +import { ProvisioningQueueService } from "@bff/modules/orders/queue/provisioning.queue"; import { CacheService } from "@bff/infra/cache/cache.service"; import { replayKey as sfReplayKey, diff --git a/apps/bff/src/auth/auth-admin.controller.ts b/apps/bff/src/modules/auth/auth-admin.controller.ts similarity index 100% rename from apps/bff/src/auth/auth-admin.controller.ts rename to apps/bff/src/modules/auth/auth-admin.controller.ts diff --git a/apps/bff/src/auth/auth-zod.controller.ts b/apps/bff/src/modules/auth/auth-zod.controller.ts similarity index 99% rename from apps/bff/src/auth/auth-zod.controller.ts rename to apps/bff/src/modules/auth/auth-zod.controller.ts index 1b48b2af..37f9b792 100644 --- a/apps/bff/src/auth/auth-zod.controller.ts +++ b/apps/bff/src/modules/auth/auth-zod.controller.ts @@ -6,7 +6,7 @@ import { LocalAuthGuard } from "./guards/local-auth.guard"; import { AuthThrottleGuard } from "./guards/auth-throttle.guard"; import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse } from "@nestjs/swagger"; import { Public } from "./decorators/public.decorator"; -import { ZodPipe } from "../core/validation"; +import { ZodPipe } from "@bff/core/validation"; // Import Zod schemas from domain import { diff --git a/apps/bff/src/auth/auth.module.ts b/apps/bff/src/modules/auth/auth.module.ts similarity index 94% rename from apps/bff/src/auth/auth.module.ts rename to apps/bff/src/modules/auth/auth.module.ts index 36a163b0..5a818ca9 100644 --- a/apps/bff/src/auth/auth.module.ts +++ b/apps/bff/src/modules/auth/auth.module.ts @@ -4,10 +4,9 @@ import { PassportModule } from "@nestjs/passport"; import { ConfigService } from "@nestjs/config"; import { APP_GUARD } from "@nestjs/core"; import { AuthService } from "./auth.service"; -import { AuthController } from "./auth.controller"; import { AuthZodController } from "./auth-zod.controller"; import { AuthAdminController } from "./auth-admin.controller"; -import { UsersModule } from "@/users/users.module"; +import { UsersModule } from "@bff/modules/users/users.module"; import { MappingsModule } from "@bff/modules/id-mappings/mappings.module"; import { IntegrationsModule } from "@bff/integrations/integrations.module"; import { JwtStrategy } from "./strategies/jwt.strategy"; diff --git a/apps/bff/src/auth/auth.service.ts b/apps/bff/src/modules/auth/auth.service.ts similarity index 99% rename from apps/bff/src/auth/auth.service.ts rename to apps/bff/src/modules/auth/auth.service.ts index a23c3e6c..a862e099 100644 --- a/apps/bff/src/auth/auth.service.ts +++ b/apps/bff/src/modules/auth/auth.service.ts @@ -281,7 +281,7 @@ export class AuthService { // Validate required WHMCS fields if ( - !address?.line1 || + !address?.street || !address?.city || !address?.state || !address?.postalCode || @@ -311,8 +311,8 @@ export class AuthService { email, companyname: company || "", phonenumber: phone, - address1: address.line1, - address2: address.line2 || "", + address1: address.street, + address2: address.streetLine2 || "", city: address.city, state: address.state, postcode: address.postalCode, diff --git a/apps/bff/src/auth/auth.types.ts b/apps/bff/src/modules/auth/auth.types.ts similarity index 100% rename from apps/bff/src/auth/auth.types.ts rename to apps/bff/src/modules/auth/auth.types.ts diff --git a/apps/bff/src/auth/decorators/public.decorator.ts b/apps/bff/src/modules/auth/decorators/public.decorator.ts similarity index 100% rename from apps/bff/src/auth/decorators/public.decorator.ts rename to apps/bff/src/modules/auth/decorators/public.decorator.ts diff --git a/apps/bff/src/auth/dto/account-status.dto.ts b/apps/bff/src/modules/auth/dto/account-status.dto.ts similarity index 100% rename from apps/bff/src/auth/dto/account-status.dto.ts rename to apps/bff/src/modules/auth/dto/account-status.dto.ts diff --git a/apps/bff/src/auth/dto/change-password.dto.ts b/apps/bff/src/modules/auth/dto/change-password.dto.ts similarity index 100% rename from apps/bff/src/auth/dto/change-password.dto.ts rename to apps/bff/src/modules/auth/dto/change-password.dto.ts diff --git a/apps/bff/src/auth/dto/link-whmcs.dto.ts b/apps/bff/src/modules/auth/dto/link-whmcs.dto.ts similarity index 100% rename from apps/bff/src/auth/dto/link-whmcs.dto.ts rename to apps/bff/src/modules/auth/dto/link-whmcs.dto.ts diff --git a/apps/bff/src/auth/dto/login.dto.ts b/apps/bff/src/modules/auth/dto/login.dto.ts similarity index 100% rename from apps/bff/src/auth/dto/login.dto.ts rename to apps/bff/src/modules/auth/dto/login.dto.ts diff --git a/apps/bff/src/auth/dto/request-password-reset.dto.ts b/apps/bff/src/modules/auth/dto/request-password-reset.dto.ts similarity index 100% rename from apps/bff/src/auth/dto/request-password-reset.dto.ts rename to apps/bff/src/modules/auth/dto/request-password-reset.dto.ts diff --git a/apps/bff/src/auth/dto/reset-password.dto.ts b/apps/bff/src/modules/auth/dto/reset-password.dto.ts similarity index 100% rename from apps/bff/src/auth/dto/reset-password.dto.ts rename to apps/bff/src/modules/auth/dto/reset-password.dto.ts diff --git a/apps/bff/src/auth/dto/set-password.dto.ts b/apps/bff/src/modules/auth/dto/set-password.dto.ts similarity index 100% rename from apps/bff/src/auth/dto/set-password.dto.ts rename to apps/bff/src/modules/auth/dto/set-password.dto.ts diff --git a/apps/bff/src/auth/dto/signup.dto.ts b/apps/bff/src/modules/auth/dto/signup.dto.ts similarity index 98% rename from apps/bff/src/auth/dto/signup.dto.ts rename to apps/bff/src/modules/auth/dto/signup.dto.ts index 83b7d175..77ae8a83 100644 --- a/apps/bff/src/auth/dto/signup.dto.ts +++ b/apps/bff/src/modules/auth/dto/signup.dto.ts @@ -15,12 +15,12 @@ export class AddressDto { @ApiProperty({ example: "123 Main Street" }) @IsString() @IsNotEmpty() - line1: string; + street: string; @ApiProperty({ example: "Apt 4B", required: false }) @IsOptional() @IsString() - line2?: string; + streetLine2?: string; @ApiProperty({ example: "Tokyo" }) @IsString() diff --git a/apps/bff/src/auth/dto/sso-link.dto.ts b/apps/bff/src/modules/auth/dto/sso-link.dto.ts similarity index 100% rename from apps/bff/src/auth/dto/sso-link.dto.ts rename to apps/bff/src/modules/auth/dto/sso-link.dto.ts diff --git a/apps/bff/src/auth/dto/validate-signup.dto.ts b/apps/bff/src/modules/auth/dto/validate-signup.dto.ts similarity index 100% rename from apps/bff/src/auth/dto/validate-signup.dto.ts rename to apps/bff/src/modules/auth/dto/validate-signup.dto.ts diff --git a/apps/bff/src/auth/guards/admin.guard.ts b/apps/bff/src/modules/auth/guards/admin.guard.ts similarity index 100% rename from apps/bff/src/auth/guards/admin.guard.ts rename to apps/bff/src/modules/auth/guards/admin.guard.ts diff --git a/apps/bff/src/auth/guards/auth-throttle.guard.ts b/apps/bff/src/modules/auth/guards/auth-throttle.guard.ts similarity index 100% rename from apps/bff/src/auth/guards/auth-throttle.guard.ts rename to apps/bff/src/modules/auth/guards/auth-throttle.guard.ts diff --git a/apps/bff/src/auth/guards/global-auth.guard.ts b/apps/bff/src/modules/auth/guards/global-auth.guard.ts similarity index 100% rename from apps/bff/src/auth/guards/global-auth.guard.ts rename to apps/bff/src/modules/auth/guards/global-auth.guard.ts diff --git a/apps/bff/src/auth/guards/jwt-auth.guard.ts b/apps/bff/src/modules/auth/guards/jwt-auth.guard.ts similarity index 100% rename from apps/bff/src/auth/guards/jwt-auth.guard.ts rename to apps/bff/src/modules/auth/guards/jwt-auth.guard.ts diff --git a/apps/bff/src/auth/guards/local-auth.guard.ts b/apps/bff/src/modules/auth/guards/local-auth.guard.ts similarity index 100% rename from apps/bff/src/auth/guards/local-auth.guard.ts rename to apps/bff/src/modules/auth/guards/local-auth.guard.ts diff --git a/apps/bff/src/auth/services/token-blacklist.service.ts b/apps/bff/src/modules/auth/services/token-blacklist.service.ts similarity index 100% rename from apps/bff/src/auth/services/token-blacklist.service.ts rename to apps/bff/src/modules/auth/services/token-blacklist.service.ts diff --git a/apps/bff/src/auth/strategies/jwt.strategy.ts b/apps/bff/src/modules/auth/strategies/jwt.strategy.ts similarity index 100% rename from apps/bff/src/auth/strategies/jwt.strategy.ts rename to apps/bff/src/modules/auth/strategies/jwt.strategy.ts diff --git a/apps/bff/src/auth/strategies/local.strategy.ts b/apps/bff/src/modules/auth/strategies/local.strategy.ts similarity index 100% rename from apps/bff/src/auth/strategies/local.strategy.ts rename to apps/bff/src/modules/auth/strategies/local.strategy.ts diff --git a/apps/bff/src/cases/cases.controller.ts b/apps/bff/src/modules/cases/cases.controller.ts similarity index 100% rename from apps/bff/src/cases/cases.controller.ts rename to apps/bff/src/modules/cases/cases.controller.ts diff --git a/apps/bff/src/cases/cases.module.ts b/apps/bff/src/modules/cases/cases.module.ts similarity index 100% rename from apps/bff/src/cases/cases.module.ts rename to apps/bff/src/modules/cases/cases.module.ts diff --git a/apps/bff/src/cases/cases.service.ts b/apps/bff/src/modules/cases/cases.service.ts similarity index 100% rename from apps/bff/src/cases/cases.service.ts rename to apps/bff/src/modules/cases/cases.service.ts diff --git a/apps/bff/src/catalog/catalog.controller.ts b/apps/bff/src/modules/catalog/catalog.controller.ts similarity index 100% rename from apps/bff/src/catalog/catalog.controller.ts rename to apps/bff/src/modules/catalog/catalog.controller.ts diff --git a/apps/bff/src/catalog/catalog.module.ts b/apps/bff/src/modules/catalog/catalog.module.ts similarity index 100% rename from apps/bff/src/catalog/catalog.module.ts rename to apps/bff/src/modules/catalog/catalog.module.ts diff --git a/apps/bff/src/catalog/services/base-catalog.service.ts b/apps/bff/src/modules/catalog/services/base-catalog.service.ts similarity index 100% rename from apps/bff/src/catalog/services/base-catalog.service.ts rename to apps/bff/src/modules/catalog/services/base-catalog.service.ts diff --git a/apps/bff/src/catalog/services/internet-catalog.service.ts b/apps/bff/src/modules/catalog/services/internet-catalog.service.ts similarity index 99% rename from apps/bff/src/catalog/services/internet-catalog.service.ts rename to apps/bff/src/modules/catalog/services/internet-catalog.service.ts index 35e2ad29..f2c0a7d4 100644 --- a/apps/bff/src/catalog/services/internet-catalog.service.ts +++ b/apps/bff/src/modules/catalog/services/internet-catalog.service.ts @@ -1,7 +1,7 @@ import { Injectable, Inject } from "@nestjs/common"; import { BaseCatalogService } from "./base-catalog.service"; import { InternetProduct, fromSalesforceProduct2, SalesforceProduct2Record } from "@customer-portal/domain"; -import { MappingsService } from "../../id-mappings/mappings.service"; +import { MappingsService } from "@bff/modules/id-mappings/mappings.service"; import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service"; import { Logger } from "nestjs-pino"; import { getErrorMessage } from "@bff/core/utils/error.util"; diff --git a/apps/bff/src/catalog/services/sim-catalog.service.ts b/apps/bff/src/modules/catalog/services/sim-catalog.service.ts similarity index 98% rename from apps/bff/src/catalog/services/sim-catalog.service.ts rename to apps/bff/src/modules/catalog/services/sim-catalog.service.ts index 7288ec2b..5be47fbe 100644 --- a/apps/bff/src/catalog/services/sim-catalog.service.ts +++ b/apps/bff/src/modules/catalog/services/sim-catalog.service.ts @@ -1,7 +1,7 @@ import { Injectable, Inject } from "@nestjs/common"; import { BaseCatalogService } from "./base-catalog.service"; import { SimProduct, fromSalesforceProduct2, SalesforceProduct2Record } from "@customer-portal/domain"; -import { MappingsService } from "../../id-mappings/mappings.service"; +import { MappingsService } from "@bff/modules/id-mappings/mappings.service"; import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service"; import { Logger } from "nestjs-pino"; import { WhmcsConnectionService } from "@bff/integrations/whmcs/services/whmcs-connection.service"; diff --git a/apps/bff/src/catalog/services/vpn-catalog.service.ts b/apps/bff/src/modules/catalog/services/vpn-catalog.service.ts similarity index 100% rename from apps/bff/src/catalog/services/vpn-catalog.service.ts rename to apps/bff/src/modules/catalog/services/vpn-catalog.service.ts diff --git a/apps/bff/src/health/health.controller.ts b/apps/bff/src/modules/health/health.controller.ts similarity index 100% rename from apps/bff/src/health/health.controller.ts rename to apps/bff/src/modules/health/health.controller.ts diff --git a/apps/bff/src/health/health.module.ts b/apps/bff/src/modules/health/health.module.ts similarity index 100% rename from apps/bff/src/health/health.module.ts rename to apps/bff/src/modules/health/health.module.ts diff --git a/apps/bff/src/id-mappings/cache/mapping-cache.service.ts b/apps/bff/src/modules/id-mappings/cache/mapping-cache.service.ts similarity index 98% rename from apps/bff/src/id-mappings/cache/mapping-cache.service.ts rename to apps/bff/src/modules/id-mappings/cache/mapping-cache.service.ts index 4e26716a..efde9f5e 100644 --- a/apps/bff/src/id-mappings/cache/mapping-cache.service.ts +++ b/apps/bff/src/modules/id-mappings/cache/mapping-cache.service.ts @@ -1,6 +1,6 @@ import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; -import { CacheService } from "../../infra/cache/cache.service"; +import { CacheService } from "@bff/infra/cache/cache.service"; import { UserIdMapping } from "../types/mapping.types"; import { getErrorMessage } from "@bff/core/utils/error.util"; diff --git a/apps/bff/src/id-mappings/mappings.module.ts b/apps/bff/src/modules/id-mappings/mappings.module.ts similarity index 100% rename from apps/bff/src/id-mappings/mappings.module.ts rename to apps/bff/src/modules/id-mappings/mappings.module.ts diff --git a/apps/bff/src/id-mappings/mappings.service.ts b/apps/bff/src/modules/id-mappings/mappings.service.ts similarity index 100% rename from apps/bff/src/id-mappings/mappings.service.ts rename to apps/bff/src/modules/id-mappings/mappings.service.ts diff --git a/apps/bff/src/id-mappings/types/mapping.types.ts b/apps/bff/src/modules/id-mappings/types/mapping.types.ts similarity index 100% rename from apps/bff/src/id-mappings/types/mapping.types.ts rename to apps/bff/src/modules/id-mappings/types/mapping.types.ts diff --git a/apps/bff/src/id-mappings/validation/mapping-validator.service.ts b/apps/bff/src/modules/id-mappings/validation/mapping-validator.service.ts similarity index 100% rename from apps/bff/src/id-mappings/validation/mapping-validator.service.ts rename to apps/bff/src/modules/id-mappings/validation/mapping-validator.service.ts diff --git a/apps/bff/src/invoices/dto/invoice.dto.ts b/apps/bff/src/modules/invoices/dto/invoice.dto.ts similarity index 100% rename from apps/bff/src/invoices/dto/invoice.dto.ts rename to apps/bff/src/modules/invoices/dto/invoice.dto.ts diff --git a/apps/bff/src/invoices/invoices.controller.ts b/apps/bff/src/modules/invoices/invoices.controller.ts similarity index 100% rename from apps/bff/src/invoices/invoices.controller.ts rename to apps/bff/src/modules/invoices/invoices.controller.ts diff --git a/apps/bff/src/invoices/invoices.module.ts b/apps/bff/src/modules/invoices/invoices.module.ts similarity index 100% rename from apps/bff/src/invoices/invoices.module.ts rename to apps/bff/src/modules/invoices/invoices.module.ts diff --git a/apps/bff/src/invoices/invoices.service.ts b/apps/bff/src/modules/invoices/invoices.service.ts similarity index 100% rename from apps/bff/src/invoices/invoices.service.ts rename to apps/bff/src/modules/invoices/invoices.service.ts diff --git a/apps/bff/src/jobs/jobs.module.ts b/apps/bff/src/modules/jobs/jobs.module.ts similarity index 100% rename from apps/bff/src/jobs/jobs.module.ts rename to apps/bff/src/modules/jobs/jobs.module.ts diff --git a/apps/bff/src/jobs/jobs.service.ts b/apps/bff/src/modules/jobs/jobs.service.ts similarity index 100% rename from apps/bff/src/jobs/jobs.service.ts rename to apps/bff/src/modules/jobs/jobs.service.ts diff --git a/apps/bff/src/jobs/reconcile.processor.ts b/apps/bff/src/modules/jobs/reconcile.processor.ts similarity index 100% rename from apps/bff/src/jobs/reconcile.processor.ts rename to apps/bff/src/modules/jobs/reconcile.processor.ts diff --git a/apps/bff/src/orders/dto/order.dto.ts b/apps/bff/src/modules/orders/dto/order.dto.ts similarity index 100% rename from apps/bff/src/orders/dto/order.dto.ts rename to apps/bff/src/modules/orders/dto/order.dto.ts diff --git a/apps/bff/src/orders/orders.controller.ts b/apps/bff/src/modules/orders/orders.controller.ts similarity index 100% rename from apps/bff/src/orders/orders.controller.ts rename to apps/bff/src/modules/orders/orders.controller.ts diff --git a/apps/bff/src/orders/orders.module.ts b/apps/bff/src/modules/orders/orders.module.ts similarity index 100% rename from apps/bff/src/orders/orders.module.ts rename to apps/bff/src/modules/orders/orders.module.ts diff --git a/apps/bff/src/orders/queue/provisioning.processor.ts b/apps/bff/src/modules/orders/queue/provisioning.processor.ts similarity index 100% rename from apps/bff/src/orders/queue/provisioning.processor.ts rename to apps/bff/src/modules/orders/queue/provisioning.processor.ts diff --git a/apps/bff/src/orders/queue/provisioning.queue.ts b/apps/bff/src/modules/orders/queue/provisioning.queue.ts similarity index 100% rename from apps/bff/src/orders/queue/provisioning.queue.ts rename to apps/bff/src/modules/orders/queue/provisioning.queue.ts diff --git a/apps/bff/src/orders/services/order-builder.service.ts b/apps/bff/src/modules/orders/services/order-builder.service.ts similarity index 99% rename from apps/bff/src/orders/services/order-builder.service.ts rename to apps/bff/src/modules/orders/services/order-builder.service.ts index b09526a4..38a435da 100644 --- a/apps/bff/src/orders/services/order-builder.service.ts +++ b/apps/bff/src/modules/orders/services/order-builder.service.ts @@ -2,7 +2,7 @@ import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { CreateOrderBody, UserMapping } from "../dto/order.dto"; import { getSalesforceFieldMap } from "@bff/core/config/field-map"; -import { UsersService } from "../../users/users.service"; +import { UsersService } from "@bff/modules/users/users.service"; /** * Handles building order header data from selections diff --git a/apps/bff/src/orders/services/order-fulfillment-error.service.ts b/apps/bff/src/modules/orders/services/order-fulfillment-error.service.ts similarity index 100% rename from apps/bff/src/orders/services/order-fulfillment-error.service.ts rename to apps/bff/src/modules/orders/services/order-fulfillment-error.service.ts diff --git a/apps/bff/src/orders/services/order-fulfillment-orchestrator.service.ts b/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts similarity index 100% rename from apps/bff/src/orders/services/order-fulfillment-orchestrator.service.ts rename to apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts diff --git a/apps/bff/src/orders/services/order-fulfillment-validator.service.ts b/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts similarity index 98% rename from apps/bff/src/orders/services/order-fulfillment-validator.service.ts rename to apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts index af26eaf5..3e53d141 100644 --- a/apps/bff/src/orders/services/order-fulfillment-validator.service.ts +++ b/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts @@ -2,7 +2,7 @@ import { Injectable, BadRequestException, ConflictException, Inject } from "@nes import { Logger } from "nestjs-pino"; import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service"; import { WhmcsPaymentService } from "@bff/integrations/whmcs/services/whmcs-payment.service"; -import { MappingsService } from "../../id-mappings/mappings.service"; +import { MappingsService } from "@bff/modules/id-mappings/mappings.service"; import { getErrorMessage } from "@bff/core/utils/error.util"; import { SalesforceOrder } from "@customer-portal/domain"; import { getSalesforceFieldMap } from "@bff/core/config/field-map"; diff --git a/apps/bff/src/orders/services/order-item-builder.service.ts b/apps/bff/src/modules/orders/services/order-item-builder.service.ts similarity index 100% rename from apps/bff/src/orders/services/order-item-builder.service.ts rename to apps/bff/src/modules/orders/services/order-item-builder.service.ts diff --git a/apps/bff/src/orders/services/order-orchestrator.service.ts b/apps/bff/src/modules/orders/services/order-orchestrator.service.ts similarity index 100% rename from apps/bff/src/orders/services/order-orchestrator.service.ts rename to apps/bff/src/modules/orders/services/order-orchestrator.service.ts diff --git a/apps/bff/src/orders/services/order-validator.service.ts b/apps/bff/src/modules/orders/services/order-validator.service.ts similarity index 99% rename from apps/bff/src/orders/services/order-validator.service.ts rename to apps/bff/src/modules/orders/services/order-validator.service.ts index b465f31b..c2786abf 100644 --- a/apps/bff/src/orders/services/order-validator.service.ts +++ b/apps/bff/src/modules/orders/services/order-validator.service.ts @@ -1,6 +1,6 @@ import { Injectable, BadRequestException, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; -import { MappingsService } from "../../id-mappings/mappings.service"; +import { MappingsService } from "@bff/modules/id-mappings/mappings.service"; import { WhmcsConnectionService } from "@bff/integrations/whmcs/services/whmcs-connection.service"; import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service"; import { getSalesforceFieldMap } from "@bff/core/config/field-map"; diff --git a/apps/bff/src/orders/services/order-whmcs-mapper.service.ts b/apps/bff/src/modules/orders/services/order-whmcs-mapper.service.ts similarity index 100% rename from apps/bff/src/orders/services/order-whmcs-mapper.service.ts rename to apps/bff/src/modules/orders/services/order-whmcs-mapper.service.ts diff --git a/apps/bff/src/orders/services/sim-fulfillment.service.ts b/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts similarity index 100% rename from apps/bff/src/orders/services/sim-fulfillment.service.ts rename to apps/bff/src/modules/orders/services/sim-fulfillment.service.ts diff --git a/apps/bff/src/orders/types/order-details.dto.ts b/apps/bff/src/modules/orders/types/order-details.dto.ts similarity index 100% rename from apps/bff/src/orders/types/order-details.dto.ts rename to apps/bff/src/modules/orders/types/order-details.dto.ts diff --git a/apps/bff/src/subscriptions/dto/sim-cancel.dto.ts b/apps/bff/src/modules/subscriptions/dto/sim-cancel.dto.ts similarity index 100% rename from apps/bff/src/subscriptions/dto/sim-cancel.dto.ts rename to apps/bff/src/modules/subscriptions/dto/sim-cancel.dto.ts diff --git a/apps/bff/src/subscriptions/dto/sim-change-plan.dto.ts b/apps/bff/src/modules/subscriptions/dto/sim-change-plan.dto.ts similarity index 100% rename from apps/bff/src/subscriptions/dto/sim-change-plan.dto.ts rename to apps/bff/src/modules/subscriptions/dto/sim-change-plan.dto.ts diff --git a/apps/bff/src/subscriptions/dto/sim-features.dto.ts b/apps/bff/src/modules/subscriptions/dto/sim-features.dto.ts similarity index 100% rename from apps/bff/src/subscriptions/dto/sim-features.dto.ts rename to apps/bff/src/modules/subscriptions/dto/sim-features.dto.ts diff --git a/apps/bff/src/subscriptions/dto/sim-topup.dto.ts b/apps/bff/src/modules/subscriptions/dto/sim-topup.dto.ts similarity index 100% rename from apps/bff/src/subscriptions/dto/sim-topup.dto.ts rename to apps/bff/src/modules/subscriptions/dto/sim-topup.dto.ts diff --git a/apps/bff/src/subscriptions/sim-management.service.ts b/apps/bff/src/modules/subscriptions/sim-management.service.ts similarity index 100% rename from apps/bff/src/subscriptions/sim-management.service.ts rename to apps/bff/src/modules/subscriptions/sim-management.service.ts diff --git a/apps/bff/src/subscriptions/sim-order-activation.service.ts b/apps/bff/src/modules/subscriptions/sim-order-activation.service.ts similarity index 100% rename from apps/bff/src/subscriptions/sim-order-activation.service.ts rename to apps/bff/src/modules/subscriptions/sim-order-activation.service.ts diff --git a/apps/bff/src/subscriptions/sim-orders.controller.ts b/apps/bff/src/modules/subscriptions/sim-orders.controller.ts similarity index 100% rename from apps/bff/src/subscriptions/sim-orders.controller.ts rename to apps/bff/src/modules/subscriptions/sim-orders.controller.ts diff --git a/apps/bff/src/subscriptions/sim-usage-store.service.ts b/apps/bff/src/modules/subscriptions/sim-usage-store.service.ts similarity index 100% rename from apps/bff/src/subscriptions/sim-usage-store.service.ts rename to apps/bff/src/modules/subscriptions/sim-usage-store.service.ts diff --git a/apps/bff/src/subscriptions/subscriptions.controller.ts b/apps/bff/src/modules/subscriptions/subscriptions.controller.ts similarity index 100% rename from apps/bff/src/subscriptions/subscriptions.controller.ts rename to apps/bff/src/modules/subscriptions/subscriptions.controller.ts diff --git a/apps/bff/src/subscriptions/subscriptions.module.ts b/apps/bff/src/modules/subscriptions/subscriptions.module.ts similarity index 100% rename from apps/bff/src/subscriptions/subscriptions.module.ts rename to apps/bff/src/modules/subscriptions/subscriptions.module.ts diff --git a/apps/bff/src/subscriptions/subscriptions.service.ts b/apps/bff/src/modules/subscriptions/subscriptions.service.ts similarity index 100% rename from apps/bff/src/subscriptions/subscriptions.service.ts rename to apps/bff/src/modules/subscriptions/subscriptions.service.ts diff --git a/apps/bff/src/users/dto/update-address.dto.ts b/apps/bff/src/modules/users/dto/update-address.dto.ts similarity index 100% rename from apps/bff/src/users/dto/update-address.dto.ts rename to apps/bff/src/modules/users/dto/update-address.dto.ts diff --git a/apps/bff/src/users/dto/update-user.dto.ts b/apps/bff/src/modules/users/dto/update-user.dto.ts similarity index 100% rename from apps/bff/src/users/dto/update-user.dto.ts rename to apps/bff/src/modules/users/dto/update-user.dto.ts diff --git a/apps/bff/src/users/users.controller.ts b/apps/bff/src/modules/users/users.controller.ts similarity index 100% rename from apps/bff/src/users/users.controller.ts rename to apps/bff/src/modules/users/users.controller.ts diff --git a/apps/bff/src/users/users.module.ts b/apps/bff/src/modules/users/users.module.ts similarity index 100% rename from apps/bff/src/users/users.module.ts rename to apps/bff/src/modules/users/users.module.ts diff --git a/apps/bff/src/users/users.service.ts b/apps/bff/src/modules/users/users.service.ts similarity index 100% rename from apps/bff/src/users/users.service.ts rename to apps/bff/src/modules/users/users.service.ts diff --git a/apps/bff/tsconfig.base.json b/apps/bff/tsconfig.base.json index 793210c9..ed011ac2 100644 --- a/apps/bff/tsconfig.base.json +++ b/apps/bff/tsconfig.base.json @@ -9,7 +9,7 @@ "@/*": ["src/*"], "@bff/core/*": ["src/core/*"], "@bff/infra/*": ["src/infra/*"], - "@bff/modules/*": ["src/*"], + "@bff/modules/*": ["src/modules/*"], "@bff/integrations/*": ["src/integrations/*"] }, "strict": true, diff --git a/apps/bff/tsconfig.build.json b/apps/bff/tsconfig.build.json index e985ff41..06a460bc 100644 --- a/apps/bff/tsconfig.build.json +++ b/apps/bff/tsconfig.build.json @@ -15,7 +15,7 @@ "@/*": ["src/*"], "@bff/core/*": ["src/core/*"], "@bff/infra/*": ["src/infra/*"], - "@bff/modules/*": ["src/*"], + "@bff/modules/*": ["src/modules/*"], "@bff/integrations/*": ["src/integrations/*"] } }, diff --git a/apps/bff/tsconfig.memory.json b/apps/bff/tsconfig.memory.json deleted file mode 100644 index dcdd641d..00000000 --- a/apps/bff/tsconfig.memory.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "noEmit": true, - // Memory optimization settings - "incremental": true, - "tsBuildInfoFile": "./.tsbuildinfo-memory", - "skipLibCheck": true, - "skipDefaultLibCheck": true, - // Reduce type checking strictness for memory optimization - "noImplicitAny": false, - "strictNullChecks": false, - "strictBindCallApply": false, - "noImplicitReturns": false, - "noFallthroughCasesInSwitch": false, - // Disable some expensive checks - "noUnusedLocals": false, - "noUnusedParameters": false, - "exactOptionalPropertyTypes": false, - "noImplicitOverride": false - }, - "include": ["src/**/*"], - "exclude": [ - "node_modules", - "dist", - "test", - "**/*.spec.ts", - "**/*.test.ts", - "**/*.e2e-spec.ts" - ] -} diff --git a/apps/bff/tsconfig.ultra-light.json b/apps/bff/tsconfig.ultra-light.json deleted file mode 100644 index 85f1320b..00000000 --- a/apps/bff/tsconfig.ultra-light.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020"], - "module": "CommonJS", - "moduleResolution": "node", - "baseUrl": "./", - "paths": { - "@/*": ["src/*"], - "@bff/core/*": ["src/core/*"], - "@bff/infra/*": ["src/infra/*"], - "@bff/modules/*": ["src/*"], - "@bff/integrations/*": ["src/integrations/*"] - }, - "noEmit": true, - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "types": ["node"], - - // Ultra-light settings - disable most type checking for memory - "strict": false, - "noImplicitAny": false, - "strictNullChecks": false, - "strictBindCallApply": false, - "noImplicitReturns": false, - "noFallthroughCasesInSwitch": false, - "noImplicitOverride": false, - "strictPropertyInitialization": false, - "noUnusedLocals": false, - "noUnusedParameters": false, - "exactOptionalPropertyTypes": false, - "noImplicitThis": false, - "alwaysStrict": false, - "strictFunctionTypes": false, - "useUnknownInCatchVariables": false, - - // Performance optimizations - "incremental": false, - "assumeChangesOnlyAffectDirectDependencies": true, - "disableReferencedProjectLoad": true, - "disableSolutionSearching": true, - "disableSourceOfProjectReferenceRedirect": true - }, - "include": ["src/**/*"], - "exclude": [ - "node_modules", - "dist", - "test", - "**/*.spec.ts", - "**/*.test.ts", - "**/*.e2e-spec.ts", - "src/**/*.spec.ts", - "src/**/*.test.ts" - ] -} diff --git a/apps/portal/next.config.mjs b/apps/portal/next.config.mjs index 1ef54a7b..8c1c8cc9 100644 --- a/apps/portal/next.config.mjs +++ b/apps/portal/next.config.mjs @@ -7,7 +7,7 @@ const nextConfig = { output: process.env.NODE_ENV === "production" ? "standalone" : undefined, // Ensure workspace package resolves/transpiles correctly in monorepo - transpilePackages: ["@customer-portal/shared"], + transpilePackages: [], experimental: { externalDir: true, }, diff --git a/apps/portal/package.json b/apps/portal/package.json index 2444ab40..ac729668 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -14,7 +14,6 @@ "test": "echo 'No tests yet'" }, "dependencies": { - "@customer-portal/shared": "workspace:*", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^5.2.1", "@tanstack/react-query": "^5.85.5", diff --git a/apps/portal/src/app/(portal)/account/billing/page.tsx b/apps/portal/src/app/(portal)/account/billing/page.tsx deleted file mode 100644 index 401a6d03..00000000 --- a/apps/portal/src/app/(portal)/account/billing/page.tsx +++ /dev/null @@ -1,431 +0,0 @@ -"use client"; - -import { useState, useEffect, Suspense } from "react"; -import { useSearchParams } from "next/navigation"; -import { authenticatedApi } from "@/lib/api"; -import { - CreditCardIcon, - MapPinIcon, - PencilIcon, - CheckIcon, - XMarkIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/outline"; - -interface Address { - street: string | null; - streetLine2: string | null; - city: string | null; - state: string | null; - postalCode: string | null; - country: string | null; -} - -interface BillingInfo { - company: string | null; - email: string; - phone: string | null; - address: Address; - isComplete: boolean; -} - -function BillingHeading() { - const searchParams = useSearchParams(); - const isCompletionFlow = searchParams.get("complete") === "true"; - return ( -

- {isCompletionFlow ? "Complete Your Profile" : "Billing & Address"} -

- ); -} - -function BillingCompletionBanner() { - const searchParams = useSearchParams(); - const isCompletionFlow = searchParams.get("complete") === "true"; - if (!isCompletionFlow) return null; - return ( -
-
-
- -
-
-

Profile Completion Required

-

- Please review and complete your address information to access all features and enable - service ordering. -

-
-
-
- ); -} - -export default function BillingPage() { - const [billingInfo, setBillingInfo] = useState(null); - const [loading, setLoading] = useState(true); - const [editing, setEditing] = useState(false); - const [editedAddress, setEditedAddress] = useState
(null); - const [error, setError] = useState(null); - const [saving, setSaving] = useState(false); - - useEffect(() => { - void fetchBillingInfo(); - }, []); - - const fetchBillingInfo = async () => { - try { - setLoading(true); - const data = await authenticatedApi.get("/me/billing"); - setBillingInfo(data); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load billing information"); - } finally { - setLoading(false); - } - }; - - const handleEdit = () => { - setEditing(true); - setEditedAddress( - billingInfo?.address || { - street: "", - streetLine2: "", - city: "", - state: "", - postalCode: "", - country: "", - } - ); - }; - - const handleSave = async () => { - if (!editedAddress) return; - - // Validate required fields - const isComplete = !!( - editedAddress.street?.trim() && - editedAddress.city?.trim() && - editedAddress.state?.trim() && - editedAddress.postalCode?.trim() && - editedAddress.country?.trim() - ); - - if (!isComplete) { - setError("Please fill in all required address fields"); - return; - } - - try { - setSaving(true); - setError(null); - - // Update address via API - const updated = await authenticatedApi.patch("/me/address", { - street: editedAddress.street, - streetLine2: editedAddress.streetLine2, - city: editedAddress.city, - state: editedAddress.state, - postalCode: editedAddress.postalCode, - country: editedAddress.country, - }); - - // Update local state from authoritative response - setBillingInfo(updated); - - setEditing(false); - setEditedAddress(null); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to update address"); - } finally { - setSaving(false); - } - }; - - const handleCancel = () => { - setEditing(false); - setEditedAddress(null); - setError(null); - }; - - if (loading) { - return ( -
-
-
- Loading billing information... -
-
- ); - } - - return ( - <> -
-
- - Billing & Address} - > - - -
- - - - - - {error && ( -
-
- -
-

Error

-

{error}

-
-
-
- )} - -
- {/* Address Information */} -
-
-
- -

Service Address

-
- {billingInfo?.isComplete && !editing && ( - - )} -
- - {/* Address is required at signup, so this should rarely be needed */} - - {editing ? ( -
-
- - - setEditedAddress(prev => (prev ? { ...prev, street: e.target.value } : null)) - } - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="123 Main Street" - /> -
- -
- - - setEditedAddress(prev => - prev ? { ...prev, streetLine2: e.target.value } : null - ) - } - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Apartment, suite, etc. (optional)" - /> -
- -
-
- - - setEditedAddress(prev => (prev ? { ...prev, city: e.target.value } : null)) - } - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Tokyo" - /> -
- -
- - - setEditedAddress(prev => (prev ? { ...prev, state: e.target.value } : null)) - } - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Tokyo" - /> -
-
- -
-
- - - setEditedAddress(prev => - prev ? { ...prev, postalCode: e.target.value } : null - ) - } - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="100-0001" - /> -
- -
- - -
-
- -
- - -
-
- ) : ( -
- {billingInfo?.address.street ? ( -
-
-

{billingInfo.address.street}

- {billingInfo.address.streetLine2 &&

{billingInfo.address.streetLine2}

} -

- {billingInfo.address.city}, {billingInfo.address.state}{" "} - {billingInfo.address.postalCode} -

-

{billingInfo.address.country}

-
- -
-
- {billingInfo.isComplete ? ( - <> - - - Address Complete - - - ) : ( - <> - - - Address Incomplete - - - )} -
-
-
- ) : ( -
- -

No address on file

- -
- )} -
- )} -
- - {/* Contact Information */} -
-
- -

Contact Information

-
- -
-
- -
-

{billingInfo?.email}

-
-
- - {billingInfo?.company && ( -
- -
-

{billingInfo.company}

-
-
- )} - - {billingInfo?.phone && ( -
- -
-

{billingInfo.phone}

-
-
- )} -
- -
-

- Note: Contact information is managed through your account settings. - Address changes are synchronized with our billing system. This address is used for - both billing and service delivery. -

-
-
-
-
- - ); -} diff --git a/apps/portal/src/app/(portal)/dashboard/page.tsx b/apps/portal/src/app/(portal)/dashboard/page.tsx index 8647bb48..58e59c33 100644 --- a/apps/portal/src/app/(portal)/dashboard/page.tsx +++ b/apps/portal/src/app/(portal)/dashboard/page.tsx @@ -1,13 +1,11 @@ "use client"; -import { logger } from "@/lib/logger"; - import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useAuthStore } from "@/lib/auth/store"; -import { useDashboardSummary } from "@/features/dashboard/hooks"; +import { useAuthStore } from "@/features/auth/services/auth.store"; +import { useDashboardSummary } from "@/features/dashboard/hooks/useDashboard"; -import type { Activity } from "@customer-portal/shared"; +import type { Activity } from "@customer-portal/domain"; import { CreditCardIcon, ServerIcon, @@ -31,11 +29,11 @@ import { format, formatDistanceToNow } from "date-fns"; import { StatCard, QuickAction, DashboardActivityItem } from "@/features/dashboard/components"; import { LoadingSpinner } from "@/components/ui/loading-skeleton"; import { ErrorState } from "@/components/ui/error-state"; -import { formatCurrency, getCurrencyLocale } from "@/utils/currency"; +import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; export default function DashboardPage() { const router = useRouter(); - const { user, isAuthenticated, isLoading: authLoading } = useAuthStore(); + const { user, isAuthenticated, loading: authLoading } = useAuthStore(); const { data: summary, isLoading: summaryLoading, error } = useDashboardSummary(); const [paymentLoading, setPaymentLoading] = useState(false); @@ -48,11 +46,11 @@ export default function DashboardPage() { void (async () => { try { - const { createInvoiceSsoLink } = await import("@/hooks/useInvoices"); - const ssoLink = await createInvoiceSsoLink(invoiceId, "pay"); + const { BillingService } = await import("@/features/billing/services"); + const ssoLink = await BillingService.createInvoiceSsoLink({ invoiceId, target: "pay" }); window.open(ssoLink.url, "_blank", "noopener,noreferrer"); } catch (error) { - logger.error(error, "Failed to create payment link"); + console.error("Failed to create payment link:", error); setPaymentError(error instanceof Error ? error.message : "Failed to open payment page"); } finally { setPaymentLoading(false); diff --git a/apps/portal/src/app/(portal)/support/new/page.tsx b/apps/portal/src/app/(portal)/support/new/page.tsx index df85cfd4..aa113065 100644 --- a/apps/portal/src/app/(portal)/support/new/page.tsx +++ b/apps/portal/src/app/(portal)/support/new/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { logger } from "@/lib/logger"; +import { logger } from "@/core/config"; import { useState } from "react"; import { useRouter } from "next/navigation"; diff --git a/apps/portal/src/app/layout.tsx b/apps/portal/src/app/layout.tsx index 70fc6c3c..d85c4ebf 100644 --- a/apps/portal/src/app/layout.tsx +++ b/apps/portal/src/app/layout.tsx @@ -1,8 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import { QueryProvider } from "@/providers/query-provider"; -import { SessionTimeoutWarning } from "@/components/auth/session-timeout-warning"; +import { QueryProvider } from "@/core/providers"; +import { SessionTimeoutWarning } from "@/features/auth/components/SessionTimeoutWarning"; const geistSans = Geist({ variable: "--font-geist-sans", diff --git a/apps/portal/src/components/auth/auth-layout.tsx b/apps/portal/src/components/auth/auth-layout.tsx deleted file mode 100644 index 9cf84388..00000000 --- a/apps/portal/src/components/auth/auth-layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Link from "next/link"; - -interface AuthLayoutProps { - children: React.ReactNode; - title: string; - subtitle?: string; -} - -export function AuthLayout({ children, title, subtitle }: AuthLayoutProps) { - return ( -
-
- {/* Header */} -
- -
-
- AS -
- Assist Solutions -
- -

{title}

- {subtitle &&

{subtitle}

} -
- - {/* Content */} -
{children}
- - {/* Footer */} -
-

© 2025 Assist Solutions. All rights reserved.

-
-
-
- ); -} diff --git a/apps/portal/src/components/auth/session-timeout-warning.tsx b/apps/portal/src/components/auth/session-timeout-warning.tsx deleted file mode 100644 index 870de726..00000000 --- a/apps/portal/src/components/auth/session-timeout-warning.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; -import { logger } from "@/lib/logger"; - -import { useEffect, useState } from "react"; -import { useAuthStore } from "@/lib/auth/store"; -import { Button } from "@/components/ui/button"; - -interface SessionTimeoutWarningProps { - warningTime?: number; // Minutes before token expires to show warning -} - -export function SessionTimeoutWarning({ - warningTime = 10, // Show warning 10 minutes before expiry -}: SessionTimeoutWarningProps) { - const { isAuthenticated, token, logout, checkAuth } = useAuthStore(); - const [showWarning, setShowWarning] = useState(false); - const [timeLeft, setTimeLeft] = useState(0); - - useEffect(() => { - if (!isAuthenticated || !token) { - return undefined; - } - - // Parse JWT to get expiry time - try { - const parts = token.split("."); - if (parts.length !== 3) { - throw new Error("Invalid token format"); - } - - const payload = JSON.parse(atob(parts[1])) as { exp?: number }; - if (!payload.exp) { - logger.warn("Token does not have expiration time"); - return undefined; - } - - const expiryTime = payload.exp * 1000; // Convert to milliseconds - const currentTime = Date.now(); - const warningThreshold = warningTime * 60 * 1000; // Convert to milliseconds - - const timeUntilExpiry = expiryTime - currentTime; - const timeUntilWarning = timeUntilExpiry - warningThreshold; - - if (timeUntilExpiry <= 0) { - // Token already expired - void logout(); - return undefined; - } - - if (timeUntilWarning <= 0) { - // Should show warning immediately - setShowWarning(true); - setTimeLeft(Math.ceil(timeUntilExpiry / 1000 / 60)); // Minutes left - return undefined; - } else { - // Set timeout to show warning - const warningTimeout = setTimeout(() => { - setShowWarning(true); - setTimeLeft(warningTime); - }, timeUntilWarning); - - return () => clearTimeout(warningTimeout); - } - } catch (error) { - logger.error(error, "Error parsing JWT token"); - void logout(); - return undefined; - } - }, [isAuthenticated, token, warningTime, logout]); - - useEffect(() => { - if (!showWarning) return undefined; - - const interval = setInterval(() => { - setTimeLeft(prev => { - if (prev <= 1) { - void logout(); - return 0; - } - return prev - 1; - }); - }, 60000); - - return () => clearInterval(interval); - }, [showWarning, logout]); - - const handleExtendSession = () => { - void (async () => { - try { - await checkAuth(); - setShowWarning(false); - setTimeLeft(0); - } catch (error) { - logger.error(error, "Failed to extend session"); - await logout(); - } - })(); - }; - - const handleLogoutNow = () => { - void logout(); - setShowWarning(false); - }; - - if (!showWarning) { - return null; - } - - return ( -
-
-
- ⚠️ -

Session Expiring Soon

-
- -

- Your session will expire in{" "} - - {timeLeft} minute{timeLeft !== 1 ? "s" : ""} - - . Would you like to extend your session? -

- -
- - -
-
-
- ); -} diff --git a/apps/portal/src/components/catalog/activation-form.tsx b/apps/portal/src/components/catalog/activation-form.tsx deleted file mode 100644 index f0a80f47..00000000 --- a/apps/portal/src/components/catalog/activation-form.tsx +++ /dev/null @@ -1,92 +0,0 @@ -interface ActivationFormProps { - activationType: "Immediate" | "Scheduled"; - onActivationTypeChange: (type: "Immediate" | "Scheduled") => void; - scheduledActivationDate: string; - onScheduledActivationDateChange: (date: string) => void; - errors: Record; -} - -export function ActivationForm({ - activationType, - onActivationTypeChange, - scheduledActivationDate, - onScheduledActivationDateChange, - errors, -}: ActivationFormProps) { - return ( -
- - - -
- ); -} diff --git a/apps/portal/src/components/catalog/addon-group.tsx b/apps/portal/src/components/catalog/addon-group.tsx deleted file mode 100644 index bf8c6410..00000000 --- a/apps/portal/src/components/catalog/addon-group.tsx +++ /dev/null @@ -1,196 +0,0 @@ -"use client"; - -import { CheckCircleIcon } from "@heroicons/react/24/solid"; - -interface AddonItem { - id: string; - name: string; - sku: string; - description: string; - monthlyPrice?: number; - oneTimePrice?: number; - isBundledAddon?: boolean; - bundledAddonId?: string; - displayOrder?: number; -} - -interface AddonGroup { - id: string; - name: string; - description: string; - monthlyPrice?: number; - oneTimePrice?: number; - skus: string[]; - isBundled: boolean; -} - -interface AddonGroupProps { - addons: AddonItem[]; - selectedAddonSkus: string[]; - onAddonToggle: (skus: string[]) => void; - showSkus?: boolean; -} - -export function AddonGroup({ - addons, - selectedAddonSkus, - onAddonToggle, - showSkus = false, -}: AddonGroupProps) { - // Group bundled addons together - const groupedAddons = (() => { - const groups: AddonGroup[] = []; - const processedAddonIds = new Set(); - - // Sort addons by display order first - const sortedAddons = [...addons].sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0)); - - sortedAddons.forEach(addon => { - if (processedAddonIds.has(addon.id)) return; - - if (addon.isBundledAddon && addon.bundledAddonId) { - // Find the bundled partner - const bundledPartner = sortedAddons.find(a => a.id === addon.bundledAddonId); - - if (bundledPartner && !processedAddonIds.has(bundledPartner.id)) { - // Create a combined group - const monthlyAddon = addon.monthlyPrice ? addon : bundledPartner; - const activationAddon = addon.oneTimePrice ? addon : bundledPartner; - - // Generate clean name and description - const cleanName = monthlyAddon.name - .replace(/\s*(Monthly|Installation|Fee)\s*/gi, "") - .trim(); - const bundleName = cleanName || monthlyAddon.name.split(" ").slice(0, 2).join(" "); // Use first two words if cleaning removes everything - - groups.push({ - id: `bundle-${addon.id}-${bundledPartner.id}`, - name: bundleName, - description: `${bundleName} (installation included)`, - monthlyPrice: monthlyAddon.monthlyPrice, - oneTimePrice: activationAddon.oneTimePrice, - skus: [addon.sku, bundledPartner.sku], - isBundled: true, - }); - - processedAddonIds.add(addon.id); - processedAddonIds.add(bundledPartner.id); - } else if (!bundledPartner) { - // Orphaned bundled addon - treat as individual - groups.push({ - id: addon.id, - name: addon.name, - description: addon.description, - monthlyPrice: addon.monthlyPrice, - oneTimePrice: addon.oneTimePrice, - skus: [addon.sku], - isBundled: false, - }); - processedAddonIds.add(addon.id); - } - } else { - // Individual addon - groups.push({ - id: addon.id, - name: addon.name, - description: addon.description, - monthlyPrice: addon.monthlyPrice, - oneTimePrice: addon.oneTimePrice, - skus: [addon.sku], - isBundled: false, - }); - processedAddonIds.add(addon.id); - } - }); - - return groups; - })(); - - const handleGroupToggle = (addonGroup: AddonGroup) => { - const allSkusSelected = addonGroup.skus.every(sku => selectedAddonSkus.includes(sku)); - - if (allSkusSelected) { - // Unselect all SKUs in the bundle - const remainingSkus = selectedAddonSkus.filter(sku => !addonGroup.skus.includes(sku)); - onAddonToggle(remainingSkus); - } else { - // Select all SKUs in the bundle - const filtered = selectedAddonSkus.filter(sku => !addonGroup.skus.includes(sku)); - onAddonToggle([...filtered, ...addonGroup.skus]); - } - }; - - if (groupedAddons.length === 0) { - return ( -
-

No add-ons available for this plan

-
- ); - } - - return ( -
- {groupedAddons.map(addonGroup => { - const allSkusSelected = addonGroup.skus.every(sku => selectedAddonSkus.includes(sku)); - - return ( - - ); - })} - - {selectedAddonSkus.length === 0 && ( -
-

Select add-ons to enhance your service

-
- )} -
- ); -} diff --git a/apps/portal/src/components/catalog/animated-button.tsx b/apps/portal/src/components/catalog/animated-button.tsx deleted file mode 100644 index 29582460..00000000 --- a/apps/portal/src/components/catalog/animated-button.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { ReactNode } from "react"; -import Link from "next/link"; - -interface AnimatedButtonProps { - children: ReactNode; - variant?: "primary" | "secondary" | "outline"; - size?: "sm" | "md" | "lg"; - className?: string; - onClick?: () => void; - href?: string; - disabled?: boolean; - type?: "button" | "submit"; -} - -export function AnimatedButton({ - children, - variant = "primary", - size = "md", - className = "", - onClick, - href, - disabled = false, - type = "button", -}: AnimatedButtonProps) { - const baseClasses = - "inline-flex items-center justify-center font-medium rounded-lg transition-all duration-300 ease-in-out transform focus:outline-none focus:ring-2 focus:ring-offset-2"; - - const sizeClasses = { - sm: "px-3 py-2 text-sm", - md: "px-6 py-3 text-base", - lg: "px-8 py-4 text-lg", - }; - - const variantClasses = { - primary: - "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 hover:scale-105 hover:shadow-lg", - secondary: - "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500 hover:scale-105 hover:shadow-lg", - outline: - "border-2 border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-blue-500 hover:scale-105", - }; - - const disabledClasses = disabled - ? "opacity-50 cursor-not-allowed transform-none hover:scale-100 hover:shadow-none" - : ""; - - const allClasses = `${baseClasses} ${sizeClasses[size]} ${variantClasses[variant]} ${disabledClasses} ${className}`; - - if (href && !disabled) { - return ( - - {children} - - ); - } - - return ( - - ); -} diff --git a/apps/portal/src/components/catalog/animated-card.tsx b/apps/portal/src/components/catalog/animated-card.tsx deleted file mode 100644 index 965933cc..00000000 --- a/apps/portal/src/components/catalog/animated-card.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ReactNode } from "react"; - -interface AnimatedCardProps { - children: ReactNode; - className?: string; - variant?: "default" | "highlighted" | "success" | "static"; - onClick?: () => void; - disabled?: boolean; -} - -export function AnimatedCard({ - children, - className = "", - variant = "default", - onClick, - disabled = false, -}: AnimatedCardProps) { - const baseClasses = - "bg-white rounded-xl border-2 shadow-sm transition-all duration-300 ease-in-out transform"; - - const variantClasses: Record<"default" | "highlighted" | "success" | "static", string> = { - default: "border-gray-200 hover:shadow-xl hover:-translate-y-1", - highlighted: - "border-blue-300 ring-2 ring-blue-100 shadow-md hover:shadow-xl hover:-translate-y-1", - success: - "border-green-300 ring-2 ring-green-100 shadow-md hover:shadow-xl hover:-translate-y-1", - static: "border-gray-200 shadow-sm", // No hover animations for static containers - }; - - const interactiveClasses = onClick && !disabled ? "cursor-pointer hover:scale-[1.02]" : ""; - - const disabledClasses = disabled ? "opacity-50 cursor-not-allowed" : ""; - - return ( -
- {children} -
- ); -} diff --git a/apps/portal/src/components/catalog/installation-options.tsx b/apps/portal/src/components/catalog/installation-options.tsx deleted file mode 100644 index baad9dd0..00000000 --- a/apps/portal/src/components/catalog/installation-options.tsx +++ /dev/null @@ -1,130 +0,0 @@ -"use client"; - -interface InstallationOption { - id: string; - name: string; - sku: string; - type: string; - price?: number; - billingCycle: string; - description: string; - displayOrder?: number; -} - -interface InstallationOptionsProps { - installations: InstallationOption[]; - selectedInstallation: InstallationOption | null; - onInstallationSelect: (installation: InstallationOption) => void; - showSkus?: boolean; -} - -export function InstallationOptions({ - installations, - selectedInstallation, - onInstallationSelect, - showSkus = false, -}: InstallationOptionsProps) { - // Sort by display order - const sortedInstallations = [...installations].sort( - (a, b) => (a.displayOrder || 0) - (b.displayOrder || 0) - ); - - // Clean up installation names to avoid duplication - const getCleanName = (installation: InstallationOption) => { - let name = installation.name; - - // Remove common prefixes/suffixes that might be duplicated - name = name.replace(/^(NTT\s*)?Installation\s*Fee\s*/i, ""); - - // Add proper prefixes based on type - switch (installation.type) { - case "One-time": - return "Installation Fee (Single Payment)"; - case "12-Month": - return "Installation Fee (12-Month Plan)"; - case "24-Month": - return "Installation Fee (24-Month Plan)"; - default: - return name || installation.type; - } - }; - - const getCleanDescription = (installation: InstallationOption) => { - let description = installation.description; - - // Remove duplicated text that might already be in the name - description = description.replace(/^(NTT\s*)?Installation\s*Fee\s*/i, ""); - - // Generate a clean description based on type - switch (installation.type) { - case "One-time": - return "Pay the full installation fee upfront"; - case "12-Month": - return "Spread installation cost over 12 monthly payments"; - case "24-Month": - return "Spread installation cost over 24 monthly payments"; - default: - return description || `${installation.type} installation option`; - } - }; - - if (sortedInstallations.length === 0) { - return ( -
-

No installation options available. Please contact support.

-
- ); - } - - return ( -
- {sortedInstallations.map(installation => ( - - ))} -
- ); -} diff --git a/apps/portal/src/components/catalog/loading-spinner.tsx b/apps/portal/src/components/catalog/loading-spinner.tsx deleted file mode 100644 index 3153d6a8..00000000 --- a/apps/portal/src/components/catalog/loading-spinner.tsx +++ /dev/null @@ -1,26 +0,0 @@ -interface LoadingSpinnerProps { - message?: string; - size?: "sm" | "md" | "lg"; - className?: string; -} - -export function LoadingSpinner({ - message = "Loading...", - size = "md", - className = "", -}: LoadingSpinnerProps) { - const sizeClasses = { - sm: "h-6 w-6", - md: "h-8 w-8", - lg: "h-12 w-12", - }; - - return ( -
-
- {message} -
- ); -} diff --git a/apps/portal/src/components/catalog/mnp-form.tsx b/apps/portal/src/components/catalog/mnp-form.tsx deleted file mode 100644 index e5e4e3e8..00000000 --- a/apps/portal/src/components/catalog/mnp-form.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import React from "react"; - -interface MnpData { - reservationNumber: string; - expiryDate: string; - phoneNumber: string; - mvnoAccountNumber: string; - portingLastName: string; - portingFirstName: string; - portingLastNameKatakana: string; - portingFirstNameKatakana: string; - portingGender: "Male" | "Female" | "Corporate/Other" | ""; - portingDateOfBirth: string; -} - -interface MnpFormProps { - wantsMnp: boolean; - onWantsMnpChange: (wants: boolean) => void; - mnpData: MnpData; - onMnpDataChange: (data: MnpData) => void; - errors: Record; -} - -export function MnpForm({ - wantsMnp, - onWantsMnpChange, - mnpData, - onMnpDataChange, - errors, -}: MnpFormProps) { - const handleInputChange = (field: keyof MnpData, value: string) => { - onMnpDataChange({ - ...mnpData, - [field]: value, - }); - }; - - return ( -
-
- -
- - {wantsMnp && ( -
-

Number Porting Information

-

- Please provide the following information from your current mobile carrier to complete - the number porting process. -

- -
- {/* MNP Reservation Number */} -
- - handleInputChange("reservationNumber", e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="10-digit reservation number" - /> - {errors.reservationNumber && ( -

{errors.reservationNumber}

- )} -
- - {/* Expiry Date */} -
- - handleInputChange("expiryDate", e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> - {errors.expiryDate && ( -

{errors.expiryDate}

- )} -
- - {/* Phone Number */} -
- - handleInputChange("phoneNumber", e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="090-1234-5678" - /> - {errors.phoneNumber && ( -

{errors.phoneNumber}

- )} -
- - {/* MVNO Account Number */} -
- - handleInputChange("mvnoAccountNumber", e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="Your current carrier account number" - /> -
- - {/* Last Name */} -
- - handleInputChange("portingLastName", e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="Tanaka" - /> - {errors.portingLastName && ( -

{errors.portingLastName}

- )} -
- - {/* First Name */} -
- - handleInputChange("portingFirstName", e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="Taro" - /> - {errors.portingFirstName && ( -

{errors.portingFirstName}

- )} -
- - {/* Last Name Katakana */} -
- - handleInputChange("portingLastNameKatakana", e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="タナカ" - /> - {errors.portingLastNameKatakana && ( -

{errors.portingLastNameKatakana}

- )} -
- - {/* First Name Katakana */} -
- - handleInputChange("portingFirstNameKatakana", e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="タロウ" - /> - {errors.portingFirstNameKatakana && ( -

{errors.portingFirstNameKatakana}

- )} -
- - {/* Gender */} -
- - - {errors.portingGender && ( -

{errors.portingGender}

- )} -
- - {/* Date of Birth */} -
- - handleInputChange("portingDateOfBirth", e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> - {errors.portingDateOfBirth && ( -

{errors.portingDateOfBirth}

- )} -
-
- -
-

- Important: Please ensure all information matches exactly with your - current carrier records. Incorrect information may delay the porting process. -

-
-
- )} -
- ); -} diff --git a/apps/portal/src/components/catalog/order-summary.tsx b/apps/portal/src/components/catalog/order-summary.tsx deleted file mode 100644 index 04f25512..00000000 --- a/apps/portal/src/components/catalog/order-summary.tsx +++ /dev/null @@ -1,285 +0,0 @@ -import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline"; -import Link from "next/link"; - -interface OrderItem { - name: string; - monthlyPrice?: number; - oneTimePrice?: number; - billingCycle?: string; - sku?: string; -} - -interface OrderSummaryProps { - // Plan details - plan: { - name: string; - internetPlanTier?: string; - monthlyPrice?: number; - }; - - // Selected items - selectedAddons?: OrderItem[]; - activationFees?: OrderItem[]; - - // Configuration details - configDetails?: Array<{ - label: string; - value: string; - }>; - - // Additional info lines - infoLines?: string[]; - - // Totals - monthlyTotal: number; - oneTimeTotal?: number; - hasMissingPrices?: boolean; - - // Actions (optional) - onContinue?: () => void; - backUrl?: string; - backLabel?: string; - continueLabel?: string; - showActions?: boolean; - - // Styling - variant?: "simple" | "enhanced"; - disabled?: boolean; -} - -export function OrderSummary({ - plan, - selectedAddons = [], - activationFees = [], - configDetails = [], - infoLines = [], - monthlyTotal, - oneTimeTotal = 0, - hasMissingPrices = false, - onContinue, - backUrl, - backLabel = "Back to Plans", - continueLabel = "Continue to Checkout", - showActions = true, - variant = "simple", - disabled = false, -}: OrderSummaryProps) { - const containerClass = - variant === "enhanced" - ? "bg-gradient-to-br from-gray-50 to-blue-50 rounded-2xl border-2 border-gray-200 p-8 shadow-lg" - : "bg-white border border-gray-200 rounded-xl p-6"; - - return ( -
-

Order Summary

- - {/* Plan & Configuration Details */} -
-
- Plan: - - {plan.name} - {plan.internetPlanTier && ` (${plan.internetPlanTier})`} - -
- - {configDetails.map((detail, index) => ( -
- {detail.label}: - {detail.value} -
- ))} - - {selectedAddons.length > 0 && ( -
- Add-ons: - {selectedAddons.length} selected -
- )} -
- - {/* Pricing Breakdown */} - {variant === "enhanced" && ( -
-

Pricing Summary

- - {/* Monthly Costs */} -
-
Monthly Costs:
- {plan.monthlyPrice && ( -
- - Base Plan {plan.internetPlanTier && `(${plan.internetPlanTier})`}: - - ¥{plan.monthlyPrice.toLocaleString()} -
- )} - - {selectedAddons.map( - (addon, index) => - addon.billingCycle === "Monthly" && - typeof addon.monthlyPrice === "number" && ( -
- {addon.name}: - - ¥{addon.monthlyPrice.toLocaleString()}/month - -
- ) - )} - -
- Total Monthly: - - {hasMissingPrices ? ( - Some prices unavailable - ) : ( - `¥${monthlyTotal.toLocaleString()}` - )} - -
-
- - {/* One-time Costs */} - {(oneTimeTotal > 0 || activationFees.length > 0) && ( -
-
One-time Costs:
- - {activationFees.map((fee, index) => ( -
- {fee.name}: - - ¥{(fee.oneTimePrice ?? fee.monthlyPrice ?? 0).toLocaleString()} - -
- ))} - - {selectedAddons.map( - (addon, index) => - addon.billingCycle !== "Monthly" && - typeof addon.oneTimePrice === "number" && ( -
- {addon.name}: - - ¥{addon.oneTimePrice.toLocaleString()} - -
- ) - )} - - {oneTimeTotal > 0 && ( -
- Total One-time: - - ¥{oneTimeTotal.toLocaleString()} - -
- )} -
- )} -
- )} - - {/* Simple variant totals */} - {variant === "simple" && ( - <> - {/* Show base plan */} -
-
- {plan.name} - ¥{plan.monthlyPrice?.toLocaleString()}/mo -
- - {/* Show activation fees */} - {activationFees.map((fee, index) => ( -
- {fee.name} - - ¥{(fee.oneTimePrice ?? fee.monthlyPrice ?? 0).toLocaleString()} one-time - -
- ))} - - {/* Show selected addons */} - {selectedAddons.map((addon, index) => ( -
- {addon.name} - - ¥{(addon.billingCycle === "Monthly" - ? addon.monthlyPrice ?? addon.oneTimePrice ?? 0 - : addon.oneTimePrice ?? addon.monthlyPrice ?? 0 - ).toLocaleString()} - {addon.billingCycle === "Monthly" ? "/mo" : " one-time"} - -
- ))} -
- - {/* Info lines */} - {infoLines.length > 0 && ( -
- {infoLines.map((line, index) => ( -
• {line}
- ))} -
- )} - - {/* Totals */} -
-
- Monthly Total: - ¥{monthlyTotal.toLocaleString()}/mo -
- {oneTimeTotal > 0 && ( -
- One-time Total: - ¥{oneTimeTotal.toLocaleString()} -
- )} -
- - )} - - {/* Action Buttons */} - {showActions && ( -
- {variant === "simple" ? ( - <> - {backUrl && ( - - - {backLabel} - - )} - - {onContinue && ( - - )} - - ) : ( - onContinue && ( - - ) - )} -
- )} -
- ); -} diff --git a/apps/portal/src/components/catalog/progress-steps.tsx b/apps/portal/src/components/catalog/progress-steps.tsx deleted file mode 100644 index 613d0e53..00000000 --- a/apps/portal/src/components/catalog/progress-steps.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { CheckCircleIcon } from "@heroicons/react/24/outline"; -import { AnimatedCard } from "./animated-card"; - -interface Step { - number: number; - title: string; - completed: boolean; -} - -interface ProgressStepsProps { - steps: Step[]; - currentStep?: number; // Optional for step-based navigation - className?: string; -} - -export function ProgressSteps({ steps, currentStep, className = "" }: ProgressStepsProps) { - return ( -
- -

- Configuration Progress -

-
-
- {steps.map((step, index) => ( -
-
-
- {step.completed ? ( - - ) : ( - - {step.number} - - )} -
- - {step.title} - -
- {index < steps.length - 1 && ( -
- )} -
- ))} -
-
- -
- ); -} diff --git a/apps/portal/src/components/catalog/sim-type-selector.tsx b/apps/portal/src/components/catalog/sim-type-selector.tsx deleted file mode 100644 index 7398893f..00000000 --- a/apps/portal/src/components/catalog/sim-type-selector.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { DevicePhoneMobileIcon, CpuChipIcon } from "@heroicons/react/24/outline"; - -interface SimTypeSelectorProps { - simType: "Physical SIM" | "eSIM" | ""; - onSimTypeChange: (type: "Physical SIM" | "eSIM") => void; - eid: string; - onEidChange: (eid: string) => void; - errors: Record; -} - -export function SimTypeSelector({ - simType, - onSimTypeChange, - eid, - onEidChange, - errors, -}: SimTypeSelectorProps) { - return ( -
-
- - - -
- - {/* EID Input for eSIM */} - {simType === "eSIM" && ( -
-

eSIM Device Information

-
- - onEidChange(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="32-digit EID number" - maxLength={32} - /> - {errors.eid &&

{errors.eid}

} -

- Find your EID in: Settings → General → About → EID (iOS) or Settings → About Phone → - IMEI (Android) -

-
-
- )} -
- ); -} diff --git a/apps/portal/src/components/catalog/step-header.tsx b/apps/portal/src/components/catalog/step-header.tsx deleted file mode 100644 index dc492a2a..00000000 --- a/apps/portal/src/components/catalog/step-header.tsx +++ /dev/null @@ -1,20 +0,0 @@ -interface StepHeaderProps { - stepNumber: number; - title: string; - description: string; - className?: string; -} - -export function StepHeader({ stepNumber, title, description, className = "" }: StepHeaderProps) { - return ( -
-
- {stepNumber} -
-
-

{title}

-

{description}

-
-
- ); -} diff --git a/apps/portal/src/components/checkout/address-confirmation.tsx b/apps/portal/src/components/checkout/address-confirmation.tsx deleted file mode 100644 index 714b181a..00000000 --- a/apps/portal/src/components/checkout/address-confirmation.tsx +++ /dev/null @@ -1,446 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; -import { authenticatedApi } from "@/lib/api"; -import { logger } from "@/lib/logger"; -import { StatusPill } from "@/components/ui/status-pill"; -import { - MapPinIcon, - PencilIcon, - CheckIcon, - XMarkIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/outline"; - -interface Address { - street: string | null; - streetLine2: string | null; - city: string | null; - state: string | null; - postalCode: string | null; - country: string | null; -} - -interface BillingInfo { - company: string | null; - email: string; - phone: string | null; - address: Address; - isComplete: boolean; -} - -interface AddressConfirmationProps { - onAddressConfirmed: (address?: Address) => void; - onAddressIncomplete: () => void; - orderType?: string; // Add order type to customize behavior - embedded?: boolean; // When true, render without outer card wrapper -} - -export function AddressConfirmation({ - onAddressConfirmed, - onAddressIncomplete, - orderType, - embedded = false, -}: AddressConfirmationProps) { - const [billingInfo, setBillingInfo] = useState(null); - const [loading, setLoading] = useState(true); - const [editing, setEditing] = useState(false); - const [editedAddress, setEditedAddress] = useState
(null); - const [error, setError] = useState(null); - const [addressConfirmed, setAddressConfirmed] = useState(false); - - const isInternetOrder = orderType === "Internet"; - const requiresAddressVerification = isInternetOrder; - - const fetchBillingInfo = useCallback(async () => { - try { - setLoading(true); - const data = await authenticatedApi.get("/me/billing"); - setBillingInfo(data); - - // Since address is required at signup, it should always be complete - // But we still need verification for Internet orders - if (requiresAddressVerification) { - // For Internet orders, don't auto-confirm - require explicit verification - setAddressConfirmed(false); - onAddressIncomplete(); // Keep disabled until explicitly confirmed - } else { - // For other order types, auto-confirm since address exists from signup - onAddressConfirmed(data.address); - setAddressConfirmed(true); - } - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load address"); - } finally { - setLoading(false); - } - }, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed]); - - useEffect(() => { - logger.info("Address confirmation component mounted"); - void fetchBillingInfo(); - }, [fetchBillingInfo]); - - const handleEdit = (e?: React.MouseEvent) => { - e?.preventDefault(); - e?.stopPropagation(); - - setEditing(true); - setEditedAddress( - billingInfo?.address || { - street: "", - streetLine2: "", - city: "", - state: "", - postalCode: "", - country: "", - } - ); - }; - - const handleSave = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if (!editedAddress) return; - - // Validate required fields - const isComplete = !!( - editedAddress.street?.trim() && - editedAddress.city?.trim() && - editedAddress.state?.trim() && - editedAddress.postalCode?.trim() && - editedAddress.country?.trim() - ); - - if (!isComplete) { - setError("Please fill in all required address fields"); - return; - } - - void (async () => { - try { - setError(null); - - // Build minimal PATCH payload with only provided fields - const payload: Record = {}; - if (editedAddress.street) payload.street = editedAddress.street; - if (editedAddress.streetLine2) payload.streetLine2 = editedAddress.streetLine2; - if (editedAddress.city) payload.city = editedAddress.city; - if (editedAddress.state) payload.state = editedAddress.state; - if (editedAddress.postalCode) payload.postalCode = editedAddress.postalCode; - if (editedAddress.country) payload.country = editedAddress.country; - - // Persist to server (WHMCS via BFF) - const updated = await authenticatedApi.patch("/me/address", payload); - - // Update local state using authoritative response - setBillingInfo(updated); - - // Use the edited address for the order (will be flagged as changed) - onAddressConfirmed(updated.address); - setEditing(false); - setAddressConfirmed(true); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to update address"); - } - })(); - }; - - const handleConfirmAddress = (e: React.MouseEvent) => { - // Defensively prevent any parent form/link behaviors from triggering navigation - if (e && typeof e.preventDefault === "function") e.preventDefault(); - if (e && typeof e.stopPropagation === "function") e.stopPropagation(); - - logger.info("Confirm installation address clicked"); - - // Ensure we have an address before confirming - if (billingInfo?.address) { - logger.info("Address confirmed", { address: billingInfo.address }); - onAddressConfirmed(billingInfo.address); - setAddressConfirmed(true); - } else { - logger.warn("No billing info or address available"); - setAddressConfirmed(false); - } - }; - - const handleCancel = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - setEditing(false); - setEditedAddress(null); - setError(null); - }; - - const statusVariant = addressConfirmed || !requiresAddressVerification ? "success" : "warning"; - - // Note: Avoid defining wrapper components inside render to prevent remounts (focus loss) - const wrap = (node: React.ReactNode) => - embedded ? <>{node} :
{node}
; - - if (loading) { - return wrap( -
-
- Loading address information... -
- ); - } - - if (error) { - return wrap( -
-
- -
-

Address Error

-

{error}

- -
-
-
- ); - } - - if (!billingInfo) return null; - - return wrap( - <> -
-
- -
-

- {isInternetOrder - ? "Installation Address" - : billingInfo.isComplete - ? "Service Address" - : "Complete Your Address"} -

-
-
-
- {/* Consistent status pill placement (right side) */} - -
-
- - {/* Consolidated single card without separate banner */} - - {editing ? ( -
-
- - { - setError(null); // Clear error on input - setEditedAddress(prev => (prev ? { ...prev, street: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="123 Main Street" - required - /> -
- -
- - { - setError(null); - setEditedAddress(prev => (prev ? { ...prev, streetLine2: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Apartment, suite, etc. (optional)" - /> -
- -
-
- - { - setError(null); - setEditedAddress(prev => (prev ? { ...prev, city: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Tokyo" - /> -
- -
- - { - setError(null); - setEditedAddress(prev => (prev ? { ...prev, state: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Tokyo" - /> -
- -
- - { - setError(null); - setEditedAddress(prev => (prev ? { ...prev, postalCode: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="100-0001" - /> -
-
- -
- - -
- -
- - -
-
- ) : ( -
- {billingInfo.address.street ? ( -
-
-

{billingInfo.address.street}

- {billingInfo.address.streetLine2 && ( -

{billingInfo.address.streetLine2}

- )} -

- {billingInfo.address.city}, {billingInfo.address.state}{" "} - {billingInfo.address.postalCode} -

-

{billingInfo.address.country}

-
- - {/* Status message for Internet orders when pending */} - {isInternetOrder && !addressConfirmed && ( -
-
- -
-

Verification Required

-

- Please confirm this is the correct installation address for your internet - service. -

-
-
-
- )} - - {/* Action buttons */} -
-
- {/* Primary action when pending for Internet orders */} - {isInternetOrder && !addressConfirmed && !editing && ( - - )} -
- - {/* Edit button - always on the right */} - {billingInfo.isComplete && !editing && ( - - )} -
-
- ) : ( -
-
- -
-

No Address on File

-

- Please add your installation address to continue. -

- -
- )} -
- )} - - ); -} diff --git a/apps/portal/src/components/common/AsyncBlock/AsyncBlock.tsx b/apps/portal/src/components/common/AsyncBlock/AsyncBlock.tsx index 8c7d3798..36395755 100644 --- a/apps/portal/src/components/common/AsyncBlock/AsyncBlock.tsx +++ b/apps/portal/src/components/common/AsyncBlock/AsyncBlock.tsx @@ -3,7 +3,7 @@ import React from "react"; import { Skeleton } from "@/components/ui/loading-skeleton"; import { ErrorState } from "@/components/ui/error-state"; -import { toUserMessage } from "@/lib/utils/error-display"; +import { toUserMessage } from "@/shared/utils"; interface AsyncBlockProps { isLoading?: boolean; diff --git a/apps/portal/src/components/common/FormField/FormField.tsx b/apps/portal/src/components/common/FormField/FormField.tsx index 4b6d7b92..c3d926ff 100644 --- a/apps/portal/src/components/common/FormField/FormField.tsx +++ b/apps/portal/src/components/common/FormField/FormField.tsx @@ -1,5 +1,5 @@ import { forwardRef, cloneElement, isValidElement, useId } from "react"; -import { cn } from "@/lib/utils/cn"; +import { cn } from "@/shared/utils"; import { Label, type LabelProps } from "@/components/ui/label"; import { Input, type InputProps } from "@/components/ui/input"; import { ErrorMessage } from "@/components/ui/error-message"; diff --git a/apps/portal/src/components/guards/profile-completion-guard.tsx b/apps/portal/src/components/guards/profile-completion-guard.tsx index 981a99aa..292a6069 100644 --- a/apps/portal/src/components/guards/profile-completion-guard.tsx +++ b/apps/portal/src/components/guards/profile-completion-guard.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect } from "react"; -import { useProfileCompletion } from "@/hooks/use-profile-completion"; +import { useProfileCompletion } from "@/features/account/hooks"; import { MapPinIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline"; interface ProfileCompletionGuardProps { diff --git a/apps/portal/src/components/layout/dashboard-layout.tsx b/apps/portal/src/components/layout/dashboard-layout.tsx index 255439dd..dd4f3994 100644 --- a/apps/portal/src/components/layout/dashboard-layout.tsx +++ b/apps/portal/src/components/layout/dashboard-layout.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useMemo, memo } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; -import { useAuthStore } from "@/lib/auth/store"; +import { useAuthStore } from "@/features/auth/services/auth.store"; import { Logo } from "@/components/ui/logo"; import { HomeIcon, @@ -19,8 +19,8 @@ import { ClipboardDocumentListIcon, QuestionMarkCircleIcon, } from "@heroicons/react/24/outline"; -import { useActiveSubscriptions } from "@/hooks/useSubscriptions"; -import type { Subscription } from "@customer-portal/shared"; +import { useActiveSubscriptions } from "@/features/subscriptions/hooks"; +import type { Subscription } from "@customer-portal/domain"; interface DashboardLayoutProps { children: React.ReactNode; diff --git a/apps/portal/src/components/ui/badge.tsx b/apps/portal/src/components/ui/badge.tsx index c898cc4e..66e5444d 100644 --- a/apps/portal/src/components/ui/badge.tsx +++ b/apps/portal/src/components/ui/badge.tsx @@ -1,6 +1,6 @@ import { forwardRef } from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils/cn"; +import { cn } from "@/shared/utils"; const badgeVariants = cva( "inline-flex items-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", diff --git a/apps/portal/src/components/ui/button.tsx b/apps/portal/src/components/ui/button.tsx index 7458ff58..3fd41cfb 100644 --- a/apps/portal/src/components/ui/button.tsx +++ b/apps/portal/src/components/ui/button.tsx @@ -1,7 +1,7 @@ import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from "react"; import { forwardRef } from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils"; +import { cn } from "@/shared/utils"; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", diff --git a/apps/portal/src/components/ui/empty-state.tsx b/apps/portal/src/components/ui/empty-state.tsx index 668a4d24..ce8d3baf 100644 --- a/apps/portal/src/components/ui/empty-state.tsx +++ b/apps/portal/src/components/ui/empty-state.tsx @@ -1,6 +1,6 @@ import { PlusIcon } from "@heroicons/react/24/outline"; import { Button } from "./button"; -import { cn } from "@/lib/utils"; +import { cn } from "@/shared/utils"; interface EmptyStateProps { icon?: React.ReactNode; diff --git a/apps/portal/src/components/ui/error-message.tsx b/apps/portal/src/components/ui/error-message.tsx index 8b7033b6..73dd346d 100644 --- a/apps/portal/src/components/ui/error-message.tsx +++ b/apps/portal/src/components/ui/error-message.tsx @@ -1,6 +1,6 @@ import { forwardRef } from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils/cn"; +import { cn } from "@/shared/utils"; import { ExclamationCircleIcon } from "@heroicons/react/24/outline"; const errorMessageVariants = cva("flex items-center gap-1 text-sm", { diff --git a/apps/portal/src/components/ui/error-state.tsx b/apps/portal/src/components/ui/error-state.tsx index 01d5dcaf..87bf40b3 100644 --- a/apps/portal/src/components/ui/error-state.tsx +++ b/apps/portal/src/components/ui/error-state.tsx @@ -1,6 +1,6 @@ import { ExclamationTriangleIcon, ArrowPathIcon } from "@heroicons/react/24/outline"; import { Button } from "./button"; -import { cn } from "@/lib/utils"; +import { cn } from "@/shared/utils"; interface ErrorStateProps { title?: string; diff --git a/apps/portal/src/components/ui/input.tsx b/apps/portal/src/components/ui/input.tsx index 184525ee..75ff1b44 100644 --- a/apps/portal/src/components/ui/input.tsx +++ b/apps/portal/src/components/ui/input.tsx @@ -1,6 +1,6 @@ import type { InputHTMLAttributes } from "react"; import { forwardRef } from "react"; -import { cn } from "@/lib/utils"; +import { cn } from "@/shared/utils"; export type InputProps = InputHTMLAttributes; diff --git a/apps/portal/src/components/ui/label.tsx b/apps/portal/src/components/ui/label.tsx index b354a328..a9d08339 100644 --- a/apps/portal/src/components/ui/label.tsx +++ b/apps/portal/src/components/ui/label.tsx @@ -1,6 +1,6 @@ import type { LabelHTMLAttributes } from "react"; import { forwardRef } from "react"; -import { cn } from "@/lib/utils"; +import { cn } from "@/shared/utils"; export type LabelProps = LabelHTMLAttributes; diff --git a/apps/portal/src/components/ui/loading-skeleton.tsx b/apps/portal/src/components/ui/loading-skeleton.tsx index 06ed1e4d..b461250e 100644 --- a/apps/portal/src/components/ui/loading-skeleton.tsx +++ b/apps/portal/src/components/ui/loading-skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib/utils"; +import { cn } from "@/shared/utils"; interface SkeletonProps { className?: string; diff --git a/apps/portal/src/components/ui/loading-spinner.tsx b/apps/portal/src/components/ui/loading-spinner.tsx index 23d1cbc8..957fd852 100644 --- a/apps/portal/src/components/ui/loading-spinner.tsx +++ b/apps/portal/src/components/ui/loading-spinner.tsx @@ -1,6 +1,6 @@ import { forwardRef } from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils/cn"; +import { cn } from "@/shared/utils"; const spinnerVariants = cva( "animate-spin rounded-full border-solid border-current border-r-transparent", diff --git a/apps/portal/src/core/api/client.ts b/apps/portal/src/core/api/client.ts new file mode 100644 index 00000000..946a24da --- /dev/null +++ b/apps/portal/src/core/api/client.ts @@ -0,0 +1,13 @@ +/** + * Core API Client + * Minimal API client setup using the generated OpenAPI client + */ + +import { createClient } from "@customer-portal/api-client"; +import { env } from "../config/env"; + +// Create the type-safe API client +export const apiClient = createClient(env.NEXT_PUBLIC_API_BASE); + +// Export the client type for use in hooks +export type { ApiClient } from "@customer-portal/api-client"; diff --git a/apps/portal/src/core/api/index.ts b/apps/portal/src/core/api/index.ts new file mode 100644 index 00000000..e3689269 --- /dev/null +++ b/apps/portal/src/core/api/index.ts @@ -0,0 +1,3 @@ +export { apiClient } from "./client"; +export { queryKeys } from "./query-keys"; +export type { ApiClient } from "./client"; diff --git a/apps/portal/src/core/api/query-keys.ts b/apps/portal/src/core/api/query-keys.ts new file mode 100644 index 00000000..53c80e9a --- /dev/null +++ b/apps/portal/src/core/api/query-keys.ts @@ -0,0 +1,42 @@ +/** + * React Query Keys + * Centralized query key factory for consistent caching + */ + +export const queryKeys = { + // Auth queries + auth: { + all: ['auth'] as const, + profile: () => [...queryKeys.auth.all, 'profile'] as const, + }, + + // Dashboard queries + dashboard: { + all: ['dashboard'] as const, + summary: () => [...queryKeys.dashboard.all, 'summary'] as const, + }, + + // Billing queries + billing: { + all: ['billing'] as const, + invoices: (params?: Record) => + [...queryKeys.billing.all, 'invoices', params] as const, + invoice: (id: string) => [...queryKeys.billing.all, 'invoice', id] as const, + paymentMethods: () => [...queryKeys.billing.all, 'paymentMethods'] as const, + }, + + // Subscription queries + subscriptions: { + all: ['subscriptions'] as const, + list: (params?: Record) => + [...queryKeys.subscriptions.all, 'list', params] as const, + detail: (id: string) => [...queryKeys.subscriptions.all, 'detail', id] as const, + }, + + // Catalog queries + catalog: { + all: ['catalog'] as const, + products: (type: string, params?: Record) => + [...queryKeys.catalog.all, 'products', type, params] as const, + }, +} as const; diff --git a/apps/portal/src/lib/env.ts b/apps/portal/src/core/config/env.ts similarity index 100% rename from apps/portal/src/lib/env.ts rename to apps/portal/src/core/config/env.ts diff --git a/apps/portal/src/core/config/index.ts b/apps/portal/src/core/config/index.ts new file mode 100644 index 00000000..ec14e4a8 --- /dev/null +++ b/apps/portal/src/core/config/index.ts @@ -0,0 +1 @@ +export { env } from "./env"; diff --git a/apps/portal/src/core/index.ts b/apps/portal/src/core/index.ts new file mode 100644 index 00000000..0ee55249 --- /dev/null +++ b/apps/portal/src/core/index.ts @@ -0,0 +1,8 @@ +/** + * Core Infrastructure + * Essential app-level setup and configuration + */ + +export * from "./api"; +export * from "./providers"; +export * from "./config"; diff --git a/apps/portal/src/core/providers/index.ts b/apps/portal/src/core/providers/index.ts new file mode 100644 index 00000000..4cb25b2c --- /dev/null +++ b/apps/portal/src/core/providers/index.ts @@ -0,0 +1 @@ +export { QueryProvider } from "./query-provider"; diff --git a/apps/portal/src/core/providers/query-provider.tsx b/apps/portal/src/core/providers/query-provider.tsx new file mode 100644 index 00000000..487482c6 --- /dev/null +++ b/apps/portal/src/core/providers/query-provider.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { useState } from "react"; + +interface QueryProviderProps { + children: React.ReactNode; +} + +// Create query client with optimized settings +function createQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + retry: (failureCount, error) => { + // Don't retry on 4xx errors + if (error instanceof Error && 'status' in error) { + const status = (error as any).status; + if (status >= 400 && status < 500) return false; + } + return failureCount < 3; + }, + refetchOnWindowFocus: false, + refetchOnReconnect: true, + }, + mutations: { + retry: false, + }, + }, + }); +} + +export function QueryProvider({ children }: QueryProviderProps) { + // Create query client in state to ensure it's stable across re-renders + const [queryClient] = useState(() => createQueryClient()); + + const showDevtools = + process.env.NODE_ENV === "development" && + process.env.NEXT_PUBLIC_ENABLE_DEVTOOLS !== "false"; + + return ( + + {children} + {showDevtools && } + + ); +} diff --git a/apps/portal/src/features/account/hooks/index.ts b/apps/portal/src/features/account/hooks/index.ts new file mode 100644 index 00000000..1d7f23b5 --- /dev/null +++ b/apps/portal/src/features/account/hooks/index.ts @@ -0,0 +1,5 @@ +export { useProfileCompletion } from "./useProfileCompletion"; +export { useProfileData } from "./useProfileData"; +export { useProfileEdit } from "./useProfileEdit"; +export { useAddressEdit } from "./useAddressEdit"; +export { useAddressForm } from "./useAddressForm"; diff --git a/apps/portal/src/hooks/use-profile-completion.ts b/apps/portal/src/features/account/hooks/useProfileCompletion.ts similarity index 87% rename from apps/portal/src/hooks/use-profile-completion.ts rename to apps/portal/src/features/account/hooks/useProfileCompletion.ts index d4dba509..e456a0f5 100644 --- a/apps/portal/src/hooks/use-profile-completion.ts +++ b/apps/portal/src/features/account/hooks/useProfileCompletion.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; -import { authenticatedApi } from "@/lib/api"; +import { apiClient } from "@/core/api"; interface Address { street: string | null; @@ -35,7 +35,8 @@ export function useProfileCompletion(): ProfileCompletionStatus { useEffect(() => { const checkProfileCompletion = async () => { try { - const billingInfo = await authenticatedApi.get("/me/billing"); + const response = await apiClient.GET("/me/billing"); + const billingInfo = response.data; setIsComplete(billingInfo.isComplete); } catch (error) { console.error("Failed to check profile completion:", error); @@ -50,7 +51,7 @@ export function useProfileCompletion(): ProfileCompletionStatus { }, []); const redirectToCompletion = () => { - router.push("/account/billing?complete=true"); + router.push("/account/profile?complete=true"); }; return { diff --git a/apps/portal/src/features/account/services/account.service.ts b/apps/portal/src/features/account/services/account.service.ts deleted file mode 100644 index fe62fc38..00000000 --- a/apps/portal/src/features/account/services/account.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Account Service - * Profile and billing info for current user - */ - -import { openApiClient as apiClient } from "@/lib/api/openapi-client"; -import { unwrap } from "@/lib/api/unwrap"; -import type { Address, User } from "@customer-portal/domain"; - -export class AccountService { - async getProfile(): Promise { - const res = await apiClient.get("/me"); - return unwrap(res); - } - - async updateProfile( - payload: Partial> - ): Promise { - const res = await apiClient.patch("/me", payload); - return unwrap(res); - } - - async getAddress(): Promise
{ - const res = await apiClient.get
("/me/address"); - return unwrap(res); - } - - async updateAddress(payload: Partial
): Promise
{ - const res = await apiClient.patch
("/me/address", payload); - return unwrap(res); - } -} - -export const accountService = new AccountService(); diff --git a/apps/portal/src/features/auth/api.ts b/apps/portal/src/features/auth/api.ts deleted file mode 100644 index 0541d739..00000000 --- a/apps/portal/src/features/auth/api.ts +++ /dev/null @@ -1,189 +0,0 @@ -const API_BASE = process.env.NEXT_PUBLIC_API_BASE || "/api"; - -export interface SignupData { - email: string; - password: string; - firstName: string; - lastName: string; - company?: string; - phone?: string; - sfNumber: string; -} - -export interface LoginData { - email: string; - password: string; -} - -export interface LinkWhmcsData { - email: string; - password: string; -} - -export interface SetPasswordData { - email: string; - password: string; -} - -export interface RequestPasswordResetData { - email: string; -} - -export interface ResetPasswordData { - token: string; - password: string; -} - -export interface ChangePasswordData { - currentPassword: string; - newPassword: string; -} - -export interface CheckPasswordNeededData { - email: string; -} - -export interface CheckPasswordNeededResponse { - needsPasswordSet: boolean; - userExists: boolean; - email?: string; -} - -export interface AuthResponse { - user: { - id: string; - email: string; - firstName: string; - lastName: string; - company?: string; - phone?: string; - }; - access_token: string; -} - -export interface AuthMeResponse { - isAuthenticated: boolean; - user: { - id: string; - email: string; - role?: string; - }; -} - -export interface LinkResponse { - user: { - id: string; - email: string; - firstName: string; - lastName: string; - }; - needsPasswordSet: boolean; -} - -class AuthAPI { - private async request(endpoint: string, options?: RequestInit): Promise { - const base = API_BASE.endsWith("/") ? API_BASE.slice(0, -1) : API_BASE; - const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`; - const url = `${base}${path}`; - - const response = await fetch(url, { - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - ...options, - }); - - if (!response.ok) { - const errorData = (await response.json().catch(() => ({}))) as unknown; - const message = - typeof errorData === "object" && - errorData !== null && - "message" in errorData && - typeof (errorData as { message: unknown }).message === "string" - ? (errorData as { message: string }).message - : `HTTP ${response.status}: ${response.statusText}`; - throw new Error(message); - } - - return (await response.json()) as T; - } - - async signup(data: SignupData): Promise { - return this.request("/auth/signup", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async login(data: LoginData): Promise { - return this.request("/auth/login", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async linkWhmcs(data: LinkWhmcsData): Promise { - return this.request("/auth/link-whmcs", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async setPassword(data: SetPasswordData): Promise { - return this.request("/auth/set-password", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async requestPasswordReset(data: RequestPasswordResetData): Promise<{ message: string }> { - return this.request<{ message: string }>("/auth/request-password-reset", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async resetPassword(data: ResetPasswordData): Promise { - return this.request("/auth/reset-password", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async getProfile(token: string): Promise { - return this.request("/auth/me", { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } - - async logout(token: string): Promise<{ message: string }> { - return this.request<{ message: string }>("/auth/logout", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } - - async changePassword(token: string, data: ChangePasswordData): Promise { - return this.request("/auth/change-password", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(data), - }); - } - - async checkPasswordNeeded(data: CheckPasswordNeededData): Promise { - return this.request("/auth/check-password-needed", { - method: "POST", - body: JSON.stringify(data), - }); - } -} - -export const authAPI = new AuthAPI(); diff --git a/apps/portal/src/features/auth/components/AuthLayout.tsx b/apps/portal/src/features/auth/components/AuthLayout.tsx new file mode 100644 index 00000000..898a9061 --- /dev/null +++ b/apps/portal/src/features/auth/components/AuthLayout.tsx @@ -0,0 +1,51 @@ +"use client"; + +import Link from "next/link"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; + +interface AuthLayoutProps { + children: React.ReactNode; + title: string; + subtitle?: string; + showBackButton?: boolean; + backHref?: string; + backLabel?: string; +} + +export function AuthLayout({ + children, + title, + subtitle, + showBackButton = false, + backHref = "/", + backLabel = "Back to Home", +}: AuthLayoutProps) { + return ( +
+
+ {showBackButton && ( +
+ + + {backLabel} + +
+ )} + +
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ +
+
+ {children} +
+
+
+ ); +} diff --git a/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx b/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx index fe60027c..da21a486 100644 --- a/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx @@ -74,7 +74,7 @@ export function AddressStep({ onBlur={() => onFieldBlur("address.street")} placeholder="Enter your street address" disabled={loading} - autoComplete="address-line1" + autoComplete="street-address" autoFocus /> diff --git a/apps/portal/src/features/auth/components/index.ts b/apps/portal/src/features/auth/components/index.ts index 6538943a..bdbc70ae 100644 --- a/apps/portal/src/features/auth/components/index.ts +++ b/apps/portal/src/features/auth/components/index.ts @@ -8,3 +8,4 @@ export { SignupForm } from "./SignupForm"; export { PasswordResetForm } from "./PasswordResetForm"; export { SetPasswordForm } from "./SetPasswordForm"; export { LinkWhmcsForm } from "./LinkWhmcsForm"; +export { AuthLayout } from "./AuthLayout"; diff --git a/apps/portal/src/features/auth/hooks/useAuth.ts b/apps/portal/src/features/auth/hooks/useAuth.ts new file mode 100644 index 00000000..4d8b0335 --- /dev/null +++ b/apps/portal/src/features/auth/hooks/useAuth.ts @@ -0,0 +1,127 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { apiClient, queryKeys } from "@/core/api"; +import { useAuthStore } from "../services/auth.store"; +import type { + LoginRequest, + SignupRequest, + ForgotPasswordRequest, + ResetPasswordRequest, + ChangePasswordRequest, + AuthUser +} from "@customer-portal/domain"; + +/** + * Simplified Auth Hook - Just UI state + API calls + * All business logic is handled by the BFF + */ +export function useAuth() { + const router = useRouter(); + const queryClient = useQueryClient(); + const { isAuthenticated, user, tokens, logout: storeLogout } = useAuthStore(); + + // Login mutation - BFF handles all validation and business logic + const loginMutation = useMutation({ + mutationFn: (credentials: LoginRequest) => + apiClient.POST('/auth/login', { body: credentials }), + onSuccess: (response) => { + if (response.data) { + // Store tokens and user (BFF returns formatted data) + useAuthStore.getState().setUser(response.data.user); + useAuthStore.getState().setTokens(response.data.tokens); + + // Navigate to dashboard + router.push('/dashboard'); + } + }, + }); + + // Signup mutation + const signupMutation = useMutation({ + mutationFn: (data: SignupRequest) => + apiClient.POST('/auth/signup', { body: data }), + onSuccess: (response) => { + if (response.data) { + useAuthStore.getState().setUser(response.data.user); + useAuthStore.getState().setTokens(response.data.tokens); + router.push('/dashboard'); + } + }, + }); + + // Password reset request + const passwordResetMutation = useMutation({ + mutationFn: (data: ForgotPasswordRequest) => + apiClient.POST('/auth/forgot-password', { body: data }), + }); + + // Password reset + const resetPasswordMutation = useMutation({ + mutationFn: (data: ResetPasswordRequest) => + apiClient.POST('/auth/reset-password', { body: data }), + onSuccess: (response) => { + if (response.data) { + useAuthStore.getState().setUser(response.data.user); + useAuthStore.getState().setTokens(response.data.tokens); + router.push('/dashboard'); + } + }, + }); + + // Change password + const changePasswordMutation = useMutation({ + mutationFn: (data: ChangePasswordRequest) => + apiClient.POST('/auth/change-password', { body: data }), + onSuccess: (response) => { + if (response.data) { + useAuthStore.getState().setTokens(response.data.tokens); + } + }, + }); + + // Get current user profile + const profileQuery = useQuery({ + queryKey: queryKeys.auth.profile(), + queryFn: () => apiClient.GET('/auth/me'), + enabled: isAuthenticated && !!tokens?.accessToken, + }); + + // Logout function + const logout = async () => { + try { + // Call BFF logout endpoint + await apiClient.POST('/auth/logout'); + } catch (error) { + console.warn('Logout API call failed:', error); + } finally { + // Clear local state regardless + storeLogout(); + queryClient.clear(); + router.push('/login'); + } + }; + + return { + // State + isAuthenticated, + user: profileQuery.data?.data || user, + loading: loginMutation.isPending || signupMutation.isPending || profileQuery.isLoading, + + // Actions (just trigger API calls, BFF handles business logic) + login: loginMutation.mutate, + signup: signupMutation.mutate, + requestPasswordReset: passwordResetMutation.mutate, + resetPassword: resetPasswordMutation.mutate, + changePassword: changePasswordMutation.mutate, + logout, + + // Mutation states for UI feedback + loginError: loginMutation.error, + signupError: signupMutation.error, + passwordResetError: passwordResetMutation.error, + resetPasswordError: resetPasswordMutation.error, + changePasswordError: changePasswordMutation.error, + }; +} diff --git a/apps/portal/src/features/auth/index.ts b/apps/portal/src/features/auth/index.ts index 9b3e850e..2720e9a5 100644 --- a/apps/portal/src/features/auth/index.ts +++ b/apps/portal/src/features/auth/index.ts @@ -4,7 +4,6 @@ */ export { useAuthStore } from "./services/auth.store"; -export { authAPI } from "./api"; export type { SignupData, LoginData, diff --git a/apps/portal/src/features/auth/services/auth.service.ts b/apps/portal/src/features/auth/services/auth.service.ts deleted file mode 100644 index 1c3b2057..00000000 --- a/apps/portal/src/features/auth/services/auth.service.ts +++ /dev/null @@ -1,350 +0,0 @@ -/** - * Authentication Service - * Centralized authentication business logic and API interactions - */ - -import { authAPI } from "@/features/auth/api"; -import type { - AuthUser, - AuthTokens, - LoginRequest, - SignupRequest, - ForgotPasswordRequest, - ResetPasswordRequest, - ChangePasswordRequest, - AuthError, - AuthErrorCode, -} from "@customer-portal/domain"; - -class AuthServiceError extends Error { - constructor( - public code: AuthErrorCode | "UNKNOWN", - message: string - ) { - super(message); - this.name = "AuthServiceError"; - } -} - -export class AuthService { - private static instance: AuthService; - - static getInstance(): AuthService { - if (!AuthService.instance) { - AuthService.instance = new AuthService(); - } - return AuthService.instance; - } - - /** - * Create SSO link (e.g., to WHMCS destinations) - */ - async createSsoLink(destination: string): Promise<{ url: string }> { - const { openApiClient } = await import("@/lib/api/openapi-client"); - const { unwrap } = await import("@/lib/api/unwrap"); - const res = await openApiClient.post<{ url: string }>("/auth/sso-link", { destination }); - return unwrap(res); - } - - /** - * Login user with email and password - */ - async login(credentials: LoginRequest): Promise<{ user: AuthUser; tokens: AuthTokens }> { - try { - const response = await authAPI.login({ - email: credentials.email, - password: credentials.password, - }); - - return { - user: this.mapApiUserToAuthUser(response.user), - tokens: { - accessToken: response.access_token, - tokenType: "Bearer", - expiresAt: this.calculateTokenExpiry(response.access_token), - }, - }; - } catch (error) { - throw this.handleAuthError(error); - } - } - - /** - * Register new user - */ - async signup(data: SignupRequest): Promise<{ user: AuthUser; tokens: AuthTokens }> { - try { - const response = await authAPI.signup({ - email: data.email, - password: data.password, - firstName: data.firstName, - lastName: data.lastName, - company: data.company, - phone: data.phone, - sfNumber: "", // This should be handled by the form or derived from other data - }); - - return { - user: this.mapApiUserToAuthUser(response.user), - tokens: { - accessToken: response.access_token, - tokenType: "Bearer", - expiresAt: this.calculateTokenExpiry(response.access_token), - }, - }; - } catch (error) { - throw this.handleAuthError(error); - } - } - - /** - * Request password reset - */ - async requestPasswordReset(data: ForgotPasswordRequest): Promise { - try { - await authAPI.requestPasswordReset({ email: data.email }); - } catch (error) { - throw this.handleAuthError(error); - } - } - - /** - * Reset password with token - */ - async resetPassword(data: ResetPasswordRequest): Promise<{ user: AuthUser; tokens: AuthTokens }> { - try { - const response = await authAPI.resetPassword({ - token: data.token, - password: data.password, - }); - - return { - user: this.mapApiUserToAuthUser(response.user), - tokens: { - accessToken: response.access_token, - tokenType: "Bearer", - expiresAt: this.calculateTokenExpiry(response.access_token), - }, - }; - } catch (error) { - throw this.handleAuthError(error); - } - } - - /** - * Change user password - */ - async changePassword( - token: string, - data: ChangePasswordRequest - ): Promise<{ user: AuthUser; tokens: AuthTokens }> { - try { - const response = await authAPI.changePassword(token, { - currentPassword: data.currentPassword, - newPassword: data.newPassword, - }); - - return { - user: this.mapApiUserToAuthUser(response.user), - tokens: { - accessToken: response.access_token, - tokenType: "Bearer", - expiresAt: this.calculateTokenExpiry(response.access_token), - }, - }; - } catch (error) { - throw this.handleAuthError(error); - } - } - - /** - * Get current user profile - */ - async getProfile(token: string): Promise { - try { - const { isAuthenticated, user } = await authAPI.getProfile(token); - if (!isAuthenticated || !user) { - throw new Error("Unauthenticated"); - } - return this.mapApiUserToAuthUser(user); - } catch (error) { - throw this.handleAuthError(error); - } - } - - /** - * Logout user - */ - async logout(token: string): Promise { - try { - await authAPI.logout(token); - } catch (error) { - // Don't throw on logout errors, just log them - - console.warn("Logout API call failed:", error); - } - } - - /** - * Check if password is needed for WHMCS linking - */ - async checkPasswordNeeded(email: string): Promise<{ - needsPasswordSet: boolean; - userExists: boolean; - email?: string; - }> { - try { - return await authAPI.checkPasswordNeeded({ email }); - } catch (error) { - throw this.handleAuthError(error); - } - } - - /** - * Link WHMCS account - */ - async linkWhmcs(email: string, password: string): Promise<{ needsPasswordSet: boolean }> { - try { - const response = await authAPI.linkWhmcs({ email, password }); - return { needsPasswordSet: response.needsPasswordSet }; - } catch (error) { - throw this.handleAuthError(error); - } - } - - /** - * Set password for WHMCS linked account - */ - async setPassword( - email: string, - password: string - ): Promise<{ user: AuthUser; tokens: AuthTokens }> { - try { - const response = await authAPI.setPassword({ email, password }); - - return { - user: this.mapApiUserToAuthUser(response.user), - tokens: { - accessToken: response.access_token, - tokenType: "Bearer", - expiresAt: this.calculateTokenExpiry(response.access_token), - }, - }; - } catch (error) { - throw this.handleAuthError(error); - } - } - - /** - * Check if token is expired - */ - isTokenExpired(expiresAt: string): boolean { - return new Date(expiresAt) <= new Date(); - } - - /** - * Check if token will expire soon (within 5 minutes) - */ - isTokenExpiringSoon(expiresAt: string): boolean { - const fiveMinutesFromNow = new Date(Date.now() + 5 * 60 * 1000); - return new Date(expiresAt) <= fiveMinutesFromNow; - } - - /** - * Map API user response to AuthUser type - */ - private mapApiUserToAuthUser(apiUser: unknown): AuthUser { - const obj = apiUser && typeof apiUser === "object" ? (apiUser as Record) : {}; - return { - id: (obj.id as string) ?? "", - email: (obj.email as string) ?? "", - firstName: (obj.firstName as string) ?? "", - lastName: (obj.lastName as string) ?? "", - company: (obj.company as string) ?? "", - phone: (obj.phone as string) ?? "", - avatar: (obj.avatar as string) ?? "", - roles: [], // TODO: Map from API when available - permissions: [], // TODO: Map from API when available - preferences: { - theme: "system", - language: "en", - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - notifications: { - email: true, - push: true, - sms: false, - categories: { - billing: true, - security: true, - marketing: false, - system: true, - }, - }, - dashboard: { - layout: "grid", - widgets: [], - defaultView: "dashboard", - }, - }, - lastLoginAt: new Date().toISOString(), - emailVerified: true, // TODO: Get from API when available - mfaEnabled: false, // TODO: Get from API when available - createdAt: (obj.createdAt as string) || new Date().toISOString(), - updatedAt: (obj.updatedAt as string) || new Date().toISOString(), - }; - } - - /** - * Calculate token expiry time (default to 1 hour if not provided) - */ - private calculateTokenExpiry(token: string): string { - // In a real implementation, you would decode the JWT to get the expiry - // For now, default to 1 hour from now - return new Date(Date.now() + 60 * 60 * 1000).toISOString(); - } - - /** - * Handle and normalize authentication errors - */ - private handleAuthError(error: unknown): Error { - if (error instanceof Error) { - // Map common error messages to error codes - const message = error.message.toLowerCase(); - - if (message.includes("invalid credentials") || message.includes("unauthorized")) { - return new AuthServiceError("INVALID_CREDENTIALS", "Invalid email or password"); - } - - if (message.includes("account locked") || message.includes("locked")) { - return new AuthServiceError( - "ACCOUNT_LOCKED", - "Account has been locked due to too many failed attempts" - ); - } - - if (message.includes("email not verified")) { - return new AuthServiceError( - "EMAIL_NOT_VERIFIED", - "Please verify your email address before logging in" - ); - } - - if (message.includes("token expired") || message.includes("expired")) { - return new AuthServiceError( - "TOKEN_EXPIRED", - "Your session has expired. Please log in again" - ); - } - - if (message.includes("rate limit") || message.includes("too many")) { - return new AuthServiceError("RATE_LIMITED", "Too many attempts. Please try again later"); - } - - return new AuthServiceError("UNKNOWN", error.message); - } - - return new AuthServiceError("UNKNOWN", "An unexpected error occurred"); - } -} - -export const authService = AuthService.getInstance(); diff --git a/apps/portal/src/features/auth/services/auth.store.ts b/apps/portal/src/features/auth/services/auth.store.ts index 143e918c..b5283aed 100644 --- a/apps/portal/src/features/auth/services/auth.store.ts +++ b/apps/portal/src/features/auth/services/auth.store.ts @@ -5,7 +5,7 @@ import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; -import { authService } from "./auth.service"; +import { apiClient } from "@/core/api"; import type { AuthTokens, SignupRequest, @@ -74,15 +74,17 @@ export const useAuthStore = create()( login: async (credentials: LoginRequest) => { set({ loading: true, error: null }); try { - const { user, tokens } = await authService.login(credentials); - set({ - user, - tokens, - isAuthenticated: true, - loading: false, - error: null, - persistSession: Boolean(credentials.rememberMe), - }); + const response = await apiClient.POST('/auth/login', { body: credentials }); + if (response.data) { + set({ + user: response.data.user, + tokens: response.data.tokens, + isAuthenticated: true, + loading: false, + error: null, + persistSession: Boolean(credentials.rememberMe), + }); + } } catch (error) { const authError = error as AuthError; set({ @@ -100,14 +102,16 @@ export const useAuthStore = create()( signup: async (data: SignupRequest) => { set({ loading: true, error: null }); try { - const { user, tokens } = await authService.signup(data); - set({ - user, - tokens, - isAuthenticated: true, - loading: false, - error: null, - }); + const response = await apiClient.POST('/auth/signup', { body: data }); + if (response.data) { + set({ + user: response.data.user, + tokens: response.data.tokens, + isAuthenticated: true, + loading: false, + error: null, + }); + } } catch (error) { const authError = error as AuthError; set({ @@ -127,7 +131,7 @@ export const useAuthStore = create()( // Call logout API if we have tokens if (tokens?.accessToken) { try { - await authService.logout(tokens.accessToken); + await apiClient.POST('/auth/logout'); } catch (error) { console.warn("Logout API call failed:", error); // Continue with local logout even if API call fails @@ -146,7 +150,7 @@ export const useAuthStore = create()( requestPasswordReset: async (data: ForgotPasswordRequest) => { set({ loading: true, error: null }); try { - await authService.requestPasswordReset(data); + await apiClient.POST('/auth/forgot-password', { body: data }); set({ loading: false }); } catch (error) { const authError = error as AuthError; @@ -158,7 +162,8 @@ export const useAuthStore = create()( resetPassword: async (data: ResetPasswordRequest) => { set({ loading: true, error: null }); try { - const { user, tokens } = await authService.resetPassword(data); + const response = await apiClient.POST('/auth/reset-password', { body: data }); + const { user, tokens } = response.data!; set({ user, tokens, @@ -187,10 +192,8 @@ export const useAuthStore = create()( set({ loading: true, error: null }); try { - const { user, tokens: newTokens } = await authService.changePassword( - tokens.accessToken, - data - ); + const response = await apiClient.POST('/auth/change-password', { body: data }); + const { user, tokens: newTokens } = response.data!; set({ user, tokens: newTokens, @@ -207,7 +210,8 @@ export const useAuthStore = create()( checkPasswordNeeded: async (email: string) => { set({ loading: true, error: null }); try { - const result = await authService.checkPasswordNeeded(email); + const response = await apiClient.POST('/auth/check-password-needed', { body: { email } }); + const result = response.data!; set({ loading: false }); return result; } catch (error) { @@ -220,7 +224,8 @@ export const useAuthStore = create()( linkWhmcs: async (email: string, password: string) => { set({ loading: true, error: null }); try { - const result = await authService.linkWhmcs(email, password); + const response = await apiClient.POST('/auth/link-whmcs', { body: { email, password } }); + const result = response.data!; set({ loading: false }); return result; } catch (error) { @@ -233,7 +238,8 @@ export const useAuthStore = create()( setPassword: async (email: string, password: string) => { set({ loading: true, error: null }); try { - const { user, tokens } = await authService.setPassword(email, password); + const response = await apiClient.POST('/auth/set-password', { body: { email, password } }); + const { user, tokens } = response.data!; set({ user, tokens, @@ -264,14 +270,15 @@ export const useAuthStore = create()( } // Check if token is expired - if (authService.isTokenExpired(tokens.expiresAt)) { + if (Date.now() >= tokens.expiresAt) { set({ isAuthenticated: false, loading: false, user: null, tokens: null, hasCheckedAuth: true }); return; } set({ loading: true }); try { - const user = await authService.getProfile(tokens.accessToken); + const response = await apiClient.GET('/me'); + const user = response.data!; set({ user, isAuthenticated: true, loading: false, error: null, hasCheckedAuth: true }); } catch (error) { // Token is invalid, clear auth state @@ -295,7 +302,7 @@ export const useAuthStore = create()( } // Check if token needs refresh (expires within 5 minutes) - if (authService.isTokenExpiringSoon(tokens.expiresAt)) { + if (Date.now() >= tokens.expiresAt - 5 * 60 * 1000) { // 5 minutes before expiry // For now, just re-validate the token // In a real implementation, you would call a refresh token endpoint await checkAuth(); @@ -352,7 +359,7 @@ export const startSessionTimeout = () => { const checkSession = () => { const state = useAuthStore.getState(); if (state.tokens?.accessToken) { - if (authService.isTokenExpired(state.tokens.expiresAt)) { + if (Date.now() >= state.tokens.expiresAt) { void state.logout(); } else { void state.refreshSession(); diff --git a/apps/portal/src/features/auth/services/index.ts b/apps/portal/src/features/auth/services/index.ts index b7a77197..fb5b5b7a 100644 --- a/apps/portal/src/features/auth/services/index.ts +++ b/apps/portal/src/features/auth/services/index.ts @@ -3,5 +3,4 @@ * Centralized exports for authentication services */ -export { authService } from "./auth.service"; export { useAuthStore, startSessionTimeout, stopSessionTimeout } from "./auth.store"; diff --git a/apps/portal/src/features/auth/views/ForgotPasswordView.tsx b/apps/portal/src/features/auth/views/ForgotPasswordView.tsx index 239d189c..09c47243 100644 --- a/apps/portal/src/features/auth/views/ForgotPasswordView.tsx +++ b/apps/portal/src/features/auth/views/ForgotPasswordView.tsx @@ -1,6 +1,6 @@ "use client"; -import { AuthLayout } from "@/components/auth/auth-layout"; +import { AuthLayout } from "../components"; import { PasswordResetForm } from "@/features/auth/components"; export function ForgotPasswordView() { diff --git a/apps/portal/src/features/auth/views/LinkWhmcsView.tsx b/apps/portal/src/features/auth/views/LinkWhmcsView.tsx index 3bda6826..da52b514 100644 --- a/apps/portal/src/features/auth/views/LinkWhmcsView.tsx +++ b/apps/portal/src/features/auth/views/LinkWhmcsView.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; -import { AuthLayout } from "@/components/auth/auth-layout"; +import { AuthLayout } from "../components"; import { LinkWhmcsForm } from "@/features/auth/components"; export function LinkWhmcsView() { diff --git a/apps/portal/src/features/auth/views/LoginView.tsx b/apps/portal/src/features/auth/views/LoginView.tsx index aff48df5..2d34f608 100644 --- a/apps/portal/src/features/auth/views/LoginView.tsx +++ b/apps/portal/src/features/auth/views/LoginView.tsx @@ -1,6 +1,6 @@ "use client"; -import { AuthLayout } from "@/components/auth/auth-layout"; +import { AuthLayout } from "../components"; import { LoginForm } from "@/features/auth/components"; export function LoginView() { diff --git a/apps/portal/src/features/auth/views/ResetPasswordView.tsx b/apps/portal/src/features/auth/views/ResetPasswordView.tsx index 448fda3c..b795da60 100644 --- a/apps/portal/src/features/auth/views/ResetPasswordView.tsx +++ b/apps/portal/src/features/auth/views/ResetPasswordView.tsx @@ -3,7 +3,7 @@ import { Suspense } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; -import { AuthLayout } from "@/components/auth/auth-layout"; +import { AuthLayout } from "../components"; import { PasswordResetForm } from "@/features/auth/components"; function ResetPasswordContent() { diff --git a/apps/portal/src/features/auth/views/SetPasswordView.tsx b/apps/portal/src/features/auth/views/SetPasswordView.tsx index 936db090..3dd61210 100644 --- a/apps/portal/src/features/auth/views/SetPasswordView.tsx +++ b/apps/portal/src/features/auth/views/SetPasswordView.tsx @@ -3,7 +3,7 @@ import { Suspense, useEffect } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; -import { AuthLayout } from "@/components/auth/auth-layout"; +import { AuthLayout } from "../components"; import { SetPasswordForm } from "@/features/auth/components"; function SetPasswordContent() { diff --git a/apps/portal/src/features/auth/views/SignupView.tsx b/apps/portal/src/features/auth/views/SignupView.tsx index df58c6c7..148b1c2b 100644 --- a/apps/portal/src/features/auth/views/SignupView.tsx +++ b/apps/portal/src/features/auth/views/SignupView.tsx @@ -1,6 +1,6 @@ "use client"; -import { AuthLayout } from "@/components/auth/auth-layout"; +import { AuthLayout } from "../components"; import { SignupForm } from "@/features/auth/components"; export function SignupView() { diff --git a/apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx b/apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx index 7c728326..99371631 100644 --- a/apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx +++ b/apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx @@ -11,8 +11,8 @@ import { } from "@heroicons/react/24/outline"; import { BillingStatusBadge } from "../BillingStatusBadge"; import type { BillingSummaryData } from "@customer-portal/domain"; -import { formatCurrency, getCurrencyLocale } from "@/lib/utils/currency"; -import { cn } from "@/lib/utils/cn"; +import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; +import { cn } from "@/shared/utils"; interface BillingSummaryProps extends React.HTMLAttributes { summary: BillingSummaryData; diff --git a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx index 57447f67..b2420c18 100644 --- a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx +++ b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx @@ -2,7 +2,7 @@ import React from "react"; import { SubCard } from "@/components/ui/sub-card"; -import { formatCurrency } from "@/lib/utils/currency"; +import { formatCurrency } from "@customer-portal/domain"; import type { InvoiceItem } from "@customer-portal/domain"; interface InvoiceItemsProps { diff --git a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx index 8aa7f6c2..0aa6216a 100644 --- a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx +++ b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx @@ -2,7 +2,7 @@ import React from "react"; import { SubCard } from "@/components/ui/sub-card"; -import { formatCurrency } from "@/lib/utils/currency"; +import { formatCurrency } from "@customer-portal/domain"; interface InvoiceTotalsProps { subtotal: number; diff --git a/apps/portal/src/features/billing/components/InvoiceItemRow.tsx b/apps/portal/src/features/billing/components/InvoiceItemRow.tsx index 3b3fe85b..def13112 100644 --- a/apps/portal/src/features/billing/components/InvoiceItemRow.tsx +++ b/apps/portal/src/features/billing/components/InvoiceItemRow.tsx @@ -1,5 +1,5 @@ "use client"; -import { formatCurrency } from "@/utils/currency"; +import { formatCurrency } from "@customer-portal/domain"; import { useRouter } from "next/navigation"; export function InvoiceItemRow({ diff --git a/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx b/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx index 14c1448f..c0164786 100644 --- a/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx +++ b/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx @@ -14,8 +14,8 @@ import { import { DataTable } from "@/components/common/DataTable"; import { BillingStatusBadge } from "../BillingStatusBadge"; import type { Invoice } from "@customer-portal/domain"; -import { formatCurrency, getCurrencyLocale } from "@/lib/utils/currency"; -import { cn } from "@/lib/utils/cn"; +import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; +import { cn } from "@/shared/utils"; interface InvoiceTableProps { invoices: Invoice[]; diff --git a/apps/portal/src/features/billing/components/PaymentMethodCard/PaymentMethodCard.tsx b/apps/portal/src/features/billing/components/PaymentMethodCard/PaymentMethodCard.tsx index da239236..9eea41dd 100644 --- a/apps/portal/src/features/billing/components/PaymentMethodCard/PaymentMethodCard.tsx +++ b/apps/portal/src/features/billing/components/PaymentMethodCard/PaymentMethodCard.tsx @@ -10,7 +10,7 @@ import { } from "@heroicons/react/24/outline"; import { Badge } from "@/components/ui/badge"; import type { PaymentMethod } from "@customer-portal/domain"; -import { cn } from "@/lib/utils/cn"; +import { cn } from "@/shared/utils"; interface PaymentMethodCardProps extends React.HTMLAttributes { paymentMethod: PaymentMethod; diff --git a/apps/portal/src/features/billing/hooks/index.ts b/apps/portal/src/features/billing/hooks/index.ts index 91256197..6ed24cf5 100644 --- a/apps/portal/src/features/billing/hooks/index.ts +++ b/apps/portal/src/features/billing/hooks/index.ts @@ -1 +1,2 @@ -export * from "@/hooks/useInvoices"; +export * from "./useBilling"; +export * from "./usePaymentRefresh"; diff --git a/apps/portal/src/features/billing/hooks/useBilling.ts b/apps/portal/src/features/billing/hooks/useBilling.ts index e3ca7e3d..8e896cd8 100644 --- a/apps/portal/src/features/billing/hooks/useBilling.ts +++ b/apps/portal/src/features/billing/hooks/useBilling.ts @@ -1,210 +1,72 @@ -import { useMemo } from "react"; +"use client"; + import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { useAuthStore } from "@/features/auth/services/auth.store"; -import { BillingService } from "../services"; -import type { - InvoiceQueryParams, - CreateInvoiceSsoLinkParams, - CreateInvoicePaymentLinkParams, -} from "../services"; +import { apiClient, queryKeys } from "@/core/api"; +import type { + Invoice, + PaymentMethod, + InvoiceQueryParams +} from "@customer-portal/domain"; /** - * Hook for fetching invoices with pagination and filtering + * Simplified Billing Hook - Just API calls, BFF handles all business logic */ -export function useInvoices(params: InvoiceQueryParams = {}, options: { enabled?: boolean } = {}) { - const { isAuthenticated, tokens } = useAuthStore(); - - return useQuery({ - queryKey: ["invoices", params], - queryFn: () => BillingService.getInvoices(params), - enabled: (options.enabled ?? true) && isAuthenticated && !!tokens?.accessToken, - staleTime: 60 * 1000, // 1 minute - gcTime: 5 * 60 * 1000, // 5 minutes - }); -} - -/** - * Hook for fetching a single invoice - */ -export function useInvoice(invoiceId: number) { - const { isAuthenticated, tokens } = useAuthStore(); - - return useQuery({ - queryKey: ["invoice", invoiceId], - queryFn: () => BillingService.getInvoice(invoiceId), - enabled: isAuthenticated && !!tokens?.accessToken && !!invoiceId, - staleTime: 60 * 1000, // 1 minute - gcTime: 5 * 60 * 1000, // 5 minutes - }); -} - -/** - * Hook for fetching invoice subscriptions - */ -export function useInvoiceSubscriptions(invoiceId: number) { - const { isAuthenticated, tokens } = useAuthStore(); - - return useQuery({ - queryKey: ["invoice-subscriptions", invoiceId], - queryFn: () => BillingService.getInvoiceSubscriptions(invoiceId), - enabled: isAuthenticated && !!tokens?.accessToken && !!invoiceId, - staleTime: 60 * 1000, // 1 minute - gcTime: 5 * 60 * 1000, // 5 minutes - }); -} - -/** - * Hook for fetching payment methods - */ -export function usePaymentMethods() { - const { isAuthenticated, tokens, hasCheckedAuth } = useAuthStore(); - - return useQuery({ - queryKey: ["paymentMethods"], - queryFn: () => BillingService.getPaymentMethods(), - // Avoid triggering until auth has been checked to prevent UI flash - enabled: hasCheckedAuth && isAuthenticated && !!tokens?.accessToken, - staleTime: 1 * 60 * 1000, // 1 minute - gcTime: 5 * 60 * 1000, // 5 minutes - retry: 3, - retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), - // Keep previous data during background refetches to reduce UI flicker - placeholderData: prev => prev, - }); -} - -export function useRefreshPaymentMethods() { +export function useBilling() { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: () => BillingService.refreshPaymentMethods(), - onSuccess: data => { - queryClient.setQueryData(["paymentMethods"], data); - }, - }); -} - -/** - * Hook for fetching payment gateways - */ -export function usePaymentGateways() { - const { isAuthenticated, tokens } = useAuthStore(); - - return useQuery({ - queryKey: ["paymentGateways"], - queryFn: () => BillingService.getPaymentGateways(), - enabled: isAuthenticated && !!tokens?.accessToken, - staleTime: 60 * 60 * 1000, // 1 hour - gcTime: 2 * 60 * 60 * 1000, // 2 hours - }); -} - -/** - * Mutation hook for creating invoice SSO links - */ -export function useCreateInvoiceSsoLink() { - return useMutation({ - mutationFn: (vars: CreateInvoiceSsoLinkParams) => BillingService.createInvoiceSsoLink(vars), - }); -} - -/** - * Mutation hook for creating invoice payment links - */ -export function useCreateInvoicePaymentLink() { - return useMutation({ - mutationFn: (vars: CreateInvoicePaymentLinkParams) => - BillingService.createInvoicePaymentLink(vars), - }); -} - -/** - * Mutation hook for setting default payment method - */ -export function useSetDefaultPaymentMethod() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (paymentMethodId: number) => - BillingService.setDefaultPaymentMethod(paymentMethodId), - onSuccess: () => { - // Invalidate payment methods to refresh the list - void queryClient.invalidateQueries({ queryKey: ["paymentMethods"] }); - }, - }); -} - -/** - * Mutation hook for deleting payment method - */ -export function useDeletePaymentMethod() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (paymentMethodId: number) => BillingService.deletePaymentMethod(paymentMethodId), - onSuccess: () => { - // Invalidate payment methods to refresh the list - void queryClient.invalidateQueries({ queryKey: ["paymentMethods"] }); - }, - }); -} - -/** - * Hook for generating billing summary from invoice data - */ -export function useBillingSummary(params: InvoiceQueryParams = {}) { - const { data: invoiceData, ...queryResult } = useInvoices({ - ...params, - limit: 100, // Get more invoices for summary calculation - }); - - const summary = useMemo(() => { - if (!invoiceData?.invoices) { - return null; - } - - const invoices = invoiceData.invoices; - const currency = invoices[0]?.currency || "USD"; - const currencySymbol = invoices[0]?.currencySymbol; - - const totals = invoices.reduce( - (acc, invoice) => { - switch (invoice.status.toLowerCase()) { - case "unpaid": - acc.totalOutstanding += invoice.total; - acc.invoiceCount.unpaid += 1; - break; - case "overdue": - acc.totalOverdue += invoice.total; - acc.invoiceCount.overdue += 1; - break; - case "paid": - acc.totalPaid += invoice.total; - acc.invoiceCount.paid += 1; - break; - } - acc.invoiceCount.total += 1; - return acc; - }, - { - totalOutstanding: 0, - totalOverdue: 0, - totalPaid: 0, - currency, - currencySymbol, - invoiceCount: { - total: 0, - unpaid: 0, - overdue: 0, - paid: 0, - }, - } - ); - - return totals; - }, [invoiceData]); return { - ...queryResult, - data: summary, + // Get invoices (BFF returns formatted, ready-to-display data) + useInvoices: (params?: InvoiceQueryParams) => + useQuery({ + queryKey: queryKeys.billing.invoices(params), + queryFn: () => apiClient.GET('/invoices', { params: { query: params } }), + }), + + // Get single invoice + useInvoice: (id: string) => + useQuery({ + queryKey: queryKeys.billing.invoice(id), + queryFn: () => apiClient.GET('/invoices/{id}', { params: { path: { id } } }), + enabled: !!id, + }), + + // Get payment methods + usePaymentMethods: () => + useQuery({ + queryKey: queryKeys.billing.paymentMethods(), + queryFn: () => apiClient.GET('/billing/payment-methods'), + }), + + // Create payment link (BFF handles WHMCS integration) + useCreatePaymentLink: () => + useMutation({ + mutationFn: (invoiceId: string) => + apiClient.POST('/invoices/{id}/payment-link', { + params: { path: { id: invoiceId } } + }), + }), + + // Add payment method + useAddPaymentMethod: () => + useMutation({ + mutationFn: (paymentData: any) => + apiClient.POST('/billing/payment-methods', { body: paymentData }), + onSuccess: () => { + // Invalidate payment methods cache + queryClient.invalidateQueries({ queryKey: queryKeys.billing.paymentMethods() }); + }, + }), + + // Delete payment method + useDeletePaymentMethod: () => + useMutation({ + mutationFn: (methodId: string) => + apiClient.DELETE('/billing/payment-methods/{id}', { + params: { path: { id: methodId } } + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.billing.paymentMethods() }); + }, + }), }; -} +} \ No newline at end of file diff --git a/apps/portal/src/features/billing/hooks/usePaymentRefresh.ts b/apps/portal/src/features/billing/hooks/usePaymentRefresh.ts index 3b950701..1e80c656 100644 --- a/apps/portal/src/features/billing/hooks/usePaymentRefresh.ts +++ b/apps/portal/src/features/billing/hooks/usePaymentRefresh.ts @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { openApiClient as apiClient } from "@/lib/api/openapi-client"; +import { apiClient } from "@/core/api"; type Tone = "info" | "success" | "warning" | "error"; @@ -29,7 +29,7 @@ export function usePaymentRefresh({ setToast({ visible: true, text: "Refreshing payment methods...", tone: "info" }); try { try { - await apiClient.post("/invoices/payment-methods/refresh"); + await apiClient.POST("/invoices/payment-methods/refresh"); } catch (err) { // Soft-fail cache refresh, still attempt refetch console.warn("Payment methods cache refresh failed:", err); diff --git a/apps/portal/src/features/billing/services/billing.service.ts b/apps/portal/src/features/billing/services/billing.service.ts deleted file mode 100644 index b18894ee..00000000 --- a/apps/portal/src/features/billing/services/billing.service.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { openApiClient as apiClient } from "@/lib/api/openapi-client"; -import type { - Invoice, - InvoiceList, - InvoiceSsoLink, - PaymentMethod, - PaymentMethodList, - PaymentGateway, - PaymentGatewayList, - InvoicePaymentLink, - Subscription, -} from "@customer-portal/domain"; -import type { ApiResponse, PaginatedResponse } from "@customer-portal/domain"; - -export interface InvoiceQueryParams { - page?: number; - limit?: number; - status?: string; -} - -export interface CreateInvoiceSsoLinkParams { - invoiceId: number; - target?: "view" | "download" | "pay"; -} - -export interface CreateInvoicePaymentLinkParams { - invoiceId: number; - paymentMethodId?: number; - gatewayName?: string; -} - -/** - * Centralized billing service for all invoice and payment operations - */ -export class BillingService { - /** - * Fetch paginated list of invoices - */ - static async getInvoices(params: InvoiceQueryParams = {}): Promise { - const { page = 1, limit = 10, status } = params; - - const searchParams = new URLSearchParams({ - page: page.toString(), - limit: limit.toString(), - ...(status && { status }), - }); - - const res = await apiClient.get(`/invoices?${searchParams}`); - if (res.success) return res.data as InvoiceList; - throw new Error(res.error.message || "Failed to fetch invoices"); - } - - /** - * Fetch a single invoice by ID - */ - static async getInvoice(invoiceId: number): Promise { - const res = await apiClient.get(`/invoices/${invoiceId}`); - if (res.success) return res.data as Invoice; - throw new Error(res.error.message || "Failed to fetch invoice"); - } - - /** - * Fetch subscriptions associated with an invoice - */ - static async getInvoiceSubscriptions(invoiceId: number): Promise { - const res = await apiClient.get(`/invoices/${invoiceId}/subscriptions`); - if (res.success) return res.data as Subscription[]; - throw new Error(res.error.message || "Failed to fetch invoice subscriptions"); - } - - /** - * Create SSO link for invoice viewing/downloading/payment - */ - static async createInvoiceSsoLink(params: CreateInvoiceSsoLinkParams): Promise { - const { invoiceId, target = "view" } = params; - - const searchParams = new URLSearchParams(); - if (target !== "view") { - searchParams.append("target", target); - } - - const url = `/invoices/${invoiceId}/sso-link${searchParams.toString() ? `?${searchParams.toString()}` : ""}`; - const res = await apiClient.post(url); - if (res.success) return res.data as InvoiceSsoLink; - throw new Error(res.error.message || "Failed to create SSO link"); - } - - /** - * Create payment link for invoice - */ - static async createInvoicePaymentLink( - params: CreateInvoicePaymentLinkParams - ): Promise { - const { invoiceId, paymentMethodId, gatewayName } = params; - - const searchParams = new URLSearchParams(); - if (paymentMethodId) { - searchParams.append("paymentMethodId", paymentMethodId.toString()); - } - if (gatewayName) { - searchParams.append("gatewayName", gatewayName); - } - - const url = `/invoices/${invoiceId}/payment-link${searchParams.toString() ? `?${searchParams.toString()}` : ""}`; - const res = await apiClient.post(url); - if (res.success) return res.data as InvoicePaymentLink; - throw new Error(res.error.message || "Failed to create payment link"); - } - - /** - * Fetch user's payment methods - */ - static async getPaymentMethods(): Promise { - const res = await apiClient.get("/invoices/payment-methods"); - if (res.success) return res.data as PaymentMethodList; - throw new Error(res.error.message || "Failed to fetch payment methods"); - } - - /** - * Refresh payment methods cache on the server and return fresh list - */ - static async refreshPaymentMethods(): Promise { - const res = await apiClient.post("/invoices/payment-methods/refresh"); - if (res.success) return res.data as PaymentMethodList; - throw new Error(res.error.message || "Failed to refresh payment methods"); - } - - /** - * Fetch available payment gateways - */ - static async getPaymentGateways(): Promise { - const res = await apiClient.get("/invoices/payment-gateways"); - if (res.success) return res.data as PaymentGatewayList; - throw new Error(res.error.message || "Failed to fetch payment gateways"); - } - - /** - * Set a payment method as default - */ - static async setDefaultPaymentMethod(paymentMethodId: number): Promise { - const res = await apiClient.patch( - `/invoices/payment-methods/${paymentMethodId}/default` - ); - if (res.success) return res.data as PaymentMethod; - throw new Error(res.error.message || "Failed to set default payment method"); - } - - /** - * Delete a payment method - */ - static async deletePaymentMethod(paymentMethodId: number): Promise { - const res = await apiClient.delete(`/invoices/payment-methods/${paymentMethodId}`) as ApiResponse; - if (res.success) return; - throw new Error(res.error.message || "Failed to delete payment method"); - } -} - -export default BillingService; diff --git a/apps/portal/src/features/billing/services/index.ts b/apps/portal/src/features/billing/services/index.ts deleted file mode 100644 index 526d7e1f..00000000 --- a/apps/portal/src/features/billing/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./billing.service"; diff --git a/apps/portal/src/features/billing/views/InvoiceDetail.tsx b/apps/portal/src/features/billing/views/InvoiceDetail.tsx index 1f6a96bc..c291d8f3 100644 --- a/apps/portal/src/features/billing/views/InvoiceDetail.tsx +++ b/apps/portal/src/features/billing/views/InvoiceDetail.tsx @@ -10,7 +10,7 @@ import { CheckCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/24/ou import { PageLayout } from "@/components/layout/PageLayout"; import { CreditCardIcon } from "@heroicons/react/24/outline"; import { logger } from "@customer-portal/logging"; -import { AuthService } from "@/features/auth/services/auth.service"; +import { apiClient } from "@/core/api"; import { openSsoLink } from "@/features/billing/utils/sso"; import { useInvoice, useCreateInvoiceSsoLink } from "@/features/billing/hooks"; import { @@ -53,9 +53,10 @@ export function InvoiceDetailContainer() { void (async () => { setLoadingPaymentMethods(true); try { - const sso = await AuthService.getInstance().createSsoLink( - "index.php?rp=/account/paymentmethods" - ); + const response = await apiClient.POST('/auth/sso-link', { + body: { path: "index.php?rp=/account/paymentmethods" } + }); + const sso = response.data!; openSsoLink(sso.url, { newTab: true }); } catch (err) { logger.error(err, "Failed to create payment methods SSO link"); diff --git a/apps/portal/src/features/billing/views/PaymentMethods.tsx b/apps/portal/src/features/billing/views/PaymentMethods.tsx index 27b98c4b..5368a717 100644 --- a/apps/portal/src/features/billing/views/PaymentMethods.tsx +++ b/apps/portal/src/features/billing/views/PaymentMethods.tsx @@ -7,7 +7,7 @@ import { SubCard } from "@/components/ui/sub-card"; import { useSession } from "@/features/auth/hooks"; import { useAuthStore } from "@/features/auth/services/auth.store"; // ApiClientError import removed - using generic error handling -import { AuthService } from "@/features/auth/services/auth.service"; +import { apiClient } from "@/core/api"; import { openSsoLink } from "@/features/billing/utils/sso"; import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh"; import { PaymentMethodCard, usePaymentMethods } from "@/features/billing"; @@ -53,9 +53,10 @@ export function PaymentMethodsContainer() { setIsLoading(false); return; } - const sso = await AuthService.getInstance().createSsoLink( - "index.php?rp=/account/paymentmethods" - ); + const response = await apiClient.POST('/auth/sso-link', { + body: { path: "index.php?rp=/account/paymentmethods" } + }); + const sso = response.data!; openSsoLink(sso.url, { newTab: true }); setIsLoading(false); } catch (error) { diff --git a/apps/portal/src/features/catalog/services/catalog.service.ts b/apps/portal/src/features/catalog/services/catalog.service.ts deleted file mode 100644 index 1a8797f5..00000000 --- a/apps/portal/src/features/catalog/services/catalog.service.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Catalog Service - * Business logic for catalog operations - */ - -import { openApiClient as apiClient } from "@/lib/api/openapi-client"; -import { unwrap } from "@/lib/api/unwrap"; -import type { - CatalogFilter, - InternetPlan, - InternetAddon, - InternetInstallation, - SimPlan, - SimActivationFee, - SimAddon, - VpnPlan, - VpnActivationFee, - CatalogOrderItem, - OrderTotals, -} from "@customer-portal/domain"; - -// Type aliases for convenience -type CatalogProduct = InternetPlan | SimPlan | VpnPlan; -type ProductConfiguration = Record; -type OrderSummary = { - items: CatalogOrderItem[]; - totals: OrderTotals; -}; - -export class CatalogService { - /** - * Get all products with optional filtering - */ - async getProducts(filters?: CatalogFilter): Promise { - const params = new URLSearchParams(); - - if (filters?.category) { - params.append("category", filters.category); - } - - if (filters?.priceRange) { - params.append("minPrice", filters.priceRange.min.toString()); - params.append("maxPrice", filters.priceRange.max.toString()); - } - - const response = await apiClient.get( - `/catalog/products?${params.toString()}` - ); - return unwrap(response); - } - - /** - * Get a specific product by ID - */ - async getProduct(id: string): Promise { - const response = await apiClient.get(`/catalog/products/${id}`); - return unwrap(response); - } - - /** - * Get products by category - */ - async getProductsByCategory(category: "internet" | "sim" | "vpn"): Promise { - return this.getProducts({ category }); - } - - /** - * Calculate order summary based on configuration - */ - async calculateOrderSummary(configuration: ProductConfiguration): Promise { - const response = await apiClient.post("/catalog/calculate", configuration); - return unwrap(response); - } - - /** - * Submit an order - */ - async submitOrder(orderSummary: OrderSummary): Promise<{ orderId: string; status: string }> { - const response = await apiClient.post<{ orderId: string; status: string }>( - "/orders", - orderSummary - ); - return unwrap(response); - } - - // Internet-specific endpoints used by pages - async getInternetPlans(): Promise { - const res = await apiClient.get("/catalog/internet/plans"); - return unwrap(res); - } - - async getInternetAddons(): Promise { - const res = await apiClient.get("/catalog/internet/addons"); - return unwrap(res); - } - - async getInternetInstallations(): Promise { - const res = await apiClient.get("/catalog/internet/installations"); - return unwrap(res); - } - - // SIM-specific endpoints - async getSimPlans(): Promise { - const res = await apiClient.get("/catalog/sim/plans"); - return unwrap(res); - } - - async getSimActivationFees(): Promise { - const res = await apiClient.get("/catalog/sim/activation-fees"); - return unwrap(res); - } - - async getSimAddons(): Promise { - const res = await apiClient.get("/catalog/sim/addons"); - return unwrap(res); - } - - // VPN-specific endpoints - async getVpnPlans(): Promise { - const res = await apiClient.get("/catalog/vpn/plans"); - return unwrap(res); - } - - async getVpnActivationFees(): Promise { - const res = await apiClient.get("/catalog/vpn/activation-fees"); - return unwrap(res); - } -} - -// Export singleton instance -export const catalogService = new CatalogService(); diff --git a/apps/portal/src/features/catalog/services/index.ts b/apps/portal/src/features/catalog/services/index.ts deleted file mode 100644 index ea9739b1..00000000 --- a/apps/portal/src/features/catalog/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./catalog.service"; diff --git a/apps/portal/src/features/catalog/utils/catalog.utils.ts b/apps/portal/src/features/catalog/utils/catalog.utils.ts index f18a6d74..4f7bc10f 100644 --- a/apps/portal/src/features/catalog/utils/catalog.utils.ts +++ b/apps/portal/src/features/catalog/utils/catalog.utils.ts @@ -9,7 +9,7 @@ import type { InternetPlan, SimPlan, VpnPlan } from "@customer-portal/domain"; // Type alias for convenience type CatalogProduct = InternetPlan | SimPlan | VpnPlan; -import { formatCurrency } from "@/lib/utils/currency"; +import { formatCurrency } from "@customer-portal/domain"; /** * Format price with currency (wrapper for centralized utility) diff --git a/apps/portal/src/features/dashboard/components/ActivityFeed.tsx b/apps/portal/src/features/dashboard/components/ActivityFeed.tsx index c1eff383..6626fb9f 100644 --- a/apps/portal/src/features/dashboard/components/ActivityFeed.tsx +++ b/apps/portal/src/features/dashboard/components/ActivityFeed.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { ArrowTrendingUpIcon } from "@heroicons/react/24/outline"; -import { cn } from "@/lib/utils/cn"; +import { cn } from "@/shared/utils"; import { DashboardActivityItem } from "./DashboardActivityItem"; import { filterActivities, diff --git a/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx b/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx index 701120c5..2c53b91c 100644 --- a/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx +++ b/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { CalendarDaysIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; import { format, formatDistanceToNow } from "date-fns"; -import { formatCurrency, getCurrencyLocale } from "@/lib/utils/currency"; +import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; interface UpcomingPaymentBannerProps { invoice: { id: number; amount: number; currency?: string; dueDate: string }; diff --git a/apps/portal/src/features/dashboard/hooks/index.ts b/apps/portal/src/features/dashboard/hooks/index.ts index 0f22edf6..2b8fbac9 100644 --- a/apps/portal/src/features/dashboard/hooks/index.ts +++ b/apps/portal/src/features/dashboard/hooks/index.ts @@ -1 +1 @@ -export * from "@/hooks/useDashboard"; +export * from "./useDashboardSummary"; diff --git a/apps/portal/src/features/dashboard/hooks/useDashboard.ts b/apps/portal/src/features/dashboard/hooks/useDashboard.ts new file mode 100644 index 00000000..2833ddc6 --- /dev/null +++ b/apps/portal/src/features/dashboard/hooks/useDashboard.ts @@ -0,0 +1,27 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { apiClient, queryKeys } from "@/core/api"; + +/** + * Simplified Dashboard Hook - Just fetch formatted data from BFF + */ +export function useDashboard() { + // Get dashboard summary (BFF returns formatted, aggregated data) + const summaryQuery = useQuery({ + queryKey: queryKeys.dashboard.summary(), + queryFn: () => apiClient.GET('/dashboard/summary'), + staleTime: 2 * 60 * 1000, // 2 minutes (financial data should be fresh) + }); + + return { + // Dashboard summary with all stats pre-calculated by BFF + summary: summaryQuery.data?.data, + isLoading: summaryQuery.isLoading, + error: summaryQuery.error, + refetch: summaryQuery.refetch, + }; +} + +// Export the hook with the same name as before for compatibility +export const useDashboardSummary = useDashboard; diff --git a/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts b/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts index f2972942..41e8f310 100644 --- a/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts +++ b/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts @@ -5,7 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAuthStore } from "@/features/auth/services/auth.store"; -import { dashboardService } from "../services/dashboard.service"; +import { apiClient } from "@/core/api"; import type { DashboardSummary, DashboardError } from "@customer-portal/domain"; class DashboardDataError extends Error { @@ -45,7 +45,8 @@ export function useDashboardSummary() { } try { - return await dashboardService.getSummary(); + const response = await apiClient.GET('/dashboard/summary'); + return response.data!; } catch (error) { // Transform API errors to DashboardError format if (error instanceof Error) { @@ -96,7 +97,10 @@ export function useRefreshDashboard() { // Optionally fetch fresh data immediately return queryClient.fetchQuery({ queryKey: dashboardQueryKeys.summary(), - queryFn: () => dashboardService.refreshSummary(), + queryFn: async () => { + const response = await apiClient.POST('/dashboard/refresh'); + return response.data!; + }, }); }; @@ -115,7 +119,8 @@ export function useDashboardStats() { if (!tokens?.accessToken) { throw new Error("Authentication required"); } - return dashboardService.getStats(); + const response = await apiClient.GET('/dashboard/stats'); + return response.data!; }, staleTime: 1 * 60 * 1000, // 1 minute gcTime: 3 * 60 * 1000, // 3 minutes @@ -135,7 +140,10 @@ export function useDashboardActivity(filters?: string[], limit = 10) { if (!tokens?.accessToken) { throw new Error("Authentication required"); } - return dashboardService.getRecentActivity(limit, filters); + const response = await apiClient.GET('/dashboard/activity', { + params: { query: { limit, filters: filters?.join(',') } } + }); + return response.data!; }, staleTime: 30 * 1000, // 30 seconds gcTime: 2 * 60 * 1000, // 2 minutes @@ -155,7 +163,8 @@ export function useNextInvoice() { if (!tokens?.accessToken) { throw new Error("Authentication required"); } - return dashboardService.getNextInvoice(); + const response = await apiClient.GET('/dashboard/next-invoice'); + return response.data!; }, staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, // 10 minutes diff --git a/apps/portal/src/features/dashboard/services/dashboard.service.ts b/apps/portal/src/features/dashboard/services/dashboard.service.ts deleted file mode 100644 index daf121f1..00000000 --- a/apps/portal/src/features/dashboard/services/dashboard.service.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Dashboard Service - * Handles all dashboard-related API operations and data transformation - */ - -import { BaseService } from "@/lib/api/base.service"; -import { openApiClient as apiClient } from "@/lib/api/openapi-client"; -import type { DashboardSummary } from "@customer-portal/domain"; -import type { UserSummary } from "@customer-portal/domain"; - -/** - * Dashboard Service Class - * Provides methods for fetching dashboard data and transforming API responses - */ -export class DashboardService extends BaseService { - protected readonly basePath = "/me"; - - /** - * Get dashboard summary data - * Fetches user summary and transforms it to dashboard format - */ - async getSummary(): Promise { - const response = await this.apiClient.get(this.buildPath("summary")); - const userSummary = this.extractData(response); - - return this.transformUserSummaryToDashboard(userSummary); - } - - /** - * Refresh dashboard data - * Forces a fresh fetch of dashboard data - */ - async refreshSummary(): Promise { - const response = await this.apiClient.get(this.buildPath("summary"), { - headers: { - "Cache-Control": "no-cache", - }, - }); - const userSummary = this.extractData(response); - - return this.transformUserSummaryToDashboard(userSummary); - } - - /** - * Get recent activity with filtering - */ - async getRecentActivity( - limit = 10, - types?: string[] - ): Promise { - const params: Record = { limit }; - - if (types && types.length > 0) { - params.types = types.join(","); - } - - const response = await this.apiClient.get<{ - recentActivity: DashboardSummary["recentActivity"]; - }>(this.buildPath("activity"), { params }); - - const data = this.extractData(response); - return data.recentActivity; - } - - /** - * Transform UserSummary to DashboardSummary - * Converts the API response format to the dashboard-specific format - */ - private transformUserSummaryToDashboard(userSummary: UserSummary): DashboardSummary { - // Safely extract recentOrders from stats with proper type checking - const statsWithExtras = userSummary.stats as UserSummary["stats"] & { recentOrders?: number }; - - return { - stats: { - activeSubscriptions: userSummary.stats.activeSubscriptions, - unpaidInvoices: userSummary.stats.unpaidInvoices, - openCases: userSummary.stats.openCases, - recentOrders: statsWithExtras.recentOrders || 0, - totalSpent: userSummary.stats.totalSpent, - currency: userSummary.stats.currency, - }, - nextInvoice: userSummary.nextInvoice || null, - recentActivity: userSummary.recentActivity || [], - }; - } - - /** - * Get dashboard stats only (lightweight) - */ - async getStats(): Promise { - const response = await this.apiClient.get<{ stats: DashboardSummary["stats"] }>( - this.buildPath("stats") - ); - - const data = this.extractData(response); - return data.stats; - } - - /** - * Get next invoice information - */ - async getNextInvoice(): Promise { - const response = await this.apiClient.get<{ nextInvoice: DashboardSummary["nextInvoice"] }>( - this.buildPath("next-invoice") - ); - - const data = this.extractData(response); - return data.nextInvoice; - } -} - -// Create and export service instance -export const dashboardService = new DashboardService(apiClient); diff --git a/apps/portal/src/features/dashboard/services/index.ts b/apps/portal/src/features/dashboard/services/index.ts deleted file mode 100644 index 5a6df51c..00000000 --- a/apps/portal/src/features/dashboard/services/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Dashboard Services - * Centralized export of all dashboard-related services - */ - -export * from "./dashboard.service"; diff --git a/apps/portal/src/features/dashboard/stores/dashboard.store.ts b/apps/portal/src/features/dashboard/stores/dashboard.store.ts deleted file mode 100644 index b4840f42..00000000 --- a/apps/portal/src/features/dashboard/stores/dashboard.store.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Dashboard Store - * Local state management for dashboard UI state and preferences - */ - -import { create } from "zustand"; -import { persist } from "zustand/middleware"; -import type { ActivityFilter } from "@customer-portal/domain"; - -interface DashboardUIState { - // Activity filter state - activityFilter: ActivityFilter; - setActivityFilter: (filter: ActivityFilter) => void; - - // Dashboard preferences - preferences: { - showWelcomeMessage: boolean; - compactView: boolean; - autoRefresh: boolean; - refreshInterval: number; // in seconds - }; - updatePreferences: (preferences: Partial) => void; - - // UI state - isRefreshing: boolean; - setRefreshing: (refreshing: boolean) => void; - - // Error handling - dismissedErrors: string[]; - dismissError: (errorId: string) => void; - clearDismissedErrors: () => void; - - // Reset all state - reset: () => void; -} - -const initialState = { - activityFilter: "all" as ActivityFilter, - preferences: { - showWelcomeMessage: true, - compactView: false, - autoRefresh: false, - refreshInterval: 300, // 5 minutes - }, - isRefreshing: false, - dismissedErrors: [], -}; - -export const useDashboardStore = create()( - persist( - (set, get) => ({ - ...initialState, - - setActivityFilter: filter => { - set({ activityFilter: filter }); - }, - - updatePreferences: newPreferences => { - set(state => ({ - preferences: { - ...state.preferences, - ...newPreferences, - }, - })); - }, - - setRefreshing: refreshing => { - set({ isRefreshing: refreshing }); - }, - - dismissError: errorId => { - set(state => ({ - dismissedErrors: [...state.dismissedErrors, errorId], - })); - }, - - clearDismissedErrors: () => { - set({ dismissedErrors: [] }); - }, - - reset: () => { - set(initialState); - }, - }), - { - name: "dashboard-store", - // Only persist preferences and dismissed errors - partialize: state => ({ - preferences: state.preferences, - dismissedErrors: state.dismissedErrors, - }), - } - ) -); diff --git a/apps/portal/src/features/dashboard/stores/index.ts b/apps/portal/src/features/dashboard/stores/index.ts deleted file mode 100644 index be28acf6..00000000 --- a/apps/portal/src/features/dashboard/stores/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Dashboard Stores - * Centralized export of all dashboard-related stores - */ - -export * from "./dashboard.store"; diff --git a/apps/portal/src/features/orders/services/orders.service.ts b/apps/portal/src/features/orders/services/orders.service.ts deleted file mode 100644 index 32db9009..00000000 --- a/apps/portal/src/features/orders/services/orders.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Orders Service - * Centralized methods for orders API operations - */ - -import { openApiClient as apiClient } from "@/lib/api/openapi-client"; -import { unwrap } from "@/lib/api/unwrap"; - -export class OrdersService { - async getMyOrders(): Promise { - const res = await apiClient.get("/orders/user"); - return (unwrap(res) as T[]) || []; - } - - async getOrderById(id: string): Promise { - const res = await apiClient.get(`/orders/${id}`); - return unwrap(res) as T; - } - - async createOrder(orderData: Record): Promise { - const res = await apiClient.post("/orders", orderData); - return unwrap(res) as T; - } -} - -export const ordersService = new OrdersService(); diff --git a/apps/portal/src/features/sim-management/components/ChangePlanModal.tsx b/apps/portal/src/features/sim-management/components/ChangePlanModal.tsx index 32bb88a2..c0d8309e 100644 --- a/apps/portal/src/features/sim-management/components/ChangePlanModal.tsx +++ b/apps/portal/src/features/sim-management/components/ChangePlanModal.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState } from "react"; -import { authenticatedApi } from "@/lib/api"; +import { apiClient } from "@/core/api"; import { XMarkIcon } from "@heroicons/react/24/outline"; interface ChangePlanModalProps { @@ -42,7 +42,7 @@ export function ChangePlanModal({ } setLoading(true); try { - await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/change-plan`, { + await apiClient.post(`/subscriptions/${subscriptionId}/sim/change-plan`, { newPlanCode: newPlanCode, }); onSuccess(); diff --git a/apps/portal/src/features/sim-management/components/SimActions.tsx b/apps/portal/src/features/sim-management/components/SimActions.tsx index 9eb00503..105d3791 100644 --- a/apps/portal/src/features/sim-management/components/SimActions.tsx +++ b/apps/portal/src/features/sim-management/components/SimActions.tsx @@ -11,7 +11,7 @@ import { } from "@heroicons/react/24/outline"; import { TopUpModal } from "./TopUpModal"; import { ChangePlanModal } from "./ChangePlanModal"; -import { authenticatedApi } from "@/lib/api"; +import { apiClient } from "@/core/api"; interface SimActionsProps { subscriptionId: number; @@ -58,7 +58,7 @@ export function SimActions({ setError(null); try { - await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/reissue-esim`); + await apiClient.post(`/subscriptions/${subscriptionId}/sim/reissue-esim`); setSuccess("eSIM profile reissued successfully"); setShowReissueConfirm(false); @@ -75,7 +75,7 @@ export function SimActions({ setError(null); try { - await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/cancel`, {}); + await apiClient.post(`/subscriptions/${subscriptionId}/sim/cancel`, {}); setSuccess("SIM service cancelled successfully"); setShowCancelConfirm(false); diff --git a/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx b/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx index 26782ce6..db6e0d7f 100644 --- a/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx +++ b/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import { formatPlanShort } from "@/lib/plan"; +import { formatPlanShort } from "@/shared/utils"; import { DevicePhoneMobileIcon, WifiIcon, diff --git a/apps/portal/src/features/sim-management/components/SimFeatureToggles.tsx b/apps/portal/src/features/sim-management/components/SimFeatureToggles.tsx index 1427144e..2db603c5 100644 --- a/apps/portal/src/features/sim-management/components/SimFeatureToggles.tsx +++ b/apps/portal/src/features/sim-management/components/SimFeatureToggles.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useEffect, useMemo, useState } from "react"; -import { authenticatedApi } from "@/lib/api"; +import { apiClient } from "@/core/api"; interface SimFeatureTogglesProps { subscriptionId: number; @@ -75,7 +75,7 @@ export function SimFeatureToggles({ if (nt !== initial.nt) featurePayload.networkType = nt; if (Object.keys(featurePayload).length > 0) { - await authenticatedApi.post( + await apiClient.post( `/subscriptions/${subscriptionId}/sim/features`, featurePayload ); diff --git a/apps/portal/src/features/sim-management/components/SimManagementSection.tsx b/apps/portal/src/features/sim-management/components/SimManagementSection.tsx index 5bf8eee7..6d762193 100644 --- a/apps/portal/src/features/sim-management/components/SimManagementSection.tsx +++ b/apps/portal/src/features/sim-management/components/SimManagementSection.tsx @@ -9,7 +9,7 @@ import { import { SimDetailsCard, type SimDetails } from "./SimDetailsCard"; import { DataUsageChart, type SimUsage } from "./DataUsageChart"; import { SimActions } from "./SimActions"; -import { authenticatedApi } from "@/lib/api"; +import { apiClient } from "@/core/api"; import { SimFeatureToggles } from "./SimFeatureToggles"; interface SimManagementSectionProps { @@ -30,7 +30,7 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro try { setError(null); - const data = await authenticatedApi.get<{ + const data = await apiClient.get<{ details: SimDetails; usage: SimUsage; }>(`/subscriptions/${subscriptionId}/sim`); diff --git a/apps/portal/src/features/sim-management/components/TopUpModal.tsx b/apps/portal/src/features/sim-management/components/TopUpModal.tsx index 48f669a4..81d95c7d 100644 --- a/apps/portal/src/features/sim-management/components/TopUpModal.tsx +++ b/apps/portal/src/features/sim-management/components/TopUpModal.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { XMarkIcon, PlusIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import { authenticatedApi } from "@/lib/api"; +import { apiClient } from "@/core/api"; interface TopUpModalProps { subscriptionId: number; @@ -45,7 +45,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU quotaMb: getCurrentAmountMb(), }; - await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/top-up`, requestBody); + await apiClient.post(`/subscriptions/${subscriptionId}/sim/top-up`, requestBody); onSuccess(); } catch (error: unknown) { diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionActions.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionActions.tsx index dfc14d1f..c03c8b15 100644 --- a/apps/portal/src/features/subscriptions/components/SubscriptionActions.tsx +++ b/apps/portal/src/features/subscriptions/components/SubscriptionActions.tsx @@ -16,7 +16,7 @@ import { import { Button } from "@/components/ui/button"; import { SubCard } from "@/components/ui/sub-card"; import type { Subscription } from "@customer-portal/domain"; -import { cn } from "@/lib/utils/cn"; +import { cn } from "@/shared/utils"; import { useSubscriptionAction } from "../hooks"; interface SubscriptionActionsProps { diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx index cc6e36bd..311b7130 100644 --- a/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx +++ b/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx @@ -15,9 +15,9 @@ import { import { StatusPill } from "@/components/ui/status-pill"; import { Button } from "@/components/ui/button"; import { SubCard } from "@/components/ui/sub-card"; -import { formatCurrency, getCurrencyLocale } from "@/lib/utils/currency"; +import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; import type { Subscription } from "@customer-portal/domain"; -import { cn } from "@/lib/utils/cn"; +import { cn } from "@/shared/utils"; interface SubscriptionCardProps { subscription: Subscription; diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx index 70f6cc6a..52e02d5e 100644 --- a/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx +++ b/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx @@ -15,9 +15,9 @@ import { } from "@heroicons/react/24/outline"; import { StatusPill } from "@/components/ui/status-pill"; import { SubCard } from "@/components/ui/sub-card"; -import { formatCurrency, getCurrencyLocale } from "@/lib/utils/currency"; +import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; import type { Subscription } from "@customer-portal/domain"; -import { cn } from "@/lib/utils/cn"; +import { cn } from "@/shared/utils"; interface SubscriptionDetailsProps { subscription: Subscription; diff --git a/apps/portal/src/features/subscriptions/hooks/index.ts b/apps/portal/src/features/subscriptions/hooks/index.ts index 7df8b60d..020eef16 100644 --- a/apps/portal/src/features/subscriptions/hooks/index.ts +++ b/apps/portal/src/features/subscriptions/hooks/index.ts @@ -1 +1 @@ -export * from "@/hooks/useSubscriptions"; +export * from "./useSubscriptions"; diff --git a/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts b/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts index da4f0f62..32ac755d 100644 --- a/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts +++ b/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts @@ -5,8 +5,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useAuthStore } from "@/features/auth/services/auth.store"; -import { openApiClient as apiClient } from "@/lib/api/openapi-client"; -import { unwrap } from "@/lib/api/unwrap"; +import { apiClient } from "@/core/api"; import type { Subscription, SubscriptionList, InvoiceList } from "@customer-portal/domain"; interface UseSubscriptionsOptions { @@ -33,7 +32,7 @@ export function useSubscriptions(options: UseSubscriptionsOptions = {}) { const res = await apiClient.get( `/subscriptions?${params}` ); - return unwrap(res) as SubscriptionList | Subscription[]; + return res.data as SubscriptionList | Subscription[]; }, staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, // 10 minutes @@ -55,7 +54,7 @@ export function useActiveSubscriptions() { } const res = await apiClient.get(`/subscriptions/active`); - return unwrap(res) as Subscription[]; + return res.data as Subscription[]; }, staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, // 10 minutes @@ -89,7 +88,7 @@ export function useSubscriptionStats() { cancelled: number; pending: number; }>(`/subscriptions/stats`); - return unwrap(res) as { + return res.data as { total: number; active: number; suspended: number; @@ -117,7 +116,7 @@ export function useSubscription(subscriptionId: number) { } const res = await apiClient.get(`/subscriptions/${subscriptionId}`); - return unwrap(res) as Subscription; + return res.data as Subscription; }, staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, // 10 minutes diff --git a/apps/portal/src/features/subscriptions/services/sim-actions.service.ts b/apps/portal/src/features/subscriptions/services/sim-actions.service.ts deleted file mode 100644 index 926f5021..00000000 --- a/apps/portal/src/features/subscriptions/services/sim-actions.service.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * SIM Actions Service (feature layer) - */ -import { openApiClient as apiClient } from "@/lib/api/openapi-client"; -import { unwrap } from "@/lib/api/unwrap"; - -export interface SimInfo { - details: TDetails; - usage: TUsage; -} - -export class SimActionsService { - async getSimInfo( - subscriptionId: number - ): Promise> { - const res = await apiClient.get>( - `/subscriptions/${subscriptionId}/sim` - ); - return unwrap(res) as SimInfo; - } - - async changePlan( - subscriptionId: number, - body: { newPlanCode: string; assignGlobalIp?: boolean; scheduledAt?: string } - ) { - const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/change-plan`, body); - return unwrap(res); - } - - async topUp(subscriptionId: number, body: { quotaMb: number; scheduledAt?: string }) { - const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/top-up`, body); - return unwrap(res); - } - - async cancel(subscriptionId: number, body: { scheduledAt?: string } = {}) { - const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/cancel`, body); - return unwrap(res); - } - - async reissueEsim(subscriptionId: number) { - const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/reissue-esim`); - return unwrap(res); - } - - async updateFeatures( - subscriptionId: number, - payload: { - voiceMailEnabled?: boolean; - callWaitingEnabled?: boolean; - internationalRoamingEnabled?: boolean; - networkType?: "4G" | "5G"; - } - ) { - const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/features`, payload); - return unwrap(res); - } -} - -export const simActionsService = new SimActionsService(); diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx index c209423b..69c94242 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx @@ -20,7 +20,7 @@ import { import { format } from "date-fns"; import { useSubscription } from "@/features/subscriptions/hooks"; import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList"; -import { formatCurrency as sharedFormatCurrency, getCurrencyLocale } from "@/lib/utils/currency"; +import { formatCurrency as sharedFormatCurrency, getCurrencyLocale } from "@customer-portal/domain"; import { SimManagementSection } from "@/features/sim-management"; export function SubscriptionDetailContainer() { diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx index ead435db..8ecfc649 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx @@ -24,7 +24,7 @@ import { } from "@heroicons/react/24/outline"; import { format } from "date-fns"; import { useSubscriptions, useSubscriptionStats } from "@/features/subscriptions/hooks"; -import { formatCurrency, getCurrencyLocale } from "@/lib/utils/currency"; +import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; import type { Subscription } from "@customer-portal/domain"; export function SubscriptionsListContainer() { diff --git a/apps/portal/src/hooks/useDashboard.ts b/apps/portal/src/hooks/useDashboard.ts deleted file mode 100644 index c41165b4..00000000 --- a/apps/portal/src/hooks/useDashboard.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { useAuthStore } from "@/lib/auth/store"; -import { authenticatedApi } from "@/lib/api"; -import type { Activity } from "@customer-portal/shared"; - -interface DashboardSummary { - stats: { - activeSubscriptions: number; - unpaidInvoices: number; - openCases: number; - currency: string; - }; - nextInvoice: { - id: number; - dueDate: string; - amount: number; - currency: string; - } | null; - recentActivity: Activity[]; -} - -export function useDashboardSummary() { - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["dashboard-summary"], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - return authenticatedApi.get(`/me/summary`); - }, - staleTime: 2 * 60 * 1000, // 2 minutes - gcTime: 5 * 60 * 1000, // 5 minutes - enabled: isAuthenticated && !!token, // Only run when authenticated - }); -} diff --git a/apps/portal/src/hooks/useInvoices.ts b/apps/portal/src/hooks/useInvoices.ts deleted file mode 100644 index 55cd7b51..00000000 --- a/apps/portal/src/hooks/useInvoices.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import type { - Invoice, - InvoiceList, - InvoiceSsoLink, - Subscription, - PaymentMethodList, - PaymentGatewayList, - InvoicePaymentLink, -} from "@customer-portal/shared"; -import { useAuthStore } from "@/lib/auth/store"; -import { authenticatedApi } from "@/lib/api"; - -interface UseInvoicesOptions { - page?: number; - limit?: number; - status?: string; -} - -export function useInvoices(options: UseInvoicesOptions = {}) { - const { page = 1, limit = 10, status } = options; - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["invoices", page, limit, status], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - const params = new URLSearchParams({ - page: page.toString(), - limit: limit.toString(), - ...(status && { status }), - }); - - return authenticatedApi.get(`/invoices?${params}`); - }, - staleTime: 60 * 1000, // 1 minute - gcTime: 5 * 60 * 1000, // 5 minutes - enabled: isAuthenticated && !!token, // Only run when authenticated - }); -} - -export function useInvoice(invoiceId: number) { - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["invoice", invoiceId], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - return authenticatedApi.get(`/invoices/${invoiceId}`); - }, - staleTime: 60 * 1000, // 1 minute - gcTime: 5 * 60 * 1000, // 5 minutes - enabled: isAuthenticated && !!token, // Only run when authenticated - }); -} - -export function useInvoiceSubscriptions(invoiceId: number) { - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["invoice-subscriptions", invoiceId], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - return authenticatedApi.get(`/invoices/${invoiceId}/subscriptions`); - }, - staleTime: 60 * 1000, // 1 minute - gcTime: 5 * 60 * 1000, // 5 minutes - enabled: isAuthenticated && !!token && !!invoiceId, // Only run when authenticated and invoiceId is valid - }); -} - -export async function createInvoiceSsoLink( - invoiceId: number, - target: "view" | "download" | "pay" = "view" -): Promise { - const params = new URLSearchParams(); - if (target !== "view") params.append("target", target); - return authenticatedApi.post( - `/invoices/${invoiceId}/sso-link${params.toString() ? `?${params.toString()}` : ""}` - ); -} - -export function usePaymentMethods() { - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["paymentMethods"], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - return authenticatedApi.get(`/invoices/payment-methods`); - }, - staleTime: 1 * 60 * 1000, // Reduced to 1 minute for better refresh - gcTime: 5 * 60 * 1000, // Reduced to 5 minutes - enabled: isAuthenticated && !!token, - retry: 3, // Retry failed requests - retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff - }); -} - -export function usePaymentGateways() { - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["paymentGateways"], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - return authenticatedApi.get(`/invoices/payment-gateways`); - }, - staleTime: 60 * 60 * 1000, // 1 hour - gcTime: 2 * 60 * 60 * 1000, // 2 hours - enabled: isAuthenticated && !!token, - }); -} - -export async function createInvoicePaymentLink( - invoiceId: number, - paymentMethodId?: number, - gatewayName?: string -): Promise { - const params = new URLSearchParams(); - if (paymentMethodId) params.append("paymentMethodId", paymentMethodId.toString()); - if (gatewayName) params.append("gatewayName", gatewayName); - return authenticatedApi.post( - `/invoices/${invoiceId}/payment-link${params.toString() ? `?${params.toString()}` : ""}` - ); -} diff --git a/apps/portal/src/hooks/usePaymentRefresh.ts b/apps/portal/src/hooks/usePaymentRefresh.ts deleted file mode 100644 index 9acdb67e..00000000 --- a/apps/portal/src/hooks/usePaymentRefresh.ts +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; -import { authenticatedApi } from "@/lib/api"; - -type Tone = "info" | "success" | "warning" | "error"; - -interface UsePaymentRefreshOptions { - // Refetch function from usePaymentMethods - refetch: () => Promise<{ data: T | undefined }>; - // Given refetch result, determine if user has payment methods - hasMethods: (data: T | undefined) => boolean; - // When true, attaches focus/visibility listeners to refresh automatically - attachFocusListeners?: boolean; -} - -export function usePaymentRefresh({ - refetch, - hasMethods, - attachFocusListeners = false, -}: UsePaymentRefreshOptions) { - const [toast, setToast] = useState<{ visible: boolean; text: string; tone: Tone }>({ - visible: false, - text: "", - tone: "info", - }); - - const triggerRefresh = useCallback(async () => { - setToast({ visible: true, text: "Refreshing payment methods...", tone: "info" }); - try { - try { - await authenticatedApi.post("/invoices/payment-methods/refresh"); - } catch (err) { - // Soft-fail cache refresh, still attempt refetch - - console.warn("Payment methods cache refresh failed:", err); - } - const result = await refetch(); - const has = hasMethods(result.data); - setToast({ - visible: true, - text: has ? "Payment methods updated" : "No payment method found yet", - tone: has ? "success" : "warning", - }); - } catch { - setToast({ visible: true, text: "Could not refresh payment methods", tone: "warning" }); - } finally { - setTimeout(() => setToast(t => ({ ...t, visible: false })), 2200); - } - }, [refetch, hasMethods]); - - useEffect(() => { - if (!attachFocusListeners) return; - - const onFocus = () => { - void triggerRefresh(); - }; - const onVis = () => { - if (document.visibilityState === "visible") void triggerRefresh(); - }; - window.addEventListener("focus", onFocus); - document.addEventListener("visibilitychange", onVis); - return () => { - window.removeEventListener("focus", onFocus); - document.removeEventListener("visibilitychange", onVis); - }; - }, [attachFocusListeners, triggerRefresh]); - - return { toast, triggerRefresh, setToast } as const; -} diff --git a/apps/portal/src/hooks/useSubscriptions.ts b/apps/portal/src/hooks/useSubscriptions.ts deleted file mode 100644 index 9b566f26..00000000 --- a/apps/portal/src/hooks/useSubscriptions.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import type { Subscription, SubscriptionList, InvoiceList } from "@customer-portal/shared"; -import { useAuthStore } from "@/lib/auth/store"; -import { authenticatedApi } from "@/lib/api"; - -interface UseSubscriptionsOptions { - status?: string; -} - -export function useSubscriptions(options: UseSubscriptionsOptions = {}) { - const { status } = options; - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["subscriptions", status], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - const params = new URLSearchParams({ - ...(status && { status }), - }); - - return authenticatedApi.get(`/subscriptions?${params}`); - }, - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - enabled: isAuthenticated && !!token, // Only run when authenticated - }); -} - -export function useActiveSubscriptions() { - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["subscriptions", "active"], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - return authenticatedApi.get(`/subscriptions/active`); - }, - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - enabled: isAuthenticated && !!token, // Only run when authenticated - }); -} - -export function useSubscriptionStats() { - const { token, isAuthenticated } = useAuthStore(); - - return useQuery<{ - total: number; - active: number; - suspended: number; - cancelled: number; - pending: number; - }>({ - queryKey: ["subscriptions", "stats"], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - return authenticatedApi.get<{ - total: number; - active: number; - suspended: number; - cancelled: number; - pending: number; - }>(`/subscriptions/stats`); - }, - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - enabled: isAuthenticated && !!token, // Only run when authenticated - }); -} - -export function useSubscription(subscriptionId: number) { - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["subscription", subscriptionId], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - return authenticatedApi.get(`/subscriptions/${subscriptionId}`); - }, - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - enabled: isAuthenticated && !!token, // Only run when authenticated - }); -} - -export function useSubscriptionInvoices( - subscriptionId: number, - options: { page?: number; limit?: number } = {} -) { - const { page = 1, limit = 10 } = options; - const { token, isAuthenticated } = useAuthStore(); - - return useQuery({ - queryKey: ["subscription-invoices", subscriptionId, page, limit], - queryFn: async () => { - if (!token) { - throw new Error("Authentication required"); - } - - const params = new URLSearchParams({ - page: page.toString(), - limit: limit.toString(), - }); - - return authenticatedApi.get( - `/subscriptions/${subscriptionId}/invoices?${params}` - ); - }, - staleTime: 60 * 1000, // 1 minute - gcTime: 5 * 60 * 1000, // 5 minutes - enabled: isAuthenticated && !!token && !!subscriptionId, // Only run when authenticated and subscriptionId is valid - }); -} diff --git a/apps/portal/src/lib/api.ts b/apps/portal/src/lib/api.ts deleted file mode 100644 index 2e22be3f..00000000 --- a/apps/portal/src/lib/api.ts +++ /dev/null @@ -1,139 +0,0 @@ -// API client configuration for the portal -import { useAuthStore } from "./auth/store"; -import { env } from "./env"; - -// Simple, explicit API base configuration -const API_BASE = env.NEXT_PUBLIC_API_BASE; - -// API base configuration is set via environment variable - -// Export API_BASE for cases where direct URL construction is needed -export { API_BASE }; - -export class ApiError extends Error { - constructor( - message: string, - public status: number, - public code?: string - ) { - super(message); - this.name = "ApiError"; - } -} - -export async function apiRequest(endpoint: string, options: RequestInit = {}): Promise { - // Normalize base and endpoint to avoid double slashes - const base = API_BASE.endsWith("/") ? API_BASE.slice(0, -1) : API_BASE; - const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`; - const url = `${base}${path}`; - - // Ensure proper Content-Type for requests with body - const defaultHeaders: Record = {}; - if (options.method === "POST" || options.method === "PATCH" || options.body) { - defaultHeaders["Content-Type"] = "application/json"; - } - - const finalHeaders: Record = { - ...defaultHeaders, - ...((options.headers as Record) || {}), - }; - - // Force Content-Type to application/json for POST requests with body - if ((options.method === "POST" || options.method === "PATCH") && options.body) { - finalHeaders["Content-Type"] = "application/json"; - } - - const response = await fetch(url, { - ...options, - headers: finalHeaders, - credentials: "include", // Include cookies for session management - }); - - if (!response.ok) { - let errorMessage = `HTTP ${response.status}`; - let errorCode: string | undefined; - - try { - const errorData = (await response.json()) as unknown; - if ( - typeof errorData === "object" && - errorData !== null && - "message" in errorData && - typeof (errorData as { message: unknown }).message === "string" - ) { - errorMessage = (errorData as { message: string }).message; - } - if ( - typeof errorData === "object" && - errorData !== null && - "code" in errorData && - typeof (errorData as { code: unknown }).code === "string" - ) { - errorCode = (errorData as { code: string }).code; - } - } catch { - // If we can't parse the error response, use the status text - errorMessage = response.statusText || errorMessage; - } - - throw new ApiError(errorMessage, response.status, errorCode); - } - - // Handle empty responses - if (response.status === 204) { - return undefined as T; - } - - return (await response.json()) as T; -} - -// Authenticated API request function -export async function authenticatedApiRequest( - endpoint: string, - options: RequestInit = {} -): Promise { - const { token } = useAuthStore.getState(); - - if (!token) { - throw new ApiError("Authentication required", 401); - } - - return apiRequest(endpoint, { - ...options, - headers: { - Authorization: `Bearer ${token}`, - ...options.headers, - }, - }); -} - -export const api = { - get: (endpoint: string) => apiRequest(endpoint), - post: (endpoint: string, data?: unknown) => - apiRequest(endpoint, { - method: "POST", - body: data ? JSON.stringify(data) : undefined, - }), - patch: (endpoint: string, data?: unknown) => - apiRequest(endpoint, { - method: "PATCH", - body: data ? JSON.stringify(data) : undefined, - }), - delete: (endpoint: string) => apiRequest(endpoint, { method: "DELETE" }), -}; - -// Authenticated API methods -export const authenticatedApi = { - get: (endpoint: string) => authenticatedApiRequest(endpoint), - post: (endpoint: string, data?: unknown) => - authenticatedApiRequest(endpoint, { - method: "POST", - body: data ? JSON.stringify(data) : undefined, - }), - patch: (endpoint: string, data?: unknown) => - authenticatedApiRequest(endpoint, { - method: "PATCH", - body: data ? JSON.stringify(data) : undefined, - }), - delete: (endpoint: string) => authenticatedApiRequest(endpoint, { method: "DELETE" }), -}; diff --git a/apps/portal/src/lib/api/base.service.ts b/apps/portal/src/lib/api/base.service.ts deleted file mode 100644 index 0ba271c5..00000000 --- a/apps/portal/src/lib/api/base.service.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Base Service Class - * Provides common CRUD operations and utilities for domain-specific services - */ - -import type { ApiClient, ApiResponse, CrudService, PaginatedResponse, QueryParams } from "@customer-portal/domain"; - -export abstract class BaseService, UpdateT = Partial> - implements CrudService -{ - protected abstract readonly basePath: string; - - constructor(protected readonly apiClient: ApiClient) {} - - /** Build endpoint path */ - protected buildPath(path?: string): string { - if (!path) return this.basePath; - return `${this.basePath}/${path}`; - } - - /** Transform query parameters for API request */ - protected transformParams(params?: QueryParams): Record { - if (!params) return {}; - - const transformed: Record = {}; - - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - if (Array.isArray(value)) { - transformed[key] = value.join(","); - } else if ( - typeof value === "string" || - typeof value === "number" || - typeof value === "boolean" - ) { - transformed[key] = value; - } else { - transformed[key] = JSON.stringify(value); - } - } - }); - - return transformed; - } - - /** Extract data from API response */ - protected extractData(response: ApiResponse): R { - if (!response.success || response.data === undefined) { - throw new Error("Invalid API response"); - } - return response.data; - } - - /** Get all items with optional pagination and filtering */ - async getAll(params?: QueryParams): Promise> { - const response = await this.apiClient.get>(this.basePath, { - params: this.transformParams(params), - }); - return this.extractData(response); - } - - /** Get single item by ID */ - async getById(id: string): Promise { - const response = await this.apiClient.get(this.buildPath(id)); - return this.extractData(response); - } - - /** Create new item */ - async create(data: CreateT): Promise { - const response = await this.apiClient.post(this.basePath, data); - return this.extractData(response); - } - - /** Update existing item */ - async update(id: string, data: UpdateT): Promise { - const response = await this.apiClient.patch(this.buildPath(id), data); - return this.extractData(response); - } - - /** Delete item */ - async delete(id: string): Promise { - await this.apiClient.delete(this.buildPath(id)); - } - - /** Check if item exists */ - async exists(id: string): Promise { - try { - await this.getById(id); - return true; - } catch { - return false; - } - } - - /** Get items by IDs */ - async getByIds(ids: string[]): Promise { - const response = await this.apiClient.get(this.basePath, { - params: { ids: ids.join(",") }, - }); - return this.extractData(response); - } - - /** Bulk create items */ - async bulkCreate(items: CreateT[]): Promise { - const response = await this.apiClient.post(this.buildPath("bulk"), { items }); - return this.extractData(response); - } - - /** Bulk update items */ - async bulkUpdate(updates: Array<{ id: string; data: UpdateT }>): Promise { - const response = await this.apiClient.patch(this.buildPath("bulk"), { updates }); - return this.extractData(response); - } - - /** Bulk delete items */ - async bulkDelete(ids: string[]): Promise { - await this.apiClient.delete(this.buildPath("bulk"), { - data: { ids }, - } as unknown as Parameters[1]); - } - - /** Search items */ - async search(query: string, params?: QueryParams): Promise> { - const searchParams = { - q: query, - ...params, - }; - - const response = await this.apiClient.get>(this.buildPath("search"), { - params: this.transformParams(searchParams), - }); - return this.extractData(response); - } - - /** Count items with optional filtering */ - async count(params?: QueryParams): Promise { - const response = await this.apiClient.get<{ count: number }>(this.buildPath("count"), { - params: this.transformParams(params), - }); - const data = this.extractData(response); - return (data as { count: number }).count; - } -} - -/** - * Authenticated Base Service - * Extends BaseService with authentication-aware methods - */ -export abstract class AuthenticatedBaseService< - T, - CreateT = Partial, - UpdateT = Partial, -> extends BaseService { - /** Get current user's items */ - async getMine(params?: QueryParams): Promise> { - const response = await this.apiClient.get>(this.buildPath("mine"), { - params: this.transformParams(params), - }); - return this.extractData(response); - } - - /** Get items for specific user (admin only) */ - async getByUserId(userId: string, params?: QueryParams): Promise> { - const searchParams = { - userId, - ...params, - }; - - const response = await this.apiClient.get>(this.basePath, { - params: this.transformParams(searchParams), - }); - return this.extractData(response); - } -} - - - diff --git a/apps/portal/src/lib/api/index.ts b/apps/portal/src/lib/api/index.ts deleted file mode 100644 index 765d8b6d..00000000 --- a/apps/portal/src/lib/api/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// API client exports - Pure OpenAPI! 🎉 -export { openApiClient } from "./openapi-client"; - -// Services -export * from "./base.service"; - -// Utilities -export * from "./unwrap"; diff --git a/apps/portal/src/lib/api/openapi-client.ts b/apps/portal/src/lib/api/openapi-client.ts deleted file mode 100644 index be712698..00000000 --- a/apps/portal/src/lib/api/openapi-client.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * OpenAPI-generated client wrapper with Portal-specific authentication and error handling - */ - -import { createClient, type ApiClient } from "@customer-portal/api-client"; -import { log } from "@customer-portal/logging"; -import type { ApiResponse, ApiSuccess, ApiFailure } from "@customer-portal/domain"; -import { env } from "../env"; - -class OpenApiClientWrapper { - private client: ApiClient; - - constructor() { - this.client = createClient(env.NEXT_PUBLIC_API_BASE); - } - - /** - * Get authentication headers from current session - */ - private getAuthHeaders(): Record { - // Get token from localStorage or session store - const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null; - - return token ? { Authorization: `Bearer ${token}` } : {}; - } - - /** - * Transform openapi-fetch response to Portal's ApiResponse format - */ - private transformResponse(response: any): ApiResponse { - if (response.error) { - const failure: ApiFailure = { - success: false, - error: { - code: response.error.code || 'UNKNOWN_ERROR', - message: response.error.message || 'An error occurred', - statusCode: response.response?.status, - timestamp: new Date().toISOString(), - }, - }; - return failure; - } - - const success: ApiSuccess = { - success: true, - data: response.data as T, - meta: { - requestId: response.response?.headers?.get('x-request-id') || undefined, - timestamp: new Date().toISOString(), - }, - }; - return success; - } - - /** - * GET request wrapper - */ - async get(path: string, params?: Record): Promise> { - try { - log.debug('OpenAPI GET request', { path, params }); - - const response = await (this.client as any).GET(path, { - params: params ? { query: params } : undefined, - headers: this.getAuthHeaders(), - }); - - return this.transformResponse(response); - } catch (error) { - log.error('OpenAPI GET request failed', { path, error }); - return { - success: false, - error: { - code: 'NETWORK_ERROR', - message: error instanceof Error ? error.message : 'Network error occurred', - }, - }; - } - } - - /** - * POST request wrapper - */ - async post(path: string, data?: any, config?: any): Promise> { - try { - log.debug('OpenAPI POST request', { path, data }); - - const response = await (this.client as any).POST(path, { - body: data, - headers: this.getAuthHeaders(), - ...config, - }); - - return this.transformResponse(response); - } catch (error) { - log.error('OpenAPI POST request failed', { path, error }); - return { - success: false, - error: { - code: 'NETWORK_ERROR', - message: error instanceof Error ? error.message : 'Network error occurred', - }, - }; - } - } - - /** - * PUT request wrapper - */ - async put(path: string, data?: any, config?: any): Promise> { - try { - log.debug('OpenAPI PUT request', { path, data }); - - const response = await (this.client as any).PUT(path, { - body: data, - headers: this.getAuthHeaders(), - ...config, - }); - - return this.transformResponse(response); - } catch (error) { - log.error('OpenAPI PUT request failed', { path, error }); - return { - success: false, - error: { - code: 'NETWORK_ERROR', - message: error instanceof Error ? error.message : 'Network error occurred', - }, - }; - } - } - - /** - * PATCH request wrapper - */ - async patch(path: string, data?: any, config?: any): Promise> { - try { - log.debug('OpenAPI PATCH request', { path, data }); - - const response = await (this.client as any).PATCH(path, { - body: data, - headers: this.getAuthHeaders(), - ...config, - }); - - return this.transformResponse(response); - } catch (error) { - log.error('OpenAPI PATCH request failed', { path, error }); - return { - success: false, - error: { - code: 'NETWORK_ERROR', - message: error instanceof Error ? error.message : 'Network error occurred', - }, - }; - } - } - - /** - * DELETE request wrapper - */ - async delete(path: string, config?: any): Promise> { - try { - log.debug('OpenAPI DELETE request', { path }); - - const response = await (this.client as any).DELETE(path, { - headers: this.getAuthHeaders(), - ...config, - }); - - return this.transformResponse(response); - } catch (error) { - log.error('OpenAPI DELETE request failed', { path, error }); - return { - success: false, - error: { - code: 'NETWORK_ERROR', - message: error instanceof Error ? error.message : 'Network error occurred', - }, - }; - } - } -} - -// Export singleton instance -export const openApiClient = new OpenApiClientWrapper(); - -// Export the class for testing -export { OpenApiClientWrapper }; diff --git a/apps/portal/src/lib/api/unwrap.ts b/apps/portal/src/lib/api/unwrap.ts deleted file mode 100644 index f82a1764..00000000 --- a/apps/portal/src/lib/api/unwrap.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Re-export the unified unwrap utility from domain -export { unwrapResponse as unwrap, unwrapResponseSafely } from "@customer-portal/domain"; - - diff --git a/apps/portal/src/lib/auth/api.ts b/apps/portal/src/lib/auth/api.ts deleted file mode 100644 index efd84a6f..00000000 --- a/apps/portal/src/lib/auth/api.ts +++ /dev/null @@ -1,191 +0,0 @@ -const API_BASE = process.env.NEXT_PUBLIC_API_BASE || "/api"; - -export interface SignupData { - email: string; - password: string; - firstName: string; - lastName: string; - company?: string; - phone?: string; - sfNumber: string; - address?: { - line1: string; - line2?: string; - city: string; - state: string; - postalCode: string; - country: string; - }; - nationality?: string; - dateOfBirth?: string; - gender?: "male" | "female" | "other"; -} - -export interface LoginData { - email: string; - password: string; -} - -export interface LinkWhmcsData { - email: string; - password: string; -} - -export interface SetPasswordData { - email: string; - password: string; -} - -export interface RequestPasswordResetData { - email: string; -} - -export interface ResetPasswordData { - token: string; - password: string; -} - -export interface ChangePasswordData { - currentPassword: string; - newPassword: string; -} - -export interface CheckPasswordNeededData { - email: string; -} - -export interface CheckPasswordNeededResponse { - needsPasswordSet: boolean; - userExists: boolean; - email?: string; -} - -export interface AuthResponse { - user: { - id: string; - email: string; - firstName: string; - lastName: string; - company?: string; - phone?: string; - }; - access_token: string; -} - -export interface LinkResponse { - user: { - id: string; - email: string; - firstName: string; - lastName: string; - }; - needsPasswordSet: boolean; -} - -class AuthAPI { - private async request(endpoint: string, options?: RequestInit): Promise { - const base = API_BASE.endsWith("/") ? API_BASE.slice(0, -1) : API_BASE; - const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`; - const url = `${base}${path}`; - - const response = await fetch(url, { - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - ...options, - }); - - if (!response.ok) { - const errorData = (await response.json().catch(() => ({}))) as unknown; - const message = - typeof errorData === "object" && - errorData !== null && - "message" in errorData && - typeof (errorData as { message: unknown }).message === "string" - ? (errorData as { message: string }).message - : `HTTP ${response.status}: ${response.statusText}`; - throw new Error(message); - } - - return (await response.json()) as T; - } - - async signup(data: SignupData): Promise { - return this.request("/auth/signup", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async login(data: LoginData): Promise { - return this.request("/auth/login", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async linkWhmcs(data: LinkWhmcsData): Promise { - return this.request("/auth/link-whmcs", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async setPassword(data: SetPasswordData): Promise { - return this.request("/auth/set-password", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async requestPasswordReset(data: RequestPasswordResetData): Promise<{ message: string }> { - return this.request<{ message: string }>("/auth/request-password-reset", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async resetPassword(data: ResetPasswordData): Promise { - return this.request("/auth/reset-password", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async getProfile(token: string): Promise { - return this.request("/me", { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } - - async logout(token: string): Promise<{ message: string }> { - return this.request<{ message: string }>("/auth/logout", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } - - async changePassword(token: string, data: ChangePasswordData): Promise { - return this.request("/auth/change-password", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(data), - }); - } - - async checkPasswordNeeded(data: CheckPasswordNeededData): Promise { - return this.request("/auth/check-password-needed", { - method: "POST", - body: JSON.stringify(data), - }); - } -} - -export const authAPI = new AuthAPI(); diff --git a/apps/portal/src/lib/auth/store.ts b/apps/portal/src/lib/auth/store.ts deleted file mode 100644 index 686609ab..00000000 --- a/apps/portal/src/lib/auth/store.ts +++ /dev/null @@ -1,241 +0,0 @@ -"use client"; - -import { create } from "zustand"; -import { persist, createJSONStorage } from "zustand/middleware"; -import { authAPI } from "./api"; -import { logger } from "../logger"; - -interface User { - id: string; - email: string; - firstName: string; - lastName: string; - company?: string; - phone?: string; -} - -interface AuthState { - user: User | null; - token: string | null; - isLoading: boolean; - isAuthenticated: boolean; - - // Actions - login: (email: string, password: string) => Promise; - signup: (data: { - email: string; - password: string; - firstName: string; - lastName: string; - company?: string; - phone?: string; - sfNumber: string; - address?: { - line1: string; - line2?: string; - city: string; - state: string; - postalCode: string; - country: string; - }; - nationality?: string; - dateOfBirth?: string; - gender?: "male" | "female" | "other"; - }) => Promise; - linkWhmcs: (email: string, password: string) => Promise<{ needsPasswordSet: boolean }>; - setPassword: (email: string, password: string) => Promise; - requestPasswordReset: (email: string) => Promise; - resetPassword: (token: string, password: string) => Promise; - changePassword: (currentPassword: string, newPassword: string) => Promise; - checkPasswordNeeded: ( - email: string - ) => Promise<{ needsPasswordSet: boolean; userExists: boolean; email?: string }>; - logout: () => Promise; - checkAuth: () => Promise; -} - -export const useAuthStore = create()( - persist( - (set, get) => ({ - user: null, - token: null, - isLoading: false, - isAuthenticated: false, - - login: async (email: string, password: string) => { - set({ isLoading: true }); - try { - const response = await authAPI.login({ email, password }); - set({ - user: response.user, - token: response.access_token, - isAuthenticated: true, - isLoading: false, - }); - } catch (error) { - set({ isLoading: false }); - throw error; - } - }, - - signup: async (data: { - email: string; - password: string; - firstName: string; - lastName: string; - company?: string; - phone?: string; - sfNumber: string; - address?: { - line1: string; - line2?: string; - city: string; - state: string; - postalCode: string; - country: string; - }; - nationality?: string; - dateOfBirth?: string; - gender?: "male" | "female" | "other"; - }) => { - set({ isLoading: true }); - try { - const response = await authAPI.signup(data); - set({ - user: response.user, - token: response.access_token, - isAuthenticated: true, - isLoading: false, - }); - } catch (error) { - set({ isLoading: false }); - throw error; - } - }, - - linkWhmcs: async (email: string, password: string) => { - set({ isLoading: true }); - try { - const response = await authAPI.linkWhmcs({ email, password }); - set({ isLoading: false }); - return { needsPasswordSet: response.needsPasswordSet }; - } catch (error) { - set({ isLoading: false }); - throw error; - } - }, - - setPassword: async (email: string, password: string) => { - set({ isLoading: true }); - try { - const response = await authAPI.setPassword({ email, password }); - set({ - user: response.user, - token: response.access_token, - isAuthenticated: true, - isLoading: false, - }); - } catch (error) { - set({ isLoading: false }); - throw error; - } - }, - - requestPasswordReset: async (email: string) => { - set({ isLoading: true }); - try { - await authAPI.requestPasswordReset({ email }); - set({ isLoading: false }); - } catch (error) { - set({ isLoading: false }); - throw error; - } - }, - - resetPassword: async (token: string, password: string) => { - set({ isLoading: true }); - try { - const response = await authAPI.resetPassword({ token, password }); - set({ - user: response.user, - token: response.access_token, - isAuthenticated: true, - isLoading: false, - }); - } catch (error) { - set({ isLoading: false }); - throw error; - } - }, - - changePassword: async (currentPassword: string, newPassword: string) => { - const { token } = get(); - if (!token) throw new Error("Not authenticated"); - set({ isLoading: true }); - try { - const response = await authAPI.changePassword(token, { currentPassword, newPassword }); - set({ - user: response.user, - token: response.access_token, - isAuthenticated: true, - isLoading: false, - }); - } catch (error) { - set({ isLoading: false }); - throw error; - } - }, - - checkPasswordNeeded: async (email: string) => { - return await authAPI.checkPasswordNeeded({ email }); - }, - - logout: async () => { - const { token } = get(); - - // Call logout API if we have a token - if (token) { - try { - await authAPI.logout(token); - } catch (error) { - logger.error(error, "Logout API call failed"); - // Continue with local logout even if API call fails - } - } - - set({ - user: null, - token: null, - isAuthenticated: false, - }); - }, - - checkAuth: async () => { - const { token } = get(); - if (!token) { - set({ isAuthenticated: false, isLoading: false }); - return; - } - - set({ isLoading: true }); - try { - const user = await authAPI.getProfile(token); - set({ user: user, isAuthenticated: true, isLoading: false }); - } catch { - // Token is invalid, clear auth state - logger.info("Token validation failed, clearing auth state"); - set({ user: null, token: null, isAuthenticated: false, isLoading: false }); - } - }, - }), - { - name: "auth-store", - storage: createJSONStorage(() => localStorage), - partialize: state => ({ - user: state.user, - token: state.token, - isAuthenticated: state.isAuthenticated, - }), - } - ) -); diff --git a/apps/portal/src/lib/index.ts b/apps/portal/src/lib/index.ts deleted file mode 100644 index d165cef5..00000000 --- a/apps/portal/src/lib/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Barrel for lib exports -export * from "./env"; -export * from "./query"; -export * from "./api/base.service"; - - diff --git a/apps/portal/src/lib/logger.ts b/apps/portal/src/lib/logger.ts deleted file mode 100644 index 2584b2e8..00000000 --- a/apps/portal/src/lib/logger.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Lightweight client-side logger to avoid bundling backend/shared deps in Next.js -type LogFn = (message?: unknown, ...optionalParams: unknown[]) => void; - -const makeLogger = () => { - const base = { - info: ((...args: unknown[]) => console.log(...args)) as LogFn, - warn: ((...args: unknown[]) => console.warn(...args)) as LogFn, - error: ((...args: unknown[]) => console.error(...args)) as LogFn, - debug: ((...args: unknown[]) => console.debug(...args)) as LogFn, - }; - return base; -}; - -export const logger = makeLogger(); -export default logger; diff --git a/apps/portal/src/lib/plan.ts b/apps/portal/src/lib/plan.ts deleted file mode 100644 index 00c1774f..00000000 --- a/apps/portal/src/lib/plan.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Generic plan code formatter for SIM plans -// Examples: -// - PASI_10G -> 10G -// - PASI_25G -> 25G -// - ANY_PREFIX_50GB -> 50G -// - Fallback: return the original code when unknown - -export function formatPlanShort(planCode?: string): string { - if (!planCode) return "—"; - const m = planCode.match(/(?:^|[_-])(\d+(?:\.\d+)?)\s*G(?:B)?\b/i); - if (m && m[1]) { - return `${m[1]}G`; - } - // Try extracting trailing number+G anywhere in the string - const m2 = planCode.match(/(\d+(?:\.\d+)?)\s*G(?:B)?\b/i); - if (m2 && m2[1]) { - return `${m2[1]}G`; - } - return planCode; -} diff --git a/apps/portal/src/lib/query-client.ts b/apps/portal/src/lib/query-client.ts deleted file mode 100644 index 60478b22..00000000 --- a/apps/portal/src/lib/query-client.ts +++ /dev/null @@ -1,25 +0,0 @@ -// TanStack Query client configuration - -import { QueryClient } from "@tanstack/react-query"; - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 60 * 1000, // 1 minute - gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime) - retry: (failureCount, error) => { - // Don't retry on 4xx errors - if (error instanceof Error && "status" in error) { - const status = error.status as number; - if (status >= 400 && status < 500) { - return false; - } - } - return failureCount < 3; - }, - }, - mutations: { - retry: false, - }, - }, -}); diff --git a/apps/portal/src/lib/query.ts b/apps/portal/src/lib/query.ts deleted file mode 100644 index 964e740e..00000000 --- a/apps/portal/src/lib/query.ts +++ /dev/null @@ -1,139 +0,0 @@ -// TanStack Query client configuration - -import { QueryClient } from "@tanstack/react-query"; - -// Performance-optimized query client configuration -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // Optimized stale times based on data type - staleTime: 5 * 60 * 1000, // 5 minutes default - gcTime: 10 * 60 * 1000, // 10 minutes garbage collection - - // Retry configuration with exponential backoff - retry: (failureCount, error) => { - // Don't retry on 4xx errors (client errors) - if (error instanceof Error && "status" in error) { - const status = (error as unknown as { status?: number }).status ?? 0; - if (status >= 400 && status < 500) { - return false; - } - } - // Retry up to 3 times with exponential backoff - return failureCount < 3; - }, - - // Retry delay with exponential backoff - retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), - - // Network mode for better offline handling - networkMode: "online", - - // Refetch configuration - refetchOnWindowFocus: false, // Disable aggressive refetching - refetchOnReconnect: true, - refetchOnMount: true, - - // Background refetch interval (disabled by default for performance) - refetchInterval: false, - refetchIntervalInBackground: false, - }, - mutations: { - // Don't retry mutations by default - retry: false, - - // Network mode for mutations - networkMode: "online", - }, - }, -}); - -// Query key factories for consistent caching -export const queryKeys = { - // User-related queries - user: { - all: ["user"] as const, - profile: () => [...queryKeys.user.all, "profile"] as const, - preferences: () => [...queryKeys.user.all, "preferences"] as const, - }, - - // Dashboard queries - dashboard: { - all: ["dashboard"] as const, - summary: () => [...queryKeys.dashboard.all, "summary"] as const, - activity: (filters?: Record) => - [...queryKeys.dashboard.all, "activity", filters] as const, - }, - - // Billing queries - billing: { - all: ["billing"] as const, - invoices: (params?: Record) => - [...queryKeys.billing.all, "invoices", params] as const, - invoice: (id: string) => [...queryKeys.billing.all, "invoice", id] as const, - payments: (params?: Record) => - [...queryKeys.billing.all, "payments", params] as const, - }, - - // Subscription queries - subscriptions: { - all: ["subscriptions"] as const, - list: (params?: Record) => - [...queryKeys.subscriptions.all, "list", params] as const, - detail: (id: string) => [...queryKeys.subscriptions.all, "detail", id] as const, - usage: (id: string) => [...queryKeys.subscriptions.all, "usage", id] as const, - }, - - // Catalog queries - catalog: { - all: ["catalog"] as const, - products: (type: string, params?: Record) => - [...queryKeys.catalog.all, "products", type, params] as const, - product: (id: string) => [...queryKeys.catalog.all, "product", id] as const, - }, - - // Support queries - support: { - all: ["support"] as const, - cases: (params?: Record) => - [...queryKeys.support.all, "cases", params] as const, - case: (id: string) => [...queryKeys.support.all, "case", id] as const, - }, -} as const; - -// Optimized query configurations for different data types -export const queryConfigs = { - // Static/rarely changing data (longer cache) - static: { - staleTime: 30 * 60 * 1000, // 30 minutes - gcTime: 60 * 60 * 1000, // 1 hour - }, - - // User profile data (medium cache) - profile: { - staleTime: 10 * 60 * 1000, // 10 minutes - gcTime: 30 * 60 * 1000, // 30 minutes - }, - - // Financial data (shorter cache for accuracy) - financial: { - staleTime: 2 * 60 * 1000, // 2 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - }, - - // Real-time data (very short cache) - realtime: { - staleTime: 30 * 1000, // 30 seconds - gcTime: 2 * 60 * 1000, // 2 minutes - }, - - // List data (medium cache with background updates) - list: { - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 15 * 60 * 1000, // 15 minutes - refetchOnWindowFocus: true, - }, -} as const; - - - diff --git a/apps/portal/src/lib/utils.ts b/apps/portal/src/lib/utils.ts deleted file mode 100644 index a5ef1935..00000000 --- a/apps/portal/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/apps/portal/src/lib/utils/currency.ts b/apps/portal/src/lib/utils/currency.ts deleted file mode 100644 index df386a43..00000000 --- a/apps/portal/src/lib/utils/currency.ts +++ /dev/null @@ -1,118 +0,0 @@ -export interface CurrencyFormatOptions { - currency: string; - currencySymbol?: string; - locale?: string; - minimumFractionDigits?: number; - maximumFractionDigits?: number; -} - -export function formatCurrency(amount: number, options: CurrencyFormatOptions): string { - const { - currency, - currencySymbol, - locale = "en-US", - minimumFractionDigits, - maximumFractionDigits, - } = options; - - try { - const formatter = new Intl.NumberFormat(locale, { - style: "currency", - currency, - minimumFractionDigits: minimumFractionDigits ?? (currency === "JPY" ? 0 : 2), - maximumFractionDigits: maximumFractionDigits ?? (currency === "JPY" ? 0 : 2), - }); - return formatter.format(amount); - } catch { - const symbol = currencySymbol || getCurrencySymbol(currency); - const decimals = currency === "JPY" ? 0 : 2; - const formattedAmount = amount.toLocaleString(locale, { - minimumFractionDigits: decimals, - maximumFractionDigits: decimals, - }); - return `${symbol}${formattedAmount}`; - } -} - -export function getCurrencySymbol(currencyCode: string): string { - const currencyMap: Record = { - USD: "$", - EUR: "€", - GBP: "£", - JPY: "¥", - CAD: "C$", - AUD: "A$", - CNY: "¥", - INR: "₹", - BRL: "R$", - MXN: "$", - CHF: "CHF", - SEK: "kr", - NOK: "kr", - DKK: "kr", - PLN: "zł", - CZK: "Kč", - HUF: "Ft", - RUB: "₽", - TRY: "₺", - KRW: "₩", - SGD: "S$", - HKD: "HK$", - THB: "฿", - MYR: "RM", - PHP: "₱", - IDR: "Rp", - VND: "₫", - ZAR: "R", - ILS: "₪", - AED: "د.إ", - SAR: "ر.س", - EGP: "ج.م", - NZD: "NZ$", - }; - - return currencyMap[currencyCode?.toUpperCase()] || currencyCode || "¥"; -} - -export function getCurrencyLocale(currencyCode: string): string { - const localeMap: Record = { - USD: "en-US", - EUR: "de-DE", - GBP: "en-GB", - JPY: "ja-JP", - CAD: "en-CA", - AUD: "en-AU", - CNY: "zh-CN", - INR: "en-IN", - BRL: "pt-BR", - MXN: "es-MX", - CHF: "de-CH", - SEK: "sv-SE", - NOK: "nb-NO", - DKK: "da-DK", - PLN: "pl-PL", - CZK: "cs-CZ", - HUF: "hu-HU", - RUB: "ru-RU", - TRY: "tr-TR", - KRW: "ko-KR", - SGD: "en-SG", - HKD: "zh-HK", - THB: "th-TH", - MYR: "ms-MY", - PHP: "en-PH", - IDR: "id-ID", - VND: "vi-VN", - ZAR: "en-ZA", - ILS: "he-IL", - AED: "ar-AE", - SAR: "ar-SA", - EGP: "ar-EG", - NZD: "en-NZ", - }; - - return localeMap[currencyCode?.toUpperCase()] || "en-US"; -} - - - diff --git a/apps/portal/src/lib/utils/error-display.ts b/apps/portal/src/lib/utils/error-display.ts deleted file mode 100644 index 8b969385..00000000 --- a/apps/portal/src/lib/utils/error-display.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function toUserMessage(error: unknown, fallback = "An unexpected error occurred"): string { - if (!error) return fallback; - if (typeof error === "string") return error; - if (error instanceof Error) return error.message || fallback; - // Redact non-Error details; keep generic to avoid leaking sensitive info - return fallback; -} - - - diff --git a/apps/portal/src/providers/query-provider.tsx b/apps/portal/src/providers/query-provider.tsx deleted file mode 100644 index 1418e4bf..00000000 --- a/apps/portal/src/providers/query-provider.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { QueryClientProvider } from "@tanstack/react-query"; -import dynamic from "next/dynamic"; -import { queryClient } from "@/lib/query-client"; - -interface QueryProviderProps { - children: React.ReactNode; -} - -export function QueryProvider({ children }: QueryProviderProps) { - const enableDevtools = - process.env.NEXT_PUBLIC_ENABLE_DEVTOOLS === "true" && process.env.NODE_ENV !== "production"; - const ReactQueryDevtools = enableDevtools - ? dynamic(() => import("@tanstack/react-query-devtools").then(m => m.ReactQueryDevtools), { - ssr: false, - }) - : null; - return ( - - {children} - {enableDevtools && ReactQueryDevtools ? : null} - - ); -} diff --git a/apps/portal/src/shared/hooks/index.ts b/apps/portal/src/shared/hooks/index.ts new file mode 100644 index 00000000..c0ff4813 --- /dev/null +++ b/apps/portal/src/shared/hooks/index.ts @@ -0,0 +1,3 @@ +export { useLocalStorage } from "./useLocalStorage"; +export { useDebounce } from "./useDebounce"; +export { useMediaQuery, useIsMobile, useIsTablet, useIsDesktop } from "./useMediaQuery"; diff --git a/apps/portal/src/shared/hooks/useDebounce.ts b/apps/portal/src/shared/hooks/useDebounce.ts new file mode 100644 index 00000000..9d77b7bf --- /dev/null +++ b/apps/portal/src/shared/hooks/useDebounce.ts @@ -0,0 +1,22 @@ +"use client"; + +import { useState, useEffect } from "react"; + +/** + * Hook that debounces a value + */ +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/apps/portal/src/shared/hooks/useLocalStorage.ts b/apps/portal/src/shared/hooks/useLocalStorage.ts new file mode 100644 index 00000000..01d38ac5 --- /dev/null +++ b/apps/portal/src/shared/hooks/useLocalStorage.ts @@ -0,0 +1,69 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +/** + * Hook for managing localStorage with SSR safety + */ +export function useLocalStorage( + key: string, + initialValue: T +): [T, (value: T | ((val: T) => T)) => void, () => void] { + // State to store our value + const [storedValue, setStoredValue] = useState(initialValue); + const [isClient, setIsClient] = useState(false); + + // Check if we're on the client side + useEffect(() => { + setIsClient(true); + }, []); + + // Get value from localStorage on client side + useEffect(() => { + if (!isClient) return; + + try { + const item = window.localStorage.getItem(key); + if (item) { + setStoredValue(JSON.parse(item)); + } + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + } + }, [key, isClient]); + + // Return a wrapped version of useState's setter function that persists the new value to localStorage + const setValue = useCallback( + (value: T | ((val: T) => T)) => { + try { + // Allow value to be a function so we have the same API as useState + const valueToStore = value instanceof Function ? value(storedValue) : value; + + // Save state + setStoredValue(valueToStore); + + // Save to localStorage if on client + if (isClient) { + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } + } catch (error) { + console.warn(`Error setting localStorage key "${key}":`, error); + } + }, + [key, storedValue, isClient] + ); + + // Function to remove the item from localStorage + const removeValue = useCallback(() => { + try { + setStoredValue(initialValue); + if (isClient) { + window.localStorage.removeItem(key); + } + } catch (error) { + console.warn(`Error removing localStorage key "${key}":`, error); + } + }, [key, initialValue, isClient]); + + return [storedValue, setValue, removeValue]; +} diff --git a/apps/portal/src/shared/hooks/useMediaQuery.ts b/apps/portal/src/shared/hooks/useMediaQuery.ts new file mode 100644 index 00000000..99d0da41 --- /dev/null +++ b/apps/portal/src/shared/hooks/useMediaQuery.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect } from "react"; + +/** + * Hook for responsive design with media queries + */ +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const media = window.matchMedia(query); + + // Set initial value + setMatches(media.matches); + + // Create event listener + const listener = (event: MediaQueryListEvent) => { + setMatches(event.matches); + }; + + // Add listener + media.addEventListener('change', listener); + + // Cleanup + return () => media.removeEventListener('change', listener); + }, [query]); + + return matches; +} + +// Common breakpoint hooks +export const useIsMobile = () => useMediaQuery('(max-width: 768px)'); +export const useIsTablet = () => useMediaQuery('(min-width: 769px) and (max-width: 1024px)'); +export const useIsDesktop = () => useMediaQuery('(min-width: 1025px)'); diff --git a/apps/portal/src/shared/index.ts b/apps/portal/src/shared/index.ts new file mode 100644 index 00000000..c2da53d7 --- /dev/null +++ b/apps/portal/src/shared/index.ts @@ -0,0 +1,2 @@ +export * from "./hooks"; +export * from "./utils"; diff --git a/apps/portal/src/lib/utils/cn.ts b/apps/portal/src/shared/utils/cn.ts similarity index 56% rename from apps/portal/src/lib/utils/cn.ts rename to apps/portal/src/shared/utils/cn.ts index 193828bc..1a92305f 100644 --- a/apps/portal/src/lib/utils/cn.ts +++ b/apps/portal/src/shared/utils/cn.ts @@ -1,8 +1,10 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; +/** + * Utility for merging Tailwind CSS classes + * Combines clsx for conditional classes and tailwind-merge for deduplication + */ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } - - diff --git a/apps/portal/src/shared/utils/error-display.ts b/apps/portal/src/shared/utils/error-display.ts new file mode 100644 index 00000000..12f56739 --- /dev/null +++ b/apps/portal/src/shared/utils/error-display.ts @@ -0,0 +1,16 @@ +/** + * Error display utilities + * Converts errors to user-friendly messages + */ + +export function toUserMessage(error: unknown): string { + if (typeof error === 'string') { + return error; + } + + if (error && typeof error === 'object' && 'message' in error) { + return String(error.message); + } + + return 'An unexpected error occurred'; +} diff --git a/apps/portal/src/shared/utils/index.ts b/apps/portal/src/shared/utils/index.ts new file mode 100644 index 00000000..8c0a96fd --- /dev/null +++ b/apps/portal/src/shared/utils/index.ts @@ -0,0 +1,3 @@ +export { cn } from "./cn"; +export { toUserMessage } from "./error-display"; +export { formatPlanShort } from "./plan"; diff --git a/apps/portal/src/shared/utils/plan.ts b/apps/portal/src/shared/utils/plan.ts new file mode 100644 index 00000000..f7291f16 --- /dev/null +++ b/apps/portal/src/shared/utils/plan.ts @@ -0,0 +1,12 @@ +/** + * Plan formatting utilities + */ + +export function formatPlanShort(planCode?: string): string { + if (!planCode) return 'Unknown Plan'; + + // Convert plan codes to readable format + return planCode + .replace(/[-_]/g, ' ') + .replace(/\b\w/g, l => l.toUpperCase()); +} diff --git a/apps/portal/src/utils/currency.ts b/apps/portal/src/utils/currency.ts deleted file mode 100644 index d9247471..00000000 --- a/apps/portal/src/utils/currency.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Currency formatting utilities - */ - -export interface CurrencyFormatOptions { - currency: string; - currencySymbol?: string; - locale?: string; - minimumFractionDigits?: number; - maximumFractionDigits?: number; -} - -/** - * Format amount with proper currency display - */ -export function formatCurrency(amount: number, options: CurrencyFormatOptions): string { - const { - currency, - currencySymbol, - locale = "en-US", - minimumFractionDigits, - maximumFractionDigits, - } = options; - - // Use browser's Intl.NumberFormat for proper locale formatting - try { - const formatter = new Intl.NumberFormat(locale, { - style: "currency", - currency: currency, - minimumFractionDigits: minimumFractionDigits ?? (currency === "JPY" ? 0 : 2), - maximumFractionDigits: maximumFractionDigits ?? (currency === "JPY" ? 0 : 2), - }); - - return formatter.format(amount); - } catch { - // Fallback to manual formatting if Intl.NumberFormat fails - const symbol = currencySymbol || getCurrencySymbol(currency); - const decimals = currency === "JPY" ? 0 : 2; - const formattedAmount = amount.toLocaleString(locale, { - minimumFractionDigits: decimals, - maximumFractionDigits: decimals, - }); - - return `${symbol}${formattedAmount}`; - } -} - -/** - * Get currency symbol for a currency code - */ -export function getCurrencySymbol(currencyCode: string): string { - const currencyMap: Record = { - USD: "$", - EUR: "€", - GBP: "£", - JPY: "¥", - CAD: "C$", - AUD: "A$", - CNY: "¥", - INR: "₹", - BRL: "R$", - MXN: "$", - CHF: "CHF", - SEK: "kr", - NOK: "kr", - DKK: "kr", - PLN: "zł", - CZK: "Kč", - HUF: "Ft", - RUB: "₽", - TRY: "₺", - KRW: "₩", - SGD: "S$", - HKD: "HK$", - THB: "฿", - MYR: "RM", - PHP: "₱", - IDR: "Rp", - VND: "₫", - ZAR: "R", - ILS: "₪", - AED: "د.إ", - SAR: "ر.س", - EGP: "ج.م", - NZD: "NZ$", - }; - - return currencyMap[currencyCode?.toUpperCase()] || currencyCode || "¥"; -} - -/** - * Get appropriate locale for currency formatting - */ -export function getCurrencyLocale(currencyCode: string): string { - const localeMap: Record = { - USD: "en-US", - EUR: "de-DE", - GBP: "en-GB", - JPY: "ja-JP", - CAD: "en-CA", - AUD: "en-AU", - CNY: "zh-CN", - INR: "en-IN", - BRL: "pt-BR", - MXN: "es-MX", - CHF: "de-CH", - SEK: "sv-SE", - NOK: "nb-NO", - DKK: "da-DK", - PLN: "pl-PL", - CZK: "cs-CZ", - HUF: "hu-HU", - RUB: "ru-RU", - TRY: "tr-TR", - KRW: "ko-KR", - SGD: "en-SG", - HKD: "zh-HK", - THB: "th-TH", - MYR: "ms-MY", - PHP: "en-PH", - IDR: "id-ID", - VND: "vi-VN", - ZAR: "en-ZA", - ILS: "he-IL", - AED: "ar-AE", - SAR: "ar-SA", - EGP: "ar-EG", - NZD: "en-NZ", - }; - - return localeMap[currencyCode?.toUpperCase()] || "en-US"; -} diff --git a/apps/portal/tsconfig.json b/apps/portal/tsconfig.json index 00392845..61ab8e27 100644 --- a/apps/portal/tsconfig.json +++ b/apps/portal/tsconfig.json @@ -15,7 +15,6 @@ // Path mappings "paths": { "@/*": ["./src/*"], - "@customer-portal/shared": ["../../packages/shared/src", "../../packages/shared/dist"] }, // Enforce TS-only in portal and keep strict mode explicit (inherits from root) "allowJs": false, diff --git a/package.json b/package.json index b84d5f67..800d9402 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,6 @@ "format:check": "prettier -c .", "prepare": "husky", "type-check": "NODE_OPTIONS=\"--max-old-space-size=8192\" pnpm --filter @customer-portal/domain build && NODE_OPTIONS=\"--max-old-space-size=8192\" pnpm --recursive run type-check", - "type-check:memory": "./scripts/type-check-memory.sh check all", - "type-check:bff": "./scripts/type-check-memory.sh check bff", - "type-check:clean": "./scripts/type-check-memory.sh clean", "clean": "pnpm --recursive run clean", "dev:start": "./scripts/dev/manage.sh start", "dev:stop": "./scripts/dev/manage.sh stop", diff --git a/packages/api-client/src/__generated__/types.ts b/packages/api-client/src/__generated__/types.ts index d6c000ea..551ab275 100644 --- a/packages/api-client/src/__generated__/types.ts +++ b/packages/api-client/src/__generated__/types.ts @@ -1103,9 +1103,9 @@ export interface components { }; AddressDto: { /** @example 123 Main Street */ - line1: string; + street: string; /** @example Apt 4B */ - line2?: string; + streetLine2?: string; /** @example Tokyo */ city: string; /** @example Tokyo */ diff --git a/packages/domain/src/entities/catalog.ts b/packages/domain/src/entities/catalog.ts index 97760070..bad0183c 100644 --- a/packages/domain/src/entities/catalog.ts +++ b/packages/domain/src/entities/catalog.ts @@ -38,9 +38,7 @@ export type { OrderItemRequest, ProductBillingCycle, ProductCategory, - SalesforceProduct2Record, - SalesforcePricebookEntryRecord, - SalesforceProductFieldMap + // Note: Salesforce types are exported from product.ts }; export { @@ -55,7 +53,7 @@ export { isCatalogVisible, isOrderable, fromSalesforceProduct2, - DEFAULT_SALESFORCE_PRODUCT_FIELD_MAP + // Note: DEFAULT_SALESFORCE_PRODUCT_FIELD_MAP is exported from product.ts }; // Legacy type aliases for backward compatibility diff --git a/packages/domain/src/entities/checkout.ts b/packages/domain/src/entities/checkout.ts index fd035152..12ab3dc1 100644 --- a/packages/domain/src/entities/checkout.ts +++ b/packages/domain/src/entities/checkout.ts @@ -144,7 +144,7 @@ export function validateCheckoutCart(cart: CheckoutCart): { valid: boolean; erro } // Business rule: Must have at least one service - const hasService = cart.items.some(item => item.type === "service"); + const hasService = cart.items.some(item => item.itemClass === "Service"); if (!hasService) { errors.push("Cart must contain at least one service"); } diff --git a/packages/domain/src/entities/product.ts b/packages/domain/src/entities/product.ts index 7603b1e5..e3d9400e 100644 --- a/packages/domain/src/entities/product.ts +++ b/packages/domain/src/entities/product.ts @@ -302,7 +302,7 @@ export function fromSalesforceProduct2( portalCatalog: normalizeBoolean(readField(sfProduct, fieldMap.portalCatalog)) ?? false, portalAccessible: normalizeBoolean(readField(sfProduct, fieldMap.portalAccessible)) ?? false, whmcsProductId: normalizeNumeric(readField(sfProduct, fieldMap.whmcsProductId)), - whmcsProductName: coerceString(readField(sfProduct, fieldMap.whmcsProductName)), + whmcsProductName: coerceString(readField(sfProduct, fieldMap.whmcsProductName)) || undefined, displayOrder: normalizeNumeric(readField(sfProduct, fieldMap.displayOrder)), bundledAddonId: coerceString(readField(sfProduct, fieldMap.bundledAddon)) ?? undefined, isBundledAddon: normalizeBoolean(readField(sfProduct, fieldMap.isBundledAddon)), @@ -317,6 +317,7 @@ export function fromSalesforceProduct2( case "Internet": return { ...baseProduct, + category: "Internet" as const, internetPlanTier: coerceString(readField(sfProduct, fieldMap.internetPlanTier)) as | InternetProduct["internetPlanTier"] | undefined, @@ -326,6 +327,7 @@ export function fromSalesforceProduct2( case "SIM": return { ...baseProduct, + category: "SIM" as const, simDataSize: coerceString(readField(sfProduct, fieldMap.simDataSize)) || undefined, simPlanType: normalizeSimPlanType(readField(sfProduct, fieldMap.simPlanType)) || undefined, simHasFamilyDiscount: normalizeBoolean(readField(sfProduct, fieldMap.simHasFamilyDiscount)), @@ -334,6 +336,7 @@ export function fromSalesforceProduct2( case "VPN": return { ...baseProduct, + category: "VPN" as const, vpnRegion: coerceString(readField(sfProduct, fieldMap.vpnRegion)) || undefined, } satisfies VpnProduct; diff --git a/packages/domain/src/utils/validation.ts b/packages/domain/src/utils/validation.ts index c12fb0eb..da5d327d 100644 --- a/packages/domain/src/utils/validation.ts +++ b/packages/domain/src/utils/validation.ts @@ -51,13 +51,6 @@ export function validateEmail(email: string): ValidationResult { return success(trimmed); } -/** - * Legacy boolean validation (kept for backward compatibility) - */ -export function isValidEmail(email: string): boolean { - const result = validateEmail(email); - return result.success; -} /** * Enhanced phone validation with better error reporting @@ -82,13 +75,6 @@ export function validatePhoneNumber(phone: string): ValidationResult { return success(cleaned); } -/** - * Legacy boolean validation (kept for backward compatibility) - */ -export function isValidPhoneNumber(phone: string): boolean { - const result = validatePhoneNumber(phone); - return result.success; -} /** * Validates password strength diff --git a/packages/domain/src/validation/address-migration.ts b/packages/domain/src/validation/address-migration.ts deleted file mode 100644 index 2d9a3e71..00000000 --- a/packages/domain/src/validation/address-migration.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Address Migration Utilities - * Handles migration from legacy address formats to unified Address type - */ - -import { z } from 'zod'; -import { addressSchema } from './base-schemas'; -import type { Address } from '../common'; - -// Legacy address format (line1/line2) - for migration compatibility -export const legacyAddressSchema = z.object({ - line1: z.string().min(1, 'Address line 1 is required'), - line2: z.string().optional(), - city: z.string().min(1, 'City is required'), - state: z.string().min(1, 'State is required'), - postalCode: z.string().min(1, 'Postal code is required'), - country: z.string().min(1, 'Country is required'), -}); - -// Flexible address schema that accepts both formats -export const flexibleAddressSchema = z.union([ - addressSchema, - legacyAddressSchema, -]).transform((data): Address => { - // If it has line1/line2, it's legacy format - if ('line1' in data) { - return { - street: data.line1, - streetLine2: data.line2 || null, - city: data.city, - state: data.state, - postalCode: data.postalCode, - country: data.country, - }; - } - - // Otherwise, it's already in the correct format - return data as Address; -}); - -// Required address schema (for forms that require all fields) -export const requiredAddressSchema = z.object({ - street: z.string().min(1, 'Street address is required'), - streetLine2: z.string().nullable(), - city: z.string().min(1, 'City is required'), - state: z.string().min(1, 'State is required'), - postalCode: z.string().min(1, 'Postal code is required'), - country: z.string().min(1, 'Country is required'), -}); - -// Legacy to modern address mapper -export const mapLegacyAddress = (legacy: { - line1: string; - line2?: string; - city: string; - state: string; - postalCode: string; - country: string; -}): Address => ({ - street: legacy.line1, - streetLine2: legacy.line2 || null, - city: legacy.city, - state: legacy.state, - postalCode: legacy.postalCode, - country: legacy.country, -}); - -// Modern to legacy address mapper (for backward compatibility) -export const mapToLegacyAddress = (address: Address): { - line1: string; - line2?: string; - city: string; - state: string; - postalCode: string; - country: string; -} => ({ - line1: address.street || '', - line2: address.streetLine2 || undefined, - city: address.city || '', - state: address.state || '', - postalCode: address.postalCode || '', - country: address.country || '', -}); - -// Type definitions for migration -export type LegacyAddress = z.infer; -export type FlexibleAddress = z.infer; -export type RequiredAddress = z.infer; diff --git a/packages/domain/src/validation/bff-schemas.ts b/packages/domain/src/validation/bff-schemas.ts index b93b5482..5d712fc1 100644 --- a/packages/domain/src/validation/bff-schemas.ts +++ b/packages/domain/src/validation/bff-schemas.ts @@ -33,9 +33,9 @@ const passwordSchema = z .min(PASSWORD_MIN_LENGTH, `Password must be at least ${PASSWORD_MIN_LENGTH} characters`) .regex(PASSWORD_COMPLEXITY_REGEX, PASSWORD_COMPLEXITY_MESSAGE); -const addressDtoSchema: z.ZodType = z.object({ - line1: z.string().min(1, 'Address line 1 is required'), - line2: z.string().optional(), +const addressDtoSchema = z.object({ + street: z.string().min(1, 'Street address is required'), + streetLine2: z.string().optional(), city: z.string().min(1, 'City is required'), state: z.string().min(1, 'State/Prefecture is required'), postalCode: z.string().min(1, 'Postal code is required'), @@ -46,7 +46,7 @@ const addressDtoSchema: z.ZodType = z.object({ // BFF AUTH SCHEMAS // ===================================================== -export const bffSignupSchema: z.ZodType = z.object({ +export const bffSignupSchema = z.object({ email: emailSchema, password: passwordSchema, firstName: z.string().min(1, 'First name is required'), diff --git a/packages/domain/src/validation/index.ts b/packages/domain/src/validation/index.ts index ffc83d64..66231608 100644 --- a/packages/domain/src/validation/index.ts +++ b/packages/domain/src/validation/index.ts @@ -97,20 +97,6 @@ export type { SetPasswordData, } from './entity-schemas'; -// Address migration utilities -export { - legacyAddressSchema, - flexibleAddressSchema, - requiredAddressSchema, - mapLegacyAddress, - mapToLegacyAddress, -} from './address-migration'; - -export type { - LegacyAddress, - FlexibleAddress, - RequiredAddress, -} from './address-migration'; // Re-export Zod for convenience export { z } from 'zod'; diff --git a/scripts/quick-syntax-check.sh b/scripts/quick-syntax-check.sh deleted file mode 100644 index 18e889b7..00000000 --- a/scripts/quick-syntax-check.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/bin/bash - -# Quick Syntax Check - Only validates TypeScript syntax without resolving imports -# This is the fastest way to check for basic syntax errors - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo -e "${BLUE}🚀 Quick TypeScript Syntax Check${NC}" -echo "==================================" - -# Function to check syntax only (no imports resolution) -quick_syntax_check() { - local project_path=$1 - - echo -e "${BLUE}🔍 Checking syntax: $project_path${NC}" - - cd "$project_path" - - # Use minimal memory - export NODE_OPTIONS="--max-old-space-size=1024" - - # Count TypeScript files - local ts_files_count=$(find src -name "*.ts" -not -name "*.spec.ts" -not -name "*.test.ts" | wc -l) - echo -e "${BLUE}Found $ts_files_count TypeScript files${NC}" - - # Create minimal tsconfig for syntax checking only - cat > tsconfig.syntax.json << 'EOF' -{ - "compilerOptions": { - "target": "ES2020", - "module": "CommonJS", - "noEmit": true, - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "noResolve": true, - "isolatedModules": true, - "allowJs": false, - "checkJs": false, - "strict": false, - "noImplicitAny": false, - "suppressImplicitAnyIndexErrors": true, - "suppressExcessPropertyErrors": true - }, - "include": ["src/**/*.ts"], - "exclude": [ - "node_modules", - "dist", - "**/*.spec.ts", - "**/*.test.ts", - "**/*.e2e-spec.ts" - ] -} -EOF - - # Run syntax check - if npx tsc --noEmit --noResolve --isolatedModules -p tsconfig.syntax.json 2>/dev/null; then - echo -e "${GREEN}✅ Syntax check passed${NC}" - rm -f tsconfig.syntax.json - return 0 - else - echo -e "${YELLOW}⚠️ Some syntax issues found, but this is expected with --noResolve${NC}" - echo -e "${GREEN}✅ Basic syntax structure is valid${NC}" - rm -f tsconfig.syntax.json - return 0 - fi -} - -# Function to check specific files for compilation errors -check_compilation_errors() { - local project_path=$1 - - echo -e "${BLUE}🔍 Checking for compilation errors: $project_path${NC}" - - cd "$project_path" - - export NODE_OPTIONS="--max-old-space-size=2048" - - # Look for common compilation issues - echo -e "${YELLOW}Checking for common issues...${NC}" - - # Check for missing imports - local missing_imports=$(grep -r "Cannot find module" . 2>/dev/null | wc -l || echo "0") - echo -e "${BLUE}Potential missing imports: $missing_imports${NC}" - - # Check for decorator issues - local decorator_issues=$(grep -r "Decorators are not valid" . 2>/dev/null | wc -l || echo "0") - echo -e "${BLUE}Decorator issues: $decorator_issues${NC}" - - # Check for TypeScript syntax errors in individual files - local error_count=0 - local checked_count=0 - - for file in $(find src -name "*.ts" -not -name "*.spec.ts" -not -name "*.test.ts" | head -20); do - if [ -f "$file" ]; then - checked_count=$((checked_count + 1)) - # Quick syntax check on individual file - if ! node -c <(echo "// Syntax check") 2>/dev/null; then - error_count=$((error_count + 1)) - fi - fi - done - - echo -e "${BLUE}Checked $checked_count files${NC}" - - if [ $error_count -eq 0 ]; then - echo -e "${GREEN}✅ No major compilation errors found${NC}" - return 0 - else - echo -e "${YELLOW}⚠️ Found $error_count potential issues${NC}" - return 1 - fi -} - -# Function to validate project structure -validate_structure() { - local project_path=$1 - - echo -e "${BLUE}🔍 Validating project structure: $project_path${NC}" - - cd "$project_path" - - # Check for required files - local required_files=("package.json" "tsconfig.json" "src") - local missing_files=() - - for file in "${required_files[@]}"; do - if [ ! -e "$file" ]; then - missing_files+=("$file") - fi - done - - if [ ${#missing_files[@]} -eq 0 ]; then - echo -e "${GREEN}✅ Project structure is valid${NC}" - return 0 - else - echo -e "${RED}❌ Missing required files: ${missing_files[*]}${NC}" - return 1 - fi -} - -# Main execution -main() { - local command=${1:-"syntax"} - local target=${2:-"apps/bff"} - - case $command in - "syntax") - validate_structure "$target" && quick_syntax_check "$target" - ;; - "compile") - validate_structure "$target" && check_compilation_errors "$target" - ;; - "all") - echo -e "${BLUE}Running all checks...${NC}" - validate_structure "$target" && \ - quick_syntax_check "$target" && \ - check_compilation_errors "$target" - ;; - "help"|*) - echo "Usage: $0 [command] [target]" - echo "" - echo "Commands:" - echo " syntax - Quick syntax check (default)" - echo " compile - Check for compilation errors" - echo " all - Run all checks" - echo " help - Show this help message" - echo "" - echo "Examples:" - echo " $0 syntax apps/bff # Quick syntax check" - echo " $0 compile apps/bff # Compilation check" - echo " $0 all apps/bff # All checks" - ;; - esac -} - -# Run main function with all arguments -main "$@" diff --git a/scripts/type-check-incremental.sh b/scripts/type-check-incremental.sh deleted file mode 100755 index df693efd..00000000 --- a/scripts/type-check-incremental.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/bin/bash - -# Incremental File-by-File TypeScript Checking -# This script checks TypeScript files in smaller batches to avoid memory issues - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo -e "${BLUE}🔧 Incremental TypeScript Type Checking${NC}" -echo "==============================================" - -# Function to check TypeScript files in batches -check_files_batch() { - local project_path=$1 - local batch_size=${2:-10} - - echo -e "${BLUE}🔍 Checking files in batches of $batch_size: $project_path${NC}" - - cd "$project_path" - - # Set conservative memory limit - export NODE_OPTIONS="--max-old-space-size=2048" - - # Find all TypeScript files - local ts_files=($(find src -name "*.ts" -not -path "*/node_modules/*" -not -name "*.spec.ts" -not -name "*.test.ts" | head -50)) - local total_files=${#ts_files[@]} - - if [ $total_files -eq 0 ]; then - echo -e "${YELLOW}⚠️ No TypeScript files found${NC}" - return 0 - fi - - echo -e "${BLUE}Found $total_files TypeScript files${NC}" - - # Process files in batches - local batch_count=0 - local failed_files=() - - for ((i=0; i<$total_files; i+=$batch_size)); do - batch_count=$((batch_count + 1)) - local batch_files=("${ts_files[@]:$i:$batch_size}") - local batch_file_count=${#batch_files[@]} - - echo -e "${YELLOW}Batch $batch_count: Checking $batch_file_count files...${NC}" - - # Create temporary tsconfig for this batch - cat > tsconfig.batch.json << EOF -{ - "extends": "./tsconfig.ultra-light.json", - "include": [$(printf '"%s",' "${batch_files[@]}" | sed 's/,$//')] -} -EOF - - # Check this batch - if npx tsc --noEmit -p tsconfig.batch.json 2>/dev/null; then - echo -e "${GREEN}✅ Batch $batch_count passed${NC}" - else - echo -e "${RED}❌ Batch $batch_count failed${NC}" - failed_files+=("${batch_files[@]}") - fi - - # Clean up - rm -f tsconfig.batch.json - - # Small delay to let memory settle - sleep 1 - done - - # Report results - if [ ${#failed_files[@]} -eq 0 ]; then - echo -e "${GREEN}✅ All batches passed successfully!${NC}" - return 0 - else - echo -e "${RED}❌ ${#failed_files[@]} files had issues:${NC}" - printf '%s\n' "${failed_files[@]}" - return 1 - fi -} - -# Function to check only syntax (fastest) -check_syntax_only() { - local project_path=$1 - - echo -e "${BLUE}🔍 Syntax-only check: $project_path${NC}" - - cd "$project_path" - - export NODE_OPTIONS="--max-old-space-size=1024" - - # Use TypeScript's syntax-only mode - if npx tsc --noEmit --skipLibCheck --skipDefaultLibCheck --noResolve --isolatedModules src/**/*.ts 2>/dev/null; then - echo -e "${GREEN}✅ Syntax check passed${NC}" - return 0 - else - echo -e "${RED}❌ Syntax errors found${NC}" - return 1 - fi -} - -# Function to check specific files only -check_specific_files() { - local project_path=$1 - shift - local files=("$@") - - echo -e "${BLUE}🔍 Checking specific files: $project_path${NC}" - - cd "$project_path" - - export NODE_OPTIONS="--max-old-space-size=1024" - - for file in "${files[@]}"; do - if [ -f "$file" ]; then - echo -e "${YELLOW}Checking: $file${NC}" - if npx tsc --noEmit --skipLibCheck "$file" 2>/dev/null; then - echo -e "${GREEN}✅ $file passed${NC}" - else - echo -e "${RED}❌ $file failed${NC}" - fi - fi - done -} - -# Main execution -main() { - local command=${1:-"batch"} - local target=${2:-"apps/bff"} - local batch_size=${3:-5} - - case $command in - "syntax") - check_syntax_only "$target" - ;; - "batch") - check_files_batch "$target" "$batch_size" - ;; - "files") - shift 2 - check_specific_files "$target" "$@" - ;; - "help"|*) - echo "Usage: $0 [command] [target] [options...]" - echo "" - echo "Commands:" - echo " batch - Check files in small batches (default)" - echo " syntax - Check syntax only (fastest)" - echo " files - Check specific files" - echo " help - Show this help message" - echo "" - echo "Examples:" - echo " $0 batch apps/bff 5 # Check BFF in batches of 5" - echo " $0 syntax apps/bff # Syntax check only" - echo " $0 files apps/bff src/main.ts # Check specific file" - ;; - esac -} - -# Run main function with all arguments -main "$@" diff --git a/scripts/type-check-memory.sh b/scripts/type-check-memory.sh deleted file mode 100755 index 1234f169..00000000 --- a/scripts/type-check-memory.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/bin/bash - -# TypeScript Memory-Optimized Type Checking Script -# This script provides various memory optimization strategies for TypeScript compilation - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo -e "${BLUE}🔧 TypeScript Memory-Optimized Type Checking${NC}" -echo "================================================" - -# Function to check available memory -check_memory() { - if command -v free >/dev/null 2>&1; then - TOTAL_MEM=$(free -m | awk 'NR==2{printf "%.0f", $2}') - AVAILABLE_MEM=$(free -m | awk 'NR==2{printf "%.0f", $7}') - echo -e "${BLUE}System Memory: ${TOTAL_MEM}MB total, ${AVAILABLE_MEM}MB available${NC}" - - if [ "$AVAILABLE_MEM" -lt 4096 ]; then - echo -e "${YELLOW}⚠️ Warning: Low memory detected. Using conservative settings.${NC}" - export MAX_MEMORY=2048 - elif [ "$AVAILABLE_MEM" -lt 8192 ]; then - echo -e "${YELLOW}⚠️ Moderate memory available. Using balanced settings.${NC}" - export MAX_MEMORY=4096 - else - echo -e "${GREEN}✅ Sufficient memory available. Using optimal settings.${NC}" - export MAX_MEMORY=8192 - fi - else - echo -e "${YELLOW}⚠️ Cannot detect system memory. Using default settings.${NC}" - export MAX_MEMORY=4096 - fi -} - -# Function to type-check with memory optimization -type_check_optimized() { - local project_path=$1 - local config_file=${2:-"tsconfig.json"} - - echo -e "${BLUE}🔍 Type-checking: $project_path${NC}" - - cd "$project_path" - - # Set Node.js memory limit based on available system memory - export NODE_OPTIONS="--max-old-space-size=$MAX_MEMORY" - - # Try incremental first (fastest) - if [ -f "tsconfig.memory.json" ]; then - echo -e "${YELLOW}Using memory-optimized configuration...${NC}" - if npx tsc --noEmit --incremental -p tsconfig.memory.json; then - echo -e "${GREEN}✅ Type-check completed successfully (memory-optimized)${NC}" - return 0 - fi - fi - - # Fallback to regular incremental - echo -e "${YELLOW}Trying incremental type-checking...${NC}" - if npx tsc --noEmit --incremental -p "$config_file"; then - echo -e "${GREEN}✅ Type-check completed successfully (incremental)${NC}" - return 0 - fi - - # Last resort: non-incremental with reduced strictness - echo -e "${YELLOW}Trying non-incremental type-checking...${NC}" - if npx tsc --noEmit -p "$config_file"; then - echo -e "${GREEN}✅ Type-check completed successfully (non-incremental)${NC}" - return 0 - fi - - echo -e "${RED}❌ Type-check failed${NC}" - return 1 -} - -# Function to clean TypeScript build info -clean_ts_cache() { - echo -e "${BLUE}🧹 Cleaning TypeScript cache...${NC}" - find . -name "*.tsbuildinfo*" -type f -delete 2>/dev/null || true - find . -name ".tsbuildinfo*" -type f -delete 2>/dev/null || true - echo -e "${GREEN}✅ TypeScript cache cleaned${NC}" -} - -# Main execution -main() { - local command=${1:-"check"} - local target=${2:-"all"} - - case $command in - "clean") - clean_ts_cache - ;; - "check") - check_memory - - case $target in - "bff") - type_check_optimized "./apps/bff" - ;; - "portal") - type_check_optimized "./apps/portal" - ;; - "domain") - type_check_optimized "./packages/domain" - ;; - "all"|*) - echo -e "${BLUE}🔍 Type-checking all packages...${NC}" - - # Check domain first (dependency) - type_check_optimized "./packages/domain" || exit 1 - - # Check apps in parallel if memory allows - if [ "$MAX_MEMORY" -gt 4096 ]; then - echo -e "${BLUE}Running parallel type-checks...${NC}" - (type_check_optimized "./apps/bff") & - (type_check_optimized "./apps/portal") & - wait - else - echo -e "${YELLOW}Running sequential type-checks (low memory)...${NC}" - type_check_optimized "./apps/bff" || exit 1 - type_check_optimized "./apps/portal" || exit 1 - fi - ;; - esac - ;; - "help"|*) - echo "Usage: $0 [command] [target]" - echo "" - echo "Commands:" - echo " check - Run type checking (default)" - echo " clean - Clean TypeScript cache files" - echo " help - Show this help message" - echo "" - echo "Targets:" - echo " all - Check all packages (default)" - echo " bff - Check BFF only" - echo " portal - Check Portal only" - echo " domain - Check Domain only" - echo "" - echo "Examples:" - echo " $0 check all # Check all packages" - echo " $0 check bff # Check BFF only" - echo " $0 clean # Clean TS cache" - ;; - esac -} - -# Run main function with all arguments -main "$@"