barsa bb4be98444 feat: Enhance About Us page with animations and updated styles
- Updated typography for headings and paragraphs in AboutUsView.
- Added animation effects for header and sections to improve user experience.
- Refactored section headers to use new display styles.

feat: Implement bilingual address handling in AddressConfirmation

- Integrated JapanAddressForm for ZIP code lookup and bilingual address input.
- Updated state management to handle bilingual addresses and validation.
- Enhanced save functionality to support dual-write to WHMCS and Salesforce.

fix: Adjust Japan Post address mapping to handle nullish values

- Updated address mapping to use nullish coalescing for optional fields.
- Ensured compatibility with API responses that may return null for certain fields.

feat: Add ServiceCard component for displaying services

- Created a flexible ServiceCard component with multiple variants (default, featured, minimal, bento).
- Implemented accent color options and responsive design for better UI.
- Added detailed props documentation and usage examples.

chore: Clean up development scripts

- Removed unnecessary build steps for validation package in manage.sh.
2026-01-14 16:25:06 +09:00
..

@customer-portal/domain

Single Source of Truth for Types and Validation

The @customer-portal/domain package is the centralized domain layer containing all types, Zod validation schemas, and provider-specific adapters for the customer portal application.


📦 Package Structure

packages/domain/
├── common/                      # Shared types and utilities
│   ├── types.ts                # Common types, API responses, pagination
│   ├── schema.ts               # Zod schemas for validation
│   └── index.ts
│
├── auth/                        # Authentication & authorization
│   ├── contract.ts             # User, AuthTokens, AuthResponse types
│   ├── schema.ts               # Login, signup, password validation
│   └── index.ts
│
├── billing/                     # Invoices and billing
│   ├── contract.ts             # Invoice, InvoiceItem, InvoiceList
│   ├── schema.ts               # Zod schemas + query params
│   ├── providers/whmcs/        # WHMCS adapter
│   └── index.ts
│
├── subscriptions/               # Service subscriptions
│   ├── contract.ts             # Subscription, SubscriptionStatus
│   ├── schema.ts               # Zod schemas + query params
│   ├── providers/whmcs/        # WHMCS adapter
│   └── index.ts
│
├── orders/                      # Order management
│   ├── contract.ts             # OrderSummary, OrderDetails
│   ├── schema.ts               # Zod schemas + query params
│   ├── providers/
│   │   ├── salesforce/         # Read orders from Salesforce
│   │   └── whmcs/              # Create orders in WHMCS
│   └── index.ts
│
├── payments/                    # Payment methods & gateways
│   ├── contract.ts             # PaymentMethod, PaymentGateway
│   ├── schema.ts               # Zod validation schemas
│   └── index.ts
│
├── sim/                         # SIM card management
│   ├── contract.ts             # SimDetails, SimUsage
│   ├── schema.ts               # Zod schemas + activation
│   ├── providers/freebit/      # Freebit adapter
│   └── index.ts
│
├── catalog/                     # Product catalog
│   ├── contract.ts             # CatalogProduct types
│   ├── schema.ts               # Product validation
│   └── index.ts
│
├── customer/                    # Customer data
│   ├── contract.ts             # Customer, CustomerAddress
│   ├── schema.ts               # Customer validation
│   └── index.ts
│
└── toolkit/                     # Utilities
    ├── formatting/             # Currency, date formatting
    ├── validation/             # Validation helpers
    └── index.ts

🎯 Design Principles

1. Domain-First Organization

Each business domain owns its:

  • contract.ts - TypeScript interfaces (provider-agnostic)
  • schema.ts - Zod validation schemas (runtime safety)
  • providers/ - Provider-specific adapters (WHMCS, Salesforce, Freebit)

2. Single Source of Truth

  • All types defined in domain package
  • All validation schemas in domain package
  • No duplicate type definitions in apps
  • Shared between frontend (Next.js) and backend (NestJS)

3. Type Safety + Runtime Validation

  • TypeScript provides compile-time type checking
  • Zod schemas provide runtime validation
  • Use z.infer<typeof schema> to derive types from schemas

📚 Usage Guide

Basic Import Pattern

// Import domain types and schemas
import { Invoice, invoiceSchema, InvoiceQueryParams } from "@customer-portal/domain/billing";
import { Subscription, subscriptionSchema } from "@customer-portal/domain/subscriptions";
import { ApiResponse, PaginationParams } from "@customer-portal/domain/common";

API Response Handling

import {
  ApiResponse,
  ApiSuccessResponse,
  ApiErrorResponse,
  apiResponseSchema,
} from "@customer-portal/domain/common";

// Type-safe API responses
const response: ApiResponse<Invoice> = {
  success: true,
  data: {
    /* invoice data */
  },
};

// With validation
const validated = apiResponseSchema(invoiceSchema).parse(rawResponse);

Query Parameters with Validation

import {
  InvoiceQueryParams,
  invoiceQueryParamsSchema
} from "@customer-portal/domain/billing";

// In BFF controller
@Get()
@UsePipes(new ZodValidationPipe(invoiceQueryParamsSchema))
async getInvoices(@Query() query: InvoiceQueryParams) {
  // query is validated and typed
}

// In frontend
const params: InvoiceQueryParams = {
  page: 1,
  limit: 20,
  status: "Unpaid"
};

Form Validation (Frontend)

import { useZodForm } from "@customer-portal/validation";
import { loginRequestSchema, type LoginRequest } from "@customer-portal/domain/auth";

function LoginForm() {
  const form = useZodForm<LoginRequest>({
    schema: loginRequestSchema,
    initialValues: { email: "", password: "" },
    onSubmit: async (data) => {
      // data is validated and typed
      await login(data);
    },
  });

  return (
    <form onSubmit={form.handleSubmit}>
      {/* form fields */}
    </form>
  );
}

Backend Validation (BFF)

import { ZodValidationPipe } from "@bff/core/validation";
import { createOrderRequestSchema, type CreateOrderRequest } from "@customer-portal/domain/orders";

@Controller("orders")
export class OrdersController {
  @Post()
  @UsePipes(new ZodValidationPipe(createOrderRequestSchema))
  async create(@Body() body: CreateOrderRequest) {
    // body is validated by Zod before reaching here
    return this.orderService.create(body);
  }
}

🔧 Common Schemas Reference

API Responses

Schema Description
apiSuccessResponseSchema(dataSchema) Successful API response wrapper
apiErrorResponseSchema Error API response with code/message
apiResponseSchema(dataSchema) Discriminated union of success/error

Pagination & Queries

Schema Description
paginationParamsSchema Page, limit, offset parameters
paginatedResponseSchema(itemSchema) Paginated list response
filterParamsSchema Search, sortBy, sortOrder
queryParamsSchema Combined pagination + filters

Domain-Specific Query Params

Schema Description
invoiceQueryParamsSchema Invoice list filtering (status, dates)
subscriptionQueryParamsSchema Subscription filtering (status, type)
orderQueryParamsSchema Order filtering (status, orderType)

Validation Primitives

Schema Description
emailSchema Email validation (lowercase, trimmed)
passwordSchema Strong password (8+ chars, mixed case, number, special)
nameSchema Name validation (1-100 chars)
phoneSchema Phone number validation
timestampSchema ISO datetime string
dateSchema ISO date string

🚀 Adding New Domain Types

1. Create Domain Files

// packages/domain/my-domain/contract.ts
export interface MyEntity {
  id: string;
  name: string;
  status: "active" | "inactive";
}

export interface MyEntityList {
  entities: MyEntity[];
  totalCount: number;
}

// packages/domain/my-domain/schema.ts
import { z } from "zod";

export const myEntityStatusSchema = z.enum(["active", "inactive"]);

export const myEntitySchema = z.object({
  id: z.string(),
  name: z.string().min(1),
  status: myEntityStatusSchema,
});

export const myEntityListSchema = z.object({
  entities: z.array(myEntitySchema),
  totalCount: z.number().int().nonnegative(),
});

// Query params
export const myEntityQueryParamsSchema = z.object({
  page: z.coerce.number().int().positive().optional(),
  limit: z.coerce.number().int().positive().max(100).optional(),
  status: myEntityStatusSchema.optional(),
});

export type MyEntityQueryParams = z.infer<typeof myEntityQueryParamsSchema>;

// packages/domain/my-domain/index.ts
export * from "./contract";
export * from "./schema";

2. Use in Backend (BFF)

import { ZodValidationPipe } from "@bff/core/validation";
import {
  myEntitySchema,
  myEntityQueryParamsSchema,
  type MyEntity,
  type MyEntityQueryParams,
} from "@customer-portal/domain/my-domain";

@Controller("my-entities")
export class MyEntitiesController {
  @Get()
  @UsePipes(new ZodValidationPipe(myEntityQueryParamsSchema))
  async list(@Query() query: MyEntityQueryParams): Promise<MyEntityList> {
    return this.service.list(query);
  }
}

3. Use in Frontend

import { useQuery } from "@tanstack/react-query";
import { myEntitySchema, type MyEntity } from "@customer-portal/domain/my-domain";

function useMyEntities() {
  return useQuery({
    queryKey: ["my-entities"],
    queryFn: async () => {
      const response = await apiClient.get("/my-entities");
      return myEntitySchema.array().parse(response.data);
    },
  });
}

Validation Best Practices

1. Always Define Both Type and Schema

// ✅ Good - Type and schema together
export const userSchema = z.object({
  id: z.string(),
  email: emailSchema,
});
export type User = z.infer<typeof userSchema>;

// ❌ Bad - Type only (no runtime validation)
export interface User {
  id: string;
  email: string;
}

2. Use Zod Schema Composition

// Base schema
const baseProductSchema = z.object({
  id: z.string(),
  name: z.string(),
});

// Extended schema
export const fullProductSchema = baseProductSchema.extend({
  description: z.string(),
  price: z.number().positive(),
});

3. Query Params Use z.coerce for URL Strings

// ✅ Good - coerce string params to numbers
export const paginationSchema = z.object({
  page: z.coerce.number().int().positive().optional(),
  limit: z.coerce.number().int().positive().optional(),
});

// ❌ Bad - will fail on URL query strings
export const paginationSchema = z.object({
  page: z.number().int().positive().optional(), // "1" !== 1
});

4. Use Refinements for Complex Validation

export const simActivationSchema = z
  .object({
    simType: z.enum(["eSIM", "Physical SIM"]),
    eid: z.string().optional(),
  })
  .refine(data => data.simType !== "eSIM" || (data.eid && data.eid.length >= 15), {
    message: "EID required for eSIM",
    path: ["eid"],
  });

🔄 Migration from Local Types

If you find types defined locally in apps, migrate them to domain:

// ❌ Before: apps/bff/src/modules/invoices/types.ts
export interface InvoiceQuery {
  status?: string;
  page?: number;
}

// ✅ After: packages/domain/billing/schema.ts
export const invoiceQueryParamsSchema = z.object({
  status: invoiceStatusSchema.optional(),
  page: z.coerce.number().int().positive().optional(),
});
export type InvoiceQueryParams = z.infer<typeof invoiceQueryParamsSchema>;

// Update imports
import { InvoiceQueryParams } from "@customer-portal/domain/billing";

📖 Additional Resources

  • Zod Documentation: https://zod.dev/
  • Provider-Aware Architecture: See docs/DOMAIN-STRUCTURE.md
  • Type System: See docs/CONSOLIDATED-TYPE-SYSTEM.md

🤝 Contributing

When adding new types or schemas:

  1. Define types in contract.ts
  2. Add Zod schemas in schema.ts
  3. Export from index.ts
  4. Update this README if adding new patterns
  5. Remove any duplicate types from apps
  6. Update imports to use domain package

Maintained by: Customer Portal Team
Last Updated: October 2025