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
|
### Repo boundaries (non-negotiable)
|
||||||
- Check `docs/development/` for implementation patterns
|
|
||||||
- Review `docs/architecture/` for system design
|
|
||||||
- Check `docs/integrations/` for external API details
|
|
||||||
|
|
||||||
**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)
|
||||||
|
|
||||||
```
|
Examples:
|
||||||
apps/
|
|
||||||
portal/ # Next.js 15 frontend (React 19)
|
|
||||||
bff/ # NestJS 11 Backend-for-Frontend
|
|
||||||
packages/
|
|
||||||
domain/ # Pure domain types/schemas/utils (isomorphic)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Technology Stack
|
```ts
|
||||||
|
// ✅ correct (apps)
|
||||||
- **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!)
|
|
||||||
import type { Invoice } from "@customer-portal/domain/billing";
|
import type { Invoice } from "@customer-portal/domain/billing";
|
||||||
|
import { invoiceSchema } from "@customer-portal/domain/billing";
|
||||||
|
|
||||||
// Utility imports
|
// ✅ correct (BFF integrations only)
|
||||||
import { apiClient } from "@/lib/api";
|
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
|
### Portal rules (Next.js)
|
||||||
// ✅ 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(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// ❌ WRONG: Will fail on URL strings
|
- **Pages are wrappers** under `apps/portal/src/app/**` (no API calls in pages)
|
||||||
export const paginationSchema = z.object({
|
- **Feature modules own logic** under `apps/portal/src/features/<feature>/**`
|
||||||
page: z.number().optional(), // "1" !== 1
|
- hooks (React Query)
|
||||||
});
|
- services (API client calls)
|
||||||
```
|
- components/views (UI composition)
|
||||||
|
- **No provider imports** from domain in Portal.
|
||||||
|
|
||||||
### Request Body Validation
|
### Naming & safety
|
||||||
|
|
||||||
```typescript
|
- No `any` in public APIs
|
||||||
// In domain schema
|
- Avoid unsafe assertions
|
||||||
export const createOrderRequestSchema = z.object({
|
- No `console.log` in production code (use logger)
|
||||||
items: z.array(orderItemSchema).min(1),
|
- Avoid `V2` suffix in service names
|
||||||
shippingAddressId: z.string().uuid(),
|
|
||||||
});
|
|
||||||
export type CreateOrderRequest = z.infer<typeof createOrderRequestSchema>;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Form Validation (Frontend)
|
### References
|
||||||
|
|
||||||
```typescript
|
- `docs/development/domain/import-hygiene.md`
|
||||||
import { useForm } from "react-hook-form";
|
- `docs/development/bff/integration-patterns.md`
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
- `docs/development/portal/architecture.md`
|
||||||
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
|
|
||||||
|
|
||||||
|
|||||||
@ -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 { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
||||||
import { ServicesModule } from "@bff/modules/services/services.module.js";
|
import { ServicesModule } from "@bff/modules/services/services.module.js";
|
||||||
import { OrdersModule } from "@bff/modules/orders/orders.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 { SubscriptionsModule } from "@bff/modules/subscriptions/subscriptions.module.js";
|
||||||
import { CurrencyModule } from "@bff/modules/currency/currency.module.js";
|
import { CurrencyModule } from "@bff/modules/currency/currency.module.js";
|
||||||
import { SupportModule } from "@bff/modules/support/support.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,
|
MappingsModule,
|
||||||
ServicesModule,
|
ServicesModule,
|
||||||
OrdersModule,
|
OrdersModule,
|
||||||
InvoicesModule,
|
BillingModule,
|
||||||
SubscriptionsModule,
|
SubscriptionsModule,
|
||||||
CurrencyModule,
|
CurrencyModule,
|
||||||
SupportModule,
|
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 { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
||||||
import { ServicesModule } from "@bff/modules/services/services.module.js";
|
import { ServicesModule } from "@bff/modules/services/services.module.js";
|
||||||
import { OrdersModule } from "@bff/modules/orders/orders.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 { SubscriptionsModule } from "@bff/modules/subscriptions/subscriptions.module.js";
|
||||||
import { CurrencyModule } from "@bff/modules/currency/currency.module.js";
|
import { CurrencyModule } from "@bff/modules/currency/currency.module.js";
|
||||||
import { SecurityModule } from "@bff/core/security/security.module.js";
|
import { SecurityModule } from "@bff/core/security/security.module.js";
|
||||||
@ -24,7 +24,7 @@ export const apiRoutes: Routes = [
|
|||||||
{ path: "", module: MappingsModule },
|
{ path: "", module: MappingsModule },
|
||||||
{ path: "", module: ServicesModule },
|
{ path: "", module: ServicesModule },
|
||||||
{ path: "", module: OrdersModule },
|
{ path: "", module: OrdersModule },
|
||||||
{ path: "", module: InvoicesModule },
|
{ path: "", module: BillingModule },
|
||||||
{ path: "", module: SubscriptionsModule },
|
{ path: "", module: SubscriptionsModule },
|
||||||
{ path: "", module: CurrencyModule },
|
{ path: "", module: CurrencyModule },
|
||||||
{ path: "", module: SupportModule },
|
{ 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 { Controller, Get } from "@nestjs/common";
|
||||||
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 { SalesforceRequestQueueService } from "@bff/core/queue/services/salesforce-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";
|
import { Public } from "@bff/modules/auth/decorators/public.decorator.js";
|
||||||
|
|
||||||
@Controller("health/queues")
|
@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 };
|
return { isValid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateToken(_token: string): void {
|
invalidateToken(): void {
|
||||||
// Stateless tokens are tied to the secret cookie; rotate cookie to invalidate.
|
// Stateless tokens are tied to the secret cookie; rotate cookie to invalidate.
|
||||||
this.logger.debug("invalidateToken called for stateless CSRF token");
|
this.logger.debug("invalidateToken called for stateless CSRF token");
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateSessionTokens(_sessionId: string): void {
|
invalidateSessionTokens(): void {
|
||||||
this.logger.debug("invalidateSessionTokens called - rotate cookie to enforce");
|
this.logger.debug("invalidateSessionTokens called - rotate cookie to enforce");
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateUserTokens(_userId: string): void {
|
invalidateUserTokens(): void {
|
||||||
this.logger.debug("invalidateUserTokens called - rotate cookie to enforce");
|
this.logger.debug("invalidateUserTokens called - rotate cookie to enforce");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -159,8 +159,7 @@ export function createDeferredPromise<T>(): {
|
|||||||
// Use native Promise.withResolvers if available (ES2024)
|
// Use native Promise.withResolvers if available (ES2024)
|
||||||
if (
|
if (
|
||||||
"withResolvers" in Promise &&
|
"withResolvers" in Promise &&
|
||||||
typeof (Promise as unknown as { withResolvers?: <_U>() => unknown }).withResolvers ===
|
typeof (Promise as unknown as { withResolvers?: () => unknown }).withResolvers === "function"
|
||||||
"function"
|
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
Promise as unknown as {
|
Promise as unknown as {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { Global, Module } from "@nestjs/common";
|
import { Global, Module } from "@nestjs/common";
|
||||||
import { PrismaService } from "./prisma.service.js";
|
import { PrismaService } from "./prisma.service.js";
|
||||||
|
import { TransactionService } from "./services/transaction.service.js";
|
||||||
|
import { DistributedTransactionService } from "./services/distributed-transaction.service.js";
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PrismaService],
|
providers: [PrismaService, TransactionService, DistributedTransactionService],
|
||||||
exports: [PrismaService],
|
exports: [PrismaService, TransactionService, DistributedTransactionService],
|
||||||
})
|
})
|
||||||
export class PrismaModule {}
|
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 { Injectable, Inject } from "@nestjs/common";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { Prisma } from "@prisma/client";
|
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";
|
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
|
|
||||||
export interface TransactionContext {
|
export interface TransactionContext {
|
||||||
@ -223,7 +223,7 @@ export class TransactionService {
|
|||||||
operation: SimpleTransactionOperation<T>,
|
operation: SimpleTransactionOperation<T>,
|
||||||
options: Omit<TransactionOptions, "autoRollback"> = {}
|
options: Omit<TransactionOptions, "autoRollback"> = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const result = await this.executeTransaction(async (tx, _context) => operation(tx), {
|
const result = await this.executeTransaction(async tx => operation(tx), {
|
||||||
...options,
|
...options,
|
||||||
autoRollback: false,
|
autoRollback: false,
|
||||||
});
|
});
|
||||||
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IdMapping as PrismaIdMapping } from "@prisma/client";
|
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
|
* Maps Prisma IdMapping entity to Domain UserIdMapping type
|
||||||
|
|||||||
@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
import type { User as PrismaUser } from "@prisma/client";
|
import type { User as PrismaUser } from "@prisma/client";
|
||||||
import type { UserAuth } from "@customer-portal/domain/customer";
|
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
|
* Maps Prisma User entity to Domain UserAuth type
|
||||||
@ -39,5 +39,5 @@ export function mapPrismaUserToDomain(user: PrismaUser): UserAuth {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Use domain provider mapper
|
// 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 { BullModule } from "@nestjs/bullmq";
|
||||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||||
import { QUEUE_NAMES } from "./queue.constants.js";
|
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) {
|
function parseRedisConnection(redisUrl: string) {
|
||||||
try {
|
try {
|
||||||
@ -24,6 +27,7 @@ function parseRedisConnection(redisUrl: string) {
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
|
CacheModule,
|
||||||
BullModule.forRootAsync({
|
BullModule.forRootAsync({
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (config: ConfigService) => ({
|
useFactory: (config: ConfigService) => ({
|
||||||
@ -37,6 +41,7 @@ function parseRedisConnection(redisUrl: string) {
|
|||||||
{ name: QUEUE_NAMES.SIM_MANAGEMENT }
|
{ name: QUEUE_NAMES.SIM_MANAGEMENT }
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
exports: [BullModule],
|
providers: [WhmcsRequestQueueService, SalesforceRequestQueueService],
|
||||||
|
exports: [BullModule, WhmcsRequestQueueService, SalesforceRequestQueueService],
|
||||||
})
|
})
|
||||||
export class QueueModule {}
|
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:
|
* Usage:
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* import { ACCOUNT_FIELDS } from "@customer-portal/domain/salesforce";
|
* import { ACCOUNT_FIELDS } from "@bff/integrations/salesforce/constants";
|
||||||
*
|
*
|
||||||
* const eligibilityValue = account[ACCOUNT_FIELDS.eligibility.value];
|
* const eligibilityValue = account[ACCOUNT_FIELDS.eligibility.value];
|
||||||
* ```
|
* ```
|
||||||
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Salesforce Domain
|
* Salesforce Constants
|
||||||
*
|
*
|
||||||
* Centralized Salesforce field maps and constants.
|
* Centralized Salesforce field maps and constants.
|
||||||
* Provides a single source of truth for field names used across
|
* 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 { CanActivate, ExecutionContext } from "@nestjs/common";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import { Logger } from "nestjs-pino";
|
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()
|
@Injectable()
|
||||||
export class SalesforceReadThrottleGuard implements CanActivate {
|
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 { CanActivate, ExecutionContext } from "@nestjs/common";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import { Logger } from "nestjs-pino";
|
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()
|
@Injectable()
|
||||||
export class SalesforceWriteThrottleGuard implements CanActivate {
|
export class SalesforceWriteThrottleGuard implements CanActivate {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
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 { SalesforceService } from "./salesforce.service.js";
|
||||||
import { SalesforceConnection } from "./services/salesforce-connection.service.js";
|
import { SalesforceConnection } from "./services/salesforce-connection.service.js";
|
||||||
import { SalesforceAccountService } from "./services/salesforce-account.service.js";
|
import { SalesforceAccountService } from "./services/salesforce-account.service.js";
|
||||||
|
|||||||
@ -22,10 +22,14 @@ import {
|
|||||||
SALESFORCE_CASE_STATUS,
|
SALESFORCE_CASE_STATUS,
|
||||||
SALESFORCE_CASE_PRIORITY,
|
SALESFORCE_CASE_PRIORITY,
|
||||||
} from "@customer-portal/domain/support/providers";
|
} from "@customer-portal/domain/support/providers";
|
||||||
import * as Providers from "@customer-portal/domain/support/providers";
|
import {
|
||||||
|
buildCaseByIdQuery,
|
||||||
// Access the mapper directly to avoid unbound method issues
|
buildCaseSelectFields,
|
||||||
const salesforceMapper = Providers.Salesforce;
|
buildCasesForAccountQuery,
|
||||||
|
toSalesforcePriority,
|
||||||
|
transformSalesforceCaseToSupportCase,
|
||||||
|
transformSalesforceCasesToSupportCases,
|
||||||
|
} from "@customer-portal/domain/support/providers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parameters for creating a case in Salesforce
|
* Parameters for creating a case in Salesforce
|
||||||
@ -52,10 +56,7 @@ export class SalesforceCaseService {
|
|||||||
const safeAccountId = assertSalesforceId(accountId, "accountId");
|
const safeAccountId = assertSalesforceId(accountId, "accountId");
|
||||||
this.logger.debug({ accountId: safeAccountId }, "Fetching portal cases for account");
|
this.logger.debug({ accountId: safeAccountId }, "Fetching portal cases for account");
|
||||||
|
|
||||||
const soql = Providers.Salesforce.buildCasesForAccountQuery(
|
const soql = buildCasesForAccountQuery(safeAccountId, SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE);
|
||||||
safeAccountId,
|
|
||||||
SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = (await this.sf.query(soql, {
|
const result = (await this.sf.query(soql, {
|
||||||
@ -69,7 +70,7 @@ export class SalesforceCaseService {
|
|||||||
"Portal cases retrieved for account"
|
"Portal cases retrieved for account"
|
||||||
);
|
);
|
||||||
|
|
||||||
return Providers.Salesforce.transformSalesforceCasesToSupportCases(cases);
|
return transformSalesforceCasesToSupportCases(cases);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.logger.error("Failed to fetch cases for account", {
|
this.logger.error("Failed to fetch cases for account", {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@ -88,7 +89,7 @@ export class SalesforceCaseService {
|
|||||||
|
|
||||||
this.logger.debug({ caseId: safeCaseId, accountId: safeAccountId }, "Fetching case by ID");
|
this.logger.debug({ caseId: safeCaseId, accountId: safeAccountId }, "Fetching case by ID");
|
||||||
|
|
||||||
const soql = Providers.Salesforce.buildCaseByIdQuery(
|
const soql = buildCaseByIdQuery(
|
||||||
safeCaseId,
|
safeCaseId,
|
||||||
safeAccountId,
|
safeAccountId,
|
||||||
SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE
|
SALESFORCE_CASE_ORIGIN.PORTAL_WEBSITE
|
||||||
@ -106,7 +107,7 @@ export class SalesforceCaseService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Providers.Salesforce.transformSalesforceCaseToSupportCase(record);
|
return transformSalesforceCaseToSupportCase(record);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.logger.error("Failed to fetch case by ID", {
|
this.logger.error("Failed to fetch case by ID", {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@ -133,7 +134,7 @@ export class SalesforceCaseService {
|
|||||||
// Build case payload with portal defaults
|
// Build case payload with portal defaults
|
||||||
// Convert portal display values to Salesforce API values
|
// Convert portal display values to Salesforce API values
|
||||||
const sfPriority = params.priority
|
const sfPriority = params.priority
|
||||||
? salesforceMapper.toSalesforcePriority(params.priority)
|
? toSalesforcePriority(params.priority)
|
||||||
: SALESFORCE_CASE_PRIORITY.MEDIUM;
|
: SALESFORCE_CASE_PRIORITY.MEDIUM;
|
||||||
|
|
||||||
const casePayload: Record<string, unknown> = {
|
const casePayload: Record<string, unknown> = {
|
||||||
@ -243,7 +244,7 @@ export class SalesforceCaseService {
|
|||||||
private async getCaseByIdInternal(caseId: string): Promise<SalesforceCaseRecord | null> {
|
private async getCaseByIdInternal(caseId: string): Promise<SalesforceCaseRecord | null> {
|
||||||
const safeCaseId = assertSalesforceId(caseId, "caseId");
|
const safeCaseId = assertSalesforceId(caseId, "caseId");
|
||||||
|
|
||||||
const fields = Providers.Salesforce.buildCaseSelectFields().join(", ");
|
const fields = buildCaseSelectFields().join(", ");
|
||||||
const soql = `
|
const soql = `
|
||||||
SELECT ${fields}
|
SELECT ${fields}
|
||||||
FROM Case
|
FROM Case
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Injectable, Inject } from "@nestjs/common";
|
|||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
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 jsforce from "jsforce";
|
||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
import { createPrivateKey } from "node:crypto";
|
import { createPrivateKey } from "node:crypto";
|
||||||
|
|||||||
@ -21,7 +21,10 @@ import type {
|
|||||||
SalesforceOrderItemRecord,
|
SalesforceOrderItemRecord,
|
||||||
SalesforceOrderRecord,
|
SalesforceOrderRecord,
|
||||||
} from "@customer-portal/domain/orders/providers";
|
} 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 type { SalesforceResponse } from "@customer-portal/domain/common/providers";
|
||||||
import { OrderFieldMapService } from "@bff/modules/orders/config/order-field-map.service.js";
|
import { OrderFieldMapService } from "@bff/modules/orders/config/order-field-map.service.js";
|
||||||
|
|
||||||
@ -98,11 +101,7 @@ export class SalesforceOrderService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Use domain mapper - single transformation!
|
// Use domain mapper - single transformation!
|
||||||
return OrderProviders.Salesforce.transformSalesforceOrderDetails(
|
return transformSalesforceOrderDetails(order, orderItems, this.orderFieldMap.fields);
|
||||||
order,
|
|
||||||
orderItems,
|
|
||||||
this.orderFieldMap.fields
|
|
||||||
);
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.logger.error("Failed to fetch order with items", {
|
this.logger.error("Failed to fetch order with items", {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@ -297,7 +296,7 @@ export class SalesforceOrderService {
|
|||||||
(order): order is SalesforceOrderRecord & { Id: string } => typeof order.Id === "string"
|
(order): order is SalesforceOrderRecord & { Id: string } => typeof order.Id === "string"
|
||||||
)
|
)
|
||||||
.map(order =>
|
.map(order =>
|
||||||
OrderProviders.Salesforce.transformSalesforceOrderSummary(
|
transformSalesforceOrderSummary(
|
||||||
order,
|
order,
|
||||||
itemsByOrder[order.Id] ?? [],
|
itemsByOrder[order.Id] ?? [],
|
||||||
this.orderFieldMap.fields
|
this.orderFieldMap.fields
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export class WhmcsCacheService {
|
|||||||
status?: string
|
status?: string
|
||||||
): Promise<InvoiceList | null> {
|
): Promise<InvoiceList | null> {
|
||||||
const key = this.buildInvoicesKey(userId, page, limit, status);
|
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
|
data: InvoiceList
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const key = this.buildInvoicesKey(userId, page, limit, status);
|
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> {
|
async getInvoice(userId: string, invoiceId: number): Promise<Invoice | null> {
|
||||||
const key = this.buildInvoiceKey(userId, invoiceId);
|
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> {
|
async setInvoice(userId: string, invoiceId: number, data: Invoice): Promise<void> {
|
||||||
const key = this.buildInvoiceKey(userId, invoiceId);
|
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> {
|
async getSubscriptionsList(userId: string): Promise<SubscriptionList | null> {
|
||||||
const key = this.buildSubscriptionsKey(userId);
|
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> {
|
async setSubscriptionsList(userId: string, data: SubscriptionList): Promise<void> {
|
||||||
const key = this.buildSubscriptionsKey(userId);
|
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> {
|
async getSubscription(userId: string, subscriptionId: number): Promise<Subscription | null> {
|
||||||
const key = this.buildSubscriptionKey(userId, subscriptionId);
|
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> {
|
async setSubscription(userId: string, subscriptionId: number, data: Subscription): Promise<void> {
|
||||||
const key = this.buildSubscriptionKey(userId, subscriptionId);
|
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
|
limit: number
|
||||||
): Promise<InvoiceList | null> {
|
): Promise<InvoiceList | null> {
|
||||||
const key = this.buildSubscriptionInvoicesKey(userId, subscriptionId, page, limit);
|
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
|
data: InvoiceList
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const key = this.buildSubscriptionInvoicesKey(userId, subscriptionId, page, limit);
|
const key = this.buildSubscriptionInvoicesKey(userId, subscriptionId, page, limit);
|
||||||
await this.set(key, data, "subscriptionInvoices", [
|
await this.set(key, data, "subscriptionInvoices");
|
||||||
`user:${userId}`,
|
|
||||||
`subscription:${subscriptionId}`,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -190,7 +187,7 @@ export class WhmcsCacheService {
|
|||||||
*/
|
*/
|
||||||
async getClientData(clientId: number): Promise<WhmcsClient | null> {
|
async getClientData(clientId: number): Promise<WhmcsClient | null> {
|
||||||
const key = this.buildClientKey(clientId);
|
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) {
|
async setClientData(clientId: number, data: WhmcsClient) {
|
||||||
const key = this.buildClientKey(clientId);
|
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> {
|
async getClientIdByEmail(email: string): Promise<number | null> {
|
||||||
const key = this.buildClientEmailKey(email);
|
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> {
|
async getPaymentMethods(userId: string): Promise<PaymentMethodList | null> {
|
||||||
const key = this.buildPaymentMethodsKey(userId);
|
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> {
|
async setPaymentMethods(userId: string, paymentMethods: PaymentMethodList): Promise<void> {
|
||||||
const key = this.buildPaymentMethodsKey(userId);
|
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> {
|
async getPaymentGateways(): Promise<PaymentGatewayList | null> {
|
||||||
const key = "whmcs:paymentgateways:global";
|
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
|
* 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 {
|
try {
|
||||||
const data = await this.cacheService.get<T>(key);
|
const data = await this.cacheService.get<T>(key);
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -396,14 +393,9 @@ export class WhmcsCacheService {
|
|||||||
/**
|
/**
|
||||||
* Generic set method with configuration
|
* Generic set method with configuration
|
||||||
*/
|
*/
|
||||||
private async set<T>(
|
private async set<T>(key: string, data: T, configKey: string): Promise<void> {
|
||||||
key: string,
|
|
||||||
data: T,
|
|
||||||
_configKey: string,
|
|
||||||
_additionalTags: string[] = []
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
const config = this.cacheConfigs[_configKey];
|
const config = this.cacheConfigs[configKey];
|
||||||
await this.cacheService.set(key, data, config.ttl);
|
await this.cacheService.set(key, data, config.ttl);
|
||||||
this.logger.debug(`Cache set: ${key} (TTL: ${config.ttl}s)`);
|
this.logger.debug(`Cache set: ${key} (TTL: ${config.ttl}s)`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
|||||||
import { WhmcsConfigService } from "../config/whmcs-config.service.js";
|
import { WhmcsConfigService } from "../config/whmcs-config.service.js";
|
||||||
import { WhmcsHttpClientService } from "./whmcs-http-client.service.js";
|
import { WhmcsHttpClientService } from "./whmcs-http-client.service.js";
|
||||||
import { WhmcsErrorHandlerService } from "./whmcs-error-handler.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 {
|
import type {
|
||||||
WhmcsAddClientParams,
|
WhmcsAddClientParams,
|
||||||
WhmcsValidateLoginParams,
|
WhmcsValidateLoginParams,
|
||||||
@ -104,7 +104,7 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle general request errors
|
// 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)) {
|
if (this.isHandledException(error)) {
|
||||||
throw 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.)
|
* 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)) {
|
if (this.isTimeoutError(error)) {
|
||||||
throw new DomainHttpException(ErrorCode.TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
|
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 { getErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
|
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
|
||||||
import { WhmcsCacheService } from "../cache/whmcs-cache.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";
|
import type { WhmcsClient } from "@customer-portal/domain/customer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,7 +43,7 @@ export class WhmcsAccountDiscoveryService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = CustomerProviders.Whmcs.transformWhmcsClientResponse(response);
|
const client = transformWhmcsClientResponse(response);
|
||||||
|
|
||||||
// 3. Cache both the data and the mapping
|
// 3. Cache both the data and the mapping
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -86,7 +86,7 @@ export class WhmcsAccountDiscoveryService {
|
|||||||
throw new NotFoundException(`Client ${clientId} not found`);
|
throw new NotFoundException(`Client ${clientId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = CustomerProviders.Whmcs.transformWhmcsClientResponse(response);
|
const client = transformWhmcsClientResponse(response);
|
||||||
await this.cacheService.setClientData(client.id, client);
|
await this.cacheService.setClientData(client.id, client);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import type {
|
|||||||
WhmcsAddClientResponse,
|
WhmcsAddClientResponse,
|
||||||
WhmcsValidateLoginResponse,
|
WhmcsValidateLoginResponse,
|
||||||
} from "@customer-portal/domain/customer/providers";
|
} 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";
|
import type { WhmcsClient } from "@customer-portal/domain/customer";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -73,7 +73,7 @@ export class WhmcsClientService {
|
|||||||
throw new NotFoundException(`Client ${clientId} not found`);
|
throw new NotFoundException(`Client ${clientId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = CustomerProviders.Whmcs.transformWhmcsClientResponse(response);
|
const client = transformWhmcsClientResponse(response);
|
||||||
await this.cacheService.setClientData(client.id, client);
|
await this.cacheService.setClientData(client.id, client);
|
||||||
|
|
||||||
this.logger.log(`Fetched client details for client ${clientId}`);
|
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 { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js";
|
||||||
import { invoiceListSchema, invoiceSchema } from "@customer-portal/domain/billing";
|
import { invoiceListSchema, invoiceSchema } from "@customer-portal/domain/billing";
|
||||||
import type { Invoice, InvoiceList } 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 { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
|
||||||
import { WhmcsCurrencyService } from "./whmcs-currency.service.js";
|
import { WhmcsCurrencyService } from "./whmcs-currency.service.js";
|
||||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
|
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
|
||||||
@ -165,7 +165,7 @@ export class WhmcsInvoiceService {
|
|||||||
|
|
||||||
// Transform invoice using domain mapper
|
// Transform invoice using domain mapper
|
||||||
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
||||||
const invoice = Providers.Whmcs.transformWhmcsInvoice(response, {
|
const invoice = transformWhmcsInvoice(response, {
|
||||||
defaultCurrencyCode: defaultCurrency.code,
|
defaultCurrencyCode: defaultCurrency.code,
|
||||||
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
||||||
});
|
});
|
||||||
@ -224,7 +224,7 @@ export class WhmcsInvoiceService {
|
|||||||
try {
|
try {
|
||||||
// Transform using domain mapper
|
// Transform using domain mapper
|
||||||
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
||||||
const transformed = Providers.Whmcs.transformWhmcsInvoice(whmcsInvoice, {
|
const transformed = transformWhmcsInvoice(whmcsInvoice, {
|
||||||
defaultCurrencyCode: defaultCurrency.code,
|
defaultCurrencyCode: defaultCurrency.code,
|
||||||
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import type {
|
|||||||
WhmcsAddOrderResponse,
|
WhmcsAddOrderResponse,
|
||||||
WhmcsOrderResult,
|
WhmcsOrderResult,
|
||||||
} from "@customer-portal/domain/orders/providers";
|
} from "@customer-portal/domain/orders/providers";
|
||||||
import * as Providers from "@customer-portal/domain/orders/providers";
|
import { buildWhmcsAddOrderPayload } from "@customer-portal/domain/orders/providers";
|
||||||
import {
|
import {
|
||||||
whmcsAddOrderResponseSchema,
|
whmcsAddOrderResponseSchema,
|
||||||
whmcsAcceptOrderResponseSchema,
|
whmcsAcceptOrderResponseSchema,
|
||||||
@ -226,7 +226,7 @@ export class WhmcsOrderService {
|
|||||||
* Delegates to shared mapper function from integration package
|
* Delegates to shared mapper function from integration package
|
||||||
*/
|
*/
|
||||||
private buildAddOrderPayload(params: WhmcsAddOrderParams): Record<string, unknown> {
|
private buildAddOrderPayload(params: WhmcsAddOrderParams): Record<string, unknown> {
|
||||||
const payload = Providers.Whmcs.buildWhmcsAddOrderPayload(params);
|
const payload = buildWhmcsAddOrderPayload(params);
|
||||||
|
|
||||||
this.logger.debug("Built WHMCS AddOrder payload", {
|
this.logger.debug("Built WHMCS AddOrder payload", {
|
||||||
clientId: params.clientId,
|
clientId: params.clientId,
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { Injectable, Inject } from "@nestjs/common";
|
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 {
|
import type {
|
||||||
PaymentMethodList,
|
PaymentMethodList,
|
||||||
PaymentGateway,
|
PaymentGateway,
|
||||||
@ -9,7 +12,7 @@ import type {
|
|||||||
PaymentMethod,
|
PaymentMethod,
|
||||||
} from "@customer-portal/domain/payments";
|
} from "@customer-portal/domain/payments";
|
||||||
import type { WhmcsCatalogProductNormalized } from "@customer-portal/domain/services/providers";
|
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 { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
|
||||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
|
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
|
||||||
import type { WhmcsCreateSsoTokenParams } from "@customer-portal/domain/customer/providers";
|
import type { WhmcsCreateSsoTokenParams } from "@customer-portal/domain/customer/providers";
|
||||||
@ -61,7 +64,7 @@ export class WhmcsPaymentService {
|
|||||||
let methods = paymentMethodsArray
|
let methods = paymentMethodsArray
|
||||||
.map((pm: WhmcsPaymentMethod) => {
|
.map((pm: WhmcsPaymentMethod) => {
|
||||||
try {
|
try {
|
||||||
return Providers.Whmcs.transformWhmcsPaymentMethod(pm);
|
return transformWhmcsPaymentMethod(pm);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to transform payment method`, {
|
this.logger.error(`Failed to transform payment method`, {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@ -136,7 +139,7 @@ export class WhmcsPaymentService {
|
|||||||
const gateways = response.gateways.gateway
|
const gateways = response.gateways.gateway
|
||||||
.map((whmcsGateway: WhmcsPaymentGateway) => {
|
.map((whmcsGateway: WhmcsPaymentGateway) => {
|
||||||
try {
|
try {
|
||||||
return Providers.Whmcs.transformWhmcsPaymentGateway(whmcsGateway);
|
return transformWhmcsPaymentGateway(whmcsGateway);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to transform payment gateway ${whmcsGateway.name}`, {
|
this.logger.error(`Failed to transform payment gateway ${whmcsGateway.name}`, {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@ -225,7 +228,7 @@ export class WhmcsPaymentService {
|
|||||||
async getProducts(): Promise<WhmcsCatalogProductNormalized[]> {
|
async getProducts(): Promise<WhmcsCatalogProductNormalized[]> {
|
||||||
try {
|
try {
|
||||||
const response = await this.connectionService.getCatalogProducts();
|
const response = await this.connectionService.getCatalogProducts();
|
||||||
return CatalogProviders.Whmcs.transformWhmcsCatalogProductsResponse(response);
|
return transformWhmcsCatalogProductsResponse(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error("Failed to get products", {
|
this.logger.error("Failed to get products", {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
|
|||||||
@ -2,7 +2,10 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
|||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
||||||
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js";
|
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 type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions";
|
||||||
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
|
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
|
||||||
import { WhmcsCurrencyService } from "./whmcs-currency.service.js";
|
import { WhmcsCurrencyService } from "./whmcs-currency.service.js";
|
||||||
@ -38,7 +41,7 @@ export class WhmcsSubscriptionService {
|
|||||||
|
|
||||||
// Apply status filter if needed
|
// Apply status filter if needed
|
||||||
if (filters.status) {
|
if (filters.status) {
|
||||||
return Providers.Whmcs.filterSubscriptionsByStatus(cached, filters.status);
|
return filterSubscriptionsByStatus(cached, filters.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cached;
|
return cached;
|
||||||
@ -65,7 +68,7 @@ export class WhmcsSubscriptionService {
|
|||||||
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
||||||
let result: SubscriptionList;
|
let result: SubscriptionList;
|
||||||
try {
|
try {
|
||||||
result = Providers.Whmcs.transformWhmcsSubscriptionListResponse(rawResponse, {
|
result = transformWhmcsSubscriptionListResponse(rawResponse, {
|
||||||
defaultCurrencyCode: defaultCurrency.code,
|
defaultCurrencyCode: defaultCurrency.code,
|
||||||
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
||||||
onItemError: (error, product) => {
|
onItemError: (error, product) => {
|
||||||
@ -92,7 +95,7 @@ export class WhmcsSubscriptionService {
|
|||||||
|
|
||||||
// Apply status filter if needed
|
// Apply status filter if needed
|
||||||
if (filters.status) {
|
if (filters.status) {
|
||||||
return Providers.Whmcs.filterSubscriptionsByStatus(result, filters.status);
|
return filterSubscriptionsByStatus(result, filters.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -151,7 +154,7 @@ export class WhmcsSubscriptionService {
|
|||||||
|
|
||||||
// Transform response
|
// Transform response
|
||||||
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
||||||
const resultList = Providers.Whmcs.transformWhmcsSubscriptionListResponse(rawResponse, {
|
const resultList = transformWhmcsSubscriptionListResponse(rawResponse, {
|
||||||
defaultCurrencyCode: defaultCurrency.code,
|
defaultCurrencyCode: defaultCurrency.code,
|
||||||
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
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 { WhmcsCacheService } from "./cache/whmcs-cache.service.js";
|
||||||
import { WhmcsService } from "./whmcs.service.js";
|
import { WhmcsService } from "./whmcs.service.js";
|
||||||
import { WhmcsInvoiceService } from "./services/whmcs-invoice.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 { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions";
|
||||||
import type { PaymentMethodList, PaymentGatewayList } from "@customer-portal/domain/payments";
|
import type { PaymentMethodList, PaymentGatewayList } from "@customer-portal/domain/payments";
|
||||||
import { addressSchema, type Address, type WhmcsClient } from "@customer-portal/domain/customer";
|
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 { WhmcsConnectionOrchestratorService } from "./connection/services/whmcs-connection-orchestrator.service.js";
|
||||||
import { WhmcsInvoiceService } from "./services/whmcs-invoice.service.js";
|
import { WhmcsInvoiceService } from "./services/whmcs-invoice.service.js";
|
||||||
import type { InvoiceFilters } 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> {
|
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;
|
if (Object.keys(updateData).length === 0) return;
|
||||||
await this.clientService.updateClient(clientId, updateData);
|
await this.clientService.updateClient(clientId, updateData);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export class TokenBlacklistService {
|
|||||||
this.failClosed = this.configService.get("AUTH_BLACKLIST_FAIL_CLOSED", "false") === "true";
|
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
|
// Validate token format first
|
||||||
if (!token || typeof token !== "string" || token.split(".").length !== 3) {
|
if (!token || typeof token !== "string" || token.split(".").length !== 3) {
|
||||||
this.logger.warn("Invalid token format provided for blacklisting");
|
this.logger.warn("Invalid token format provided for blacklisting");
|
||||||
|
|||||||
@ -27,9 +27,8 @@ import {
|
|||||||
type ValidateSignupRequest,
|
type ValidateSignupRequest,
|
||||||
} from "@customer-portal/domain/auth";
|
} from "@customer-portal/domain/auth";
|
||||||
import { ErrorCode } from "@customer-portal/domain/common";
|
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 { 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 { CacheService } from "@bff/infra/cache/cache.service.js";
|
||||||
import {
|
import {
|
||||||
PORTAL_SOURCE_NEW_SIGNUP,
|
PORTAL_SOURCE_NEW_SIGNUP,
|
||||||
@ -38,11 +37,6 @@ import {
|
|||||||
} from "@bff/modules/auth/constants/portal.constants.js";
|
} from "@bff/modules/auth/constants/portal.constants.js";
|
||||||
import type { AuthResultInternal } from "@bff/modules/auth/auth.types.js";
|
import type { AuthResultInternal } from "@bff/modules/auth/auth.types.js";
|
||||||
|
|
||||||
type _SanitizedPrismaUser = Omit<
|
|
||||||
PrismaUser,
|
|
||||||
"passwordHash" | "failedLoginAttempts" | "lockedUntil"
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface SignupAccountSnapshot {
|
interface SignupAccountSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
Name?: string | null;
|
Name?: string | null;
|
||||||
@ -358,8 +352,7 @@ export class SignupWorkflowService {
|
|||||||
postcode: address.postcode,
|
postcode: address.postcode,
|
||||||
country: address.country,
|
country: address.country,
|
||||||
password2: password,
|
password2: password,
|
||||||
customfields:
|
customfields: serializeWhmcsKeyValueMap(customfieldsMap) || undefined,
|
||||||
CustomerProviders.Whmcs.serializeWhmcsKeyValueMap(customfieldsMap) || undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log("WHMCS client created successfully", {
|
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 { SalesforceService } from "@bff/integrations/salesforce/salesforce.service.js";
|
||||||
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.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 type { User } from "@customer-portal/domain/customer";
|
||||||
import {
|
import {
|
||||||
PORTAL_SOURCE_MIGRATED,
|
PORTAL_SOURCE_MIGRATED,
|
||||||
@ -115,11 +115,8 @@ export class WhmcsLinkWorkflowService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customerNumber =
|
const customerNumber =
|
||||||
CustomerProviders.Whmcs.getCustomFieldValue(clientDetails.customfields, "198")?.trim() ??
|
getCustomFieldValue(clientDetails.customfields, "198")?.trim() ??
|
||||||
CustomerProviders.Whmcs.getCustomFieldValue(
|
getCustomFieldValue(clientDetails.customfields, "Customer Number")?.trim();
|
||||||
clientDetails.customfields,
|
|
||||||
"Customer Number"
|
|
||||||
)?.trim();
|
|
||||||
|
|
||||||
if (!customerNumber) {
|
if (!customerNumber) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
|
|||||||
@ -231,7 +231,7 @@ export class AuthController {
|
|||||||
@UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard)
|
@UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard)
|
||||||
@RateLimit({ limit: 5, ttl: 600 }) // 5 attempts per 10 minutes per IP (industry standard)
|
@RateLimit({ limit: 5, ttl: 600 }) // 5 attempts per 10 minutes per IP (industry standard)
|
||||||
@ZodResponse({ status: 200, description: "Migrate/link account", type: LinkWhmcsResponseDto })
|
@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);
|
const result = await this.authFacade.linkWhmcsUser(linkData);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,13 +39,13 @@ class PaymentGatewayListDto extends createZodDto(paymentGatewayListSchema) {}
|
|||||||
class InvoicePaymentLinkDto extends createZodDto(invoicePaymentLinkSchema) {}
|
class InvoicePaymentLinkDto extends createZodDto(invoicePaymentLinkSchema) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice Controller
|
* Billing Controller
|
||||||
*
|
*
|
||||||
* All request validation is handled by Zod schemas via global ZodValidationPipe.
|
* All request validation is handled by Zod schemas via global ZodValidationPipe.
|
||||||
* Business logic is delegated to service layer.
|
* Business logic is delegated to service layer.
|
||||||
*/
|
*/
|
||||||
@Controller("invoices")
|
@Controller("invoices")
|
||||||
export class InvoicesController {
|
export class BillingController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly invoicesService: InvoicesOrchestratorService,
|
private readonly invoicesService: InvoicesOrchestratorService,
|
||||||
private readonly whmcsService: WhmcsService,
|
private readonly whmcsService: WhmcsService,
|
||||||
@ -102,10 +102,7 @@ export class InvoicesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(":id/subscriptions")
|
@Get(":id/subscriptions")
|
||||||
getInvoiceSubscriptions(
|
getInvoiceSubscriptions(): Subscription[] {
|
||||||
@Request() _req: RequestWithUser,
|
|
||||||
@Param() _params: InvoiceIdParamDto
|
|
||||||
): Subscription[] {
|
|
||||||
// This functionality has been moved to WHMCS directly
|
// This functionality has been moved to WHMCS directly
|
||||||
// For now, return empty array as subscriptions are managed in WHMCS
|
// For now, return empty array as subscriptions are managed in WHMCS
|
||||||
return [];
|
return [];
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Module } from "@nestjs/common";
|
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 { WhmcsModule } from "@bff/integrations/whmcs/whmcs.module.js";
|
||||||
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
||||||
// New modular invoice services
|
// New modular invoice services
|
||||||
@ -8,15 +8,15 @@ import { InvoiceRetrievalService } from "./services/invoice-retrieval.service.js
|
|||||||
import { InvoiceHealthService } from "./services/invoice-health.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).
|
* Validation is handled by Zod schemas via Zod DTOs + the global ZodValidationPipe (APP_PIPE).
|
||||||
* No separate validator service needed.
|
* No separate validator service needed.
|
||||||
*/
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
imports: [WhmcsModule, MappingsModule],
|
imports: [WhmcsModule, MappingsModule],
|
||||||
controllers: [InvoicesController],
|
controllers: [BillingController],
|
||||||
providers: [InvoicesOrchestratorService, InvoiceRetrievalService, InvoiceHealthService],
|
providers: [InvoicesOrchestratorService, InvoiceRetrievalService, InvoiceHealthService],
|
||||||
exports: [InvoicesOrchestratorService],
|
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 "./billing.module.js";
|
||||||
export * from "./invoices.controller.js";
|
export * from "./billing.controller.js";
|
||||||
export * from "./services/invoices-orchestrator.service.js";
|
export * from "./services/invoices-orchestrator.service.js";
|
||||||
export * from "./services/invoice-retrieval.service.js";
|
export * from "./services/invoice-retrieval.service.js";
|
||||||
export * from "./services/invoice-health.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 { HealthController } from "./health.controller.js";
|
||||||
import { PrismaModule } from "@bff/infra/database/prisma.module.js";
|
import { PrismaModule } from "@bff/infra/database/prisma.module.js";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
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";
|
import { QueueHealthController } from "@bff/core/health/queue-health.controller.js";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Injectable, Inject } from "@nestjs/common";
|
import { Injectable, Inject } from "@nestjs/common";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { CacheService } from "@bff/infra/cache/cache.service.js";
|
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";
|
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Normalized types for mapping portal users to external systems.
|
* 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 {
|
export interface UserIdMapping {
|
||||||
id: string;
|
id: string;
|
||||||
@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* ID Mapping Domain
|
* ID Mapping Domain
|
||||||
|
*
|
||||||
|
* Types, schemas, and validation for mapping portal users to external systems.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./contract.js";
|
export * from "./contract.js";
|
||||||
@ -16,7 +16,7 @@ import type {
|
|||||||
UpdateMappingRequest,
|
UpdateMappingRequest,
|
||||||
MappingSearchFilters,
|
MappingSearchFilters,
|
||||||
MappingStats,
|
MappingStats,
|
||||||
} from "@customer-portal/domain/mappings";
|
} from "./domain/index.js";
|
||||||
import {
|
import {
|
||||||
createMappingRequestSchema,
|
createMappingRequestSchema,
|
||||||
updateMappingRequestSchema,
|
updateMappingRequestSchema,
|
||||||
@ -25,7 +25,7 @@ import {
|
|||||||
checkMappingCompleteness,
|
checkMappingCompleteness,
|
||||||
sanitizeCreateRequest,
|
sanitizeCreateRequest,
|
||||||
sanitizeUpdateRequest,
|
sanitizeUpdateRequest,
|
||||||
} from "@customer-portal/domain/mappings";
|
} from "./domain/index.js";
|
||||||
import type { Prisma, IdMapping as PrismaIdMapping } from "@prisma/client";
|
import type { Prisma, IdMapping as PrismaIdMapping } from "@prisma/client";
|
||||||
import { mapPrismaMappingToDomain } from "@bff/infra/mappers/index.js";
|
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
|
* BFF-specific mapping types
|
||||||
@ -18,4 +18,4 @@ export interface CachedMapping {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-export validation result from domain for backward compatibility
|
// 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,
|
type SalesforceOrderFieldMap,
|
||||||
} from "@customer-portal/domain/orders/providers";
|
} 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> = {
|
const SECTION_PREFIX: Record<keyof SalesforceOrderFieldMap, string> = {
|
||||||
order: "ORDER",
|
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 { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
||||||
import { UsersModule } from "@bff/modules/users/users.module.js";
|
import { UsersModule } from "@bff/modules/users/users.module.js";
|
||||||
import { CoreConfigModule } from "@bff/core/config/config.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 { ServicesModule } from "@bff/modules/services/services.module.js";
|
||||||
import { CacheModule } from "@bff/infra/cache/cache.module.js";
|
import { CacheModule } from "@bff/infra/cache/cache.module.js";
|
||||||
import { VerificationModule } from "@bff/modules/verification/verification.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,
|
MappingsModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
CoreConfigModule,
|
CoreConfigModule,
|
||||||
DatabaseModule,
|
|
||||||
ServicesModule,
|
ServicesModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
VerificationModule,
|
VerificationModule,
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export class OrderBuilder {
|
|||||||
this.addSimFields(orderFields, body, orderFieldNames);
|
this.addSimFields(orderFields, body, orderFieldNames);
|
||||||
break;
|
break;
|
||||||
case "VPN":
|
case "VPN":
|
||||||
this.addVpnFields(orderFields, body);
|
this.addVpnFields();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,10 +111,7 @@ export class OrderBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addVpnFields(
|
private addVpnFields(): void {
|
||||||
_orderFields: Record<string, unknown>,
|
|
||||||
_body: OrderBusinessValidation
|
|
||||||
): void {
|
|
||||||
// No additional fields for VPN orders at this time.
|
// 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 { OrderFulfillmentValidator } from "./order-fulfillment-validator.service.js";
|
||||||
import { OrderFulfillmentErrorService } from "./order-fulfillment-error.service.js";
|
import { OrderFulfillmentErrorService } from "./order-fulfillment-error.service.js";
|
||||||
import { SimFulfillmentService } from "./sim-fulfillment.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 { getErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
import { OrderEventsService } from "./order-events.service.js";
|
import { OrderEventsService } from "./order-events.service.js";
|
||||||
import { OrdersCacheService } from "./orders-cache.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 { NotificationService } from "@bff/modules/notifications/notifications.service.js";
|
||||||
import type { OrderDetails } from "@customer-portal/domain/orders";
|
import type { OrderDetails } from "@customer-portal/domain/orders";
|
||||||
import type { OrderFulfillmentValidationResult } from "@customer-portal/domain/orders/providers";
|
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 { OPPORTUNITY_STAGE } from "@customer-portal/domain/opportunity";
|
||||||
import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications";
|
import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications";
|
||||||
import { salesforceAccountIdSchema } from "@customer-portal/domain/common";
|
import { salesforceAccountIdSchema } from "@customer-portal/domain/common";
|
||||||
@ -26,7 +26,7 @@ import {
|
|||||||
WhmcsOperationException,
|
WhmcsOperationException,
|
||||||
} from "@bff/core/exceptions/domain-exceptions.js";
|
} from "@bff/core/exceptions/domain-exceptions.js";
|
||||||
|
|
||||||
type WhmcsOrderItemMappingResult = ReturnType<typeof OrderProviders.Whmcs.mapOrderToWhmcsItems>;
|
type WhmcsOrderItemMappingResult = ReturnType<typeof mapOrderToWhmcsItems>;
|
||||||
|
|
||||||
export interface OrderFulfillmentStep {
|
export interface OrderFulfillmentStep {
|
||||||
step: string;
|
step: string;
|
||||||
@ -210,7 +210,7 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
return Promise.reject(new Error("Order details are required for mapping"));
|
return Promise.reject(new Error("Order details are required for mapping"));
|
||||||
}
|
}
|
||||||
// Use domain mapper directly - single transformation!
|
// Use domain mapper directly - single transformation!
|
||||||
const result = OrderProviders.Whmcs.mapOrderToWhmcsItems(context.orderDetails);
|
const result = mapOrderToWhmcsItems(context.orderDetails);
|
||||||
mappingResult = result;
|
mappingResult = result;
|
||||||
|
|
||||||
this.logger.log("OrderItems mapped to WHMCS", {
|
this.logger.log("OrderItems mapped to WHMCS", {
|
||||||
@ -240,7 +240,7 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderNotes = OrderProviders.Whmcs.createOrderNotes(
|
const orderNotes = createOrderNotes(
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
`Provisioned from Salesforce Order ${sfOrderId}`
|
`Provisioned from Salesforce Order ${sfOrderId}`
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
} from "@customer-portal/domain/orders";
|
} from "@customer-portal/domain/orders";
|
||||||
import type * as Providers from "@customer-portal/domain/subscriptions/providers";
|
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 { SimServicesService } from "@bff/modules/services/services/sim-services.service.js";
|
||||||
import { InternetServicesService } from "@bff/modules/services/services/internet-services.service.js";
|
import { InternetServicesService } from "@bff/modules/services/services/internet-services.service.js";
|
||||||
import { OrderPricebookService, type PricebookProductMeta } from "./order-pricebook.service.js";
|
import { OrderPricebookService, type PricebookProductMeta } from "./order-pricebook.service.js";
|
||||||
@ -227,7 +227,7 @@ export class OrderValidator {
|
|||||||
|
|
||||||
// 3. SKU validation
|
// 3. SKU validation
|
||||||
const pricebookId = await this.pricebookService.findPortalPricebookId();
|
const pricebookId = await this.pricebookService.findPortalPricebookId();
|
||||||
const _productMeta = await this.validateSKUs(businessValidatedBody.skus, pricebookId);
|
await this.validateSKUs(businessValidatedBody.skus, pricebookId);
|
||||||
|
|
||||||
if (businessValidatedBody.orderType === "SIM") {
|
if (businessValidatedBody.orderType === "SIM") {
|
||||||
const verification = await this.residenceCards.getStatusForUser(userId);
|
const verification = await this.residenceCards.getStatusForUser(userId);
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export class AccountServicesController {
|
|||||||
@Get("vpn/plans")
|
@Get("vpn/plans")
|
||||||
@RateLimit({ limit: 60, ttl: 60 })
|
@RateLimit({ limit: 60, ttl: 60 })
|
||||||
@Header("Cache-Control", "private, no-store")
|
@Header("Cache-Control", "private, no-store")
|
||||||
async getVpnCatalogForAccount(@Request() _req: RequestWithUser): Promise<VpnCatalogCollection> {
|
async getVpnCatalogForAccount(): Promise<VpnCatalogCollection> {
|
||||||
const catalog = await this.vpnCatalog.getCatalogData();
|
const catalog = await this.vpnCatalog.getCatalogData();
|
||||||
return parseVpnCatalog(catalog);
|
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 { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
||||||
import { CoreConfigModule } from "@bff/core/config/config.module.js";
|
import { CoreConfigModule } from "@bff/core/config/config.module.js";
|
||||||
import { CacheModule } from "@bff/infra/cache/cache.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 { BaseServicesService } from "./services/base-services.service.js";
|
||||||
import { InternetServicesService } from "./services/internet-services.service.js";
|
import { InternetServicesService } from "./services/internet-services.service.js";
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import type {
|
|||||||
SalesforcePricebookEntryRecord,
|
SalesforcePricebookEntryRecord,
|
||||||
SalesforceProduct2WithPricebookEntries,
|
SalesforceProduct2WithPricebookEntries,
|
||||||
} from "@customer-portal/domain/services/providers";
|
} 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";
|
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -55,7 +55,7 @@ export class BaseServicesService {
|
|||||||
protected extractPricebookEntry(
|
protected extractPricebookEntry(
|
||||||
record: SalesforceProduct2WithPricebookEntries
|
record: SalesforceProduct2WithPricebookEntries
|
||||||
): SalesforcePricebookEntryRecord | undefined {
|
): SalesforcePricebookEntryRecord | undefined {
|
||||||
const entry = CatalogProviders.Salesforce.extractPricebookEntry(record);
|
const entry = extractSalesforcePricebookEntry(record);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
const sku = record.StockKeepingUnit ?? undefined;
|
const sku = record.StockKeepingUnit ?? undefined;
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
|
|||||||
@ -16,7 +16,11 @@ import {
|
|||||||
inferInstallationTermFromSku,
|
inferInstallationTermFromSku,
|
||||||
internetEligibilityDetailsSchema,
|
internetEligibilityDetailsSchema,
|
||||||
} from "@customer-portal/domain/services";
|
} 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 { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
|
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
|
||||||
import { OpportunityResolutionService } from "@bff/integrations/salesforce/services/opportunity-resolution.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 plans = records.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
const plan = CatalogProviders.Salesforce.mapInternetPlan(record, entry);
|
const plan = mapInternetPlan(record, entry);
|
||||||
return enrichInternetPlanMetadata(plan);
|
return enrichInternetPlanMetadata(plan);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -99,7 +103,7 @@ export class InternetServicesService extends BaseServicesService {
|
|||||||
return records
|
return records
|
||||||
.map(record => {
|
.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
const installation = CatalogProviders.Salesforce.mapInternetInstallation(record, entry);
|
const installation = mapInternetInstallation(record, entry);
|
||||||
return {
|
return {
|
||||||
...installation,
|
...installation,
|
||||||
catalogMetadata: {
|
catalogMetadata: {
|
||||||
@ -140,7 +144,7 @@ export class InternetServicesService extends BaseServicesService {
|
|||||||
return records
|
return records
|
||||||
.map(record => {
|
.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
const addon = CatalogProviders.Salesforce.mapInternetAddon(record, entry);
|
const addon = mapInternetAddon(record, entry);
|
||||||
return {
|
return {
|
||||||
...addon,
|
...addon,
|
||||||
catalogMetadata: {
|
catalogMetadata: {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import type {
|
|||||||
SimActivationFeeCatalogItem,
|
SimActivationFeeCatalogItem,
|
||||||
} from "@customer-portal/domain/services";
|
} from "@customer-portal/domain/services";
|
||||||
import type { SalesforceProduct2WithPricebookEntries } from "@customer-portal/domain/services/providers";
|
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 { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
|
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
@ -45,7 +45,7 @@ export class SimServicesService extends BaseServicesService {
|
|||||||
|
|
||||||
return records.map(record => {
|
return records.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
const product = CatalogProviders.Salesforce.mapSimProduct(record, entry);
|
const product = mapSimProduct(record, entry);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
@ -76,7 +76,7 @@ export class SimServicesService extends BaseServicesService {
|
|||||||
const activationFees = records
|
const activationFees = records
|
||||||
.map(record => {
|
.map(record => {
|
||||||
const entry = this.extractPricebookEntry(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));
|
.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0));
|
||||||
|
|
||||||
@ -130,7 +130,7 @@ export class SimServicesService extends BaseServicesService {
|
|||||||
return records
|
return records
|
||||||
.map(record => {
|
.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
const product = CatalogProviders.Salesforce.mapSimProduct(record, entry);
|
const product = mapSimProduct(record, entry);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { BaseServicesService } from "./base-services.service.js";
|
|||||||
import { ServicesCacheService } from "./services-cache.service.js";
|
import { ServicesCacheService } from "./services-cache.service.js";
|
||||||
import type { VpnCatalogProduct } from "@customer-portal/domain/services";
|
import type { VpnCatalogProduct } from "@customer-portal/domain/services";
|
||||||
import type { SalesforceProduct2WithPricebookEntries } from "@customer-portal/domain/services/providers";
|
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()
|
@Injectable()
|
||||||
export class VpnServicesService extends BaseServicesService {
|
export class VpnServicesService extends BaseServicesService {
|
||||||
@ -32,7 +32,7 @@ export class VpnServicesService extends BaseServicesService {
|
|||||||
|
|
||||||
return records.map(record => {
|
return records.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
const product = CatalogProviders.Salesforce.mapVpnProduct(record, entry);
|
const product = mapVpnProduct(record, entry);
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
description: product.description || product.name,
|
description: product.description || product.name,
|
||||||
@ -64,7 +64,7 @@ export class VpnServicesService extends BaseServicesService {
|
|||||||
|
|
||||||
return records.map(record => {
|
return records.map(record => {
|
||||||
const pricebookEntry = this.extractPricebookEntry(record);
|
const pricebookEntry = this.extractPricebookEntry(record);
|
||||||
const product = CatalogProviders.Salesforce.mapVpnProduct(record, pricebookEntry);
|
const product = mapVpnProduct(record, pricebookEntry);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
|
|||||||
@ -191,7 +191,7 @@ export class SimCallHistoryService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [phoneNumber, dateStr, timeStr, sentTo, _callType, smsTypeStr] = columns;
|
const [phoneNumber, dateStr, timeStr, sentTo, , smsTypeStr] = columns;
|
||||||
|
|
||||||
// Parse date
|
// Parse date
|
||||||
const smsDate = this.parseDate(dateStr);
|
const smsDate = this.parseDate(dateStr);
|
||||||
|
|||||||
@ -107,13 +107,13 @@ export class SimValidationService {
|
|||||||
const expectedEid = "89049032000001000000043598005455";
|
const expectedEid = "89049032000001000000043598005455";
|
||||||
|
|
||||||
const foundSimNumber = Object.entries(subscription.customFields || {}).find(
|
const foundSimNumber = Object.entries(subscription.customFields || {}).find(
|
||||||
([_key, value]) =>
|
([, value]) =>
|
||||||
value !== undefined &&
|
value !== undefined &&
|
||||||
value !== null &&
|
value !== null &&
|
||||||
this.formatCustomFieldValue(value).includes(expectedSimNumber)
|
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;
|
if (value === undefined || value === null) return false;
|
||||||
return this.formatCustomFieldValue(value).includes(expectedEid);
|
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 { Logger } from "nestjs-pino";
|
||||||
import type * as Providers from "@customer-portal/domain/subscriptions/providers";
|
import type * as Providers from "@customer-portal/domain/subscriptions/providers";
|
||||||
|
|
||||||
type WhmcsProduct = Providers.WhmcsRaw.WhmcsProductRaw;
|
type WhmcsProduct = Providers.WhmcsProductRaw;
|
||||||
|
|
||||||
export interface GetSubscriptionsOptions {
|
export interface GetSubscriptionsOptions {
|
||||||
status?: SubscriptionStatus;
|
status?: SubscriptionStatus;
|
||||||
|
|||||||
@ -15,7 +15,10 @@ import {
|
|||||||
type Address,
|
type Address,
|
||||||
type User,
|
type User,
|
||||||
} from "@customer-portal/domain/customer";
|
} from "@customer-portal/domain/customer";
|
||||||
import * as CustomerProviders from "@customer-portal/domain/customer/providers";
|
import {
|
||||||
|
getCustomFieldValue,
|
||||||
|
mapPrismaUserToUserAuth,
|
||||||
|
} from "@customer-portal/domain/customer/providers";
|
||||||
import {
|
import {
|
||||||
updateCustomerProfileRequestSchema,
|
updateCustomerProfileRequestSchema,
|
||||||
type UpdateCustomerProfileRequest,
|
type UpdateCustomerProfileRequest,
|
||||||
@ -149,7 +152,11 @@ export class UserProfileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allow phone/company/language updates through to WHMCS
|
// 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) {
|
if (Object.keys(whmcsUpdate).length > 0) {
|
||||||
await this.whmcsService.updateClient(mapping.whmcsClientId, whmcsUpdate);
|
await this.whmcsService.updateClient(mapping.whmcsClientId, whmcsUpdate);
|
||||||
}
|
}
|
||||||
@ -432,7 +439,7 @@ export class UserProfileService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const whmcsClient = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
|
const whmcsClient = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
|
||||||
const userAuth = CustomerProviders.Portal.mapPrismaUserToUserAuth(user);
|
const userAuth = mapPrismaUserToUserAuth(user);
|
||||||
const base = combineToUser(userAuth, whmcsClient);
|
const base = combineToUser(userAuth, whmcsClient);
|
||||||
|
|
||||||
// Portal-visible identifiers (read-only). These are stored in WHMCS custom fields.
|
// 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 genderFieldId = this.configService.get<string>("WHMCS_GENDER_FIELD_ID");
|
||||||
|
|
||||||
const rawSfNumber = customerNumberFieldId
|
const rawSfNumber = customerNumberFieldId
|
||||||
? CustomerProviders.Whmcs.getCustomFieldValue(
|
? getCustomFieldValue(whmcsClient.customfields, customerNumberFieldId)
|
||||||
whmcsClient.customfields,
|
|
||||||
customerNumberFieldId
|
|
||||||
)
|
|
||||||
: undefined;
|
: undefined;
|
||||||
const rawDob = dobFieldId
|
const rawDob = dobFieldId
|
||||||
? CustomerProviders.Whmcs.getCustomFieldValue(whmcsClient.customfields, dobFieldId)
|
? getCustomFieldValue(whmcsClient.customfields, dobFieldId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const rawGender = genderFieldId
|
const rawGender = genderFieldId
|
||||||
? CustomerProviders.Whmcs.getCustomFieldValue(whmcsClient.customfields, genderFieldId)
|
? getCustomFieldValue(whmcsClient.customfields, genderFieldId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const sfNumber = rawSfNumber?.trim() ? rawSfNumber.trim() : null;
|
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,
|
addressFormToRequest,
|
||||||
type AddressFormData,
|
type AddressFormData,
|
||||||
} from "@customer-portal/domain/customer";
|
} from "@customer-portal/domain/customer";
|
||||||
import { useZodForm } from "@/hooks/useZodForm";
|
import { useZodForm } from "@/lib/hooks/useZodForm";
|
||||||
|
|
||||||
export function useAddressEdit(initial: AddressFormData) {
|
export function useAddressEdit(initial: AddressFormData) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
type ProfileEditFormData,
|
type ProfileEditFormData,
|
||||||
} from "@customer-portal/domain/customer";
|
} from "@customer-portal/domain/customer";
|
||||||
import { type UpdateCustomerProfileRequest } from "@customer-portal/domain/auth";
|
import { type UpdateCustomerProfileRequest } from "@customer-portal/domain/auth";
|
||||||
import { useZodForm } from "@/hooks/useZodForm";
|
import { useZodForm } from "@/lib/hooks/useZodForm";
|
||||||
|
|
||||||
export function useProfileEdit(initial: ProfileEditFormData) {
|
export function useProfileEdit(initial: ProfileEditFormData) {
|
||||||
const handleSave = useCallback(async (formData: 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 { FormField } from "@/components/molecules/FormField/FormField";
|
||||||
import { useWhmcsLink } from "@/features/auth/hooks";
|
import { useWhmcsLink } from "@/features/auth/hooks";
|
||||||
import { linkWhmcsRequestSchema, type LinkWhmcsResponse } from "@customer-portal/domain/auth";
|
import { linkWhmcsRequestSchema, type LinkWhmcsResponse } from "@customer-portal/domain/auth";
|
||||||
import { useZodForm } from "@/hooks/useZodForm";
|
import { useZodForm } from "@/lib/hooks/useZodForm";
|
||||||
|
|
||||||
interface LinkWhmcsFormProps {
|
interface LinkWhmcsFormProps {
|
||||||
onTransferred?: (result: LinkWhmcsResponse) => void;
|
onTransferred?: (result: LinkWhmcsResponse) => void;
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { Button, Input, ErrorMessage } from "@/components/atoms";
|
|||||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||||
import { useLogin } from "../../hooks/use-auth";
|
import { useLogin } from "../../hooks/use-auth";
|
||||||
import { loginRequestSchema } from "@customer-portal/domain/auth";
|
import { loginRequestSchema } from "@customer-portal/domain/auth";
|
||||||
import { useZodForm } from "@/hooks/useZodForm";
|
import { useZodForm } from "@/lib/hooks/useZodForm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getSafeRedirect } from "@/features/auth/utils/route-protection";
|
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 { Button, Input, ErrorMessage } from "@/components/atoms";
|
||||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||||
import { usePasswordReset } from "../../hooks/use-auth";
|
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 { passwordResetRequestSchema, passwordResetSchema } from "@customer-portal/domain/auth";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import Link from "next/link";
|
|||||||
import { Button, Input, ErrorMessage } from "@/components/atoms";
|
import { Button, Input, ErrorMessage } from "@/components/atoms";
|
||||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||||
import { useWhmcsLink } from "../../hooks/use-auth";
|
import { useWhmcsLink } from "../../hooks/use-auth";
|
||||||
import { useZodForm } from "@/hooks/useZodForm";
|
import { useZodForm } from "@/lib/hooks/useZodForm";
|
||||||
import {
|
import {
|
||||||
setPasswordRequestSchema,
|
setPasswordRequestSchema,
|
||||||
checkPasswordStrength,
|
checkPasswordStrength,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { ErrorMessage } from "@/components/atoms";
|
|||||||
import { useSignupWithRedirect } from "../../hooks/use-auth";
|
import { useSignupWithRedirect } from "../../hooks/use-auth";
|
||||||
import { signupInputSchema, buildSignupRequest } from "@customer-portal/domain/auth";
|
import { signupInputSchema, buildSignupRequest } from "@customer-portal/domain/auth";
|
||||||
import { addressFormSchema } from "@customer-portal/domain/customer";
|
import { addressFormSchema } from "@customer-portal/domain/customer";
|
||||||
import { useZodForm } from "@/hooks/useZodForm";
|
import { useZodForm } from "@/lib/hooks/useZodForm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getSafeRedirect } from "@/features/auth/utils/route-protection";
|
import { getSafeRedirect } from "@/features/auth/utils/route-protection";
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
DevicePhoneMobileIcon,
|
DevicePhoneMobileIcon,
|
||||||
ShieldCheckIcon,
|
ShieldCheckIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { SimManagementSection } from "@/features/sim-management";
|
import { SimManagementSection } from "@/features/sim";
|
||||||
|
|
||||||
interface ServiceManagementSectionProps {
|
interface ServiceManagementSectionProps {
|
||||||
subscriptionId: number;
|
subscriptionId: number;
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { MapPinIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { MapPinIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
import { COUNTRY_OPTIONS, getCountryCodeByName } from "@/lib/constants/countries";
|
import { COUNTRY_OPTIONS, getCountryCodeByName } from "@/lib/constants/countries";
|
||||||
import { useZodForm } from "@/hooks/useZodForm";
|
import { useZodForm } from "@/lib/hooks/useZodForm";
|
||||||
import {
|
import {
|
||||||
addressFormSchema,
|
addressFormSchema,
|
||||||
type AddressFormData,
|
type AddressFormData,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useParams, useRouter } from "next/navigation";
|
|||||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
||||||
import { useAuthStore } from "@/features/auth/services/auth.store";
|
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;
|
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 { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
||||||
import type { SimReissueFullRequest } from "@customer-portal/domain/sim";
|
import type { SimReissueFullRequest } from "@customer-portal/domain/sim";
|
||||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
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";
|
import { Button } from "@/components/atoms";
|
||||||
|
|
||||||
type SimType = "physical" | "esim";
|
type SimType = "physical" | "esim";
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import { PageLayout } from "@/components/templates/PageLayout";
|
|||||||
import { StatusPill } from "@/components/atoms/status-pill";
|
import { StatusPill } from "@/components/atoms/status-pill";
|
||||||
|
|
||||||
const { formatCurrency: sharedFormatCurrency } = Formatting;
|
const { formatCurrency: sharedFormatCurrency } = Formatting;
|
||||||
import { SimManagementSection } from "@/features/sim-management";
|
import { SimManagementSection } from "@/features/sim";
|
||||||
import {
|
import {
|
||||||
getBillingCycleLabel,
|
getBillingCycleLabel,
|
||||||
getSubscriptionStatusVariant,
|
getSubscriptionStatusVariant,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import Link from "next/link";
|
|||||||
import { Button, Input } from "@/components/atoms";
|
import { Button, Input } from "@/components/atoms";
|
||||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
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 { Mail, CheckCircle, MapPin } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
publicContactRequestSchema,
|
publicContactRequestSchema,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export { useLocalStorage } from "./useLocalStorage";
|
export { useLocalStorage } from "./useLocalStorage";
|
||||||
export { useDebounce } from "./useDebounce";
|
export { useDebounce } from "./useDebounce";
|
||||||
export { useMediaQuery, useIsMobile, useIsTablet, useIsDesktop } from "./useMediaQuery";
|
export { useMediaQuery, useIsMobile, useIsTablet, useIsDesktop } from "./useMediaQuery";
|
||||||
|
export { useZodForm } from "./useZodForm";
|
||||||
|
|||||||
@ -99,7 +99,7 @@ export function mapPrismaUserToDomain(user: PrismaUser): AuthenticatedUser {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { IdMapping as PrismaIdMapping } from "@prisma/client";
|
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
|
* Maps Prisma IdMapping entity to Domain UserIdMapping type
|
||||||
|
|||||||
@ -14,6 +14,20 @@
|
|||||||
- **BFF-only (integration/infrastructure)**:
|
- **BFF-only (integration/infrastructure)**:
|
||||||
- `@customer-portal/domain/<module>/providers`
|
- `@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
|
## Quick Reference
|
||||||
|
|
||||||
| Context | Import Pattern | Example |
|
| 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