diff --git a/.cursorrules b/.cursorrules index f19cff72..4df8e744 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,530 +1,92 @@ -# Customer Portal - AI Agent Coding Rules +## Customer Portal - AI Coding Rules (Focused) -This document defines the coding standards, architecture patterns, and conventions for the Customer Portal project. AI agents (Cursor, Copilot, etc.) must follow these rules when generating or modifying code. +These rules are the fast β€œguardrails” for AI agents in this repo. Detailed patterns live in docs; when unsure, read them first. ---- +### Read before coding -## πŸ“š Documentation First +- `docs/README.md` (entrypoint) +- `docs/development/` (BFF/Portal/Domain patterns) +- `docs/architecture/` (boundaries) +- `docs/integrations/` (external API details) -**Before writing any code, read and understand the relevant documentation:** +Rule: **Never guess** endpoint behavior or payload shapes. Find docs or an existing implementation first. -- Start from `docs/README.md` for navigation -- Check `docs/development/` for implementation patterns -- Review `docs/architecture/` for system design -- Check `docs/integrations/` for external API details +### Repo boundaries (non-negotiable) -**Never guess API response structures or endpoint signatures.** Always read the documentation or existing implementations first. +- **Domain (`packages/domain/`)**: shared contracts + Zod validation + cross-app utilities. Framework-agnostic. +- **BFF (`apps/bff/`)**: NestJS HTTP boundary + orchestration + integrations (Salesforce/WHMCS/Freebit). +- **Portal (`apps/portal/`)**: Next.js UI; pages are thin wrappers over feature modules. ---- +### Import rules (enforced by ESLint) -## πŸ—οΈ Monorepo Architecture +- **Allowed (Portal + BFF)**: + - `@customer-portal/domain/` + - `@customer-portal/domain/toolkit` +- **Allowed (BFF only)**: + - `@customer-portal/domain//providers` +- **Forbidden everywhere**: + - `@customer-portal/domain` (root import) + - any deep import like `@customer-portal/domain//**` + - any deep import like `@customer-portal/domain//providers/**` +- **Forbidden in Portal**: + - `@customer-portal/domain/*/providers` (Portal must never import provider adapters) -``` -apps/ - portal/ # Next.js 15 frontend (React 19) - bff/ # NestJS 11 Backend-for-Frontend -packages/ - domain/ # Pure domain types/schemas/utils (isomorphic) -``` +Examples: -### Technology Stack - -- **Frontend**: Next.js 15, React 19, Tailwind CSS 4, shadcn/ui, TanStack Query, Zustand -- **Backend**: NestJS 11, Prisma 6, PostgreSQL 17, Redis 7, Pino -- **Integrations**: Salesforce (jsforce + Pub/Sub API), WHMCS, Freebit -- **Validation**: Zod (shared between frontend and backend) - ---- - -## πŸ“¦ Domain Package (`@customer-portal/domain`) - -The domain package is the **single source of truth** for all types and validation. - -### Structure - -``` -packages/domain/ -β”œβ”€β”€ {domain}/ -β”‚ β”œβ”€β”€ contract.ts # Normalized types (provider-agnostic) -β”‚ β”œβ”€β”€ schema.ts # Zod schemas + derived types -β”‚ β”œβ”€β”€ constants.ts # Domain constants -β”‚ β”œβ”€β”€ providers/ # Provider-specific adapters (BFF only!) -β”‚ β”‚ └── {provider}/ -β”‚ β”‚ β”œβ”€β”€ raw.types.ts # Raw API response types -β”‚ β”‚ └── mapper.ts # Transform raw β†’ domain -β”‚ └── index.ts # Public exports -β”œβ”€β”€ common/ # Shared types (ApiResponse, pagination) -└── toolkit/ # Utilities (formatting, validation helpers) -``` - -### Import Rules - -```typescript -// βœ… CORRECT: Import from domain module -import type { Invoice, InvoiceList } from "@customer-portal/domain/billing"; -import { invoiceSchema, invoiceListSchema } from "@customer-portal/domain/billing"; - -// βœ… CORRECT (BFF only): Import provider mappers -import { Providers } from "@customer-portal/domain/billing/providers"; - -// ❌ WRONG: Never import from domain root -import { Invoice } from "@customer-portal/domain"; - -// ❌ WRONG: Never deep-import internals -import { Invoice } from "@customer-portal/domain/billing/contract"; -import { mapper } from "@customer-portal/domain/billing/providers/whmcs/mapper"; - -// ❌ WRONG: Portal must NEVER import providers -// (in apps/portal/**) -import { Providers } from "@customer-portal/domain/billing/providers"; // FORBIDDEN -``` - -### Schema-First Approach - -Always define Zod schemas first, then derive TypeScript types: - -```typescript -// βœ… CORRECT: Schema-first -export const invoiceSchema = z.object({ - id: z.number(), - status: z.enum(["Paid", "Unpaid", "Cancelled"]), - total: z.number(), -}); -export type Invoice = z.infer; - -// ❌ WRONG: Type-only (no runtime validation) -export interface Invoice { - id: number; - status: string; - total: number; -} -``` - ---- - -## πŸ–₯️ BFF (NestJS Backend) - -### Directory Structure - -``` -apps/bff/src/ -β”œβ”€β”€ modules/ # Feature-aligned modules -β”‚ └── {feature}/ -β”‚ β”œβ”€β”€ {feature}.controller.ts -β”‚ β”œβ”€β”€ {feature}.module.ts -β”‚ └── services/ -β”‚ β”œβ”€β”€ {feature}-orchestrator.service.ts -β”‚ └── {feature}.service.ts -β”œβ”€β”€ integrations/ # External system clients -β”‚ └── {provider}/ -β”‚ β”œβ”€β”€ services/ -β”‚ β”‚ β”œβ”€β”€ {provider}-connection.service.ts -β”‚ β”‚ └── {provider}-{entity}.service.ts -β”‚ β”œβ”€β”€ utils/ -β”‚ β”‚ └── {entity}-query-builder.ts -β”‚ └── {provider}.module.ts -β”œβ”€β”€ core/ # Framework setup (guards, filters, config) -β”œβ”€β”€ infra/ # Infrastructure (Redis, queues, logging) -└── main.ts -``` - -### Controller Pattern - -Controllers use Zod DTOs via `createZodDto()` and the global `ZodValidationPipe`: - -```typescript -import { Controller, Get, Query, Request } from "@nestjs/common"; -import { createZodDto, ZodResponse } from "nestjs-zod"; -import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; -import type { InvoiceList } from "@customer-portal/domain/billing"; -import { - invoiceListQuerySchema, - invoiceListSchema, -} from "@customer-portal/domain/billing"; - -class InvoiceListQueryDto extends createZodDto(invoiceListQuerySchema) {} -class InvoiceListDto extends createZodDto(invoiceListSchema) {} - -@Controller("invoices") -export class InvoicesController { - constructor(private readonly orchestrator: InvoicesOrchestratorService) {} - - @Get() - @ZodResponse({ description: "List invoices", type: InvoiceListDto }) - async getInvoices( - @Request() req: RequestWithUser, - @Query() query: InvoiceListQueryDto - ): Promise { - return this.orchestrator.getInvoices(req.user.id, query); - } -} -``` - -**Controller Rules:** -- ❌ Never import `zod` directly in controllers -- βœ… Use schemas from `@customer-portal/domain/{module}` -- βœ… Use `createZodDto(schema)` for DTO classes -- βœ… Delegate all business logic to orchestrator services - -### Integration Service Pattern - -Integration services handle external API communication: - -```typescript -import { Injectable } from "@nestjs/common"; -import { SalesforceConnection } from "./salesforce-connection.service"; -import { buildOrderSelectFields } from "../utils/order-query-builder"; -import { - Providers, - type OrderDetails, -} from "@customer-portal/domain/orders"; - -@Injectable() -export class SalesforceOrderService { - constructor(private readonly sf: SalesforceConnection) {} - - async getOrderById(orderId: string): Promise { - // 1. Build query (infrastructure concern) - const fields = buildOrderSelectFields().join(", "); - const soql = `SELECT ${fields} FROM Order WHERE Id = '${orderId}'`; - - // 2. Execute query - const result = await this.sf.query(soql); - if (!result.records?.[0]) return null; - - // 3. Transform with domain mapper (SINGLE transformation!) - return Providers.Salesforce.transformOrderDetails(result.records[0], []); - } -} -``` - -**Integration Service Rules:** -- βœ… Build queries in utils (query builders) -- βœ… Execute API calls -- βœ… Use domain mappers for transformation -- βœ… Return domain types -- ❌ Never add business logic -- ❌ Never create wrapper/transformer services -- ❌ Never transform data twice - -### Error Handling - -**Never expose sensitive information to customers:** - -```typescript -// βœ… CORRECT: Generic user-facing message -throw new HttpException( - "Unable to process your request. Please try again.", - HttpStatus.BAD_REQUEST -); - -// ❌ WRONG: Exposes internal details -throw new HttpException( - `WHMCS API error: ${error.message}`, - HttpStatus.BAD_REQUEST -); -``` - -Log detailed errors server-side, return generic messages to clients. - ---- - -## 🌐 Portal (Next.js Frontend) - -### Directory Structure - -``` -apps/portal/src/ -β”œβ”€β”€ app/ # Next.js App Router (thin route wrappers) -β”‚ β”œβ”€β”€ (public)/ # Marketing + auth routes -β”‚ β”œβ”€β”€ (authenticated)/ # Signed-in portal routes -β”‚ └── api/ # API routes -β”œβ”€β”€ components/ # Shared UI (design system) -β”‚ β”œβ”€β”€ ui/ # Atoms (Button, Input, Card) -β”‚ β”œβ”€β”€ layout/ # Layout components -β”‚ └── common/ # Molecules (DataTable, SearchBar) -β”œβ”€β”€ features/ # Feature modules -β”‚ └── {feature}/ -β”‚ β”œβ”€β”€ components/ # Feature-specific UI -β”‚ β”œβ”€β”€ hooks/ # React Query hooks -β”‚ β”œβ”€β”€ services/ # API service functions -β”‚ β”œβ”€β”€ views/ # Page-level views -β”‚ └── index.ts # Public exports -β”œβ”€β”€ lib/ # Core utilities -β”‚ β”œβ”€β”€ api/ # HTTP client -β”‚ β”œβ”€β”€ hooks/ # Shared hooks -β”‚ β”œβ”€β”€ services/ # Shared services -β”‚ └── utils/ # Utility functions -└── styles/ # Global styles -``` - -### Feature Module Pattern - -```typescript -// features/billing/hooks/use-invoices.ts -import { useQuery } from "@tanstack/react-query"; -import type { InvoiceList } from "@customer-portal/domain/billing"; -import { billingService } from "../services"; - -export function useInvoices(params?: { status?: string }) { - return useQuery({ - queryKey: ["invoices", params], - queryFn: () => billingService.getInvoices(params), - }); -} - -// features/billing/services/billing.service.ts -import { apiClient } from "@/lib/api"; -import type { InvoiceList } from "@customer-portal/domain/billing"; - -export const billingService = { - async getInvoices(params?: { status?: string }): Promise { - const response = await apiClient.get("/invoices", { params }); - return response.data; - }, -}; - -// features/billing/index.ts -export * from "./hooks"; -export * from "./components"; -``` - -### Page Component Rules - -Pages are thin shells that compose features: - -```typescript -// app/(authenticated)/billing/page.tsx -import { InvoicesView } from "@/features/billing"; - -export default function BillingPage() { - return ; -} -``` - -**Frontend Rules:** -- βœ… Pages delegate to feature views -- βœ… Data fetching lives in feature hooks -- βœ… Business logic lives in feature services -- βœ… Use `@/` path aliases -- ❌ Never call APIs directly in page components -- ❌ Never import provider types (only domain contracts) - -### Import Patterns - -```typescript -// Feature imports -import { LoginForm, useAuth } from "@/features/auth"; - -// Component imports -import { Button, Input } from "@/components/ui"; -import { DataTable } from "@/components/common"; - -// Type imports (domain types only!) +```ts +// βœ… correct (apps) import type { Invoice } from "@customer-portal/domain/billing"; +import { invoiceSchema } from "@customer-portal/domain/billing"; -// Utility imports -import { apiClient } from "@/lib/api"; +// βœ… correct (BFF integrations only) +import { Whmcs } from "@customer-portal/domain/billing/providers"; + +// ❌ forbidden +import { Billing } from "@customer-portal/domain"; +import { Invoice } from "@customer-portal/domain/billing/contract"; +import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers/whmcs/mapper"; ``` ---- +### Validation rules (Zod-first) -## βœ… Validation Patterns +- **Schemas live in domain**: `packages/domain//schema.ts` +- **Derive types from schemas**: `export type X = z.infer` +- **Query params**: use `z.coerce.*` for URL strings. -### Query Parameters +### BFF rules (NestJS) -Use `z.coerce` for URL query strings: +- **Controllers are thin**: + - no business logic + - no `zod` imports in controllers + - use `createZodDto(schema)` + global `ZodValidationPipe` +- **Integrations are thin**: + - build queries in `apps/bff/src/integrations//utils` + - fetch data + - transform once via domain mappers + - return domain types +- **Errors**: + - never leak sensitive details to clients + - log details server-side, return generic user messages -```typescript -// βœ… CORRECT: Coerce string to number -export const paginationSchema = z.object({ - page: z.coerce.number().int().positive().optional(), - limit: z.coerce.number().int().positive().max(100).optional(), -}); +### Portal rules (Next.js) -// ❌ WRONG: Will fail on URL strings -export const paginationSchema = z.object({ - page: z.number().optional(), // "1" !== 1 -}); -``` +- **Pages are wrappers** under `apps/portal/src/app/**` (no API calls in pages) +- **Feature modules own logic** under `apps/portal/src/features//**` + - hooks (React Query) + - services (API client calls) + - components/views (UI composition) +- **No provider imports** from domain in Portal. -### Request Body Validation +### Naming & safety -```typescript -// In domain schema -export const createOrderRequestSchema = z.object({ - items: z.array(orderItemSchema).min(1), - shippingAddressId: z.string().uuid(), -}); -export type CreateOrderRequest = z.infer; -``` +- No `any` in public APIs +- Avoid unsafe assertions +- No `console.log` in production code (use logger) +- Avoid `V2` suffix in service names -### Form Validation (Frontend) +### References -```typescript -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { loginRequestSchema, type LoginRequest } from "@customer-portal/domain/auth"; - -function LoginForm() { - const form = useForm({ - resolver: zodResolver(loginRequestSchema), - }); - // ... -} -``` - ---- - -## 🎨 UI Design Guidelines - -Follow minimal, clean UI design principles: - -- **Avoid excessive animations** - Use subtle, purposeful transitions -- **Clean typography** - Use system fonts or carefully chosen web fonts -- **Consistent spacing** - Follow the design system tokens -- **Accessibility first** - Proper ARIA labels, keyboard navigation -- **Mobile responsive** - Test on all breakpoints - -Use shadcn/ui components and Tailwind CSS utilities. - ---- - -## πŸ”’ Security Rules - -1. **Never expose sensitive data** in error messages or responses -2. **Validate all inputs** at API boundaries (Zod schemas) -3. **Use parameterized queries** (Prisma handles this) -4. **Sanitize user content** before display -5. **Check authorization** in every endpoint - ---- - -## πŸ“ Naming Conventions - -### Files - -- **Components**: PascalCase (`InvoiceCard.tsx`) -- **Hooks**: camelCase with `use` prefix (`useInvoices.ts`) -- **Services**: kebab-case (`billing.service.ts`) -- **Types/Schemas**: kebab-case (`invoice.schema.ts`) -- **Utils**: kebab-case (`format-currency.ts`) - -### Code - -- **Types/Interfaces**: PascalCase (`Invoice`, `OrderDetails`) -- **Schemas**: camelCase with `Schema` suffix (`invoiceSchema`) -- **Constants**: SCREAMING_SNAKE_CASE (`INVOICE_STATUS`) -- **Functions**: camelCase (`getInvoices`, `transformOrder`) -- **React Components**: PascalCase (`InvoiceList`) - -### Services - -- ❌ Avoid `V2` suffix in service names -- βœ… Use clear, descriptive names (`InvoicesOrchestratorService`) - ---- - -## 🚫 Anti-Patterns to Avoid - -### Domain Layer - -- ❌ Framework-specific imports (no React/NestJS in domain) -- ❌ Circular dependencies -- ❌ Exposing raw provider types to application code - -### BFF - -- ❌ Business logic in controllers -- ❌ Direct Zod imports in controllers (use schemas from domain) -- ❌ Creating transformer/wrapper services -- ❌ Multiple data transformations -- ❌ Exposing internal error details - -### Portal - -- ❌ API calls in page components -- ❌ Business logic in UI components -- ❌ Importing provider types/mappers -- ❌ Duplicate type definitions -- ❌ Using `window.location` for navigation (use `next/link` or `useRouter`) - -### General - -- ❌ Using `any` type (especially in public APIs) -- ❌ Unsafe type assertions -- ❌ Console.log in production code (use proper logger) -- ❌ Guessing API response structures - ---- - -## πŸ”„ Data Flow Summary - -### Inbound (External API β†’ Application) - -``` -External API Response - ↓ -Raw Provider Types (domain/*/providers/*/raw.types.ts) - ↓ -Provider Mapper (domain/*/providers/*/mapper.ts) [BFF only] - ↓ -Zod Schema Validation (domain/*/schema.ts) - ↓ -Domain Contract (domain/*/contract.ts) - ↓ -Application Code (BFF services, Portal hooks) -``` - -### Outbound (Application β†’ External API) - -``` -Application Intent - ↓ -Domain Contract - ↓ -Provider Mapper (BFF only) - ↓ -Raw Provider Payload - ↓ -External API Request -``` - ---- - -## πŸ“‹ Checklist for New Features - -1. [ ] Define types in `packages/domain/{feature}/contract.ts` -2. [ ] Add Zod schemas in `packages/domain/{feature}/schema.ts` -3. [ ] Export from `packages/domain/{feature}/index.ts` -4. [ ] (If provider needed) Add raw types and mapper in `providers/{provider}/` -5. [ ] Create BFF module in `apps/bff/src/modules/{feature}/` -6. [ ] Create controller with Zod DTOs -7. [ ] Create orchestrator service -8. [ ] Create portal feature in `apps/portal/src/features/{feature}/` -9. [ ] Add hooks, services, and components -10. [ ] Create page in `apps/portal/src/app/` - ---- - -## πŸ› οΈ Development Commands - -```bash -# Development -pnpm dev # Start all apps -pnpm dev:bff # Start BFF only -pnpm dev:portal # Start Portal only - -# Type checking -pnpm typecheck # Check all packages -pnpm lint # Run ESLint - -# Database -pnpm db:migrate # Run migrations -pnpm db:generate # Generate Prisma client -``` - ---- - -**Last Updated**: December 2025 +- `docs/development/domain/import-hygiene.md` +- `docs/development/bff/integration-patterns.md` +- `docs/development/portal/architecture.md` diff --git a/apps/bff/src/app.module.ts b/apps/bff/src/app.module.ts index 268fa4db..540ef21b 100644 --- a/apps/bff/src/app.module.ts +++ b/apps/bff/src/app.module.ts @@ -33,7 +33,7 @@ import { MeStatusModule } from "@bff/modules/me-status/me-status.module.js"; import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js"; import { ServicesModule } from "@bff/modules/services/services.module.js"; import { OrdersModule } from "@bff/modules/orders/orders.module.js"; -import { InvoicesModule } from "@bff/modules/invoices/invoices.module.js"; +import { BillingModule } from "@bff/modules/billing/billing.module.js"; import { SubscriptionsModule } from "@bff/modules/subscriptions/subscriptions.module.js"; import { CurrencyModule } from "@bff/modules/currency/currency.module.js"; import { SupportModule } from "@bff/modules/support/support.module.js"; @@ -87,7 +87,7 @@ import { HealthModule } from "@bff/modules/health/health.module.js"; MappingsModule, ServicesModule, OrdersModule, - InvoicesModule, + BillingModule, SubscriptionsModule, CurrencyModule, SupportModule, diff --git a/apps/bff/src/core/config/router.config.ts b/apps/bff/src/core/config/router.config.ts index 08b80968..441f0f7a 100644 --- a/apps/bff/src/core/config/router.config.ts +++ b/apps/bff/src/core/config/router.config.ts @@ -5,7 +5,7 @@ import { MeStatusModule } from "@bff/modules/me-status/me-status.module.js"; import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js"; import { ServicesModule } from "@bff/modules/services/services.module.js"; import { OrdersModule } from "@bff/modules/orders/orders.module.js"; -import { InvoicesModule } from "@bff/modules/invoices/invoices.module.js"; +import { BillingModule } from "@bff/modules/billing/billing.module.js"; import { SubscriptionsModule } from "@bff/modules/subscriptions/subscriptions.module.js"; import { CurrencyModule } from "@bff/modules/currency/currency.module.js"; import { SecurityModule } from "@bff/core/security/security.module.js"; @@ -24,7 +24,7 @@ export const apiRoutes: Routes = [ { path: "", module: MappingsModule }, { path: "", module: ServicesModule }, { path: "", module: OrdersModule }, - { path: "", module: InvoicesModule }, + { path: "", module: BillingModule }, { path: "", module: SubscriptionsModule }, { path: "", module: CurrencyModule }, { path: "", module: SupportModule }, diff --git a/apps/bff/src/core/database/database.module.ts b/apps/bff/src/core/database/database.module.ts deleted file mode 100644 index b13e124f..00000000 --- a/apps/bff/src/core/database/database.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from "@nestjs/common"; -import { PrismaModule } from "@bff/infra/database/prisma.module.js"; -import { TransactionService } from "./services/transaction.service.js"; -import { DistributedTransactionService } from "./services/distributed-transaction.service.js"; - -@Module({ - imports: [PrismaModule], - providers: [TransactionService, DistributedTransactionService], - exports: [TransactionService, DistributedTransactionService], -}) -export class DatabaseModule {} diff --git a/apps/bff/src/core/health/queue-health.controller.ts b/apps/bff/src/core/health/queue-health.controller.ts index 9bd7cdfa..7082f05c 100644 --- a/apps/bff/src/core/health/queue-health.controller.ts +++ b/apps/bff/src/core/health/queue-health.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get } from "@nestjs/common"; -import { WhmcsRequestQueueService } from "@bff/core/queue/services/whmcs-request-queue.service.js"; -import { SalesforceRequestQueueService } from "@bff/core/queue/services/salesforce-request-queue.service.js"; +import { WhmcsRequestQueueService } from "@bff/infra/queue/services/whmcs-request-queue.service.js"; +import { SalesforceRequestQueueService } from "@bff/infra/queue/services/salesforce-request-queue.service.js"; import { Public } from "@bff/modules/auth/decorators/public.decorator.js"; @Controller("health/queues") diff --git a/apps/bff/src/core/queue/queue.module.ts b/apps/bff/src/core/queue/queue.module.ts deleted file mode 100644 index 7ebef2e1..00000000 --- a/apps/bff/src/core/queue/queue.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from "@nestjs/common"; -import { CacheModule } from "@bff/infra/cache/cache.module.js"; -import { WhmcsRequestQueueService } from "./services/whmcs-request-queue.service.js"; -import { SalesforceRequestQueueService } from "./services/salesforce-request-queue.service.js"; - -@Module({ - imports: [CacheModule], - providers: [WhmcsRequestQueueService, SalesforceRequestQueueService], - exports: [WhmcsRequestQueueService, SalesforceRequestQueueService], -}) -export class QueueModule {} diff --git a/apps/bff/src/core/security/services/csrf.service.ts b/apps/bff/src/core/security/services/csrf.service.ts index cda799aa..0bdc89f2 100644 --- a/apps/bff/src/core/security/services/csrf.service.ts +++ b/apps/bff/src/core/security/services/csrf.service.ts @@ -129,16 +129,16 @@ export class CsrfService { return { isValid: true }; } - invalidateToken(_token: string): void { + invalidateToken(): void { // Stateless tokens are tied to the secret cookie; rotate cookie to invalidate. this.logger.debug("invalidateToken called for stateless CSRF token"); } - invalidateSessionTokens(_sessionId: string): void { + invalidateSessionTokens(): void { this.logger.debug("invalidateSessionTokens called - rotate cookie to enforce"); } - invalidateUserTokens(_userId: string): void { + invalidateUserTokens(): void { this.logger.debug("invalidateUserTokens called - rotate cookie to enforce"); } diff --git a/apps/bff/src/core/utils/error.util.ts b/apps/bff/src/core/utils/error.util.ts index b3339424..c0168762 100644 --- a/apps/bff/src/core/utils/error.util.ts +++ b/apps/bff/src/core/utils/error.util.ts @@ -159,8 +159,7 @@ export function createDeferredPromise(): { // Use native Promise.withResolvers if available (ES2024) if ( "withResolvers" in Promise && - typeof (Promise as unknown as { withResolvers?: <_U>() => unknown }).withResolvers === - "function" + typeof (Promise as unknown as { withResolvers?: () => unknown }).withResolvers === "function" ) { return ( Promise as unknown as { diff --git a/apps/bff/src/infra/database/prisma.module.ts b/apps/bff/src/infra/database/prisma.module.ts index d7294c48..796effb4 100644 --- a/apps/bff/src/infra/database/prisma.module.ts +++ b/apps/bff/src/infra/database/prisma.module.ts @@ -1,9 +1,11 @@ import { Global, Module } from "@nestjs/common"; import { PrismaService } from "./prisma.service.js"; +import { TransactionService } from "./services/transaction.service.js"; +import { DistributedTransactionService } from "./services/distributed-transaction.service.js"; @Global() @Module({ - providers: [PrismaService], - exports: [PrismaService], + providers: [PrismaService, TransactionService, DistributedTransactionService], + exports: [PrismaService, TransactionService, DistributedTransactionService], }) export class PrismaModule {} diff --git a/apps/bff/src/core/database/services/distributed-transaction.service.ts b/apps/bff/src/infra/database/services/distributed-transaction.service.ts similarity index 100% rename from apps/bff/src/core/database/services/distributed-transaction.service.ts rename to apps/bff/src/infra/database/services/distributed-transaction.service.ts diff --git a/apps/bff/src/infra/database/services/index.ts b/apps/bff/src/infra/database/services/index.ts new file mode 100644 index 00000000..33c1a10c --- /dev/null +++ b/apps/bff/src/infra/database/services/index.ts @@ -0,0 +1,5 @@ +/** + * Database Services + */ +export * from "./transaction.service.js"; +export * from "./distributed-transaction.service.js"; diff --git a/apps/bff/src/core/database/services/transaction.service.ts b/apps/bff/src/infra/database/services/transaction.service.ts similarity index 98% rename from apps/bff/src/core/database/services/transaction.service.ts rename to apps/bff/src/infra/database/services/transaction.service.ts index 690fe61a..be6ad293 100644 --- a/apps/bff/src/core/database/services/transaction.service.ts +++ b/apps/bff/src/infra/database/services/transaction.service.ts @@ -1,7 +1,7 @@ import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { Prisma } from "@prisma/client"; -import { PrismaService } from "@bff/infra/database/prisma.service.js"; +import { PrismaService } from "../prisma.service.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js"; export interface TransactionContext { @@ -223,7 +223,7 @@ export class TransactionService { operation: SimpleTransactionOperation, options: Omit = {} ): Promise { - const result = await this.executeTransaction(async (tx, _context) => operation(tx), { + const result = await this.executeTransaction(async tx => operation(tx), { ...options, autoRollback: false, }); diff --git a/apps/bff/src/infra/mappers/mapping.mapper.ts b/apps/bff/src/infra/mappers/mapping.mapper.ts index 12fa7d5b..894b9e13 100644 --- a/apps/bff/src/infra/mappers/mapping.mapper.ts +++ b/apps/bff/src/infra/mappers/mapping.mapper.ts @@ -8,7 +8,7 @@ */ import type { IdMapping as PrismaIdMapping } from "@prisma/client"; -import type { UserIdMapping } from "@customer-portal/domain/mappings"; +import type { UserIdMapping } from "@bff/modules/id-mappings/domain/index.js"; /** * Maps Prisma IdMapping entity to Domain UserIdMapping type diff --git a/apps/bff/src/infra/mappers/user.mapper.ts b/apps/bff/src/infra/mappers/user.mapper.ts index 80047bcc..c742a75d 100644 --- a/apps/bff/src/infra/mappers/user.mapper.ts +++ b/apps/bff/src/infra/mappers/user.mapper.ts @@ -9,9 +9,9 @@ import type { User as PrismaUser } from "@prisma/client"; import type { UserAuth } from "@customer-portal/domain/customer"; -import * as CustomerProviders from "@customer-portal/domain/customer/providers"; +import { mapPrismaUserToUserAuth } from "@customer-portal/domain/customer/providers"; -type PrismaUserRaw = Parameters[0]; +type PrismaUserRaw = Parameters[0]; /** * Maps Prisma User entity to Domain UserAuth type @@ -39,5 +39,5 @@ export function mapPrismaUserToDomain(user: PrismaUser): UserAuth { }; // Use domain provider mapper - return CustomerProviders.Portal.mapPrismaUserToUserAuth(prismaUserRaw); + return mapPrismaUserToUserAuth(prismaUserRaw); } diff --git a/apps/bff/src/infra/queue/queue.module.ts b/apps/bff/src/infra/queue/queue.module.ts index 9c83e27c..b58634a9 100644 --- a/apps/bff/src/infra/queue/queue.module.ts +++ b/apps/bff/src/infra/queue/queue.module.ts @@ -2,6 +2,9 @@ import { Global, Module } from "@nestjs/common"; import { BullModule } from "@nestjs/bullmq"; import { ConfigModule, ConfigService } from "@nestjs/config"; import { QUEUE_NAMES } from "./queue.constants.js"; +import { WhmcsRequestQueueService } from "./services/whmcs-request-queue.service.js"; +import { SalesforceRequestQueueService } from "./services/salesforce-request-queue.service.js"; +import { CacheModule } from "@bff/infra/cache/cache.module.js"; function parseRedisConnection(redisUrl: string) { try { @@ -24,6 +27,7 @@ function parseRedisConnection(redisUrl: string) { @Module({ imports: [ ConfigModule, + CacheModule, BullModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ @@ -37,6 +41,7 @@ function parseRedisConnection(redisUrl: string) { { name: QUEUE_NAMES.SIM_MANAGEMENT } ), ], - exports: [BullModule], + providers: [WhmcsRequestQueueService, SalesforceRequestQueueService], + exports: [BullModule, WhmcsRequestQueueService, SalesforceRequestQueueService], }) export class QueueModule {} diff --git a/apps/bff/src/infra/queue/services/index.ts b/apps/bff/src/infra/queue/services/index.ts new file mode 100644 index 00000000..6f73de9d --- /dev/null +++ b/apps/bff/src/infra/queue/services/index.ts @@ -0,0 +1,5 @@ +/** + * Queue Services + */ +export * from "./whmcs-request-queue.service.js"; +export * from "./salesforce-request-queue.service.js"; diff --git a/apps/bff/src/core/queue/services/salesforce-request-queue.service.ts b/apps/bff/src/infra/queue/services/salesforce-request-queue.service.ts similarity index 100% rename from apps/bff/src/core/queue/services/salesforce-request-queue.service.ts rename to apps/bff/src/infra/queue/services/salesforce-request-queue.service.ts diff --git a/apps/bff/src/core/queue/services/whmcs-request-queue.service.ts b/apps/bff/src/infra/queue/services/whmcs-request-queue.service.ts similarity index 100% rename from apps/bff/src/core/queue/services/whmcs-request-queue.service.ts rename to apps/bff/src/infra/queue/services/whmcs-request-queue.service.ts diff --git a/packages/domain/salesforce/field-maps.ts b/apps/bff/src/integrations/salesforce/constants/field-maps.ts similarity index 98% rename from packages/domain/salesforce/field-maps.ts rename to apps/bff/src/integrations/salesforce/constants/field-maps.ts index 5b70c769..cdc4b2fb 100644 --- a/packages/domain/salesforce/field-maps.ts +++ b/apps/bff/src/integrations/salesforce/constants/field-maps.ts @@ -7,7 +7,7 @@ * * Usage: * ```typescript - * import { ACCOUNT_FIELDS } from "@customer-portal/domain/salesforce"; + * import { ACCOUNT_FIELDS } from "@bff/integrations/salesforce/constants"; * * const eligibilityValue = account[ACCOUNT_FIELDS.eligibility.value]; * ``` diff --git a/packages/domain/salesforce/index.ts b/apps/bff/src/integrations/salesforce/constants/index.ts similarity index 94% rename from packages/domain/salesforce/index.ts rename to apps/bff/src/integrations/salesforce/constants/index.ts index a78ed468..fc4badc8 100644 --- a/packages/domain/salesforce/index.ts +++ b/apps/bff/src/integrations/salesforce/constants/index.ts @@ -1,5 +1,5 @@ /** - * Salesforce Domain + * Salesforce Constants * * Centralized Salesforce field maps and constants. * Provides a single source of truth for field names used across diff --git a/apps/bff/src/integrations/salesforce/guards/salesforce-read-throttle.guard.ts b/apps/bff/src/integrations/salesforce/guards/salesforce-read-throttle.guard.ts index bd65cee6..b35c4a1d 100644 --- a/apps/bff/src/integrations/salesforce/guards/salesforce-read-throttle.guard.ts +++ b/apps/bff/src/integrations/salesforce/guards/salesforce-read-throttle.guard.ts @@ -2,7 +2,7 @@ import { Inject, Injectable, HttpException, HttpStatus } from "@nestjs/common"; import type { CanActivate, ExecutionContext } from "@nestjs/common"; import type { Request } from "express"; import { Logger } from "nestjs-pino"; -import { SalesforceRequestQueueService } from "@bff/core/queue/services/salesforce-request-queue.service.js"; +import { SalesforceRequestQueueService } from "@bff/infra/queue/services/salesforce-request-queue.service.js"; @Injectable() export class SalesforceReadThrottleGuard implements CanActivate { diff --git a/apps/bff/src/integrations/salesforce/guards/salesforce-write-throttle.guard.ts b/apps/bff/src/integrations/salesforce/guards/salesforce-write-throttle.guard.ts index 5a42b453..c4fa416b 100644 --- a/apps/bff/src/integrations/salesforce/guards/salesforce-write-throttle.guard.ts +++ b/apps/bff/src/integrations/salesforce/guards/salesforce-write-throttle.guard.ts @@ -2,7 +2,7 @@ import { Inject, Injectable, HttpException, HttpStatus } from "@nestjs/common"; import type { CanActivate, ExecutionContext } from "@nestjs/common"; import type { Request } from "express"; import { Logger } from "nestjs-pino"; -import { SalesforceRequestQueueService } from "@bff/core/queue/services/salesforce-request-queue.service.js"; +import { SalesforceRequestQueueService } from "@bff/infra/queue/services/salesforce-request-queue.service.js"; @Injectable() export class SalesforceWriteThrottleGuard implements CanActivate { diff --git a/apps/bff/src/integrations/salesforce/salesforce.module.ts b/apps/bff/src/integrations/salesforce/salesforce.module.ts index 5398b5c7..c98881f2 100644 --- a/apps/bff/src/integrations/salesforce/salesforce.module.ts +++ b/apps/bff/src/integrations/salesforce/salesforce.module.ts @@ -1,6 +1,6 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; -import { QueueModule } from "@bff/core/queue/queue.module.js"; +import { QueueModule } from "@bff/infra/queue/queue.module.js"; import { SalesforceService } from "./salesforce.service.js"; import { SalesforceConnection } from "./services/salesforce-connection.service.js"; import { SalesforceAccountService } from "./services/salesforce-account.service.js"; diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-case.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-case.service.ts index fca05890..7b74503d 100644 --- a/apps/bff/src/integrations/salesforce/services/salesforce-case.service.ts +++ b/apps/bff/src/integrations/salesforce/services/salesforce-case.service.ts @@ -22,10 +22,14 @@ import { SALESFORCE_CASE_STATUS, SALESFORCE_CASE_PRIORITY, } from "@customer-portal/domain/support/providers"; -import * as Providers from "@customer-portal/domain/support/providers"; - -// Access the mapper directly to avoid unbound method issues -const salesforceMapper = Providers.Salesforce; +import { + buildCaseByIdQuery, + buildCaseSelectFields, + buildCasesForAccountQuery, + toSalesforcePriority, + transformSalesforceCaseToSupportCase, + transformSalesforceCasesToSupportCases, +} from "@customer-portal/domain/support/providers"; /** * Parameters for creating a case in Salesforce @@ -52,10 +56,7 @@ export class SalesforceCaseService { const safeAccountId = assertSalesforceId(accountId, "accountId"); this.logger.debug({ accountId: safeAccountId }, "Fetching portal cases for account"); - const soql = Providers.Salesforce.buildCasesForAccountQuery( - safeAccountId, - SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE - ); + const soql = buildCasesForAccountQuery(safeAccountId, SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE); try { const result = (await this.sf.query(soql, { @@ -69,7 +70,7 @@ export class SalesforceCaseService { "Portal cases retrieved for account" ); - return Providers.Salesforce.transformSalesforceCasesToSupportCases(cases); + return transformSalesforceCasesToSupportCases(cases); } catch (error: unknown) { this.logger.error("Failed to fetch cases for account", { error: getErrorMessage(error), @@ -88,7 +89,7 @@ export class SalesforceCaseService { this.logger.debug({ caseId: safeCaseId, accountId: safeAccountId }, "Fetching case by ID"); - const soql = Providers.Salesforce.buildCaseByIdQuery( + const soql = buildCaseByIdQuery( safeCaseId, safeAccountId, SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE @@ -106,7 +107,7 @@ export class SalesforceCaseService { return null; } - return Providers.Salesforce.transformSalesforceCaseToSupportCase(record); + return transformSalesforceCaseToSupportCase(record); } catch (error: unknown) { this.logger.error("Failed to fetch case by ID", { error: getErrorMessage(error), @@ -133,7 +134,7 @@ export class SalesforceCaseService { // Build case payload with portal defaults // Convert portal display values to Salesforce API values const sfPriority = params.priority - ? salesforceMapper.toSalesforcePriority(params.priority) + ? toSalesforcePriority(params.priority) : SALESFORCE_CASE_PRIORITY.MEDIUM; const casePayload: Record = { @@ -243,7 +244,7 @@ export class SalesforceCaseService { private async getCaseByIdInternal(caseId: string): Promise { const safeCaseId = assertSalesforceId(caseId, "caseId"); - const fields = Providers.Salesforce.buildCaseSelectFields().join(", "); + const fields = buildCaseSelectFields().join(", "); const soql = ` SELECT ${fields} FROM Case diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-connection.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-connection.service.ts index a4de8e9e..28ac90d0 100644 --- a/apps/bff/src/integrations/salesforce/services/salesforce-connection.service.ts +++ b/apps/bff/src/integrations/salesforce/services/salesforce-connection.service.ts @@ -2,7 +2,7 @@ import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { ConfigService } from "@nestjs/config"; import { getErrorMessage } from "@bff/core/utils/error.util.js"; -import { SalesforceRequestQueueService } from "@bff/core/queue/services/salesforce-request-queue.service.js"; +import { SalesforceRequestQueueService } from "@bff/infra/queue/services/salesforce-request-queue.service.js"; import jsforce from "jsforce"; import { SignJWT } from "jose"; import { createPrivateKey } from "node:crypto"; diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts index 7aa15d93..165f084d 100644 --- a/apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts +++ b/apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts @@ -21,7 +21,10 @@ import type { SalesforceOrderItemRecord, SalesforceOrderRecord, } from "@customer-portal/domain/orders/providers"; -import * as OrderProviders from "@customer-portal/domain/orders/providers"; +import { + transformSalesforceOrderDetails, + transformSalesforceOrderSummary, +} from "@customer-portal/domain/orders/providers"; import type { SalesforceResponse } from "@customer-portal/domain/common/providers"; import { OrderFieldMapService } from "@bff/modules/orders/config/order-field-map.service.js"; @@ -98,11 +101,7 @@ export class SalesforceOrderService { ); // Use domain mapper - single transformation! - return OrderProviders.Salesforce.transformSalesforceOrderDetails( - order, - orderItems, - this.orderFieldMap.fields - ); + return transformSalesforceOrderDetails(order, orderItems, this.orderFieldMap.fields); } catch (error: unknown) { this.logger.error("Failed to fetch order with items", { error: getErrorMessage(error), @@ -297,7 +296,7 @@ export class SalesforceOrderService { (order): order is SalesforceOrderRecord & { Id: string } => typeof order.Id === "string" ) .map(order => - OrderProviders.Salesforce.transformSalesforceOrderSummary( + transformSalesforceOrderSummary( order, itemsByOrder[order.Id] ?? [], this.orderFieldMap.fields diff --git a/apps/bff/src/integrations/whmcs/cache/whmcs-cache.service.ts b/apps/bff/src/integrations/whmcs/cache/whmcs-cache.service.ts index a4d8da7d..750c9166 100644 --- a/apps/bff/src/integrations/whmcs/cache/whmcs-cache.service.ts +++ b/apps/bff/src/integrations/whmcs/cache/whmcs-cache.service.ts @@ -89,7 +89,7 @@ export class WhmcsCacheService { status?: string ): Promise { const key = this.buildInvoicesKey(userId, page, limit, status); - return this.get(key, "invoices"); + return this.get(key); } /** @@ -103,7 +103,7 @@ export class WhmcsCacheService { data: InvoiceList ): Promise { const key = this.buildInvoicesKey(userId, page, limit, status); - await this.set(key, data, "invoices", [`user:${userId}`]); + await this.set(key, data, "invoices"); } /** @@ -111,7 +111,7 @@ export class WhmcsCacheService { */ async getInvoice(userId: string, invoiceId: number): Promise { const key = this.buildInvoiceKey(userId, invoiceId); - return this.get(key, "invoice"); + return this.get(key); } /** @@ -119,7 +119,7 @@ export class WhmcsCacheService { */ async setInvoice(userId: string, invoiceId: number, data: Invoice): Promise { const key = this.buildInvoiceKey(userId, invoiceId); - await this.set(key, data, "invoice", [`user:${userId}`, `invoice:${invoiceId}`]); + await this.set(key, data, "invoice"); } /** @@ -127,7 +127,7 @@ export class WhmcsCacheService { */ async getSubscriptionsList(userId: string): Promise { const key = this.buildSubscriptionsKey(userId); - return this.get(key, "subscriptions"); + return this.get(key); } /** @@ -135,7 +135,7 @@ export class WhmcsCacheService { */ async setSubscriptionsList(userId: string, data: SubscriptionList): Promise { const key = this.buildSubscriptionsKey(userId); - await this.set(key, data, "subscriptions", [`user:${userId}`]); + await this.set(key, data, "subscriptions"); } /** @@ -143,7 +143,7 @@ export class WhmcsCacheService { */ async getSubscription(userId: string, subscriptionId: number): Promise { const key = this.buildSubscriptionKey(userId, subscriptionId); - return this.get(key, "subscription"); + return this.get(key); } /** @@ -151,7 +151,7 @@ export class WhmcsCacheService { */ async setSubscription(userId: string, subscriptionId: number, data: Subscription): Promise { const key = this.buildSubscriptionKey(userId, subscriptionId); - await this.set(key, data, "subscription", [`user:${userId}`, `subscription:${subscriptionId}`]); + await this.set(key, data, "subscription"); } /** @@ -164,7 +164,7 @@ export class WhmcsCacheService { limit: number ): Promise { const key = this.buildSubscriptionInvoicesKey(userId, subscriptionId, page, limit); - return this.get(key, "subscriptionInvoices"); + return this.get(key); } /** @@ -178,10 +178,7 @@ export class WhmcsCacheService { data: InvoiceList ): Promise { const key = this.buildSubscriptionInvoicesKey(userId, subscriptionId, page, limit); - await this.set(key, data, "subscriptionInvoices", [ - `user:${userId}`, - `subscription:${subscriptionId}`, - ]); + await this.set(key, data, "subscriptionInvoices"); } /** @@ -190,7 +187,7 @@ export class WhmcsCacheService { */ async getClientData(clientId: number): Promise { const key = this.buildClientKey(clientId); - return this.get(key, "client"); + return this.get(key); } /** @@ -198,7 +195,7 @@ export class WhmcsCacheService { */ async setClientData(clientId: number, data: WhmcsClient) { const key = this.buildClientKey(clientId); - await this.set(key, data, "client", [`client:${clientId}`]); + await this.set(key, data, "client"); } /** @@ -206,7 +203,7 @@ export class WhmcsCacheService { */ async getClientIdByEmail(email: string): Promise { const key = this.buildClientEmailKey(email); - return this.get(key, "clientEmail"); + return this.get(key); } /** @@ -324,7 +321,7 @@ export class WhmcsCacheService { */ async getPaymentMethods(userId: string): Promise { const key = this.buildPaymentMethodsKey(userId); - return this.get(key, "paymentMethods"); + return this.get(key); } /** @@ -332,7 +329,7 @@ export class WhmcsCacheService { */ async setPaymentMethods(userId: string, paymentMethods: PaymentMethodList): Promise { const key = this.buildPaymentMethodsKey(userId); - await this.set(key, paymentMethods, "paymentMethods", [userId]); + await this.set(key, paymentMethods, "paymentMethods"); } /** @@ -340,7 +337,7 @@ export class WhmcsCacheService { */ async getPaymentGateways(): Promise { const key = "whmcs:paymentgateways:global"; - return this.get(key, "paymentGateways"); + return this.get(key); } /** @@ -380,7 +377,7 @@ export class WhmcsCacheService { /** * Generic get method with configuration */ - private async get(key: string, _configKey: string): Promise { + private async get(key: string): Promise { try { const data = await this.cacheService.get(key); if (data) { @@ -396,14 +393,9 @@ export class WhmcsCacheService { /** * Generic set method with configuration */ - private async set( - key: string, - data: T, - _configKey: string, - _additionalTags: string[] = [] - ): Promise { + private async set(key: string, data: T, configKey: string): Promise { try { - const config = this.cacheConfigs[_configKey]; + const config = this.cacheConfigs[configKey]; await this.cacheService.set(key, data, config.ttl); this.logger.debug(`Cache set: ${key} (TTL: ${config.ttl}s)`); } catch (error) { diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts index c8bf171a..220436b5 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts @@ -5,7 +5,7 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { WhmcsConfigService } from "../config/whmcs-config.service.js"; import { WhmcsHttpClientService } from "./whmcs-http-client.service.js"; import { WhmcsErrorHandlerService } from "./whmcs-error-handler.service.js"; -import { WhmcsRequestQueueService } from "@bff/core/queue/services/whmcs-request-queue.service.js"; +import { WhmcsRequestQueueService } from "@bff/infra/queue/services/whmcs-request-queue.service.js"; import type { WhmcsAddClientParams, WhmcsValidateLoginParams, @@ -104,7 +104,7 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit { } // Handle general request errors - this.errorHandler.handleRequestError(error, action, params); + this.errorHandler.handleRequestError(error); } }, { @@ -139,7 +139,7 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit { if (this.isHandledException(error)) { throw error; } - this.errorHandler.handleRequestError(error, action, params); + this.errorHandler.handleRequestError(error); } }); } diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts index 1f2ac048..67a9787b 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts @@ -28,7 +28,7 @@ export class WhmcsErrorHandlerService { /** * Handle general request errors (network, timeout, etc.) */ - handleRequestError(error: unknown, _action: string, _params: Record): never { + handleRequestError(error: unknown): never { if (this.isTimeoutError(error)) { throw new DomainHttpException(ErrorCode.TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-account-discovery.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-account-discovery.service.ts index d56b6330..9bcb2819 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-account-discovery.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-account-discovery.service.ts @@ -3,7 +3,7 @@ import { Logger } from "nestjs-pino"; import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsCacheService } from "../cache/whmcs-cache.service.js"; -import * as CustomerProviders from "@customer-portal/domain/customer/providers"; +import { transformWhmcsClientResponse } from "@customer-portal/domain/customer/providers"; import type { WhmcsClient } from "@customer-portal/domain/customer"; /** @@ -43,7 +43,7 @@ export class WhmcsAccountDiscoveryService { return null; } - const client = CustomerProviders.Whmcs.transformWhmcsClientResponse(response); + const client = transformWhmcsClientResponse(response); // 3. Cache both the data and the mapping await Promise.all([ @@ -86,7 +86,7 @@ export class WhmcsAccountDiscoveryService { throw new NotFoundException(`Client ${clientId} not found`); } - const client = CustomerProviders.Whmcs.transformWhmcsClientResponse(response); + const client = transformWhmcsClientResponse(response); await this.cacheService.setClientData(client.id, client); return client; } diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts index 7ddd77d6..08856ad9 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts @@ -10,7 +10,7 @@ import type { WhmcsAddClientResponse, WhmcsValidateLoginResponse, } from "@customer-portal/domain/customer/providers"; -import * as CustomerProviders from "@customer-portal/domain/customer/providers"; +import { transformWhmcsClientResponse } from "@customer-portal/domain/customer/providers"; import type { WhmcsClient } from "@customer-portal/domain/customer"; @Injectable() @@ -73,7 +73,7 @@ export class WhmcsClientService { throw new NotFoundException(`Client ${clientId} not found`); } - const client = CustomerProviders.Whmcs.transformWhmcsClientResponse(response); + const client = transformWhmcsClientResponse(response); await this.cacheService.setClientData(client.id, client); this.logger.log(`Fetched client details for client ${clientId}`); diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts index ba4cfafc..98583458 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts @@ -4,7 +4,7 @@ import { Injectable, NotFoundException, Inject } from "@nestjs/common"; import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js"; import { invoiceListSchema, invoiceSchema } from "@customer-portal/domain/billing"; import type { Invoice, InvoiceList } from "@customer-portal/domain/billing"; -import * as Providers from "@customer-portal/domain/billing/providers"; +import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsCurrencyService } from "./whmcs-currency.service.js"; import { WhmcsCacheService } from "../cache/whmcs-cache.service.js"; @@ -165,7 +165,7 @@ export class WhmcsInvoiceService { // Transform invoice using domain mapper const defaultCurrency = this.currencyService.getDefaultCurrency(); - const invoice = Providers.Whmcs.transformWhmcsInvoice(response, { + const invoice = transformWhmcsInvoice(response, { defaultCurrencyCode: defaultCurrency.code, defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix, }); @@ -224,7 +224,7 @@ export class WhmcsInvoiceService { try { // Transform using domain mapper const defaultCurrency = this.currencyService.getDefaultCurrency(); - const transformed = Providers.Whmcs.transformWhmcsInvoice(whmcsInvoice, { + const transformed = transformWhmcsInvoice(whmcsInvoice, { defaultCurrencyCode: defaultCurrency.code, defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix, }); diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-order.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-order.service.ts index 69e8f03b..59895167 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-order.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-order.service.ts @@ -10,7 +10,7 @@ import type { WhmcsAddOrderResponse, WhmcsOrderResult, } from "@customer-portal/domain/orders/providers"; -import * as Providers from "@customer-portal/domain/orders/providers"; +import { buildWhmcsAddOrderPayload } from "@customer-portal/domain/orders/providers"; import { whmcsAddOrderResponseSchema, whmcsAcceptOrderResponseSchema, @@ -226,7 +226,7 @@ export class WhmcsOrderService { * Delegates to shared mapper function from integration package */ private buildAddOrderPayload(params: WhmcsAddOrderParams): Record { - const payload = Providers.Whmcs.buildWhmcsAddOrderPayload(params); + const payload = buildWhmcsAddOrderPayload(params); this.logger.debug("Built WHMCS AddOrder payload", { clientId: params.clientId, diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-payment.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-payment.service.ts index 3c4dba12..52f264ff 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-payment.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-payment.service.ts @@ -1,7 +1,10 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { Logger } from "nestjs-pino"; import { Injectable, Inject } from "@nestjs/common"; -import * as Providers from "@customer-portal/domain/payments/providers"; +import { + transformWhmcsPaymentGateway, + transformWhmcsPaymentMethod, +} from "@customer-portal/domain/payments/providers"; import type { PaymentMethodList, PaymentGateway, @@ -9,7 +12,7 @@ import type { PaymentMethod, } from "@customer-portal/domain/payments"; import type { WhmcsCatalogProductNormalized } from "@customer-portal/domain/services/providers"; -import * as CatalogProviders from "@customer-portal/domain/services/providers"; +import { transformWhmcsCatalogProductsResponse } from "@customer-portal/domain/services/providers"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsCacheService } from "../cache/whmcs-cache.service.js"; import type { WhmcsCreateSsoTokenParams } from "@customer-portal/domain/customer/providers"; @@ -61,7 +64,7 @@ export class WhmcsPaymentService { let methods = paymentMethodsArray .map((pm: WhmcsPaymentMethod) => { try { - return Providers.Whmcs.transformWhmcsPaymentMethod(pm); + return transformWhmcsPaymentMethod(pm); } catch (error) { this.logger.error(`Failed to transform payment method`, { error: getErrorMessage(error), @@ -136,7 +139,7 @@ export class WhmcsPaymentService { const gateways = response.gateways.gateway .map((whmcsGateway: WhmcsPaymentGateway) => { try { - return Providers.Whmcs.transformWhmcsPaymentGateway(whmcsGateway); + return transformWhmcsPaymentGateway(whmcsGateway); } catch (error) { this.logger.error(`Failed to transform payment gateway ${whmcsGateway.name}`, { error: getErrorMessage(error), @@ -225,7 +228,7 @@ export class WhmcsPaymentService { async getProducts(): Promise { try { const response = await this.connectionService.getCatalogProducts(); - return CatalogProviders.Whmcs.transformWhmcsCatalogProductsResponse(response); + return transformWhmcsCatalogProductsResponse(response); } catch (error) { this.logger.error("Failed to get products", { error: getErrorMessage(error), diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts index 652169a4..11e1aa01 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts @@ -2,7 +2,10 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { Logger } from "nestjs-pino"; import { Injectable, NotFoundException, Inject } from "@nestjs/common"; import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js"; -import * as Providers from "@customer-portal/domain/subscriptions/providers"; +import { + filterSubscriptionsByStatus, + transformWhmcsSubscriptionListResponse, +} from "@customer-portal/domain/subscriptions/providers"; import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsCurrencyService } from "./whmcs-currency.service.js"; @@ -38,7 +41,7 @@ export class WhmcsSubscriptionService { // Apply status filter if needed if (filters.status) { - return Providers.Whmcs.filterSubscriptionsByStatus(cached, filters.status); + return filterSubscriptionsByStatus(cached, filters.status); } return cached; @@ -65,7 +68,7 @@ export class WhmcsSubscriptionService { const defaultCurrency = this.currencyService.getDefaultCurrency(); let result: SubscriptionList; try { - result = Providers.Whmcs.transformWhmcsSubscriptionListResponse(rawResponse, { + result = transformWhmcsSubscriptionListResponse(rawResponse, { defaultCurrencyCode: defaultCurrency.code, defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix, onItemError: (error, product) => { @@ -92,7 +95,7 @@ export class WhmcsSubscriptionService { // Apply status filter if needed if (filters.status) { - return Providers.Whmcs.filterSubscriptionsByStatus(result, filters.status); + return filterSubscriptionsByStatus(result, filters.status); } return result; @@ -151,7 +154,7 @@ export class WhmcsSubscriptionService { // Transform response const defaultCurrency = this.currencyService.getDefaultCurrency(); - const resultList = Providers.Whmcs.transformWhmcsSubscriptionListResponse(rawResponse, { + const resultList = transformWhmcsSubscriptionListResponse(rawResponse, { defaultCurrencyCode: defaultCurrency.code, defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix, }); diff --git a/apps/bff/src/integrations/whmcs/whmcs.module.ts b/apps/bff/src/integrations/whmcs/whmcs.module.ts index da982bd6..c1bf4b0e 100644 --- a/apps/bff/src/integrations/whmcs/whmcs.module.ts +++ b/apps/bff/src/integrations/whmcs/whmcs.module.ts @@ -1,6 +1,6 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; -import { QueueModule } from "@bff/core/queue/queue.module.js"; +import { QueueModule } from "@bff/infra/queue/queue.module.js"; import { WhmcsCacheService } from "./cache/whmcs-cache.service.js"; import { WhmcsService } from "./whmcs.service.js"; import { WhmcsInvoiceService } from "./services/whmcs-invoice.service.js"; diff --git a/apps/bff/src/integrations/whmcs/whmcs.service.ts b/apps/bff/src/integrations/whmcs/whmcs.service.ts index 2ffe0028..63cd078a 100644 --- a/apps/bff/src/integrations/whmcs/whmcs.service.ts +++ b/apps/bff/src/integrations/whmcs/whmcs.service.ts @@ -3,7 +3,7 @@ import type { Invoice, InvoiceList } from "@customer-portal/domain/billing"; import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions"; import type { PaymentMethodList, PaymentGatewayList } from "@customer-portal/domain/payments"; import { addressSchema, type Address, type WhmcsClient } from "@customer-portal/domain/customer"; -import * as CustomerProviders from "@customer-portal/domain/customer/providers"; +import { prepareWhmcsClientAddressUpdate } from "@customer-portal/domain/customer/providers"; import { WhmcsConnectionOrchestratorService } from "./connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsInvoiceService } from "./services/whmcs-invoice.service.js"; import type { InvoiceFilters } from "./services/whmcs-invoice.service.js"; @@ -151,7 +151,8 @@ export class WhmcsService { } async updateClientAddress(clientId: number, address: Partial
): Promise { - const updateData = CustomerProviders.Whmcs.prepareWhmcsClientAddressUpdate(address); + const parsed = addressSchema.partial().parse(address ?? {}); + const updateData = prepareWhmcsClientAddressUpdate(parsed); if (Object.keys(updateData).length === 0) return; await this.clientService.updateClient(clientId, updateData); } diff --git a/apps/bff/src/modules/auth/infra/token/token-blacklist.service.ts b/apps/bff/src/modules/auth/infra/token/token-blacklist.service.ts index b739996c..97488594 100644 --- a/apps/bff/src/modules/auth/infra/token/token-blacklist.service.ts +++ b/apps/bff/src/modules/auth/infra/token/token-blacklist.service.ts @@ -19,7 +19,7 @@ export class TokenBlacklistService { this.failClosed = this.configService.get("AUTH_BLACKLIST_FAIL_CLOSED", "false") === "true"; } - async blacklistToken(token: string, _expiresIn?: number): Promise { + async blacklistToken(token: string): Promise { // Validate token format first if (!token || typeof token !== "string" || token.split(".").length !== 3) { this.logger.warn("Invalid token format provided for blacklisting"); diff --git a/apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.ts index c758874a..9ab0c140 100644 --- a/apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.ts @@ -27,9 +27,8 @@ import { type ValidateSignupRequest, } from "@customer-portal/domain/auth"; import { ErrorCode } from "@customer-portal/domain/common"; -import * as CustomerProviders from "@customer-portal/domain/customer/providers"; +import { serializeWhmcsKeyValueMap } from "@customer-portal/domain/customer/providers"; import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js"; -import type { User as PrismaUser } from "@prisma/client"; import { CacheService } from "@bff/infra/cache/cache.service.js"; import { PORTAL_SOURCE_NEW_SIGNUP, @@ -38,11 +37,6 @@ import { } from "@bff/modules/auth/constants/portal.constants.js"; import type { AuthResultInternal } from "@bff/modules/auth/auth.types.js"; -type _SanitizedPrismaUser = Omit< - PrismaUser, - "passwordHash" | "failedLoginAttempts" | "lockedUntil" ->; - interface SignupAccountSnapshot { id: string; Name?: string | null; @@ -358,8 +352,7 @@ export class SignupWorkflowService { postcode: address.postcode, country: address.country, password2: password, - customfields: - CustomerProviders.Whmcs.serializeWhmcsKeyValueMap(customfieldsMap) || undefined, + customfields: serializeWhmcsKeyValueMap(customfieldsMap) || undefined, }); this.logger.log("WHMCS client created successfully", { diff --git a/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts index ddf185b8..71bcc1af 100644 --- a/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts @@ -14,7 +14,7 @@ import { WhmcsAccountDiscoveryService } from "@bff/integrations/whmcs/services/w import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js"; -import * as CustomerProviders from "@customer-portal/domain/customer/providers"; +import { getCustomFieldValue } from "@customer-portal/domain/customer/providers"; import type { User } from "@customer-portal/domain/customer"; import { PORTAL_SOURCE_MIGRATED, @@ -115,11 +115,8 @@ export class WhmcsLinkWorkflowService { } const customerNumber = - CustomerProviders.Whmcs.getCustomFieldValue(clientDetails.customfields, "198")?.trim() ?? - CustomerProviders.Whmcs.getCustomFieldValue( - clientDetails.customfields, - "Customer Number" - )?.trim(); + getCustomFieldValue(clientDetails.customfields, "198")?.trim() ?? + getCustomFieldValue(clientDetails.customfields, "Customer Number")?.trim(); if (!customerNumber) { throw new BadRequestException( diff --git a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts index 841223d6..1410ef9c 100644 --- a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts +++ b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts @@ -231,7 +231,7 @@ export class AuthController { @UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard) @RateLimit({ limit: 5, ttl: 600 }) // 5 attempts per 10 minutes per IP (industry standard) @ZodResponse({ status: 200, description: "Migrate/link account", type: LinkWhmcsResponseDto }) - async migrateAccount(@Body() linkData: LinkWhmcsRequestDto, @Req() _req: Request) { + async migrateAccount(@Body() linkData: LinkWhmcsRequestDto) { const result = await this.authFacade.linkWhmcsUser(linkData); return result; } diff --git a/apps/bff/src/modules/invoices/invoices.controller.ts b/apps/bff/src/modules/billing/billing.controller.ts similarity index 97% rename from apps/bff/src/modules/invoices/invoices.controller.ts rename to apps/bff/src/modules/billing/billing.controller.ts index 0a5b6eb7..cb8f067e 100644 --- a/apps/bff/src/modules/invoices/invoices.controller.ts +++ b/apps/bff/src/modules/billing/billing.controller.ts @@ -39,13 +39,13 @@ class PaymentGatewayListDto extends createZodDto(paymentGatewayListSchema) {} class InvoicePaymentLinkDto extends createZodDto(invoicePaymentLinkSchema) {} /** - * Invoice Controller + * Billing Controller * * All request validation is handled by Zod schemas via global ZodValidationPipe. * Business logic is delegated to service layer. */ @Controller("invoices") -export class InvoicesController { +export class BillingController { constructor( private readonly invoicesService: InvoicesOrchestratorService, private readonly whmcsService: WhmcsService, @@ -102,10 +102,7 @@ export class InvoicesController { } @Get(":id/subscriptions") - getInvoiceSubscriptions( - @Request() _req: RequestWithUser, - @Param() _params: InvoiceIdParamDto - ): Subscription[] { + getInvoiceSubscriptions(): Subscription[] { // This functionality has been moved to WHMCS directly // For now, return empty array as subscriptions are managed in WHMCS return []; diff --git a/apps/bff/src/modules/invoices/invoices.module.ts b/apps/bff/src/modules/billing/billing.module.ts similarity index 84% rename from apps/bff/src/modules/invoices/invoices.module.ts rename to apps/bff/src/modules/billing/billing.module.ts index 9090feb5..ea447a67 100644 --- a/apps/bff/src/modules/invoices/invoices.module.ts +++ b/apps/bff/src/modules/billing/billing.module.ts @@ -1,5 +1,5 @@ import { Module } from "@nestjs/common"; -import { InvoicesController } from "./invoices.controller.js"; +import { BillingController } from "./billing.controller.js"; import { WhmcsModule } from "@bff/integrations/whmcs/whmcs.module.js"; import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js"; // New modular invoice services @@ -8,15 +8,15 @@ import { InvoiceRetrievalService } from "./services/invoice-retrieval.service.js import { InvoiceHealthService } from "./services/invoice-health.service.js"; /** - * Invoice Module + * Billing Module * * Validation is handled by Zod schemas via Zod DTOs + the global ZodValidationPipe (APP_PIPE). * No separate validator service needed. */ @Module({ imports: [WhmcsModule, MappingsModule], - controllers: [InvoicesController], + controllers: [BillingController], providers: [InvoicesOrchestratorService, InvoiceRetrievalService, InvoiceHealthService], exports: [InvoicesOrchestratorService], }) -export class InvoicesModule {} +export class BillingModule {} diff --git a/apps/bff/src/modules/invoices/index.ts b/apps/bff/src/modules/billing/index.ts similarity index 76% rename from apps/bff/src/modules/invoices/index.ts rename to apps/bff/src/modules/billing/index.ts index 253c1708..800a39bb 100644 --- a/apps/bff/src/modules/invoices/index.ts +++ b/apps/bff/src/modules/billing/index.ts @@ -1,9 +1,9 @@ /** - * Invoice Module Exports + * Billing Module Exports */ -export * from "./invoices.module.js"; -export * from "./invoices.controller.js"; +export * from "./billing.module.js"; +export * from "./billing.controller.js"; export * from "./services/invoices-orchestrator.service.js"; export * from "./services/invoice-retrieval.service.js"; export * from "./services/invoice-health.service.js"; diff --git a/apps/bff/src/modules/invoices/services/invoice-health.service.ts b/apps/bff/src/modules/billing/services/invoice-health.service.ts similarity index 100% rename from apps/bff/src/modules/invoices/services/invoice-health.service.ts rename to apps/bff/src/modules/billing/services/invoice-health.service.ts diff --git a/apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts b/apps/bff/src/modules/billing/services/invoice-retrieval.service.ts similarity index 100% rename from apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts rename to apps/bff/src/modules/billing/services/invoice-retrieval.service.ts diff --git a/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts b/apps/bff/src/modules/billing/services/invoices-orchestrator.service.ts similarity index 100% rename from apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts rename to apps/bff/src/modules/billing/services/invoices-orchestrator.service.ts diff --git a/apps/bff/src/modules/invoices/types/invoice-monitoring.types.ts b/apps/bff/src/modules/billing/types/invoice-monitoring.types.ts similarity index 100% rename from apps/bff/src/modules/invoices/types/invoice-monitoring.types.ts rename to apps/bff/src/modules/billing/types/invoice-monitoring.types.ts diff --git a/apps/bff/src/modules/health/health.module.ts b/apps/bff/src/modules/health/health.module.ts index 4258b33f..ae5d4721 100644 --- a/apps/bff/src/modules/health/health.module.ts +++ b/apps/bff/src/modules/health/health.module.ts @@ -2,7 +2,7 @@ import { Module } from "@nestjs/common"; import { HealthController } from "./health.controller.js"; import { PrismaModule } from "@bff/infra/database/prisma.module.js"; import { ConfigModule } from "@nestjs/config"; -import { QueueModule } from "@bff/core/queue/queue.module.js"; +import { QueueModule } from "@bff/infra/queue/queue.module.js"; import { QueueHealthController } from "@bff/core/health/queue-health.controller.js"; @Module({ diff --git a/apps/bff/src/modules/id-mappings/cache/mapping-cache.service.ts b/apps/bff/src/modules/id-mappings/cache/mapping-cache.service.ts index 643e1511..50dc30cc 100644 --- a/apps/bff/src/modules/id-mappings/cache/mapping-cache.service.ts +++ b/apps/bff/src/modules/id-mappings/cache/mapping-cache.service.ts @@ -1,7 +1,7 @@ import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { CacheService } from "@bff/infra/cache/cache.service.js"; -import type { UserIdMapping } from "@customer-portal/domain/mappings"; +import type { UserIdMapping } from "../domain/index.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js"; @Injectable() diff --git a/packages/domain/mappings/contract.ts b/apps/bff/src/modules/id-mappings/domain/contract.ts similarity index 90% rename from packages/domain/mappings/contract.ts rename to apps/bff/src/modules/id-mappings/domain/contract.ts index 54cf8089..76251711 100644 --- a/packages/domain/mappings/contract.ts +++ b/apps/bff/src/modules/id-mappings/domain/contract.ts @@ -4,7 +4,7 @@ * Normalized types for mapping portal users to external systems. */ -import type { IsoDateTimeString } from "../common/types.js"; +import type { IsoDateTimeString } from "@customer-portal/domain/common"; export interface UserIdMapping { id: string; diff --git a/packages/domain/mappings/index.ts b/apps/bff/src/modules/id-mappings/domain/index.ts similarity index 59% rename from packages/domain/mappings/index.ts rename to apps/bff/src/modules/id-mappings/domain/index.ts index 85ace165..6128faff 100644 --- a/packages/domain/mappings/index.ts +++ b/apps/bff/src/modules/id-mappings/domain/index.ts @@ -1,5 +1,7 @@ /** * ID Mapping Domain + * + * Types, schemas, and validation for mapping portal users to external systems. */ export * from "./contract.js"; diff --git a/packages/domain/mappings/schema.ts b/apps/bff/src/modules/id-mappings/domain/schema.ts similarity index 100% rename from packages/domain/mappings/schema.ts rename to apps/bff/src/modules/id-mappings/domain/schema.ts diff --git a/packages/domain/mappings/validation.ts b/apps/bff/src/modules/id-mappings/domain/validation.ts similarity index 100% rename from packages/domain/mappings/validation.ts rename to apps/bff/src/modules/id-mappings/domain/validation.ts diff --git a/apps/bff/src/modules/id-mappings/mappings.service.ts b/apps/bff/src/modules/id-mappings/mappings.service.ts index 9aa30016..9414e2b6 100644 --- a/apps/bff/src/modules/id-mappings/mappings.service.ts +++ b/apps/bff/src/modules/id-mappings/mappings.service.ts @@ -16,7 +16,7 @@ import type { UpdateMappingRequest, MappingSearchFilters, MappingStats, -} from "@customer-portal/domain/mappings"; +} from "./domain/index.js"; import { createMappingRequestSchema, updateMappingRequestSchema, @@ -25,7 +25,7 @@ import { checkMappingCompleteness, sanitizeCreateRequest, sanitizeUpdateRequest, -} from "@customer-portal/domain/mappings"; +} from "./domain/index.js"; import type { Prisma, IdMapping as PrismaIdMapping } from "@prisma/client"; import { mapPrismaMappingToDomain } from "@bff/infra/mappers/index.js"; diff --git a/apps/bff/src/modules/id-mappings/types/mapping.types.ts b/apps/bff/src/modules/id-mappings/types/mapping.types.ts index 85969d28..9e0ab407 100644 --- a/apps/bff/src/modules/id-mappings/types/mapping.types.ts +++ b/apps/bff/src/modules/id-mappings/types/mapping.types.ts @@ -1,4 +1,4 @@ -import type { UserIdMapping } from "@customer-portal/domain/mappings"; +import type { UserIdMapping, MappingValidationResult } from "../domain/index.js"; /** * BFF-specific mapping types @@ -18,4 +18,4 @@ export interface CachedMapping { } // Re-export validation result from domain for backward compatibility -export type { MappingValidationResult } from "@customer-portal/domain/mappings"; +export type { MappingValidationResult }; diff --git a/apps/bff/src/modules/orders/config/order-field-map.service.ts b/apps/bff/src/modules/orders/config/order-field-map.service.ts index df1c8406..af47671d 100644 --- a/apps/bff/src/modules/orders/config/order-field-map.service.ts +++ b/apps/bff/src/modules/orders/config/order-field-map.service.ts @@ -7,7 +7,7 @@ import { type SalesforceOrderFieldMap, } from "@customer-portal/domain/orders/providers"; -const unique = (values: T[]): T[] => Array.from(new Set(values)); +const unique = (values: string[]): string[] => Array.from(new Set(values)); const SECTION_PREFIX: Record = { order: "ORDER", diff --git a/apps/bff/src/modules/orders/orders.module.ts b/apps/bff/src/modules/orders/orders.module.ts index 74a4d28a..8af709d5 100644 --- a/apps/bff/src/modules/orders/orders.module.ts +++ b/apps/bff/src/modules/orders/orders.module.ts @@ -5,7 +5,6 @@ import { IntegrationsModule } from "@bff/integrations/integrations.module.js"; import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js"; import { UsersModule } from "@bff/modules/users/users.module.js"; import { CoreConfigModule } from "@bff/core/config/config.module.js"; -import { DatabaseModule } from "@bff/core/database/database.module.js"; import { ServicesModule } from "@bff/modules/services/services.module.js"; import { CacheModule } from "@bff/infra/cache/cache.module.js"; import { VerificationModule } from "@bff/modules/verification/verification.module.js"; @@ -38,7 +37,6 @@ import { OrderFieldConfigModule } from "./config/order-field-config.module.js"; MappingsModule, UsersModule, CoreConfigModule, - DatabaseModule, ServicesModule, CacheModule, VerificationModule, diff --git a/apps/bff/src/modules/orders/services/order-builder.service.ts b/apps/bff/src/modules/orders/services/order-builder.service.ts index 1d76d239..beba568b 100644 --- a/apps/bff/src/modules/orders/services/order-builder.service.ts +++ b/apps/bff/src/modules/orders/services/order-builder.service.ts @@ -49,7 +49,7 @@ export class OrderBuilder { this.addSimFields(orderFields, body, orderFieldNames); break; case "VPN": - this.addVpnFields(orderFields, body); + this.addVpnFields(); break; } @@ -111,10 +111,7 @@ export class OrderBuilder { } } - private addVpnFields( - _orderFields: Record, - _body: OrderBusinessValidation - ): void { + private addVpnFields(): void { // No additional fields for VPN orders at this time. } diff --git a/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts b/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts index 3b0962f8..284d080e 100644 --- a/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts +++ b/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts @@ -8,7 +8,7 @@ import { OrderOrchestrator } from "./order-orchestrator.service.js"; import { OrderFulfillmentValidator } from "./order-fulfillment-validator.service.js"; import { OrderFulfillmentErrorService } from "./order-fulfillment-error.service.js"; import { SimFulfillmentService } from "./sim-fulfillment.service.js"; -import { DistributedTransactionService } from "@bff/core/database/services/distributed-transaction.service.js"; +import { DistributedTransactionService } from "@bff/infra/database/services/distributed-transaction.service.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { OrderEventsService } from "./order-events.service.js"; import { OrdersCacheService } from "./orders-cache.service.js"; @@ -16,7 +16,7 @@ import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; import { NotificationService } from "@bff/modules/notifications/notifications.service.js"; import type { OrderDetails } from "@customer-portal/domain/orders"; import type { OrderFulfillmentValidationResult } from "@customer-portal/domain/orders/providers"; -import * as OrderProviders from "@customer-portal/domain/orders/providers"; +import { createOrderNotes, mapOrderToWhmcsItems } from "@customer-portal/domain/orders/providers"; import { OPPORTUNITY_STAGE } from "@customer-portal/domain/opportunity"; import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications"; import { salesforceAccountIdSchema } from "@customer-portal/domain/common"; @@ -26,7 +26,7 @@ import { WhmcsOperationException, } from "@bff/core/exceptions/domain-exceptions.js"; -type WhmcsOrderItemMappingResult = ReturnType; +type WhmcsOrderItemMappingResult = ReturnType; export interface OrderFulfillmentStep { step: string; @@ -210,7 +210,7 @@ export class OrderFulfillmentOrchestrator { return Promise.reject(new Error("Order details are required for mapping")); } // Use domain mapper directly - single transformation! - const result = OrderProviders.Whmcs.mapOrderToWhmcsItems(context.orderDetails); + const result = mapOrderToWhmcsItems(context.orderDetails); mappingResult = result; this.logger.log("OrderItems mapped to WHMCS", { @@ -240,7 +240,7 @@ export class OrderFulfillmentOrchestrator { }); } - const orderNotes = OrderProviders.Whmcs.createOrderNotes( + const orderNotes = createOrderNotes( sfOrderId, `Provisioned from Salesforce Order ${sfOrderId}` ); diff --git a/apps/bff/src/modules/orders/services/order-validator.service.ts b/apps/bff/src/modules/orders/services/order-validator.service.ts index 50d15be4..78457c53 100644 --- a/apps/bff/src/modules/orders/services/order-validator.service.ts +++ b/apps/bff/src/modules/orders/services/order-validator.service.ts @@ -11,7 +11,7 @@ import { } from "@customer-portal/domain/orders"; import type * as Providers from "@customer-portal/domain/subscriptions/providers"; -type WhmcsProduct = Providers.WhmcsRaw.WhmcsProductRaw; +type WhmcsProduct = Providers.WhmcsProductRaw; import { SimServicesService } from "@bff/modules/services/services/sim-services.service.js"; import { InternetServicesService } from "@bff/modules/services/services/internet-services.service.js"; import { OrderPricebookService, type PricebookProductMeta } from "./order-pricebook.service.js"; @@ -227,7 +227,7 @@ export class OrderValidator { // 3. SKU validation const pricebookId = await this.pricebookService.findPortalPricebookId(); - const _productMeta = await this.validateSKUs(businessValidatedBody.skus, pricebookId); + await this.validateSKUs(businessValidatedBody.skus, pricebookId); if (businessValidatedBody.orderType === "SIM") { const verification = await this.residenceCards.getStatusForUser(userId); diff --git a/apps/bff/src/modules/services/account-services.controller.ts b/apps/bff/src/modules/services/account-services.controller.ts index 5101e8be..9c43147f 100644 --- a/apps/bff/src/modules/services/account-services.controller.ts +++ b/apps/bff/src/modules/services/account-services.controller.ts @@ -54,7 +54,7 @@ export class AccountServicesController { @Get("vpn/plans") @RateLimit({ limit: 60, ttl: 60 }) @Header("Cache-Control", "private, no-store") - async getVpnCatalogForAccount(@Request() _req: RequestWithUser): Promise { + async getVpnCatalogForAccount(): Promise { const catalog = await this.vpnCatalog.getCatalogData(); return parseVpnCatalog(catalog); } diff --git a/apps/bff/src/modules/services/services.module.ts b/apps/bff/src/modules/services/services.module.ts index a9db1775..57a5f689 100644 --- a/apps/bff/src/modules/services/services.module.ts +++ b/apps/bff/src/modules/services/services.module.ts @@ -8,7 +8,7 @@ import { IntegrationsModule } from "@bff/integrations/integrations.module.js"; import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js"; import { CoreConfigModule } from "@bff/core/config/config.module.js"; import { CacheModule } from "@bff/infra/cache/cache.module.js"; -import { QueueModule } from "@bff/core/queue/queue.module.js"; +import { QueueModule } from "@bff/infra/queue/queue.module.js"; import { BaseServicesService } from "./services/base-services.service.js"; import { InternetServicesService } from "./services/internet-services.service.js"; diff --git a/apps/bff/src/modules/services/services/base-services.service.ts b/apps/bff/src/modules/services/services/base-services.service.ts index 45908ed2..61a00efd 100644 --- a/apps/bff/src/modules/services/services/base-services.service.ts +++ b/apps/bff/src/modules/services/services/base-services.service.ts @@ -15,7 +15,7 @@ import type { SalesforcePricebookEntryRecord, SalesforceProduct2WithPricebookEntries, } from "@customer-portal/domain/services/providers"; -import * as CatalogProviders from "@customer-portal/domain/services/providers"; +import { extractPricebookEntry as extractSalesforcePricebookEntry } from "@customer-portal/domain/services/providers"; import type { SalesforceResponse } from "@customer-portal/domain/common/providers"; @Injectable() @@ -55,7 +55,7 @@ export class BaseServicesService { protected extractPricebookEntry( record: SalesforceProduct2WithPricebookEntries ): SalesforcePricebookEntryRecord | undefined { - const entry = CatalogProviders.Salesforce.extractPricebookEntry(record); + const entry = extractSalesforcePricebookEntry(record); if (!entry) { const sku = record.StockKeepingUnit ?? undefined; this.logger.warn( diff --git a/apps/bff/src/modules/services/services/internet-services.service.ts b/apps/bff/src/modules/services/services/internet-services.service.ts index bc80f0f8..275cb50f 100644 --- a/apps/bff/src/modules/services/services/internet-services.service.ts +++ b/apps/bff/src/modules/services/services/internet-services.service.ts @@ -16,7 +16,11 @@ import { inferInstallationTermFromSku, internetEligibilityDetailsSchema, } from "@customer-portal/domain/services"; -import * as CatalogProviders from "@customer-portal/domain/services/providers"; +import { + mapInternetAddon, + mapInternetInstallation, + mapInternetPlan, +} from "@customer-portal/domain/services/providers"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js"; import { OpportunityResolutionService } from "@bff/integrations/salesforce/services/opportunity-resolution.service.js"; @@ -63,7 +67,7 @@ export class InternetServicesService extends BaseServicesService { const plans = records.map(record => { const entry = this.extractPricebookEntry(record); - const plan = CatalogProviders.Salesforce.mapInternetPlan(record, entry); + const plan = mapInternetPlan(record, entry); return enrichInternetPlanMetadata(plan); }); @@ -99,7 +103,7 @@ export class InternetServicesService extends BaseServicesService { return records .map(record => { const entry = this.extractPricebookEntry(record); - const installation = CatalogProviders.Salesforce.mapInternetInstallation(record, entry); + const installation = mapInternetInstallation(record, entry); return { ...installation, catalogMetadata: { @@ -140,7 +144,7 @@ export class InternetServicesService extends BaseServicesService { return records .map(record => { const entry = this.extractPricebookEntry(record); - const addon = CatalogProviders.Salesforce.mapInternetAddon(record, entry); + const addon = mapInternetAddon(record, entry); return { ...addon, catalogMetadata: { diff --git a/apps/bff/src/modules/services/services/sim-services.service.ts b/apps/bff/src/modules/services/services/sim-services.service.ts index 9cdc57b1..3dd5e4f0 100644 --- a/apps/bff/src/modules/services/services/sim-services.service.ts +++ b/apps/bff/src/modules/services/services/sim-services.service.ts @@ -7,7 +7,7 @@ import type { SimActivationFeeCatalogItem, } from "@customer-portal/domain/services"; import type { SalesforceProduct2WithPricebookEntries } from "@customer-portal/domain/services/providers"; -import * as CatalogProviders from "@customer-portal/domain/services/providers"; +import { mapSimActivationFee, mapSimProduct } from "@customer-portal/domain/services/providers"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js"; import { Logger } from "nestjs-pino"; @@ -45,7 +45,7 @@ export class SimServicesService extends BaseServicesService { return records.map(record => { const entry = this.extractPricebookEntry(record); - const product = CatalogProviders.Salesforce.mapSimProduct(record, entry); + const product = mapSimProduct(record, entry); return { ...product, @@ -76,7 +76,7 @@ export class SimServicesService extends BaseServicesService { const activationFees = records .map(record => { const entry = this.extractPricebookEntry(record); - return CatalogProviders.Salesforce.mapSimActivationFee(record, entry); + return mapSimActivationFee(record, entry); }) .sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)); @@ -130,7 +130,7 @@ export class SimServicesService extends BaseServicesService { return records .map(record => { const entry = this.extractPricebookEntry(record); - const product = CatalogProviders.Salesforce.mapSimProduct(record, entry); + const product = mapSimProduct(record, entry); return { ...product, diff --git a/apps/bff/src/modules/services/services/vpn-services.service.ts b/apps/bff/src/modules/services/services/vpn-services.service.ts index 531db7c5..b2bcb2b2 100644 --- a/apps/bff/src/modules/services/services/vpn-services.service.ts +++ b/apps/bff/src/modules/services/services/vpn-services.service.ts @@ -6,7 +6,7 @@ import { BaseServicesService } from "./base-services.service.js"; import { ServicesCacheService } from "./services-cache.service.js"; import type { VpnCatalogProduct } from "@customer-portal/domain/services"; import type { SalesforceProduct2WithPricebookEntries } from "@customer-portal/domain/services/providers"; -import * as CatalogProviders from "@customer-portal/domain/services/providers"; +import { mapVpnProduct } from "@customer-portal/domain/services/providers"; @Injectable() export class VpnServicesService extends BaseServicesService { @@ -32,7 +32,7 @@ export class VpnServicesService extends BaseServicesService { return records.map(record => { const entry = this.extractPricebookEntry(record); - const product = CatalogProviders.Salesforce.mapVpnProduct(record, entry); + const product = mapVpnProduct(record, entry); return { ...product, description: product.description || product.name, @@ -64,7 +64,7 @@ export class VpnServicesService extends BaseServicesService { return records.map(record => { const pricebookEntry = this.extractPricebookEntry(record); - const product = CatalogProviders.Salesforce.mapVpnProduct(record, pricebookEntry); + const product = mapVpnProduct(record, pricebookEntry); return { ...product, diff --git a/apps/bff/src/modules/subscriptions/sim-management/services/sim-call-history.service.ts b/apps/bff/src/modules/subscriptions/sim-management/services/sim-call-history.service.ts index de3df57b..abf4f709 100644 --- a/apps/bff/src/modules/subscriptions/sim-management/services/sim-call-history.service.ts +++ b/apps/bff/src/modules/subscriptions/sim-management/services/sim-call-history.service.ts @@ -191,7 +191,7 @@ export class SimCallHistoryService { continue; } - const [phoneNumber, dateStr, timeStr, sentTo, _callType, smsTypeStr] = columns; + const [phoneNumber, dateStr, timeStr, sentTo, , smsTypeStr] = columns; // Parse date const smsDate = this.parseDate(dateStr); diff --git a/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts b/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts index 782a031c..ae9c707d 100644 --- a/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts +++ b/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts @@ -107,13 +107,13 @@ export class SimValidationService { const expectedEid = "89049032000001000000043598005455"; const foundSimNumber = Object.entries(subscription.customFields || {}).find( - ([_key, value]) => + ([, value]) => value !== undefined && value !== null && this.formatCustomFieldValue(value).includes(expectedSimNumber) ); - const eidField = Object.entries(subscription.customFields || {}).find(([_key, value]) => { + const eidField = Object.entries(subscription.customFields || {}).find(([, value]) => { if (value === undefined || value === null) return false; return this.formatCustomFieldValue(value).includes(expectedEid); }); diff --git a/apps/bff/src/modules/subscriptions/subscriptions.service.ts b/apps/bff/src/modules/subscriptions/subscriptions.service.ts index 09cef0a8..babdaff2 100644 --- a/apps/bff/src/modules/subscriptions/subscriptions.service.ts +++ b/apps/bff/src/modules/subscriptions/subscriptions.service.ts @@ -17,7 +17,7 @@ import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; import { Logger } from "nestjs-pino"; import type * as Providers from "@customer-portal/domain/subscriptions/providers"; -type WhmcsProduct = Providers.WhmcsRaw.WhmcsProductRaw; +type WhmcsProduct = Providers.WhmcsProductRaw; export interface GetSubscriptionsOptions { status?: SubscriptionStatus; diff --git a/apps/bff/src/modules/users/infra/user-profile.service.ts b/apps/bff/src/modules/users/infra/user-profile.service.ts index 8562b350..7afa1102 100644 --- a/apps/bff/src/modules/users/infra/user-profile.service.ts +++ b/apps/bff/src/modules/users/infra/user-profile.service.ts @@ -15,7 +15,10 @@ import { type Address, type User, } from "@customer-portal/domain/customer"; -import * as CustomerProviders from "@customer-portal/domain/customer/providers"; +import { + getCustomFieldValue, + mapPrismaUserToUserAuth, +} from "@customer-portal/domain/customer/providers"; import { updateCustomerProfileRequestSchema, type UpdateCustomerProfileRequest, @@ -149,7 +152,11 @@ export class UserProfileService { } // Allow phone/company/language updates through to WHMCS - const { email: _email, firstname: _fn, lastname: _ln, ...whmcsUpdate } = parsed; + // Exclude email/firstname/lastname from WHMCS update (handled separately above or disallowed) + const { email, firstname, lastname, ...whmcsUpdate } = parsed; + void email; // Email is handled above in a separate flow + void firstname; // Name changes are explicitly disallowed + void lastname; if (Object.keys(whmcsUpdate).length > 0) { await this.whmcsService.updateClient(mapping.whmcsClientId, whmcsUpdate); } @@ -432,7 +439,7 @@ export class UserProfileService { try { const whmcsClient = await this.whmcsService.getClientDetails(mapping.whmcsClientId); - const userAuth = CustomerProviders.Portal.mapPrismaUserToUserAuth(user); + const userAuth = mapPrismaUserToUserAuth(user); const base = combineToUser(userAuth, whmcsClient); // Portal-visible identifiers (read-only). These are stored in WHMCS custom fields. @@ -444,16 +451,13 @@ export class UserProfileService { const genderFieldId = this.configService.get("WHMCS_GENDER_FIELD_ID"); const rawSfNumber = customerNumberFieldId - ? CustomerProviders.Whmcs.getCustomFieldValue( - whmcsClient.customfields, - customerNumberFieldId - ) + ? getCustomFieldValue(whmcsClient.customfields, customerNumberFieldId) : undefined; const rawDob = dobFieldId - ? CustomerProviders.Whmcs.getCustomFieldValue(whmcsClient.customfields, dobFieldId) + ? getCustomFieldValue(whmcsClient.customfields, dobFieldId) : undefined; const rawGender = genderFieldId - ? CustomerProviders.Whmcs.getCustomFieldValue(whmcsClient.customfields, genderFieldId) + ? getCustomFieldValue(whmcsClient.customfields, genderFieldId) : undefined; const sfNumber = rawSfNumber?.trim() ? rawSfNumber.trim() : null; diff --git a/apps/portal/src/components/ui/shadcn-io/aurora-background/index.tsx b/apps/portal/src/components/ui/shadcn-io/aurora-background/index.tsx deleted file mode 100644 index ce6da896..00000000 --- a/apps/portal/src/components/ui/shadcn-io/aurora-background/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import React, { ReactNode } from "react"; - -interface AuroraBackgroundProps extends React.HTMLAttributes { - children: ReactNode; - showRadialGradient?: boolean; -} - -export const AuroraBackground = ({ - className, - children, - showRadialGradient = true, - ...props -}: AuroraBackgroundProps) => { - return ( -
-
-
-
-
- {children} -
-
- ); -}; diff --git a/apps/portal/src/features/account/hooks/useAddressEdit.ts b/apps/portal/src/features/account/hooks/useAddressEdit.ts index 9d29f3a8..6333f032 100644 --- a/apps/portal/src/features/account/hooks/useAddressEdit.ts +++ b/apps/portal/src/features/account/hooks/useAddressEdit.ts @@ -9,7 +9,7 @@ import { addressFormToRequest, type AddressFormData, } from "@customer-portal/domain/customer"; -import { useZodForm } from "@/hooks/useZodForm"; +import { useZodForm } from "@/lib/hooks/useZodForm"; export function useAddressEdit(initial: AddressFormData) { const queryClient = useQueryClient(); diff --git a/apps/portal/src/features/account/hooks/useProfileEdit.ts b/apps/portal/src/features/account/hooks/useProfileEdit.ts index d8213329..cb29951d 100644 --- a/apps/portal/src/features/account/hooks/useProfileEdit.ts +++ b/apps/portal/src/features/account/hooks/useProfileEdit.ts @@ -9,7 +9,7 @@ import { type ProfileEditFormData, } from "@customer-portal/domain/customer"; import { type UpdateCustomerProfileRequest } from "@customer-portal/domain/auth"; -import { useZodForm } from "@/hooks/useZodForm"; +import { useZodForm } from "@/lib/hooks/useZodForm"; export function useProfileEdit(initial: ProfileEditFormData) { const handleSave = useCallback(async (formData: ProfileEditFormData) => { diff --git a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx b/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx index 0fafd52c..09079989 100644 --- a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx +++ b/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx @@ -8,7 +8,7 @@ import { Button, Input, ErrorMessage } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; import { useWhmcsLink } from "@/features/auth/hooks"; import { linkWhmcsRequestSchema, type LinkWhmcsResponse } from "@customer-portal/domain/auth"; -import { useZodForm } from "@/hooks/useZodForm"; +import { useZodForm } from "@/lib/hooks/useZodForm"; interface LinkWhmcsFormProps { onTransferred?: (result: LinkWhmcsResponse) => void; diff --git a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx index 1f6cf88a..5860b6bb 100644 --- a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx +++ b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx @@ -12,7 +12,7 @@ import { Button, Input, ErrorMessage } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; import { useLogin } from "../../hooks/use-auth"; import { loginRequestSchema } from "@customer-portal/domain/auth"; -import { useZodForm } from "@/hooks/useZodForm"; +import { useZodForm } from "@/lib/hooks/useZodForm"; import { z } from "zod"; import { getSafeRedirect } from "@/features/auth/utils/route-protection"; diff --git a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx index fbc7a06a..0a2ee5c0 100644 --- a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx +++ b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx @@ -10,7 +10,7 @@ import Link from "next/link"; import { Button, Input, ErrorMessage } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; import { usePasswordReset } from "../../hooks/use-auth"; -import { useZodForm } from "@/hooks/useZodForm"; +import { useZodForm } from "@/lib/hooks/useZodForm"; import { passwordResetRequestSchema, passwordResetSchema } from "@customer-portal/domain/auth"; import { z } from "zod"; diff --git a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx index 343878fa..6c98fae2 100644 --- a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx +++ b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx @@ -9,7 +9,7 @@ import Link from "next/link"; import { Button, Input, ErrorMessage } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; import { useWhmcsLink } from "../../hooks/use-auth"; -import { useZodForm } from "@/hooks/useZodForm"; +import { useZodForm } from "@/lib/hooks/useZodForm"; import { setPasswordRequestSchema, checkPasswordStrength, diff --git a/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx b/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx index 66b37eb2..c72aa6ec 100644 --- a/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx @@ -12,7 +12,7 @@ import { ErrorMessage } from "@/components/atoms"; import { useSignupWithRedirect } from "../../hooks/use-auth"; import { signupInputSchema, buildSignupRequest } from "@customer-portal/domain/auth"; import { addressFormSchema } from "@customer-portal/domain/customer"; -import { useZodForm } from "@/hooks/useZodForm"; +import { useZodForm } from "@/lib/hooks/useZodForm"; import { z } from "zod"; import { getSafeRedirect } from "@/features/auth/utils/route-protection"; diff --git a/apps/portal/src/features/service-management/components/ServiceManagementSection.tsx b/apps/portal/src/features/service-management/components/ServiceManagementSection.tsx index 8715bf80..397188c5 100644 --- a/apps/portal/src/features/service-management/components/ServiceManagementSection.tsx +++ b/apps/portal/src/features/service-management/components/ServiceManagementSection.tsx @@ -9,7 +9,7 @@ import { DevicePhoneMobileIcon, ShieldCheckIcon, } from "@heroicons/react/24/outline"; -import { SimManagementSection } from "@/features/sim-management"; +import { SimManagementSection } from "@/features/sim"; interface ServiceManagementSectionProps { subscriptionId: number; diff --git a/apps/portal/src/features/services/components/base/AddressForm.tsx b/apps/portal/src/features/services/components/base/AddressForm.tsx index 0d35770f..89f14145 100644 --- a/apps/portal/src/features/services/components/base/AddressForm.tsx +++ b/apps/portal/src/features/services/components/base/AddressForm.tsx @@ -3,7 +3,7 @@ import { useEffect } from "react"; import { MapPinIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { COUNTRY_OPTIONS, getCountryCodeByName } from "@/lib/constants/countries"; -import { useZodForm } from "@/hooks/useZodForm"; +import { useZodForm } from "@/lib/hooks/useZodForm"; import { addressFormSchema, type AddressFormData, diff --git a/apps/portal/src/features/sim-management/components/ChangePlanModal.tsx b/apps/portal/src/features/sim/components/ChangePlanModal.tsx similarity index 100% rename from apps/portal/src/features/sim-management/components/ChangePlanModal.tsx rename to apps/portal/src/features/sim/components/ChangePlanModal.tsx diff --git a/apps/portal/src/features/sim-management/components/DataUsageChart.tsx b/apps/portal/src/features/sim/components/DataUsageChart.tsx similarity index 100% rename from apps/portal/src/features/sim-management/components/DataUsageChart.tsx rename to apps/portal/src/features/sim/components/DataUsageChart.tsx diff --git a/apps/portal/src/features/sim-management/components/ReissueSimModal.tsx b/apps/portal/src/features/sim/components/ReissueSimModal.tsx similarity index 100% rename from apps/portal/src/features/sim-management/components/ReissueSimModal.tsx rename to apps/portal/src/features/sim/components/ReissueSimModal.tsx diff --git a/apps/portal/src/features/sim-management/components/SimActions.tsx b/apps/portal/src/features/sim/components/SimActions.tsx similarity index 100% rename from apps/portal/src/features/sim-management/components/SimActions.tsx rename to apps/portal/src/features/sim/components/SimActions.tsx diff --git a/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx b/apps/portal/src/features/sim/components/SimDetailsCard.tsx similarity index 100% rename from apps/portal/src/features/sim-management/components/SimDetailsCard.tsx rename to apps/portal/src/features/sim/components/SimDetailsCard.tsx diff --git a/apps/portal/src/features/sim-management/components/SimFeatureToggles.tsx b/apps/portal/src/features/sim/components/SimFeatureToggles.tsx similarity index 100% rename from apps/portal/src/features/sim-management/components/SimFeatureToggles.tsx rename to apps/portal/src/features/sim/components/SimFeatureToggles.tsx diff --git a/apps/portal/src/features/sim-management/components/SimManagementSection.tsx b/apps/portal/src/features/sim/components/SimManagementSection.tsx similarity index 100% rename from apps/portal/src/features/sim-management/components/SimManagementSection.tsx rename to apps/portal/src/features/sim/components/SimManagementSection.tsx diff --git a/apps/portal/src/features/sim-management/components/TopUpModal.tsx b/apps/portal/src/features/sim/components/TopUpModal.tsx similarity index 100% rename from apps/portal/src/features/sim-management/components/TopUpModal.tsx rename to apps/portal/src/features/sim/components/TopUpModal.tsx diff --git a/apps/portal/src/features/sim-management/hooks/useSimTopUpPricing.ts b/apps/portal/src/features/sim/hooks/useSimTopUpPricing.ts similarity index 100% rename from apps/portal/src/features/sim-management/hooks/useSimTopUpPricing.ts rename to apps/portal/src/features/sim/hooks/useSimTopUpPricing.ts diff --git a/apps/portal/src/features/sim-management/index.ts b/apps/portal/src/features/sim/index.ts similarity index 100% rename from apps/portal/src/features/sim-management/index.ts rename to apps/portal/src/features/sim/index.ts diff --git a/apps/portal/src/features/sim-management/utils/plan.ts b/apps/portal/src/features/sim/utils/plan.ts similarity index 100% rename from apps/portal/src/features/sim-management/utils/plan.ts rename to apps/portal/src/features/sim/utils/plan.ts diff --git a/apps/portal/src/features/subscriptions/containers/SimCancel.tsx b/apps/portal/src/features/subscriptions/containers/SimCancel.tsx index 357cfd7f..c41b362b 100644 --- a/apps/portal/src/features/subscriptions/containers/SimCancel.tsx +++ b/apps/portal/src/features/subscriptions/containers/SimCancel.tsx @@ -5,7 +5,7 @@ import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { simActionsService } from "@/features/subscriptions/services/sim-actions.service"; import { useAuthStore } from "@/features/auth/services/auth.store"; -import type { SimDetails } from "@/features/sim-management/components/SimDetailsCard"; +import type { SimDetails } from "@/features/sim/components/SimDetailsCard"; type Step = 1 | 2 | 3; diff --git a/apps/portal/src/features/subscriptions/views/SimReissue.tsx b/apps/portal/src/features/subscriptions/views/SimReissue.tsx index d020b79a..dd5137d3 100644 --- a/apps/portal/src/features/subscriptions/views/SimReissue.tsx +++ b/apps/portal/src/features/subscriptions/views/SimReissue.tsx @@ -9,7 +9,7 @@ import { DevicePhoneMobileIcon, DeviceTabletIcon, CpuChipIcon } from "@heroicons import { simActionsService } from "@/features/subscriptions/services/sim-actions.service"; import type { SimReissueFullRequest } from "@customer-portal/domain/sim"; import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; -import type { SimDetails } from "@/features/sim-management/components/SimDetailsCard"; +import type { SimDetails } from "@/features/sim/components/SimDetailsCard"; import { Button } from "@/components/atoms"; type SimType = "physical" | "esim"; diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx index 55810e92..0176ab52 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx @@ -18,7 +18,7 @@ import { PageLayout } from "@/components/templates/PageLayout"; import { StatusPill } from "@/components/atoms/status-pill"; const { formatCurrency: sharedFormatCurrency } = Formatting; -import { SimManagementSection } from "@/features/sim-management"; +import { SimManagementSection } from "@/features/sim"; import { getBillingCycleLabel, getSubscriptionStatusVariant, diff --git a/apps/portal/src/features/support/views/PublicContactView.tsx b/apps/portal/src/features/support/views/PublicContactView.tsx index af6007d3..a280023a 100644 --- a/apps/portal/src/features/support/views/PublicContactView.tsx +++ b/apps/portal/src/features/support/views/PublicContactView.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import { Button, Input } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; -import { useZodForm } from "@/hooks/useZodForm"; +import { useZodForm } from "@/lib/hooks/useZodForm"; import { Mail, CheckCircle, MapPin } from "lucide-react"; import { publicContactRequestSchema, diff --git a/apps/portal/src/lib/hooks/index.ts b/apps/portal/src/lib/hooks/index.ts index c0ff4813..3ad34f8d 100644 --- a/apps/portal/src/lib/hooks/index.ts +++ b/apps/portal/src/lib/hooks/index.ts @@ -1,3 +1,4 @@ export { useLocalStorage } from "./useLocalStorage"; export { useDebounce } from "./useDebounce"; export { useMediaQuery, useIsMobile, useIsTablet, useIsDesktop } from "./useMediaQuery"; +export { useZodForm } from "./useZodForm"; diff --git a/apps/portal/src/hooks/useZodForm.ts b/apps/portal/src/lib/hooks/useZodForm.ts similarity index 100% rename from apps/portal/src/hooks/useZodForm.ts rename to apps/portal/src/lib/hooks/useZodForm.ts diff --git a/docs/development/bff/db-mappers.md b/docs/development/bff/db-mappers.md index 1225f18a..b944da81 100644 --- a/docs/development/bff/db-mappers.md +++ b/docs/development/bff/db-mappers.md @@ -99,7 +99,7 @@ export function mapPrismaUserToDomain(user: PrismaUser): AuthenticatedUser { ```typescript import type { IdMapping as PrismaIdMapping } from "@prisma/client"; -import type { UserIdMapping } from "@customer-portal/domain/mappings"; +import type { UserIdMapping } from "@bff/modules/id-mappings/domain/index.js"; /** * Maps Prisma IdMapping entity to Domain UserIdMapping type diff --git a/docs/development/domain/import-hygiene.md b/docs/development/domain/import-hygiene.md index 0681693a..bb9631da 100644 --- a/docs/development/domain/import-hygiene.md +++ b/docs/development/domain/import-hygiene.md @@ -14,6 +14,20 @@ - **BFF-only (integration/infrastructure)**: - `@customer-portal/domain//providers` +### Domain-internal helpers (for domain code only) + +Sometimes provider helpers need to be shared across multiple domain mappers **without** +becoming part of the public contract (and without enabling deep imports). + +In those cases, keep the helpers in a stable internal location (e.g. +`packages/domain/common/providers/whmcs-utils/`) and import them via **relative imports** +from within `packages/domain/**`. + +- **Allowed**: inside `packages/domain/**` only +- **Never**: imported from `apps/**` +- **Purpose**: share provider-only helpers (parsing, encoding/serialization, provider quirks) + across multiple domain mappers while keeping the app-facing API surface clean. + ## Quick Reference | Context | Import Pattern | Example | diff --git a/docs/development/domain/packages.md b/docs/development/domain/packages.md index f035a58a..b35c477d 100644 --- a/docs/development/domain/packages.md +++ b/docs/development/domain/packages.md @@ -18,14 +18,15 @@ The `@customer-portal/domain` package should contain **ONLY** pure domain logic ## πŸ“¦ Package Structure Matrix -| Type | Location | Examples | Reasoning | -| ---------------------- | ----------------------------------------- | ----------------------------------------- | ------------------------- | -| **Domain Types** | `packages/domain/*/contract.ts` | `Invoice`, `Order`, `Customer` | Pure business entities | -| **Validation Schemas** | `packages/domain/*/schema.ts` | `invoiceSchema`, `orderQueryParamsSchema` | Runtime validation | -| **Pure Utilities** | `packages/domain/toolkit/` | `formatCurrency()`, `parseDate()` | No framework dependencies | -| **Provider Mappers** | `packages/domain/*/providers/` | `transformWhmcsInvoice()` | Data transformation logic | -| **Framework Utils** | `apps/*/src/lib/` or `apps/*/src/core/` | API clients, React hooks | Framework-specific code | -| **Shared Infra** | `packages/validation`, `packages/logging` | `ZodPipe`, `useZodForm` | Framework bridges | +| Type | Location | Examples | Reasoning | +| ---------------------- | ----------------------------------------------- | ----------------------------------------- | ------------------------------------------------ | +| **Domain Types** | `packages/domain/*/contract.ts` | `Invoice`, `Order`, `Customer` | Pure business entities | +| **Validation Schemas** | `packages/domain/*/schema.ts` | `invoiceSchema`, `orderQueryParamsSchema` | Runtime validation | +| **Pure Utilities** | `packages/domain/toolkit/` | `formatCurrency()`, `parseDate()` | No framework dependencies | +| **Provider Mappers** | `packages/domain/*/providers/` | `transformWhmcsInvoice()` | Data transformation logic | +| **Provider Internals** | `packages/domain/common/providers/whmcs-utils/` | WHMCS parsing/serialization helpers | Keep provider-only helpers shared but not public | +| **Framework Utils** | `apps/*/src/lib/` or `apps/*/src/core/` | API clients, React hooks | Framework-specific code | +| **Shared Infra** | `packages/validation`, `packages/logging` | `ZodPipe`, `useZodForm` | Framework bridges | --- diff --git a/eslint.config.mjs b/eslint.config.mjs index 58ae95e3..e6729df3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -68,7 +68,7 @@ export default [ // Backend + domain packages: sensible defaults // ============================================================================= { - files: [...BFF_TS_FILES, "packages/domain/**/*.ts"], + files: ["packages/domain/**/*.ts"], rules: { "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-unused-vars": [ @@ -76,6 +76,18 @@ export default [ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, ], "no-console": ["warn", { allow: ["warn", "error"] }], + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["#toolkit/whmcs", "#toolkit/whmcs/*"], + message: + "WHMCS provider utilities must not live under toolkit. Keep them under `packages/domain/common/providers/whmcs-utils/` and import via relative paths inside the domain package.", + }, + ], + }, + ], }, }, @@ -200,6 +212,11 @@ export default [ }, ], patterns: [ + { + group: ["@/hooks/*"], + message: + "Do not import from @/hooks. Use @/lib/hooks (app-level hooks) or feature hooks under src/features//hooks.", + }, { group: ["@customer-portal/domain/**/src/**"], message: "Import from @customer-portal/domain/ instead of internals.", diff --git a/packages/domain/billing/index.ts b/packages/domain/billing/index.ts index 7462f8bb..e3e644e1 100644 --- a/packages/domain/billing/index.ts +++ b/packages/domain/billing/index.ts @@ -1,7 +1,9 @@ /** * Billing Domain * - * Exports all billing-related contracts, schemas, and provider mappers. + * Exports billing contracts + schemas. + * + * Provider adapters (BFF-only) live under: `@customer-portal/domain/billing/providers`. * * Types are derived from Zod schemas (Schema-First Approach) */ diff --git a/packages/domain/billing/providers/index.ts b/packages/domain/billing/providers/index.ts index f50c5a27..7b82987a 100644 --- a/packages/domain/billing/providers/index.ts +++ b/packages/domain/billing/providers/index.ts @@ -1,16 +1,5 @@ /** * Billing Domain - Providers */ - -import * as WhmcsMapper from "./whmcs/mapper.js"; -import * as WhmcsRaw from "./whmcs/raw.types.js"; - -export const Whmcs = { - ...WhmcsMapper, - mapper: WhmcsMapper, - raw: WhmcsRaw, -}; - -export { WhmcsMapper, WhmcsRaw }; export * from "./whmcs/mapper.js"; export * from "./whmcs/raw.types.js"; diff --git a/packages/domain/billing/providers/whmcs/mapper.ts b/packages/domain/billing/providers/whmcs/mapper.ts index f7f6c22e..162c4b27 100644 --- a/packages/domain/billing/providers/whmcs/mapper.ts +++ b/packages/domain/billing/providers/whmcs/mapper.ts @@ -13,7 +13,7 @@ import { whmcsInvoiceListItemSchema, whmcsInvoiceItemsRawSchema, } from "./raw.types.js"; -import { parseAmount, formatDate } from "../../../providers/whmcs/utils.js"; +import { parseAmount, formatDate } from "../../../common/providers/whmcs-utils/index.js"; export interface TransformInvoiceOptions { defaultCurrencyCode?: string; diff --git a/packages/domain/customer/providers/index.ts b/packages/domain/customer/providers/index.ts index daaeb747..8d66b4a1 100644 --- a/packages/domain/customer/providers/index.ts +++ b/packages/domain/customer/providers/index.ts @@ -6,10 +6,10 @@ * - Whmcs: WHMCS API β†’ WhmcsClient */ -export * as Portal from "./portal/index.js"; -export * as Whmcs from "./whmcs/index.js"; +export * from "./portal/index.js"; +export * from "./whmcs/index.js"; -// Provider-specific integration types (BFF-only) +// Provider-specific integration types (BFF-only convenience re-exports) export type { SalesforceAccountFieldMap, SalesforceAccountRecord } from "../contract.js"; export type { WhmcsAddClientParams, diff --git a/packages/domain/customer/providers/whmcs/index.ts b/packages/domain/customer/providers/whmcs/index.ts index 8f82f013..707879b0 100644 --- a/packages/domain/customer/providers/whmcs/index.ts +++ b/packages/domain/customer/providers/whmcs/index.ts @@ -11,7 +11,7 @@ export { getCustomFieldValue, getCustomFieldsMap, serializeWhmcsKeyValueMap, -} from "../../../providers/whmcs/utils.js"; +} from "../../../common/providers/whmcs-utils/index.js"; // Re-export domain types for provider namespace convenience export type { WhmcsClient, EmailPreferences, SubUser, Stats } from "../../schema.js"; diff --git a/packages/domain/index.ts b/packages/domain/index.ts index 6fa3935b..40b685a6 100644 --- a/packages/domain/index.ts +++ b/packages/domain/index.ts @@ -1,21 +1,23 @@ /** * @customer-portal/domain * Unified domain package with Provider-Aware Structure. + * + * NOTE: Import from specific modules (e.g., @customer-portal/domain/billing) + * rather than from this root index for better tree-shaking. */ // Re-export domain modules +export * as Auth from "./auth/index.js"; export * as Billing from "./billing/index.js"; -export * as Services from "./services/index.js"; export * as Checkout from "./checkout/index.js"; export * as Common from "./common/index.js"; export * as Customer from "./customer/index.js"; export * as Dashboard from "./dashboard/index.js"; -export * as Auth from "./auth/index.js"; -export * as Mappings from "./mappings/index.js"; +export * as Notifications from "./notifications/index.js"; export * as Opportunity from "./opportunity/index.js"; export * as Orders from "./orders/index.js"; export * as Payments from "./payments/index.js"; -export * as Realtime from "./realtime/index.js"; +export * as Services from "./services/index.js"; export * as Sim from "./sim/index.js"; export * as Subscriptions from "./subscriptions/index.js"; export * as Support from "./support/index.js"; diff --git a/packages/domain/orders/contract.ts b/packages/domain/orders/contract.ts index 046e3e34..36a75d4c 100644 --- a/packages/domain/orders/contract.ts +++ b/packages/domain/orders/contract.ts @@ -5,7 +5,6 @@ * Validated types are derived from schemas (see schema.ts). */ -import type { UserIdMapping } from "../mappings/contract.js"; import type { OrderConfigurations } from "./schema.js"; // ============================================================================ @@ -125,9 +124,13 @@ export type OrderStatus = string; export type OrderType = string; /** - * User mapping for order creation (subset of UserIdMapping) + * User mapping for order creation */ -export type UserMapping = Pick; +export interface UserMapping { + userId: string; + whmcsClientId: number; + sfAccountId?: string | null; +} // ============================================================================ // Checkout Types diff --git a/packages/domain/orders/index.ts b/packages/domain/orders/index.ts index da73b87e..113bb760 100644 --- a/packages/domain/orders/index.ts +++ b/packages/domain/orders/index.ts @@ -1,7 +1,9 @@ /** * Orders Domain * - * Exports all order-related contracts, schemas, and provider mappers. + * Exports orders contracts + schemas. + * + * Provider adapters (BFF-only) live under: `@customer-portal/domain/orders/providers`. * * Types are derived from Zod schemas (Schema-First Approach) */ diff --git a/packages/domain/orders/providers/index.ts b/packages/domain/orders/providers/index.ts index ee16431d..62479452 100644 --- a/packages/domain/orders/providers/index.ts +++ b/packages/domain/orders/providers/index.ts @@ -1,28 +1,7 @@ /** * Orders Domain - Providers */ - -import * as WhmcsMapper from "./whmcs/mapper.js"; -import * as WhmcsRaw from "./whmcs/raw.types.js"; -import * as SalesforceFieldMap from "./salesforce/field-map.js"; -import * as SalesforceMapper from "./salesforce/mapper.js"; -import * as SalesforceRaw from "./salesforce/raw.types.js"; export * from "./types.js"; - -export const Whmcs = { - ...WhmcsMapper, - mapper: WhmcsMapper, - raw: WhmcsRaw, -}; - -export const Salesforce = { - ...SalesforceMapper, - mapper: SalesforceMapper, - raw: SalesforceRaw, - fieldMap: SalesforceFieldMap, -}; - -export { WhmcsMapper, WhmcsRaw, SalesforceMapper, SalesforceRaw }; export * from "./whmcs/mapper.js"; export * from "./whmcs/raw.types.js"; export * from "./salesforce/mapper.js"; diff --git a/packages/domain/orders/providers/salesforce/field-map.ts b/packages/domain/orders/providers/salesforce/field-map.ts index 78ac6f83..30dc09b8 100644 --- a/packages/domain/orders/providers/salesforce/field-map.ts +++ b/packages/domain/orders/providers/salesforce/field-map.ts @@ -1,58 +1,62 @@ -import type { SalesforceProduct2WithPricebookEntries } from "../../../services/providers/salesforce/raw.types.js"; +import type { SalesforceProduct2WithPricebookEntries } from "#internal/services/providers/salesforce"; import type { SalesforceOrderItemRecord, SalesforceOrderRecord } from "./raw.types.js"; +type SalesforceOrderRecordKey = Extract; +type SalesforceOrderItemRecordKey = Extract; +type SalesforceProductKey = Extract; + export interface SalesforceOrderFieldMap { order: { - type: keyof SalesforceOrderRecord; - activationType: keyof SalesforceOrderRecord; - activationScheduledAt: keyof SalesforceOrderRecord; - activationStatus: keyof SalesforceOrderRecord; - activationErrorCode: keyof SalesforceOrderRecord; - activationErrorMessage: keyof SalesforceOrderRecord; - activationLastAttemptAt: keyof SalesforceOrderRecord; - internetPlanTier: keyof SalesforceOrderRecord; - installationType: keyof SalesforceOrderRecord; - weekendInstall: keyof SalesforceOrderRecord; - accessMode: keyof SalesforceOrderRecord; - hikariDenwa: keyof SalesforceOrderRecord; - vpnRegion: keyof SalesforceOrderRecord; - simType: keyof SalesforceOrderRecord; - simVoiceMail: keyof SalesforceOrderRecord; - simCallWaiting: keyof SalesforceOrderRecord; - eid: keyof SalesforceOrderRecord; - whmcsOrderId: keyof SalesforceOrderRecord; - addressChanged: keyof SalesforceOrderRecord; - billingStreet: keyof SalesforceOrderRecord; - billingCity: keyof SalesforceOrderRecord; - billingState: keyof SalesforceOrderRecord; - billingPostalCode: keyof SalesforceOrderRecord; - billingCountry: keyof SalesforceOrderRecord; - mnpApplication: keyof SalesforceOrderRecord; - mnpReservation: keyof SalesforceOrderRecord; - mnpExpiry: keyof SalesforceOrderRecord; - mnpPhone: keyof SalesforceOrderRecord; - mvnoAccountNumber: keyof SalesforceOrderRecord; - portingDateOfBirth: keyof SalesforceOrderRecord; - portingFirstName: keyof SalesforceOrderRecord; - portingLastName: keyof SalesforceOrderRecord; - portingFirstNameKatakana: keyof SalesforceOrderRecord; - portingLastNameKatakana: keyof SalesforceOrderRecord; - portingGender: keyof SalesforceOrderRecord; + type: SalesforceOrderRecordKey; + activationType: SalesforceOrderRecordKey; + activationScheduledAt: SalesforceOrderRecordKey; + activationStatus: SalesforceOrderRecordKey; + activationErrorCode: SalesforceOrderRecordKey; + activationErrorMessage: SalesforceOrderRecordKey; + activationLastAttemptAt: SalesforceOrderRecordKey; + internetPlanTier: SalesforceOrderRecordKey; + installationType: SalesforceOrderRecordKey; + weekendInstall: SalesforceOrderRecordKey; + accessMode: SalesforceOrderRecordKey; + hikariDenwa: SalesforceOrderRecordKey; + vpnRegion: SalesforceOrderRecordKey; + simType: SalesforceOrderRecordKey; + simVoiceMail: SalesforceOrderRecordKey; + simCallWaiting: SalesforceOrderRecordKey; + eid: SalesforceOrderRecordKey; + whmcsOrderId: SalesforceOrderRecordKey; + addressChanged: SalesforceOrderRecordKey; + billingStreet: SalesforceOrderRecordKey; + billingCity: SalesforceOrderRecordKey; + billingState: SalesforceOrderRecordKey; + billingPostalCode: SalesforceOrderRecordKey; + billingCountry: SalesforceOrderRecordKey; + mnpApplication: SalesforceOrderRecordKey; + mnpReservation: SalesforceOrderRecordKey; + mnpExpiry: SalesforceOrderRecordKey; + mnpPhone: SalesforceOrderRecordKey; + mvnoAccountNumber: SalesforceOrderRecordKey; + portingDateOfBirth: SalesforceOrderRecordKey; + portingFirstName: SalesforceOrderRecordKey; + portingLastName: SalesforceOrderRecordKey; + portingFirstNameKatakana: SalesforceOrderRecordKey; + portingLastNameKatakana: SalesforceOrderRecordKey; + portingGender: SalesforceOrderRecordKey; }; orderItem: { - billingCycle: keyof SalesforceOrderItemRecord; - whmcsServiceId: keyof SalesforceOrderItemRecord; + billingCycle: SalesforceOrderItemRecordKey; + whmcsServiceId: SalesforceOrderItemRecordKey; }; product: { - sku: keyof SalesforceProduct2WithPricebookEntries; - itemClass: keyof SalesforceProduct2WithPricebookEntries; - billingCycle: keyof SalesforceProduct2WithPricebookEntries; - whmcsProductId: keyof SalesforceProduct2WithPricebookEntries; - internetOfferingType: keyof SalesforceProduct2WithPricebookEntries; - internetPlanTier: keyof SalesforceProduct2WithPricebookEntries; - vpnRegion: keyof SalesforceProduct2WithPricebookEntries; - bundledAddon: keyof SalesforceProduct2WithPricebookEntries; - isBundledAddon: keyof SalesforceProduct2WithPricebookEntries; + sku: SalesforceProductKey; + itemClass: SalesforceProductKey; + billingCycle: SalesforceProductKey; + whmcsProductId: SalesforceProductKey; + internetOfferingType: SalesforceProductKey; + internetPlanTier: SalesforceProductKey; + vpnRegion: SalesforceProductKey; + bundledAddon: SalesforceProductKey; + isBundledAddon: SalesforceProductKey; }; } @@ -68,7 +72,7 @@ export const defaultSalesforceOrderFieldMap: SalesforceOrderFieldMap = { activationStatus: "Activation_Status__c", activationErrorCode: "Activation_Error_Code__c", activationErrorMessage: "Activation_Error_Message__c", - activationLastAttemptAt: "Activation_Last_Attempt_At__c" as keyof SalesforceOrderRecord, + activationLastAttemptAt: "Activation_Last_Attempt_At__c", internetPlanTier: "Internet_Plan_Tier__c", installationType: "Installment_Plan__c", weekendInstall: "Weekend_Install__c", diff --git a/packages/domain/orders/providers/salesforce/mapper.ts b/packages/domain/orders/providers/salesforce/mapper.ts index 3286f25e..50ec695b 100644 --- a/packages/domain/orders/providers/salesforce/mapper.ts +++ b/packages/domain/orders/providers/salesforce/mapper.ts @@ -15,7 +15,7 @@ import { orderDetailsSchema, orderSummarySchema, orderItemDetailsSchema } from " import type { SalesforceProduct2WithPricebookEntries, SalesforcePricebookEntryRecord, -} from "../../../services/providers/salesforce/raw.types.js"; +} from "#internal/services/providers/salesforce"; import { defaultSalesforceOrderFieldMap, type SalesforceOrderFieldMap } from "./field-map.js"; import type { SalesforceOrderItemRecord, SalesforceOrderRecord } from "./raw.types.js"; diff --git a/packages/domain/orders/providers/whmcs/mapper.ts b/packages/domain/orders/providers/whmcs/mapper.ts index 70b8cabf..a743eeaa 100644 --- a/packages/domain/orders/providers/whmcs/mapper.ts +++ b/packages/domain/orders/providers/whmcs/mapper.ts @@ -6,7 +6,7 @@ import type { OrderDetails, OrderItemDetails } from "../../contract.js"; import { normalizeBillingCycle } from "../../helpers.js"; -import { serializeWhmcsKeyValueMap } from "../../../providers/whmcs/utils.js"; +import { serializeWhmcsKeyValueMap } from "../../../common/providers/whmcs-utils/index.js"; import { type WhmcsOrderItem, type WhmcsAddOrderParams, diff --git a/packages/domain/package.json b/packages/domain/package.json index 0055f5c7..63b06e3c 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -10,6 +10,10 @@ "files": [ "dist" ], + "imports": { + "#toolkit/*": "./dist/toolkit/*", + "#internal/services/providers/salesforce": "./dist/services/providers/salesforce/index.js" + }, "exports": { "./auth": { "import": "./dist/auth/index.js", @@ -55,10 +59,6 @@ "import": "./dist/dashboard/index.js", "types": "./dist/dashboard/index.d.ts" }, - "./mappings": { - "import": "./dist/mappings/index.js", - "types": "./dist/mappings/index.d.ts" - }, "./opportunity": { "import": "./dist/opportunity/index.js", "types": "./dist/opportunity/index.d.ts" @@ -79,10 +79,6 @@ "import": "./dist/payments/providers/index.js", "types": "./dist/payments/providers/index.d.ts" }, - "./realtime": { - "import": "./dist/realtime/index.js", - "types": "./dist/realtime/index.d.ts" - }, "./sim": { "import": "./dist/sim/index.js", "types": "./dist/sim/index.d.ts" @@ -114,10 +110,6 @@ "./notifications": { "import": "./dist/notifications/index.js", "types": "./dist/notifications/index.d.ts" - }, - "./salesforce": { - "import": "./dist/salesforce/index.js", - "types": "./dist/salesforce/index.d.ts" } }, "scripts": { diff --git a/packages/domain/payments/index.ts b/packages/domain/payments/index.ts index 18de0bf4..278d019e 100644 --- a/packages/domain/payments/index.ts +++ b/packages/domain/payments/index.ts @@ -1,7 +1,9 @@ /** * Payments Domain * - * Exports all payment-related contracts, schemas, and provider mappers. + * Exports payments contracts + schemas. + * + * Provider adapters (BFF-only) live under: `@customer-portal/domain/payments/providers`. * * Types are derived from Zod schemas (Schema-First Approach) */ diff --git a/packages/domain/payments/providers/index.ts b/packages/domain/payments/providers/index.ts index aa066ccc..5d5417a6 100644 --- a/packages/domain/payments/providers/index.ts +++ b/packages/domain/payments/providers/index.ts @@ -1,16 +1,5 @@ /** * Payments Domain - Providers */ - -import * as WhmcsMapper from "./whmcs/mapper.js"; -import * as WhmcsRaw from "./whmcs/raw.types.js"; - -export const Whmcs = { - ...WhmcsMapper, - mapper: WhmcsMapper, - raw: WhmcsRaw, -}; - -export { WhmcsMapper, WhmcsRaw }; export * from "./whmcs/mapper.js"; export * from "./whmcs/raw.types.js"; diff --git a/packages/domain/providers/index.ts b/packages/domain/providers/index.ts deleted file mode 100644 index 07d2684b..00000000 --- a/packages/domain/providers/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Domain Providers - * Re-exports shared provider utilities - */ - -export * as Whmcs from "./whmcs/index.js"; diff --git a/packages/domain/providers/whmcs/index.ts b/packages/domain/providers/whmcs/index.ts deleted file mode 100644 index 0da6c219..00000000 --- a/packages/domain/providers/whmcs/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * WHMCS Provider Utilities - * Re-exports shared WHMCS provider utilities - */ - -export * from "./utils.js"; diff --git a/packages/domain/providers/whmcs/utils.ts b/packages/domain/providers/whmcs/utils.ts deleted file mode 100644 index a6fabd9e..00000000 --- a/packages/domain/providers/whmcs/utils.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Shared WHMCS Provider Utilities - * Single source of truth for WHMCS data parsing - * - * Raw API types are source of truth - no fallbacks or variations expected. - */ - -/** - * Parse amount from WHMCS API response - * WHMCS returns amounts as strings or numbers - */ -export function parseAmount(amount: string | number | undefined): number { - if (typeof amount === "number") return amount; - if (!amount) return 0; - - const cleaned = String(amount).replace(/[^\d.-]/g, ""); - const parsed = Number.parseFloat(cleaned); - return Number.isNaN(parsed) ? 0 : parsed; -} - -/** - * Format date from WHMCS API to ISO string - * Returns undefined if input is invalid - */ -export function formatDate(input?: string | null): string | undefined { - if (!input) return undefined; - - const date = new Date(input); - if (Number.isNaN(date.getTime())) return undefined; - - return date.toISOString(); -} - -/** - * Normalize status using provided status map - * Generic helper for consistent status mapping - */ -export function normalizeStatus( - status: string | null | undefined, - statusMap: Record, - defaultStatus: T -): T { - if (!status) return defaultStatus; - const mapped = statusMap[status.trim().toLowerCase()]; - return mapped ?? defaultStatus; -} - -/** - * Normalize billing cycle using provided cycle map - * Generic helper for consistent cycle mapping - */ -export function normalizeCycle( - cycle: string | null | undefined, - cycleMap: Record, - defaultCycle: T -): T { - if (!cycle) return defaultCycle; - const normalized = cycle - .trim() - .toLowerCase() - .replace(/[_\s-]+/g, " "); - return cycleMap[normalized] ?? defaultCycle; -} - -const isObject = (value: unknown): value is Record => - typeof value === "object" && value !== null; - -const normalizeCustomFieldEntries = (value: unknown): Array> => { - if (Array.isArray(value)) return value.filter(isObject); - if (isObject(value) && "customfield" in value) { - const custom = (value as { customfield?: unknown }).customfield; - if (Array.isArray(custom)) return custom.filter(isObject); - if (isObject(custom)) return [custom]; - return []; - } - return []; -}; - -/** - * Build a lightweight map of WHMCS custom field identifiers to values. - * Accepts the documented WHMCS response shapes (array or { customfield }). - */ -export function getCustomFieldsMap(customFields: unknown): Record { - if (!customFields) return {}; - - if (isObject(customFields) && !Array.isArray(customFields) && !("customfield" in customFields)) { - return Object.entries(customFields).reduce>((acc, [key, value]) => { - if (typeof value === "string") { - const trimmedKey = key.trim(); - if (trimmedKey) acc[trimmedKey] = value; - } - return acc; - }, {}); - } - - const map: Record = {}; - for (const entry of normalizeCustomFieldEntries(customFields)) { - const idRaw = "id" in entry ? entry.id : undefined; - const id = - typeof idRaw === "string" - ? idRaw.trim() - : typeof idRaw === "number" - ? String(idRaw) - : undefined; - const name = "name" in entry && typeof entry.name === "string" ? entry.name.trim() : undefined; - const rawValue = "value" in entry ? entry.value : undefined; - if (rawValue === undefined || rawValue === null) continue; - const value = - typeof rawValue === "string" - ? rawValue - : typeof rawValue === "number" || typeof rawValue === "boolean" - ? String(rawValue) - : undefined; - if (!value) continue; - - if (id) map[id] = value; - if (name) map[name] = value; - } - - return map; -} - -/** - * Retrieve a custom field value by numeric id or name. - */ -export function getCustomFieldValue( - customFields: unknown, - key: string | number -): string | undefined { - if (key === undefined || key === null) return undefined; - const map = getCustomFieldsMap(customFields); - const primary = map[String(key)]; - if (primary !== undefined) return primary; - - if (typeof key === "string") { - const numeric = Number.parseInt(key, 10); - if (!Number.isNaN(numeric)) { - const numericValue = map[String(numeric)]; - if (numericValue !== undefined) return numericValue; - } - } - - return undefined; -} - -/** - * Serialize a key/value map into the format WHMCS expects for request parameters like `customfields`. - * - * Official docs: - * - AddClient: customfields = "Base64 encoded serialized array of custom field values." - * @see https://developers.whmcs.com/api-reference/addclient/ - * - * Notes: - * - WHMCS uses PHP serialization. For our usage, keys/values are strings. - * - Output is base64 of the serialized string. - */ -export function serializeWhmcsKeyValueMap(data?: Record): string { - if (!data) return ""; - const entries = Object.entries(data).filter(([k]) => String(k).trim().length > 0); - if (entries.length === 0) return ""; - - const serializedEntries = entries.map(([key, value]) => { - const safeKey = key ?? ""; - const safeValue = value ?? ""; - return ( - `s:${byteLengthUtf8(safeKey)}:"${escapePhpString(safeKey)}";` + - `s:${byteLengthUtf8(safeValue)}:"${escapePhpString(safeValue)}";` - ); - }); - - const serialized = `a:${serializedEntries.length}:{${serializedEntries.join("")}}`; - // Node runtime: base64 via Buffer - return Buffer.from(serialized, "utf8").toString("base64"); -} - -function escapePhpString(value: string): string { - return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); -} - -function byteLengthUtf8(value: string): number { - return Buffer.byteLength(value, "utf8"); -} diff --git a/packages/domain/realtime/events.ts b/packages/domain/realtime/events.ts deleted file mode 100644 index 4976851b..00000000 --- a/packages/domain/realtime/events.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Realtime Events - Shared Contracts - * - * Shared SSE payload shapes for portal + BFF. - */ - -export interface RealtimeEventEnvelope { - event: TEvent; - data: TData; -} - -export interface ServicesEligibilityChangedPayload { - accountId: string; - eligibility: string | null; - timestamp: string; -} - -export type AccountStreamEvent = - | RealtimeEventEnvelope<"account.stream.ready", { topic: string; timestamp: string }> - | RealtimeEventEnvelope<"account.stream.heartbeat", { topic: string; timestamp: string }> - | RealtimeEventEnvelope<"services.eligibility.changed", ServicesEligibilityChangedPayload>; diff --git a/packages/domain/realtime/index.ts b/packages/domain/realtime/index.ts deleted file mode 100644 index 930686e7..00000000 --- a/packages/domain/realtime/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./events.js"; diff --git a/packages/domain/services/index.ts b/packages/domain/services/index.ts index 0210f704..7613e538 100644 --- a/packages/domain/services/index.ts +++ b/packages/domain/services/index.ts @@ -1,7 +1,9 @@ /** * Services Domain * - * Exports all services-related contracts, schemas, and provider mappers. + * Exports services contracts + schemas. + * + * Provider adapters (BFF-only) live under: `@customer-portal/domain/services/providers`. * * Types are derived from Zod schemas (Schema-First Approach) */ diff --git a/packages/domain/services/providers/index.ts b/packages/domain/services/providers/index.ts index cbf4173b..5925af7b 100644 --- a/packages/domain/services/providers/index.ts +++ b/packages/domain/services/providers/index.ts @@ -1,24 +1,6 @@ /** * Services Domain - Providers */ - -import * as SalesforceMapper from "./salesforce/mapper.js"; -import * as SalesforceRaw from "./salesforce/raw.types.js"; -import * as WhmcsMapper from "./whmcs/mapper.js"; -import * as WhmcsRaw from "./whmcs/raw.types.js"; - -export const Salesforce = { - ...SalesforceMapper, - mapper: SalesforceMapper, - raw: SalesforceRaw, -}; - -export const Whmcs = { - ...WhmcsMapper, - raw: WhmcsRaw, -}; - -export { SalesforceMapper, SalesforceRaw, WhmcsMapper, WhmcsRaw }; export * from "./salesforce/mapper.js"; export * from "./salesforce/raw.types.js"; export * from "./whmcs/mapper.js"; diff --git a/packages/domain/services/providers/whmcs/mapper.ts b/packages/domain/services/providers/whmcs/mapper.ts index 978cf053..52e382f0 100644 --- a/packages/domain/services/providers/whmcs/mapper.ts +++ b/packages/domain/services/providers/whmcs/mapper.ts @@ -1,4 +1,4 @@ -import { parseAmount } from "../../../providers/whmcs/utils.js"; +import { parseAmount } from "../../../common/providers/whmcs-utils/index.js"; import { whmcsCatalogProductListResponseSchema, type WhmcsCatalogProductListResponse, diff --git a/packages/domain/sim/index.ts b/packages/domain/sim/index.ts index 7ecd9b67..28fd8754 100644 --- a/packages/domain/sim/index.ts +++ b/packages/domain/sim/index.ts @@ -1,7 +1,9 @@ /** * SIM Domain * - * Exports all SIM-related contracts, schemas, and provider mappers. + * Exports SIM contracts + schemas. + * + * Provider adapters (BFF-only) live under: `@customer-portal/domain/sim/providers`. * * Types are derived from Zod schemas (Schema-First Approach) */ diff --git a/packages/domain/sim/providers/index.ts b/packages/domain/sim/providers/index.ts index a698aaa4..4d5d6e63 100644 --- a/packages/domain/sim/providers/index.ts +++ b/packages/domain/sim/providers/index.ts @@ -1,10 +1,4 @@ /** * SIM Domain - Providers */ - -import { Freebit as FreebitAggregated } from "./freebit/index.js"; -import * as FreebitModule from "./freebit/index.js"; - -export const Freebit = FreebitAggregated; -export { FreebitModule }; export * from "./freebit/index.js"; diff --git a/packages/domain/subscriptions/index.ts b/packages/domain/subscriptions/index.ts index fb2af4cc..a8d6161f 100644 --- a/packages/domain/subscriptions/index.ts +++ b/packages/domain/subscriptions/index.ts @@ -1,7 +1,9 @@ /** * Subscriptions Domain * - * Exports all subscription-related contracts, schemas, and provider mappers. + * Exports subscriptions contracts + schemas. + * + * Provider adapters (BFF-only) live under: `@customer-portal/domain/subscriptions/providers`. * * Types are derived from Zod schemas (Schema-First Approach) */ diff --git a/packages/domain/subscriptions/providers/index.ts b/packages/domain/subscriptions/providers/index.ts index a1dd4436..73485ae9 100644 --- a/packages/domain/subscriptions/providers/index.ts +++ b/packages/domain/subscriptions/providers/index.ts @@ -1,16 +1,5 @@ /** * Subscriptions Domain - Providers */ - -import * as WhmcsMapper from "./whmcs/mapper.js"; -import * as WhmcsRaw from "./whmcs/raw.types.js"; - -export const Whmcs = { - ...WhmcsMapper, - mapper: WhmcsMapper, - raw: WhmcsRaw, -}; - -export { WhmcsMapper, WhmcsRaw }; export * from "./whmcs/mapper.js"; export * from "./whmcs/raw.types.js"; diff --git a/packages/domain/subscriptions/providers/whmcs/mapper.ts b/packages/domain/subscriptions/providers/whmcs/mapper.ts index e1e5f34a..28ba9aa5 100644 --- a/packages/domain/subscriptions/providers/whmcs/mapper.ts +++ b/packages/domain/subscriptions/providers/whmcs/mapper.ts @@ -26,7 +26,7 @@ import { formatDate, normalizeStatus, normalizeCycle, -} from "../../../providers/whmcs/utils.js"; +} from "../../../common/providers/whmcs-utils/index.js"; export interface TransformSubscriptionOptions { defaultCurrencyCode?: string; diff --git a/packages/domain/support/index.ts b/packages/domain/support/index.ts index 6a86dba4..0d583597 100644 --- a/packages/domain/support/index.ts +++ b/packages/domain/support/index.ts @@ -1,7 +1,9 @@ /** * Support Domain * - * Exports all support-related contracts, schemas, and provider mappers. + * Exports support contracts + schemas. + * + * Provider adapters (BFF-only) live under: `@customer-portal/domain/support/providers`. * * Types are derived from Zod schemas (Schema-First Approach) */ diff --git a/packages/domain/support/providers/index.ts b/packages/domain/support/providers/index.ts index 99fcf690..f78c7fb5 100644 --- a/packages/domain/support/providers/index.ts +++ b/packages/domain/support/providers/index.ts @@ -1,17 +1,5 @@ /** * Support Domain - Providers */ - -import * as SalesforceMapper from "./salesforce/mapper.js"; -import * as SalesforceRaw from "./salesforce/raw.types.js"; - -export const Salesforce = { - ...SalesforceMapper, - ...SalesforceRaw, - mapper: SalesforceMapper, - raw: SalesforceRaw, -}; - -export { SalesforceMapper, SalesforceRaw }; export * from "./salesforce/mapper.js"; export * from "./salesforce/raw.types.js"; diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json index 1eed2b4a..7ba8c14f 100644 --- a/packages/domain/tsconfig.json +++ b/packages/domain/tsconfig.json @@ -7,7 +7,12 @@ "outDir": "./dist", "rootDir": ".", "sourceMap": true, - "tsBuildInfoFile": "./dist/.tsbuildinfo" + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "baseUrl": ".", + "paths": { + "#toolkit/*": ["./toolkit/*"], + "#internal/services/providers/salesforce": ["./services/providers/salesforce/index.ts"] + } }, "include": [ "auth/**/*",