barsa 7c929eb4dc Update Customer Portal Documentation and Remove Deprecated Files
- Streamlined the README.md for clarity and conciseness.
- Deleted outdated documentation files related to Freebit SIM management, SIM management API data flow, and various architectural guides to reduce clutter and improve maintainability.
- Updated the last modified date in the README to reflect the latest changes.
2025-12-23 15:43:36 +09:00

11 KiB

Package Organization & "Lib" Files Strategy

Status: Implemented
Date: October 2025


🎯 Core Principle: Domain Package = Pure Domain Logic

The @customer-portal/domain package should contain ONLY pure domain logic that is:

  • Framework-agnostic
  • Reusable across frontend and backend
  • Pure TypeScript (no React, no NestJS, no Next.js)
  • No external infrastructure dependencies

📦 Package Structure Matrix

Type Location Examples Reasoning
Domain Types packages/domain/*/contract.ts Invoice, Order, Customer Pure business entities
Validation Schemas packages/domain/*/schema.ts invoiceSchema, orderQueryParamsSchema Runtime validation
Pure Utilities packages/domain/toolkit/ formatCurrency(), parseDate() No framework dependencies
Provider Mappers packages/domain/*/providers/ transformWhmcsInvoice() Data transformation logic
Framework Utils apps/*/src/lib/ or apps/*/src/core/ API clients, React hooks Framework-specific code
Shared Infra packages/validation, packages/logging ZodPipe, useZodForm Framework bridges

📂 Detailed Breakdown

What Belongs in packages/domain/

1. Domain Types & Contracts

// packages/domain/billing/contract.ts
export interface Invoice {
  id: number;
  status: InvoiceStatus;
  total: number;
}

2. Validation Schemas (Zod)

// packages/domain/billing/schema.ts
export const invoiceSchema = z.object({
  id: z.number(),
  status: invoiceStatusSchema,
  total: z.number(),
});

// Domain-specific query params
export const invoiceQueryParamsSchema = z.object({
  page: z.coerce.number().int().positive().optional(),
  status: invoiceStatusSchema.optional(),
  dateFrom: z.string().datetime().optional(),
});

3. Pure Utility Functions

// packages/domain/toolkit/formatting/currency.ts
export function formatCurrency(amount: number, currency: SupportedCurrency): string {
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency,
  }).format(amount);
}

Pure function - no React, no DOM, no framework Business logic - directly related to domain entities Reusable - both frontend and backend can use it

4. Provider Mappers

// packages/domain/billing/providers/whmcs/mapper.ts
export function transformWhmcsInvoice(raw: WhmcsInvoiceRaw): Invoice {
  return {
    id: raw.invoiceid,
    status: mapStatus(raw.status),
    total: parseFloat(raw.total),
  };
}

What Should NOT Be in packages/domain/

1. Framework-Specific API Clients

// ❌ DO NOT put in domain
// apps/portal/src/lib/api/client.ts
import { ApiClient } from "@hey-api/client-fetch"; // ← Framework dependency

export const apiClient = new ApiClient({
  baseUrl: process.env.NEXT_PUBLIC_API_URL, // ← Next.js specific
});

Why?

  • Depends on @hey-api/client-fetch (external library)
  • Uses Next.js environment variables
  • Runtime infrastructure code

2. React Hooks

// ❌ DO NOT put in domain
// apps/portal/src/lib/hooks/useInvoices.ts
import { useQuery } from "@tanstack/react-query";  // ← React dependency

export function useInvoices() {
  return useQuery({ ... });  // ← React-specific
}

Why? React-specific - backend can't use this

3. Error Handling with Framework Dependencies

// ❌ DO NOT put in domain
// apps/portal/src/lib/utils/error-handling.ts
import { ApiError as ClientApiError } from "@/lib/api"; // ← Framework client

export function getErrorInfo(error: unknown): ApiErrorInfo {
  if (error instanceof ClientApiError) {
    // ← Framework-specific error type
    // ...
  }
}

Why? Depends on the API client implementation

4. NestJS-Specific Utilities

// ❌ DO NOT put in domain
// apps/bff/src/core/utils/error.util.ts
export function getErrorMessage(error: unknown): string {
  // Generic error extraction - could be in domain
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}

This one is borderline - it's generic enough it COULD be in domain, but:

  • Only used by backend
  • Not needed for type definitions
  • Better to keep application-specific utils in apps

🎨 Current Architecture (Correct!)

packages/
├── domain/                          # ✅ Pure domain logic
│   ├── billing/
│   │   ├── contract.ts             # ✅ Types
│   │   ├── schema.ts               # ✅ Zod schemas + domain query params
│   │   └── providers/whmcs/        # ✅ Data mappers
│   ├── common/
│   │   ├── types.ts                # ✅ Truly generic types (ApiResponse, PaginationParams)
│   │   └── schema.ts               # ✅ Truly generic schemas (paginationParamsSchema, emailSchema)
│   └── toolkit/
│       ├── formatting/             # ✅ Pure functions (formatCurrency, formatDate)
│       └── validation/             # ✅ Pure validation helpers
│
├── validation/                      # ✅ Framework bridges
│   ├── src/zod-pipe.ts             # NestJS Zod pipe
│   └── src/zod-form.ts             # React Zod form hook
│
└── logging/                         # ✅ Infrastructure
    └── src/logger.ts                # Logging utilities

apps/
├── portal/ (Next.js)
│   └── src/lib/
│       ├── api/                    # ❌ Framework-specific
│       │   ├── client.ts           # API client instance
│       │   └── runtime/            # Generated client code
│       ├── hooks/                  # ❌ React-specific
│       │   └── useAuth.ts          # React Query hooks
│       └── utils/                  # ❌ App-specific
│           ├── error-handling.ts   # Portal error handling
│           └── cn.ts               # Tailwind utility
│
└── bff/ (NestJS)
    └── src/core/
        ├── validation/             # ❌ NestJS-specific
        │   └── zod-validation.filter.ts
        └── utils/                  # ❌ App-specific
            ├── error.util.ts       # BFF error utilities
            └── validation.util.ts  # BFF validation helpers

🔄 Decision Framework: "Should This Be in Domain?"

Ask these questions:

Move to Domain If:

  1. Is it a pure function with no framework dependencies?
  2. Does it work with domain types?
  3. Could both frontend and backend use it?
  4. Is it business logic (not infrastructure)?

Keep in Apps If:

  1. Does it import React, Next.js, or NestJS?
  2. Does it use environment variables?
  3. Does it depend on external libraries (API clients, HTTP libs)?
  4. Is it UI-specific or framework-specific?
  5. Is it only used in one app?

📋 Examples with Decisions

Utility Decision Location Why
formatCurrency(amount, currency) Domain domain/toolkit/formatting/ Pure function, no deps
invoiceQueryParamsSchema Domain domain/billing/schema.ts Domain-specific validation
paginationParamsSchema Domain domain/common/schema.ts Truly generic
useInvoices() React hook Portal App portal/lib/hooks/ React-specific
apiClient instance Portal App portal/lib/api/ Framework-specific
ZodValidationPipe Validation Package packages/validation/ Reusable bridge
getErrorMessage(error) BFF App bff/core/utils/ App-specific utility
transformWhmcsInvoice() Domain domain/billing/providers/whmcs/ Data transformation

🚀 What About Query Parameters?

Current Structure ( Correct!)

Generic building blocks in domain/common/:

// packages/domain/common/schema.ts
export const paginationParamsSchema = z.object({
  page: z.coerce.number().int().positive().optional(),
  limit: z.coerce.number().int().positive().max(100).optional(),
});

export const filterParamsSchema = z.object({
  search: z.string().optional(),
  sortBy: z.string().optional(),
  sortOrder: z.enum(["asc", "desc"]).optional(),
});

Domain-specific query params in their own domains:

// packages/domain/billing/schema.ts
export const invoiceQueryParamsSchema = z.object({
  page: z.coerce.number().int().positive().optional(),
  limit: z.coerce.number().int().positive().max(100).optional(),
  status: invoiceStatusSchema.optional(), // ← Domain-specific
  dateFrom: z.string().datetime().optional(), // ← Domain-specific
  dateTo: z.string().datetime().optional(), // ← Domain-specific
});

// packages/domain/subscriptions/schema.ts
export const subscriptionQueryParamsSchema = z.object({
  page: z.coerce.number().int().positive().optional(),
  limit: z.coerce.number().int().positive().max(100).optional(),
  status: subscriptionStatusSchema.optional(), // ← Domain-specific
  type: z.string().optional(), // ← Domain-specific
});

Why this works:

  • common has truly generic utilities
  • Each domain owns its specific query parameters
  • No duplication of business logic

📖 Summary

Domain Package (packages/domain/)

Contains:

  • Domain types & interfaces
  • Zod validation schemas
  • Provider mappers (WHMCS, Salesforce, Freebit)
  • Pure utility functions (formatting, parsing)
  • Domain-specific query parameter schemas

Does NOT contain:

  • React hooks
  • API client instances
  • Framework-specific code
  • Infrastructure code

App Lib/Core Directories

Contains:

  • Framework-specific utilities
  • API clients & HTTP interceptors
  • React hooks & custom hooks
  • Error handling with framework dependencies
  • Application-specific helpers

Key Takeaway: The domain package is your single source of truth for types and validation. Everything else stays in apps where it belongs!