Refactor Domain Mappings and Update Import Paths
- Removed the domain mappings module, consolidating related types and schemas into the id-mappings feature. - Updated import paths across the BFF to reflect the new structure, ensuring compliance with import hygiene rules. - Cleaned up unused files and optimized the codebase for better maintainability and clarity.
This commit is contained in:
parent
3030d12138
commit
465a62a3e8
574
.cursorrules
574
.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/<module>`
|
||||
- `@customer-portal/domain/toolkit`
|
||||
- **Allowed (BFF only)**:
|
||||
- `@customer-portal/domain/<module>/providers`
|
||||
- **Forbidden everywhere**:
|
||||
- `@customer-portal/domain` (root import)
|
||||
- any deep import like `@customer-portal/domain/<module>/**`
|
||||
- any deep import like `@customer-portal/domain/<module>/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<typeof invoiceSchema>;
|
||||
|
||||
// ❌ 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<InvoiceList> {
|
||||
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<OrderDetails | null> {
|
||||
// 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<InvoiceList>({
|
||||
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<InvoiceList> {
|
||||
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 <InvoicesView />;
|
||||
}
|
||||
```
|
||||
|
||||
**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/<module>/schema.ts`
|
||||
- **Derive types from schemas**: `export type X = z.infer<typeof xSchema>`
|
||||
- **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/<provider>/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/<feature>/**`
|
||||
- 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<typeof createOrderRequestSchema>;
|
||||
```
|
||||
- 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<LoginRequest>({
|
||||
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`
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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 {}
|
||||
@ -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")
|
||||
|
||||
@ -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 {}
|
||||
@ -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");
|
||||
}
|
||||
|
||||
|
||||
@ -159,8 +159,7 @@ export function createDeferredPromise<T>(): {
|
||||
// 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 {
|
||||
|
||||
@ -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 {}
|
||||
|
||||
5
apps/bff/src/infra/database/services/index.ts
Normal file
5
apps/bff/src/infra/database/services/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Database Services
|
||||
*/
|
||||
export * from "./transaction.service.js";
|
||||
export * from "./distributed-transaction.service.js";
|
||||
@ -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<T>,
|
||||
options: Omit<TransactionOptions, "autoRollback"> = {}
|
||||
): Promise<T> {
|
||||
const result = await this.executeTransaction(async (tx, _context) => operation(tx), {
|
||||
const result = await this.executeTransaction(async tx => operation(tx), {
|
||||
...options,
|
||||
autoRollback: false,
|
||||
});
|
||||
@ -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
|
||||
|
||||
@ -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<typeof CustomerProviders.Portal.mapPrismaUserToUserAuth>[0];
|
||||
type PrismaUserRaw = Parameters<typeof mapPrismaUserToUserAuth>[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);
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
5
apps/bff/src/infra/queue/services/index.ts
Normal file
5
apps/bff/src/infra/queue/services/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Queue Services
|
||||
*/
|
||||
export * from "./whmcs-request-queue.service.js";
|
||||
export * from "./salesforce-request-queue.service.js";
|
||||
@ -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];
|
||||
* ```
|
||||
@ -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
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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<string, unknown> = {
|
||||
@ -243,7 +244,7 @@ export class SalesforceCaseService {
|
||||
private async getCaseByIdInternal(caseId: string): Promise<SalesforceCaseRecord | null> {
|
||||
const safeCaseId = assertSalesforceId(caseId, "caseId");
|
||||
|
||||
const fields = Providers.Salesforce.buildCaseSelectFields().join(", ");
|
||||
const fields = buildCaseSelectFields().join(", ");
|
||||
const soql = `
|
||||
SELECT ${fields}
|
||||
FROM Case
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -89,7 +89,7 @@ export class WhmcsCacheService {
|
||||
status?: string
|
||||
): Promise<InvoiceList | null> {
|
||||
const key = this.buildInvoicesKey(userId, page, limit, status);
|
||||
return this.get<InvoiceList>(key, "invoices");
|
||||
return this.get<InvoiceList>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,7 +103,7 @@ export class WhmcsCacheService {
|
||||
data: InvoiceList
|
||||
): Promise<void> {
|
||||
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<Invoice | null> {
|
||||
const key = this.buildInvoiceKey(userId, invoiceId);
|
||||
return this.get<Invoice>(key, "invoice");
|
||||
return this.get<Invoice>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,7 +119,7 @@ export class WhmcsCacheService {
|
||||
*/
|
||||
async setInvoice(userId: string, invoiceId: number, data: Invoice): Promise<void> {
|
||||
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<SubscriptionList | null> {
|
||||
const key = this.buildSubscriptionsKey(userId);
|
||||
return this.get<SubscriptionList>(key, "subscriptions");
|
||||
return this.get<SubscriptionList>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -135,7 +135,7 @@ export class WhmcsCacheService {
|
||||
*/
|
||||
async setSubscriptionsList(userId: string, data: SubscriptionList): Promise<void> {
|
||||
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<Subscription | null> {
|
||||
const key = this.buildSubscriptionKey(userId, subscriptionId);
|
||||
return this.get<Subscription>(key, "subscription");
|
||||
return this.get<Subscription>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -151,7 +151,7 @@ export class WhmcsCacheService {
|
||||
*/
|
||||
async setSubscription(userId: string, subscriptionId: number, data: Subscription): Promise<void> {
|
||||
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<InvoiceList | null> {
|
||||
const key = this.buildSubscriptionInvoicesKey(userId, subscriptionId, page, limit);
|
||||
return this.get<InvoiceList>(key, "subscriptionInvoices");
|
||||
return this.get<InvoiceList>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -178,10 +178,7 @@ export class WhmcsCacheService {
|
||||
data: InvoiceList
|
||||
): Promise<void> {
|
||||
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<WhmcsClient | null> {
|
||||
const key = this.buildClientKey(clientId);
|
||||
return this.get<WhmcsClient>(key, "client");
|
||||
return this.get<WhmcsClient>(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<number | null> {
|
||||
const key = this.buildClientEmailKey(email);
|
||||
return this.get<number>(key, "clientEmail");
|
||||
return this.get<number>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -324,7 +321,7 @@ export class WhmcsCacheService {
|
||||
*/
|
||||
async getPaymentMethods(userId: string): Promise<PaymentMethodList | null> {
|
||||
const key = this.buildPaymentMethodsKey(userId);
|
||||
return this.get<PaymentMethodList>(key, "paymentMethods");
|
||||
return this.get<PaymentMethodList>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -332,7 +329,7 @@ export class WhmcsCacheService {
|
||||
*/
|
||||
async setPaymentMethods(userId: string, paymentMethods: PaymentMethodList): Promise<void> {
|
||||
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<PaymentGatewayList | null> {
|
||||
const key = "whmcs:paymentgateways:global";
|
||||
return this.get<PaymentGatewayList>(key, "paymentGateways");
|
||||
return this.get<PaymentGatewayList>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -380,7 +377,7 @@ export class WhmcsCacheService {
|
||||
/**
|
||||
* Generic get method with configuration
|
||||
*/
|
||||
private async get<T>(key: string, _configKey: string): Promise<T | null> {
|
||||
private async get<T>(key: string): Promise<T | null> {
|
||||
try {
|
||||
const data = await this.cacheService.get<T>(key);
|
||||
if (data) {
|
||||
@ -396,14 +393,9 @@ export class WhmcsCacheService {
|
||||
/**
|
||||
* Generic set method with configuration
|
||||
*/
|
||||
private async set<T>(
|
||||
key: string,
|
||||
data: T,
|
||||
_configKey: string,
|
||||
_additionalTags: string[] = []
|
||||
): Promise<void> {
|
||||
private async set<T>(key: string, data: T, configKey: string): Promise<void> {
|
||||
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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ export class WhmcsErrorHandlerService {
|
||||
/**
|
||||
* Handle general request errors (network, timeout, etc.)
|
||||
*/
|
||||
handleRequestError(error: unknown, _action: string, _params: Record<string, unknown>): never {
|
||||
handleRequestError(error: unknown): never {
|
||||
if (this.isTimeoutError(error)) {
|
||||
throw new DomainHttpException(ErrorCode.TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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<string, unknown> {
|
||||
const payload = Providers.Whmcs.buildWhmcsAddOrderPayload(params);
|
||||
const payload = buildWhmcsAddOrderPayload(params);
|
||||
|
||||
this.logger.debug("Built WHMCS AddOrder payload", {
|
||||
clientId: params.clientId,
|
||||
|
||||
@ -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<WhmcsCatalogProductNormalized[]> {
|
||||
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),
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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<Address>): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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<void> {
|
||||
async blacklistToken(token: string): Promise<void> {
|
||||
// Validate token format first
|
||||
if (!token || typeof token !== "string" || token.split(".").length !== 3) {
|
||||
this.logger.warn("Invalid token format provided for blacklisting");
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 [];
|
||||
@ -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 {}
|
||||
@ -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";
|
||||
@ -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({
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
@ -1,5 +1,7 @@
|
||||
/**
|
||||
* ID Mapping Domain
|
||||
*
|
||||
* Types, schemas, and validation for mapping portal users to external systems.
|
||||
*/
|
||||
|
||||
export * from "./contract.js";
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
type SalesforceOrderFieldMap,
|
||||
} from "@customer-portal/domain/orders/providers";
|
||||
|
||||
const unique = <T>(values: T[]): T[] => Array.from(new Set(values));
|
||||
const unique = (values: string[]): string[] => Array.from(new Set(values));
|
||||
|
||||
const SECTION_PREFIX: Record<keyof SalesforceOrderFieldMap, string> = {
|
||||
order: "ORDER",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<string, unknown>,
|
||||
_body: OrderBusinessValidation
|
||||
): void {
|
||||
private addVpnFields(): void {
|
||||
// No additional fields for VPN orders at this time.
|
||||
}
|
||||
|
||||
|
||||
@ -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<typeof OrderProviders.Whmcs.mapOrderToWhmcsItems>;
|
||||
type WhmcsOrderItemMappingResult = ReturnType<typeof mapOrderToWhmcsItems>;
|
||||
|
||||
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}`
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<VpnCatalogCollection> {
|
||||
async getVpnCatalogForAccount(): Promise<VpnCatalogCollection> {
|
||||
const catalog = await this.vpnCatalog.getCatalogData();
|
||||
return parseVpnCatalog(catalog);
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<string>("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;
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
interface AuroraBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
showRadialGradient?: boolean;
|
||||
}
|
||||
|
||||
export const AuroraBackground = ({
|
||||
className,
|
||||
children,
|
||||
showRadialGradient = true,
|
||||
...props
|
||||
}: AuroraBackgroundProps) => {
|
||||
return (
|
||||
<main>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-bg relative flex h-[100vh] flex-col items-center justify-center bg-zinc-50 text-slate-950 dark:bg-zinc-900",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={
|
||||
{
|
||||
"--aurora":
|
||||
"repeating-linear-gradient(100deg,#3b82f6_10%,#a5b4fc_15%,#93c5fd_20%,#ddd6fe_25%,#60a5fa_30%)",
|
||||
"--dark-gradient":
|
||||
"repeating-linear-gradient(100deg,#000_0%,#000_7%,transparent_10%,transparent_12%,#000_16%)",
|
||||
"--white-gradient":
|
||||
"repeating-linear-gradient(100deg,#fff_0%,#fff_7%,transparent_10%,transparent_12%,#fff_16%)",
|
||||
|
||||
"--blue-300": "#93c5fd",
|
||||
"--blue-400": "#60a5fa",
|
||||
"--blue-500": "#3b82f6",
|
||||
"--indigo-300": "#a5b4fc",
|
||||
"--violet-200": "#ddd6fe",
|
||||
"--black": "#000",
|
||||
"--white": "#fff",
|
||||
"--transparent": "transparent",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
// I'm sorry but this is what peak developer performance looks like // trigger warning
|
||||
className={cn(
|
||||
`after:animate-aurora pointer-events-none absolute -inset-[10px] [background-image:var(--white-gradient),var(--aurora)] [background-size:300%,_200%] [background-position:50%_50%,50%_50%] opacity-50 blur-[10px] invert filter will-change-transform [--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)] [--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)] [--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)] after:absolute after:inset-0 after:[background-image:var(--white-gradient),var(--aurora)] after:[background-size:200%,_100%] after:[background-attachment:fixed] after:mix-blend-difference after:content-[""] dark:[background-image:var(--dark-gradient),var(--aurora)] dark:invert-0 after:dark:[background-image:var(--dark-gradient),var(--aurora)]`,
|
||||
|
||||
showRadialGradient &&
|
||||
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@ -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();
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export { useLocalStorage } from "./useLocalStorage";
|
||||
export { useDebounce } from "./useDebounce";
|
||||
export { useMediaQuery, useIsMobile, useIsTablet, useIsDesktop } from "./useMediaQuery";
|
||||
export { useZodForm } from "./useZodForm";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -14,6 +14,20 @@
|
||||
- **BFF-only (integration/infrastructure)**:
|
||||
- `@customer-portal/domain/<module>/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 |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user