Update Configuration Files and Refactor Code Structure

- Adjusted .prettierrc to ensure consistent formatting with a newline at the end of the file.
- Reformatted eslint.config.mjs for improved readability by aligning array elements.
- Updated pnpm-lock.yaml to use single quotes for consistency across dependencies.
- Simplified worktree setup in .cursor/worktrees.json for cleaner configuration.
- Enhanced documentation in .cursor/plans to clarify architecture refactoring.
- Refactored various service files for improved readability and maintainability, including rate-limiting and auth services.
- Updated imports and exports across multiple files for consistency and clarity.
- Improved error handling and logging in service methods to enhance debugging capabilities.
- Streamlined utility functions for better performance and maintainability across the domain packages.
This commit is contained in:
barsa 2025-12-25 17:30:02 +09:00
parent 75dc6ec15d
commit 1b944f57aa
92 changed files with 509 additions and 456 deletions

View File

@ -1,4 +1,5 @@
<!-- 67f8fea5-b6cb-4187-8097-25ccb37e1dcf fa268fdd-dd67-4003-bb94-8236ed95ab44 -->
# Domain & BFF Clean Architecture Refactoring
## Overview
@ -173,7 +174,7 @@ const result = this.orderWhmcsMapper.mapOrderItemsToWhmcs(items);
With:
```typescript
import { Providers } from '@customer-portal/domain/orders';
import { Providers } from "@customer-portal/domain/orders";
const result = Providers.Whmcs.mapFulfillmentOrderItems(items);
```
@ -346,4 +347,4 @@ Flow: Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directl
- [x] Update orders.module.ts and salesforce.module.ts with new services
- [x] Verify catalog services follow same clean pattern (already correct)
- [x] Update domain README and architecture documentation with clean patterns
- [x] Test order creation and fulfillment flows end-to-end
- [x] Test order creation and fulfillment flows end-to-end

View File

@ -1,5 +1,3 @@
{
"setup-worktree": [
"pnpm install"
]
"setup-worktree": ["pnpm install"]
}

View File

@ -1 +1 @@
"./config/prettier.config.js"
"./config/prettier.config.js"

View File

@ -13,6 +13,7 @@ Prisma embeds the schema path into the generated client. We regenerate the clien
## Directory Structure
### Development (Monorepo)
```
/project-root/
├── apps/bff/
@ -24,6 +25,7 @@ Prisma embeds the schema path into the generated client. We regenerate the clien
```
### Production (Docker Container)
```
/app/
├── prisma/
@ -36,12 +38,12 @@ Prisma embeds the schema path into the generated client. We regenerate the clien
## Commands
| Command | Description |
|---------|-------------|
| Command | Description |
| ------------------ | ---------------------------------------------------------- |
| `pnpm db:generate` | Regenerate Prisma client (`--schema=prisma/schema.prisma`) |
| `pnpm db:migrate` | Run migrations (dev) with explicit schema path |
| `pnpm db:studio` | Open Prisma Studio with explicit schema path |
| `pnpm db:reset` | Reset database with explicit schema path |
| `pnpm db:migrate` | Run migrations (dev) with explicit schema path |
| `pnpm db:studio` | Open Prisma Studio with explicit schema path |
| `pnpm db:reset` | Reset database with explicit schema path |
## Binary Targets

View File

@ -19,4 +19,3 @@ export default defineConfig({
url: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
},
});

View File

@ -1,4 +1,8 @@
export { RateLimitModule } from "./rate-limit.module.js";
export { RateLimitGuard } from "./rate-limit.guard.js";
export { RateLimit, SkipRateLimit, type RateLimitOptions, RATE_LIMIT_KEY } from "./rate-limit.decorator.js";
export {
RateLimit,
SkipRateLimit,
type RateLimitOptions,
RATE_LIMIT_KEY,
} from "./rate-limit.decorator.js";

View File

@ -41,4 +41,3 @@ export const RateLimit = (options: RateLimitOptions) => SetMetadata(RATE_LIMIT_K
* Skip rate limiting for this route (useful when applied at controller level)
*/
export const SkipRateLimit = () => SetMetadata(RATE_LIMIT_KEY, { skip: true } as RateLimitOptions);

View File

@ -124,7 +124,10 @@ export class RateLimitGuard implements CanActivate {
/**
* Get or create a rate limiter for the given options
*/
private getOrCreateLimiter(options: RateLimitOptions, context: ExecutionContext): RateLimiterRedis {
private getOrCreateLimiter(
options: RateLimitOptions,
context: ExecutionContext
): RateLimiterRedis {
const handlerName = context.getHandler().name;
const controllerName = context.getClass().name;
const cacheKey = `${controllerName}:${handlerName}:${options.limit}:${options.ttl}`;
@ -163,4 +166,3 @@ export class RateLimitGuard implements CanActivate {
}
}
}

View File

@ -26,4 +26,3 @@ import { RateLimitGuard } from "./rate-limit.guard.js";
exports: [RateLimitGuard],
})
export class RateLimitModule {}

View File

@ -1,4 +1,10 @@
import { Inject, Injectable, InternalServerErrorException, HttpException, HttpStatus } from "@nestjs/common";
import {
Inject,
Injectable,
InternalServerErrorException,
HttpException,
HttpStatus,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import type { Request } from "express";

View File

@ -39,17 +39,32 @@ export class BaseServicesService {
soql: string,
context: string
): Promise<TRecord[]> {
this.logger.debug(`Executing Salesforce query for ${context}`, {
soql: soql.replace(/\s+/g, " ").trim(),
portalPricebookId: this.portalPriceBookId,
portalCategoryField: this.portalCategoryField,
});
try {
const res = (await this.sf.query(soql, {
label: `services:${context.replace(/\s+/g, "_").toLowerCase()}`,
})) as SalesforceResponse<TRecord>;
return res.records ?? [];
} catch (error: unknown) {
this.logger.error(`Query failed: ${context}`, {
error: getErrorMessage(error),
soql,
context,
const records = res.records ?? [];
this.logger.debug(`Query result for ${context}`, {
recordCount: records.length,
records: records.map(r => ({
id: r.Id,
name: r.Name,
sku: r.StockKeepingUnit,
itemClass: r.Item_Class__c,
hasPricebookEntries: Boolean(r.PricebookEntries?.records?.length),
})),
});
return records;
} catch (error: unknown) {
const errorMessage = getErrorMessage(error);
this.logger.error(`Query failed: ${context} - ${errorMessage}`);
this.logger.error(`Failed SOQL: ${soql.replace(/\s+/g, " ").trim()}`);
return [];
}
}

View File

@ -67,11 +67,7 @@ export class SimServicesService extends BaseServicesService {
return this.catalogCache.getCachedServices(
cacheKey,
async () => {
const soql = this.buildProductQuery("SIM", "Activation", [
"Catalog_Order__c",
"Auto_Add__c",
"Is_Default__c",
]);
const soql = this.buildProductQuery("SIM", "Activation", ["Catalog_Order__c"]);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
"SIM Activation Fees"

View File

@ -55,7 +55,10 @@ export class VpnServicesService extends BaseServicesService {
return this.catalogCache.getCachedServices(
cacheKey,
async () => {
const soql = this.buildProductQuery("VPN", "Activation", ["VPN_Region__c"]);
const soql = this.buildProductQuery("VPN", "Activation", [
"VPN_Region__c",
"Catalog_Order__c",
]);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
"VPN Activation Fees"

View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -5,7 +5,7 @@
"scripts": {
"predev": "node ./scripts/dev-prep.mjs",
"dev": "next dev -p ${NEXT_PORT:-3000}",
"build": "next build --webpack",
"build": "next build",
"build:turbo": "next build",
"build:analyze": "ANALYZE=true next build",
"analyze": "pnpm run build:analyze",
@ -40,6 +40,7 @@
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.1.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "catalog:"
}
}

View File

@ -1,5 +1,6 @@
/* Tailwind CSS v4 */
@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "../styles/tokens.css";
@import "../styles/utilities.css";
@import "../styles/responsive.css";

View File

@ -17,8 +17,7 @@ const errorMessageVariants = cva("flex items-center gap-1 text-sm", {
});
interface ErrorMessageProps
extends React.HTMLAttributes<HTMLParagraphElement>,
VariantProps<typeof errorMessageVariants> {
extends React.HTMLAttributes<HTMLParagraphElement>, VariantProps<typeof errorMessageVariants> {
showIcon?: boolean;
}

View File

@ -1,2 +1 @@
export { AgentforceWidget } from "./AgentforceWidget";

View File

@ -19,7 +19,7 @@ export const AuroraBackground = ({
<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,
className
)}
{...props}
>
@ -51,7 +51,7 @@ export const AuroraBackground = ({
`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%)]`,
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
)}
></div>
</div>

View File

@ -2,4 +2,3 @@ export { AccountStep } from "./AccountStep";
export { AddressStep } from "./AddressStep";
export { PasswordStep } from "./PasswordStep";
export { ReviewStep } from "./ReviewStep";

View File

@ -5,10 +5,7 @@
import type { OrderDisplayItem } from "@customer-portal/domain/orders";
export {
buildOrderDisplayItems,
categorizeOrderItem,
} from "@customer-portal/domain/orders";
export { buildOrderDisplayItems, categorizeOrderItem } from "@customer-portal/domain/orders";
export type {
OrderDisplayItem,
@ -20,10 +17,7 @@ export type {
/**
* Summarize order display items for compact display
*/
export function summarizeOrderDisplayItems(
items: OrderDisplayItem[],
fallback: string
): string {
export function summarizeOrderDisplayItems(items: OrderDisplayItem[], fallback: string): string {
if (items.length === 0) {
return fallback;
}

View File

@ -93,7 +93,7 @@ export function AddressConfirmation({
} finally {
setLoading(false);
}
}, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed]);
}, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed, setAddressConfirmed]);
useEffect(() => {
log.info("Address confirmation component mounted");

View File

@ -1,9 +1,6 @@
import { useEffect, useState } from "react";
import { apiClient } from "@/lib/api";
import type {
SimTopUpPricing,
SimTopUpPricingPreviewResponse,
} from "@customer-portal/domain/sim";
import type { SimTopUpPricing, SimTopUpPricingPreviewResponse } from "@customer-portal/domain/sim";
interface UseSimTopUpPricingResult {
pricing: SimTopUpPricing | null;
@ -24,7 +21,7 @@ export function useSimTopUpPricing(): UseSimTopUpPricingResult {
try {
setLoading(true);
const response = await apiClient.GET("/api/subscriptions/sim/top-up/pricing");
if (mounted && response.data) {
const data = response.data as { success: boolean; data: SimTopUpPricing };
setPricing(data.data);
@ -66,4 +63,3 @@ export function useSimTopUpPricing(): UseSimTopUpPricingResult {
return { pricing, loading, error, calculatePreview };
}

View File

@ -42,7 +42,7 @@ export function getSimPlanSku(planCode?: string): string | undefined {
*/
export function mapToSimplifiedFormat(planCode?: string): string {
if (!planCode) return "";
// Handle Freebit format (PASI_5G, PASI_25G, etc.)
if (planCode.startsWith("PASI_")) {
const match = planCode.match(/PASI_(\d+)G/);
@ -50,13 +50,13 @@ export function mapToSimplifiedFormat(planCode?: string): string {
return `${match[1]}GB`;
}
}
// Handle other formats that might end with G or GB
const match = planCode.match(/(\d+)\s*G(?:B)?\b/i);
if (match) {
return `${match[1]}GB`;
}
// Return as-is if no pattern matches
return planCode;
}

View File

@ -25,4 +25,3 @@ export function useCreateCase() {
},
});
}

View File

@ -1,2 +1 @@
export * from "./case-presenters";

View File

@ -68,15 +68,15 @@ export const JAPAN_PREFECTURES: PrefectureOption[] = [
*/
export function formatJapanesePostalCode(value: string): string {
const digits = value.replace(/\D/g, "");
if (digits.length <= 3) {
return digits;
}
if (digits.length <= 7) {
return `${digits.slice(0, 3)}-${digits.slice(3)}`;
}
return `${digits.slice(0, 3)}-${digits.slice(3, 7)}`;
}

View File

@ -1,6 +1,7 @@
# Complete Portainer Guide for Customer Portal
## Table of Contents
1. [Creating a Stack in Portainer](#creating-a-stack-in-portainer)
2. [Repository vs Upload vs Web Editor](#stack-creation-methods)
3. [Security Concerns & Best Practices](#security-concerns)
@ -22,6 +23,7 @@
Click **"+ Add stack"** button
You'll see three creation methods:
- **Web editor** - Paste compose file directly
- **Upload** - Upload a compose file
- **Repository** - Pull from Git repository
@ -45,16 +47,19 @@ Click **"Deploy the stack"**
### Method 1: Web Editor (Simplest)
**How:**
1. Select "Web editor"
2. Paste your `docker-compose.yml` content
3. Add environment variables manually or load from file
**Pros:**
- ✅ Quick and simple
- ✅ No external dependencies
- ✅ Full control over content
**Cons:**
- ❌ Manual updates required
- ❌ No version control
- ❌ Easy to make mistakes when editing
@ -66,17 +71,20 @@ Click **"Deploy the stack"**
### Method 2: Upload (Recommended for Your Case)
**How:**
1. Select "Upload"
2. Upload your `docker-compose.yml` file
3. Optionally upload a `.env` file for environment variables
**Pros:**
- ✅ Version control on your local machine
- ✅ Can prepare and test locally
- ✅ No external network dependencies
- ✅ Works in air-gapped environments
**Cons:**
- ❌ Manual upload for each update
- ❌ Need to manage files locally
@ -87,12 +95,14 @@ Click **"Deploy the stack"**
### Method 3: Repository (Git Integration)
**How:**
1. Select "Repository"
2. Enter repository URL (GitHub, GitLab, Bitbucket, etc.)
3. Specify branch and compose file path
4. Add authentication if private repo
**Example Configuration:**
```
Repository URL: https://github.com/your-org/customer-portal
Reference: main
@ -100,16 +110,19 @@ Compose path: docker/portainer/docker-compose.yml
```
**For Private Repos:**
- Use a Personal Access Token (PAT) as password
- Or use deploy keys
**Pros:**
- ✅ Version controlled
- ✅ Easy to update (just click "Pull and redeploy")
- ✅ Team can review changes via PR
- ✅ Audit trail of changes
**Cons:**
- ❌ Requires network access to repo
- ❌ Secrets in repo = security risk
- ❌ Need to manage repo access tokens
@ -124,6 +137,7 @@ Compose path: docker/portainer/docker-compose.yml
**Use: Upload + Environment Variables in Portainer UI**
Why:
1. Your compose file rarely changes (it's just orchestration)
2. Sensitive data stays in Portainer, not in Git
3. Image updates are done via environment variables
@ -136,6 +150,7 @@ Why:
### 🔴 Critical Security Issues
#### 1. Never Store Secrets in Git
```yaml
# ❌ BAD - Secrets in compose file
environment:
@ -149,6 +164,7 @@ environment:
```
#### 2. Never Store Secrets in Docker Images
```dockerfile
# ❌ BAD - Secrets baked into image
ENV JWT_SECRET="my-secret"
@ -159,6 +175,7 @@ COPY secrets/ /app/secrets/
```
#### 3. Portainer Access Control
```
⚠️ Portainer has full Docker access = root on the host
@ -173,6 +190,7 @@ Best practices:
### 🟡 Medium Security Concerns
#### 4. Environment Variables in Portainer
```
Portainer stores env vars in its database.
This is generally safe, but consider:
@ -188,6 +206,7 @@ Mitigation:
```
#### 5. Image Trust
```
⚠️ You're loading .tar files - verify their integrity
@ -198,6 +217,7 @@ Best practice:
```
Add to build script:
```bash
# Generate checksums
sha256sum portal-frontend.latest.tar > portal-frontend.latest.tar.sha256
@ -209,6 +229,7 @@ sha256sum -c portal-backend.latest.tar.sha256
```
#### 6. Network Exposure
```yaml
# ❌ BAD - Database exposed to host
database:
@ -225,6 +246,7 @@ database:
### 🟢 Good Security Practices (Already in Place)
Your current setup does these right:
- ✅ Non-root users in containers
- ✅ Health checks configured
- ✅ Database/Redis not exposed externally
@ -252,12 +274,14 @@ watchtower:
```
**Why NOT recommended:**
- ❌ No control over when updates happen
- ❌ No rollback mechanism
- ❌ Can break production unexpectedly
- ❌ Requires images in a registry (not .tar files)
We've disabled Watchtower in your compose:
```yaml
labels:
- "com.centurylinklabs.watchtower.enable=false"
@ -270,27 +294,32 @@ labels:
Portainer can expose a webhook URL that triggers stack redeployment.
**Setup:**
1. Go to Stack → Settings
2. Enable "Webhook"
3. Copy the webhook URL
**Trigger from CI/CD:**
```bash
# In your GitHub Actions / GitLab CI
curl -X POST "https://your-portainer:9443/api/stacks/webhook/abc123"
```
**Workflow:**
```
Build Images → Push to Registry → Trigger Webhook → Portainer Redeploys
```
**Pros:**
- ✅ Controlled updates
- ✅ Integrated with CI/CD
- ✅ Can add approval gates
**Cons:**
- ❌ Requires images in a registry
- ❌ Webhook URL is a secret
- ❌ Limited rollback options
@ -313,6 +342,7 @@ ssh user@server "cd /path/to/portal && ./update-stack.sh v1.2.3"
```
**Make it a one-liner:**
```bash
# deploy.sh - Run locally
#!/bin/bash
@ -339,11 +369,13 @@ echo "✅ Deployed ${TAG}"
If you want auto-updates, use a registry:
**Free Options:**
- GitHub Container Registry (ghcr.io) - free for public repos
- GitLab Container Registry - free
- Docker Hub - 1 private repo free
**Setup:**
```bash
# Build and push
./scripts/plesk/build-images.sh --tag v1.2.3 --push ghcr.io/your-org
@ -377,6 +409,7 @@ services:
```
**Steps:**
1. Build: `./scripts/plesk/build-images.sh --tag 20241201-abc`
2. Upload: `scp *.tar server:/path/images/`
3. Load: `docker load -i *.tar`
@ -404,17 +437,19 @@ services:
## Quick Reference: Portainer Stack Commands
### Via Portainer UI
| Action | Steps |
|--------|-------|
| Create stack | Stacks → Add stack → Configure → Deploy |
| Update stack | Stacks → Select → Editor → Update |
| Action | Steps |
| ------------ | -------------------------------------------------- |
| Create stack | Stacks → Add stack → Configure → Deploy |
| Update stack | Stacks → Select → Editor → Update |
| Change image | Stacks → Select → Env vars → Change IMAGE → Update |
| View logs | Stacks → Select → Container → Logs |
| Restart | Stacks → Select → Container → Restart |
| Stop | Stacks → Select → Stop |
| Delete | Stacks → Select → Delete |
| View logs | Stacks → Select → Container → Logs |
| Restart | Stacks → Select → Container → Restart |
| Stop | Stacks → Select → Stop |
| Delete | Stacks → Select → Delete |
### Via CLI (on server)
```bash
# Navigate to stack directory
cd /path/to/portal
@ -442,11 +477,10 @@ docker compose --env-file stack.env down -v
## Summary
| Aspect | Recommendation |
|--------|---------------|
| Stack creation | **Upload** method (version control locally, no secrets in git) |
| Secrets management | **Portainer env vars** or **mounted secrets volume** |
| Image updates | **Manual script** for now, migrate to **registry + webhook** later |
| Auto-updates | **Not recommended** for production; use controlled deployments |
| Rollback | Keep previous image tags, update env vars to rollback |
| Aspect | Recommendation |
| ------------------ | ------------------------------------------------------------------ |
| Stack creation | **Upload** method (version control locally, no secrets in git) |
| Secrets management | **Portainer env vars** or **mounted secrets volume** |
| Image updates | **Manual script** for now, migrate to **registry + webhook** later |
| Auto-updates | **Not recommended** for production; use controlled deployments |
| Rollback | Keep previous image tags, update env vars to rollback |

View File

@ -39,4 +39,3 @@ volumes:
networks:
default:
name: portal_dev

View File

@ -71,7 +71,10 @@ export default [
files: [...BFF_TS_FILES, "packages/domain/**/*.ts"],
rules: {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"no-console": ["warn", { allow: ["warn", "error"] }],
},
},

View File

@ -72,18 +72,22 @@ packages/domain/
## 🎯 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
@ -104,17 +108,19 @@ import { ApiResponse, PaginationParams } from "@customer-portal/domain/common";
### **API Response Handling**
```typescript
import {
ApiResponse,
ApiSuccessResponse,
import {
ApiResponse,
ApiSuccessResponse,
ApiErrorResponse,
apiResponseSchema
apiResponseSchema,
} from "@customer-portal/domain/common";
// Type-safe API responses
const response: ApiResponse<Invoice> = {
success: true,
data: { /* invoice data */ }
data: {
/* invoice data */
},
};
// With validation
@ -124,9 +130,9 @@ const validated = apiResponseSchema(invoiceSchema).parse(rawResponse);
### **Query Parameters with Validation**
```typescript
import {
InvoiceQueryParams,
invoiceQueryParamsSchema
import {
InvoiceQueryParams,
invoiceQueryParamsSchema
} from "@customer-portal/domain/billing";
// In BFF controller
@ -172,10 +178,7 @@ function LoginForm() {
```typescript
import { ZodValidationPipe } from "@bff/core/validation";
import {
createOrderRequestSchema,
type CreateOrderRequest
} from "@customer-portal/domain/orders";
import { createOrderRequestSchema, type CreateOrderRequest } from "@customer-portal/domain/orders";
@Controller("orders")
export class OrdersController {
@ -194,39 +197,39 @@ export class OrdersController {
### **API Responses**
| Schema | Description |
|--------|-------------|
| `apiSuccessResponseSchema(dataSchema)` | Successful API response wrapper |
| `apiErrorResponseSchema` | Error API response with code/message |
| `apiResponseSchema(dataSchema)` | Discriminated union of success/error |
| 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 |
| 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) |
| 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 |
| 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 |
---
@ -281,11 +284,11 @@ export * from "./schema";
```typescript
import { ZodValidationPipe } from "@bff/core/validation";
import {
myEntitySchema,
import {
myEntitySchema,
myEntityQueryParamsSchema,
type MyEntity,
type MyEntityQueryParams
type MyEntityQueryParams,
} from "@customer-portal/domain/my-domain";
@Controller("my-entities")
@ -370,13 +373,15 @@ export const paginationSchema = z.object({
### 4. **Use Refinements for Complex Validation**
```typescript
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"] }
);
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"],
});
```
---
@ -428,4 +433,3 @@ When adding new types or schemas:
**Maintained by**: Customer Portal Team
**Last Updated**: October 2025

View File

@ -11,4 +11,3 @@ type SignupRequestInput = z.input<typeof signupRequestSchema>;
export function buildSignupRequest(input: SignupRequestInput) {
return signupRequestSchema.parse(input);
}

View File

@ -1,6 +1,6 @@
/**
* Billing Domain - Contract
*
*
* Constants and types for the billing domain.
* All validated types are derived from schemas (see schema.ts).
*/
@ -35,5 +35,4 @@ export type {
BillingSummary,
InvoiceQueryParams,
InvoiceListQuery,
} from './schema.js';
} from "./schema.js";

View File

@ -1,8 +1,8 @@
/**
* Billing Domain
*
*
* Exports all billing-related contracts, schemas, and provider mappers.
*
*
* Types are derived from Zod schemas (Schema-First Approach)
*/
@ -25,7 +25,7 @@ export type {
BillingSummary,
InvoiceQueryParams,
InvoiceListQuery,
} from './schema.js';
} from "./schema.js";
// Provider adapters
export * as Providers from "./providers/index.js";

View File

@ -1,6 +1,6 @@
/**
* WHMCS Billing Provider - Mapper
*
*
* Transforms raw WHMCS invoice data into normalized billing domain types.
*/
@ -53,7 +53,7 @@ function mapItems(rawItems: unknown): InvoiceItem[] {
const parsed = whmcsInvoiceItemsRawSchema.parse(rawItems);
const itemArray = Array.isArray(parsed.item) ? parsed.item : [parsed.item];
return itemArray.map(item => ({
id: item.id,
description: item.description,
@ -83,9 +83,7 @@ export function transformWhmcsInvoice(
const currency = whmcsInvoice.currencycode || options.defaultCurrencyCode || "JPY";
const currencySymbol =
whmcsInvoice.currencyprefix ||
whmcsInvoice.currencysuffix ||
options.defaultCurrencySymbol;
whmcsInvoice.currencyprefix || whmcsInvoice.currencysuffix || options.defaultCurrencySymbol;
// Transform to domain model
const invoice: Invoice = {

View File

@ -1,10 +1,10 @@
/**
* WHMCS Billing Provider - Raw Types
*
*
* Type definitions for the WHMCS billing API contract:
* - Request parameter types (API inputs)
* - Response types (API outputs)
*
*
* These represent the exact structure used by WHMCS APIs.
*/
@ -268,19 +268,27 @@ export type WhmcsCurrency = z.infer<typeof whmcsCurrencySchema>;
/**
* WHMCS GetCurrencies API response schema
*
*
* WHMCS can return currencies in different formats:
* 1. Nested format: { currencies: { currency: [...] } }
* 2. Flat format: currencies[currency][0][id], currencies[currency][0][code], etc.
* 3. Missing result field in some cases
*/
export const whmcsCurrenciesResponseSchema = z.object({
result: z.enum(["success", "error"]).optional(),
totalresults: z.string().transform(val => parseInt(val, 10)).or(z.number()).optional(),
currencies: z.object({
currency: z.array(whmcsCurrencySchema).or(whmcsCurrencySchema),
}).optional(),
// Allow any additional flat currency keys for flat format
}).catchall(z.string().or(z.number()));
export const whmcsCurrenciesResponseSchema = z
.object({
result: z.enum(["success", "error"]).optional(),
totalresults: z
.string()
.transform(val => parseInt(val, 10))
.or(z.number())
.optional(),
currencies: z
.object({
currency: z.array(whmcsCurrencySchema).or(whmcsCurrencySchema),
})
.optional(),
// Allow any additional flat currency keys for flat format
})
.catchall(z.string().or(z.number()));
export type WhmcsCurrenciesResponse = z.infer<typeof whmcsCurrenciesResponseSchema>;

View File

@ -1,6 +1,6 @@
/**
* Billing Domain - Schemas
*
*
* Zod validation schemas for billing domain types.
* Used for runtime validation of data from any source.
*/

View File

@ -15,4 +15,3 @@ export * as CommonProviders from "./providers/index.js";
// Re-export provider types for convenience
export type { WhmcsResponse, WhmcsErrorResponse } from "./providers/whmcs.js";
export type { SalesforceResponse } from "./providers/salesforce.js";

View File

@ -1,6 +1,6 @@
/**
* Common Provider Types
*
*
* Generic provider-specific response structures used across multiple domains.
*/

View File

@ -1,6 +1,6 @@
/**
* Common Salesforce Provider Types
*
*
* Generic Salesforce API response structures used across multiple domains.
*/
@ -24,10 +24,10 @@ type SalesforceResponseBase = z.infer<typeof salesforceResponseBaseSchema>;
/**
* Generic type for Salesforce query results derived from schema
* All SOQL queries return this structure regardless of SObject type
*
*
* Usage: SalesforceResponse<SalesforceOrderRecord>
*/
export type SalesforceResponse<TRecord> = Omit<SalesforceResponseBase, 'records'> & {
export type SalesforceResponse<TRecord> = Omit<SalesforceResponseBase, "records"> & {
records: TRecord[];
};
@ -39,4 +39,3 @@ export const salesforceResponseSchema = <TRecord extends z.ZodTypeAny>(recordSch
salesforceResponseBaseSchema.extend({
records: z.array(recordSchema),
});

View File

@ -1,2 +1 @@
export * from "./raw.types.js";

View File

@ -1,6 +1,6 @@
/**
* Common Salesforce Provider Types
*
*
* Generic Salesforce API response structures used across multiple domains.
*/
@ -24,7 +24,7 @@ export const salesforceQueryResultSchema = <TRecord extends z.ZodTypeAny>(record
/**
* Generic type for Salesforce query results
* All SOQL queries return this structure regardless of SObject type
*
*
* Usage: SalesforceQueryResult<SalesforceOrderRecord>
*/
export interface SalesforceQueryResult<TRecord = unknown> {

View File

@ -1,6 +1,6 @@
/**
* Common WHMCS Provider Types
*
*
* Generic WHMCS API response structures used across multiple domains.
*/
@ -23,7 +23,7 @@ type WhmcsResponseBase = z.infer<typeof whmcsResponseBaseSchema>;
/**
* Generic type for WHMCS API responses derived from schema
* All WHMCS API endpoints return this structure
*
*
* Usage: WhmcsResponse<InvoiceData>
*/
export type WhmcsResponse<T> = WhmcsResponseBase & {
@ -53,4 +53,3 @@ export const whmcsErrorResponseSchema = z.object({
});
export type WhmcsErrorResponse = z.infer<typeof whmcsErrorResponseSchema>;

View File

@ -20,7 +20,11 @@ export const passwordSchema = z
.regex(/[0-9]/, "Password must contain at least one number")
.regex(/[^A-Za-z0-9]/, "Password must contain at least one special character");
export const nameSchema = z.string().min(1, "Name is required").max(100, "Name must be less than 100 characters").trim();
export const nameSchema = z
.string()
.min(1, "Name is required")
.max(100, "Name must be less than 100 characters")
.trim();
export const phoneSchema = z
.string()
@ -78,7 +82,10 @@ export const nonEmptyStringSchema = z.string().min(1, "Value cannot be empty").t
/**
* Schema for validating SOQL field names
*/
export const soqlFieldNameSchema = z.string().trim().regex(/^[A-Za-z0-9_.]+$/, "Invalid SOQL field name");
export const soqlFieldNameSchema = z
.string()
.trim()
.regex(/^[A-Za-z0-9_.]+$/, "Invalid SOQL field name");
// ============================================================================
// API Response Schemas
@ -111,10 +118,7 @@ export const apiErrorResponseSchema = z.object({
* Usage: apiResponseSchema(yourDataSchema)
*/
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.discriminatedUnion("success", [
apiSuccessResponseSchema(dataSchema),
apiErrorResponseSchema,
]);
z.discriminatedUnion("success", [apiSuccessResponseSchema(dataSchema), apiErrorResponseSchema]);
// ============================================================================
// Pagination Schemas

View File

@ -1,6 +1,6 @@
/**
* Common Domain - Types
*
*
* Shared utility types and branded types used across all domains.
*/

View File

@ -1,6 +1,6 @@
/**
* Common Domain - Validation Utilities
*
*
* Generic validation functions used across all domains.
* These are pure functions with no infrastructure dependencies.
*/
@ -26,23 +26,26 @@ export const customerNumberSchema = z.string().min(1, "Customer number is requir
/**
* Normalize and validate an email address
*
*
* This is a convenience wrapper that throws on invalid input.
* For validation without throwing, use the emailSchema directly with .safeParse()
*
*
* @throws Error if email format is invalid
*/
export function normalizeAndValidateEmail(email: string): string {
const emailValidationSchema = z.string().email().transform(e => e.toLowerCase().trim());
const emailValidationSchema = z
.string()
.email()
.transform(e => e.toLowerCase().trim());
return emailValidationSchema.parse(email);
}
/**
* Validate a UUID (v4)
*
*
* This is a convenience wrapper that throws on invalid input.
* For validation without throwing, use the uuidSchema directly with .safeParse()
*
*
* @throws Error if UUID format is invalid
*/
export function validateUuidV4OrThrow(id: string): string {
@ -74,10 +77,10 @@ export const urlSchema = z.string().url();
/**
* Validate a URL
*
*
* This is a convenience wrapper that throws on invalid input.
* For validation without throwing, use the urlSchema directly with .safeParse()
*
*
* @throws Error if URL format is invalid
*/
export function validateUrlOrThrow(url: string): string {
@ -90,7 +93,7 @@ export function validateUrlOrThrow(url: string): string {
/**
* Validate a URL (non-throwing)
*
*
* Returns validation result with errors if any.
* Prefer using urlSchema.safeParse() directly for more control.
*/

View File

@ -1,6 +1,6 @@
/**
* Customer Domain - Providers
*
*
* Providers handle mapping from external systems to domain types:
* - Portal: Prisma (portal DB) UserAuth
* - Whmcs: WHMCS API WhmcsClient

View File

@ -1,9 +1,8 @@
/**
* Portal Provider
*
*
* Handles mapping from Prisma (portal database) to UserAuth domain type
*/
export * from "./mapper.js";
export * from "./types.js";

View File

@ -1,6 +1,6 @@
/**
* Portal Provider - Mapper
*
*
* Maps Prisma user data to UserAuth domain type using schema validation
*/
@ -10,9 +10,9 @@ import type { UserAuth } from "../../schema.js";
/**
* Maps raw Prisma user data to UserAuth domain type
*
*
* Uses schema validation for runtime type safety
*
*
* @param raw - Raw Prisma user data from portal database
* @returns Validated UserAuth with only authentication state
*/
@ -28,4 +28,3 @@ export function mapPrismaUserToUserAuth(raw: PrismaUserRaw): UserAuth {
updatedAt: raw.updatedAt.toISOString(),
});
}

View File

@ -1,6 +1,6 @@
/**
* Portal Provider - Raw Types
*
*
* Raw Prisma user data interface.
* Domain doesn't depend on @prisma/client directly.
*/
@ -24,4 +24,3 @@ export interface PrismaUserRaw {
createdAt: Date;
updatedAt: Date;
}

View File

@ -1,6 +1,6 @@
/**
* WHMCS Provider - Mapper
*
*
* Maps WHMCS API responses to domain types.
* Minimal transformation - validates and normalizes only address structure.
*/
@ -32,7 +32,7 @@ export function transformWhmcsClientResponse(response: unknown): WhmcsClient {
/**
* Transform raw WHMCS client to domain WhmcsClient
*
*
* Keeps raw WHMCS field names, only normalizes:
* - Address structure to domain Address type
* - Type coercions (strings to numbers/booleans)

View File

@ -3,27 +3,17 @@
*/
import { z } from "zod";
import type {
CreateMappingRequest,
UpdateMappingRequest,
UserIdMapping,
} from "./contract.js";
import type { CreateMappingRequest, UpdateMappingRequest, UserIdMapping } from "./contract.js";
export const createMappingRequestSchema: z.ZodType<CreateMappingRequest> = z.object({
userId: z.string().uuid(),
whmcsClientId: z.number().int().positive(),
sfAccountId: z
.string()
.min(1, "Salesforce account ID must be at least 1 character")
.optional(),
sfAccountId: z.string().min(1, "Salesforce account ID must be at least 1 character").optional(),
});
export const updateMappingRequestSchema: z.ZodType<UpdateMappingRequest> = z.object({
whmcsClientId: z.number().int().positive().optional(),
sfAccountId: z
.string()
.min(1, "Salesforce account ID must be at least 1 character")
.optional(),
sfAccountId: z.string().min(1, "Salesforce account ID must be at least 1 character").optional(),
});
export const userIdMappingSchema: z.ZodType<UserIdMapping> = z.object({

View File

@ -1,6 +1,6 @@
/**
* ID Mapping Domain - Validation
*
*
* Pure business validation functions for ID mappings.
* These functions contain no infrastructure dependencies (no DB, no HTTP, no logging).
*/
@ -33,7 +33,7 @@ export function checkMappingCompleteness(request: CreateMappingRequest | UserIdM
/**
* Validate no conflicts exist with existing mappings
* Business rule: Each userId, whmcsClientId should be unique
*
*
* Note: This assumes the request has already been validated by schema.
* Use createMappingRequestSchema.parse() before calling this function.
*/
@ -72,11 +72,13 @@ export function validateNoConflicts(
/**
* Validate deletion constraints
* Business rule: Warn about data access impacts
*
*
* Note: This assumes the mapping has already been validated.
* This function adds business warnings about the impact of deletion.
*/
export function validateDeletion(mapping: UserIdMapping | null | undefined): MappingValidationResult {
export function validateDeletion(
mapping: UserIdMapping | null | undefined
): MappingValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
@ -85,9 +87,7 @@ export function validateDeletion(mapping: UserIdMapping | null | undefined): Map
return { isValid: false, errors, warnings };
}
warnings.push(
"Deleting this mapping will prevent access to WHMCS/Salesforce data for this user"
);
warnings.push("Deleting this mapping will prevent access to WHMCS/Salesforce data for this user");
if (mapping.sfAccountId) {
warnings.push(
"This mapping includes Salesforce integration - deletion will affect case management"
@ -99,7 +99,7 @@ export function validateDeletion(mapping: UserIdMapping | null | undefined): Map
/**
* Sanitize and normalize a create mapping request
*
*
* Note: This performs basic string trimming before validation.
* The schema handles validation; this is purely for data cleanup.
*/
@ -113,7 +113,7 @@ export function sanitizeCreateRequest(request: CreateMappingRequest): CreateMapp
/**
* Sanitize and normalize an update mapping request
*
*
* Note: This performs basic string trimming before validation.
* The schema handles validation; this is purely for data cleanup.
*/
@ -130,4 +130,3 @@ export function sanitizeUpdateRequest(request: UpdateMappingRequest): UpdateMapp
return sanitized;
}

View File

@ -1,6 +1,6 @@
/**
* Orders Domain - Checkout Types
*
*
* Minimal type definitions for checkout flow.
* Frontend handles its own URL param serialization.
*/
@ -8,5 +8,5 @@
// This file is intentionally minimal after cleanup.
// The build/derive/normalize functions were removed as they were
// unnecessary abstractions that should be handled by the frontend.
//
//
// See CLEANUP_PROPOSAL_NORMALIZERS.md for details.

View File

@ -64,7 +64,10 @@ const BILLING_CYCLE_ALIASES: Record<string, BillingCycle> = {
};
const normalizeBillingCycleKey = (value: string): string =>
value.trim().toLowerCase().replace(/[\s_-]+/g, "");
value
.trim()
.toLowerCase()
.replace(/[\s_-]+/g, "");
const DEFAULT_BILLING_CYCLE: BillingCycle = "monthly";
@ -337,7 +340,7 @@ export function buildOrderDisplayItems(
primaryCategory: category,
categories: [category],
charges,
included: charges.every((charge) => charge.amount <= 0),
included: charges.every(charge => charge.amount <= 0),
sourceItems: [item],
isBundle: Boolean(item.isBundledAddon),
};

View File

@ -1,16 +1,16 @@
/**
* Orders Domain
*
*
* Exports all order-related contracts, schemas, and provider mappers.
*
*
* Types are derived from Zod schemas (Schema-First Approach)
*/
// Business types and constants
export {
type OrderCreationType,
type OrderStatus,
type OrderType,
export {
type OrderCreationType,
type OrderStatus,
type OrderType,
type OrderTypeValue,
type UserMapping,
// Checkout types
@ -79,7 +79,7 @@ export type {
OrderDisplayItemCategory,
OrderDisplayItemCharge,
OrderDisplayItemChargeKind,
} from './schema.js';
} from "./schema.js";
// Provider adapters
export * as Providers from "./providers/index.js";

View File

@ -21,12 +21,7 @@ export const Salesforce = {
fieldMap: SalesforceFieldMap,
};
export {
WhmcsMapper,
WhmcsRaw,
SalesforceMapper,
SalesforceRaw,
};
export { WhmcsMapper, WhmcsRaw, SalesforceMapper, SalesforceRaw };
export * from "./whmcs/mapper.js";
export * from "./whmcs/raw.types.js";
export * from "./salesforce/mapper.js";

View File

@ -44,7 +44,7 @@ export function createOrderRequest(payload: {
/**
* Transform CheckoutCart into CreateOrderRequest
* Handles SKU extraction, validation, and payload formatting
*
*
* @throws Error if no products are selected
*/
export function prepareOrderFromCart(
@ -65,13 +65,11 @@ export function prepareOrderFromCart(
// Note: Zod validation of the final structure should happen at the boundary or via schema.parse
// This function focuses on the structural transformation logic.
const orderData: CreateOrderRequest = {
orderType,
skus: uniqueSkus,
...(Object.keys(cart.configuration).length > 0
? { configurations: cart.configuration }
: {}),
...(Object.keys(cart.configuration).length > 0 ? { configurations: cart.configuration } : {}),
};
return orderData;

View File

@ -1,6 +1,6 @@
/**
* Payments Domain - Contract
*
*
* Constants and types for the payments domain.
* All validated types are derived from schemas (see schema.ts).
*/
@ -53,4 +53,4 @@ export type {
PaymentGatewayType,
PaymentGateway,
PaymentGatewayList,
} from './schema.js';
} from "./schema.js";

View File

@ -1,8 +1,8 @@
/**
* Payments Domain
*
*
* Exports all payment-related contracts, schemas, and provider mappers.
*
*
* Types are derived from Zod schemas (Schema-First Approach)
*/
@ -20,7 +20,7 @@ export type {
PaymentGatewayType,
PaymentGateway,
PaymentGatewayList,
} from './schema.js';
} from "./schema.js";
// Provider adapters
export * as Providers from "./providers/index.js";

View File

@ -77,4 +77,3 @@ export function transformWhmcsPaymentGateway(raw: unknown): PaymentGateway {
return paymentGatewaySchema.parse(gateway);
}

View File

@ -4,4 +4,3 @@
*/
export * as Whmcs from "./whmcs/index.js";

View File

@ -4,4 +4,3 @@
*/
export * from "./utils.js";

View File

@ -1,6 +1,6 @@
/**
* SIM Domain - Contract
*
*
* Constants and types for the SIM domain.
* All validated types are derived from schemas (see schema.ts).
*/
@ -64,4 +64,4 @@ export type {
SimOrderActivationRequest,
SimOrderActivationMnp,
SimOrderActivationAddons,
} from './schema.js';
} from "./schema.js";

View File

@ -1,8 +1,8 @@
/**
* SIM Domain
*
*
* Exports all SIM-related contracts, schemas, and provider mappers.
*
*
* Types are derived from Zod schemas (Schema-First Approach)
*/
@ -59,7 +59,7 @@ export type {
SimTopUpPricing,
SimTopUpPricingPreviewRequest,
SimTopUpPricingPreviewResponse,
} from './schema.js';
} from "./schema.js";
export type { SimPlanCode } from "./contract.js";
export type { SimPlanOption, SimFeatureToggleSnapshot } from "./helpers.js";

View File

@ -138,4 +138,3 @@ export const SIM_MANAGEMENT_FLOW: ManagementFlow = {
},
],
};

View File

@ -47,8 +47,12 @@ export type PlanChangeResponse = ReturnType<typeof Mapper.transformFreebitPlanCh
export type CancelPlanResponse = ReturnType<typeof Mapper.transformFreebitCancelPlanResponse>;
export type CancelAccountResponse = ReturnType<typeof Mapper.transformFreebitCancelAccountResponse>;
export type EsimReissueResponse = ReturnType<typeof Mapper.transformFreebitEsimReissueResponse>;
export type EsimAddAccountResponse = ReturnType<typeof Mapper.transformFreebitEsimAddAccountResponse>;
export type EsimActivationResponse = ReturnType<typeof Mapper.transformFreebitEsimActivationResponse>;
export type EsimAddAccountResponse = ReturnType<
typeof Mapper.transformFreebitEsimAddAccountResponse
>;
export type EsimActivationResponse = ReturnType<
typeof Mapper.transformFreebitEsimActivationResponse
>;
export type AuthResponse = ReturnType<typeof Mapper.transformFreebitAuthResponse>;
export * from "./mapper.js";

View File

@ -48,11 +48,13 @@ export const freebitTrafficInfoRawSchema = z.object({
})
.optional(),
account: z.union([z.string(), z.number()]).optional(),
traffic: z.object({
today: z.union([z.string(), z.number()]).optional(),
inRecentDays: z.union([z.string(), z.number()]).optional(),
blackList: z.union([z.string(), z.number()]).optional(),
}).optional(),
traffic: z
.object({
today: z.union([z.string(), z.number()]).optional(),
inRecentDays: z.union([z.string(), z.number()]).optional(),
blackList: z.union([z.string(), z.number()]).optional(),
})
.optional(),
});
export const freebitTopUpRawSchema = z.object({
resultCode: z.string().optional(),
@ -154,14 +156,16 @@ export const freebitQuotaHistoryRawSchema = z.object({
account: z.union([z.string(), z.number()]).optional(),
total: z.union([z.string(), z.number()]).optional(),
count: z.union([z.string(), z.number()]).optional(),
quotaHistory: z.array(
z.object({
addQuotaKb: z.union([z.string(), z.number()]).optional(),
addDate: z.string().optional(),
expireDate: z.string().optional(),
campaignCode: z.string().optional(),
})
).optional(),
quotaHistory: z
.array(
z.object({
addQuotaKb: z.union([z.string(), z.number()]).optional(),
addDate: z.string().optional(),
expireDate: z.string().optional(),
campaignCode: z.string().optional(),
})
)
.optional(),
});
export type FreebitQuotaHistoryRaw = z.infer<typeof freebitQuotaHistoryRawSchema>;
@ -178,4 +182,3 @@ export const freebitAuthResponseRawSchema = z.object({
});
export type FreebitAuthResponseRaw = z.infer<typeof freebitAuthResponseRawSchema>;

View File

@ -1,6 +1,6 @@
/**
* SIM Domain - Freebit Provider Request Schemas
*
*
* Zod schemas for all Freebit API request payloads.
*/
@ -135,7 +135,10 @@ export const freebitEsimAddAccountRequestSchema = z.object({
account: z.string().min(1, "Account is required"),
eid: z.string().min(1, "EID is required"),
addKind: z.enum(["N", "R"]).default("N"),
shipDate: z.string().regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format").optional(),
shipDate: z
.string()
.regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format")
.optional(),
planCode: z.string().optional(),
contractLine: z.enum(["4G", "5G"]).optional(),
mnp: freebitEsimMnpSchema.optional(),
@ -178,7 +181,10 @@ export const freebitEsimIdentitySchema = z.object({
firstnameZenKana: z.string().optional(),
lastnameZenKana: z.string().optional(),
gender: z.enum(["M", "F"]).optional(),
birthday: z.string().regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format").optional(),
birthday: z
.string()
.regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format")
.optional(),
});
/**
@ -194,7 +200,10 @@ export const freebitEsimActivationRequestSchema = z.object({
simkind: z.enum(["esim", "psim"]).default("esim"),
planCode: z.string().optional(),
contractLine: z.enum(["4G", "5G"]).optional(),
shipDate: z.string().regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format").optional(),
shipDate: z
.string()
.regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format")
.optional(),
mnp: freebitEsimMnpSchema.optional(),
// Identity fields (flattened for API)
firstnameKanji: z.string().optional(),
@ -202,7 +211,10 @@ export const freebitEsimActivationRequestSchema = z.object({
firstnameZenKana: z.string().optional(),
lastnameZenKana: z.string().optional(),
gender: z.enum(["M", "F"]).optional(),
birthday: z.string().regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format").optional(),
birthday: z
.string()
.regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format")
.optional(),
// Additional fields for reissue/exchange
masterAccount: z.string().optional(),
masterPassword: z.string().optional(),
@ -224,10 +236,12 @@ export const freebitEsimActivationResponseSchema = z.object({
resultCode: z.string(),
resultMessage: z.string().optional(),
data: z.unknown().optional(),
status: z.object({
statusCode: z.union([z.string(), z.number()]),
message: z.string(),
}).optional(),
status: z
.object({
statusCode: z.union([z.string(), z.number()]),
message: z.string(),
})
.optional(),
message: z.string().optional(),
});
@ -241,7 +255,10 @@ export const freebitEsimActivationParamsSchema = z.object({
planCode: z.string().optional(),
contractLine: z.enum(["4G", "5G"]).optional(),
aladinOperated: z.enum(["10", "20"]).default("10"),
shipDate: z.string().regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format").optional(),
shipDate: z
.string()
.regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format")
.optional(),
mnp: freebitEsimMnpSchema.optional(),
identity: freebitEsimIdentitySchema.optional(),
});
@ -271,4 +288,3 @@ export type FreebitQuotaHistoryResponse = z.infer<typeof freebitQuotaHistoryResp
export type FreebitEsimAddAccountRequest = z.infer<typeof freebitEsimAddAccountRequestSchema>;
export type FreebitAuthRequest = z.infer<typeof freebitAuthRequestSchema>;
export type FreebitCancelAccountRequest = z.infer<typeof freebitCancelAccountRequestSchema>;

View File

@ -1,6 +1,6 @@
/**
* Freebit Provider Utilities
*
*
* Provider-specific utilities for Freebit SIM API integration
*/
@ -9,7 +9,7 @@
* Removes all non-digit characters from account string
*/
export function normalizeAccount(account: string): string {
return account.replace(/[^0-9]/g, '');
return account.replace(/[^0-9]/g, "");
}
/**
@ -25,8 +25,8 @@ export function validateAccount(account: string): boolean {
*/
export function formatDateForApi(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}${month}${day}`;
}
@ -36,11 +36,10 @@ export function formatDateForApi(date: Date): string {
*/
export function parseDateFromApi(dateString: string): Date | null {
if (!/^\d{8}$/.test(dateString)) return null;
const year = parseInt(dateString.substring(0, 4), 10);
const month = parseInt(dateString.substring(4, 6), 10) - 1; // Month is 0-indexed
const day = parseInt(dateString.substring(6, 8), 10);
return new Date(year, month, day);
}

View File

@ -138,12 +138,8 @@ export const simCancelRequestSchema = z.object({
});
export const simTopUpHistoryRequestSchema = z.object({
fromDate: z
.string()
.regex(/^\d{8}$/, "From date must be in YYYYMMDD format"),
toDate: z
.string()
.regex(/^\d{8}$/, "To date must be in YYYYMMDD format"),
fromDate: z.string().regex(/^\d{8}$/, "From date must be in YYYYMMDD format"),
toDate: z.string().regex(/^\d{8}$/, "To date must be in YYYYMMDD format"),
});
export const simFeaturesUpdateRequestSchema = z.object({
@ -162,16 +158,19 @@ export const simReissueRequestSchema = z.object({
});
// Enhanced cancellation request with more details
export const simCancelFullRequestSchema = z.object({
cancellationMonth: z.string().regex(/^\d{4}-\d{2}$/, "Cancellation month must be in YYYY-MM format"),
confirmRead: z.boolean(),
confirmCancel: z.boolean(),
alternativeEmail: z.string().email().optional().or(z.literal("")),
comments: z.string().max(1000).optional(),
}).refine(
(data) => data.confirmRead === true && data.confirmCancel === true,
{ message: "You must confirm both checkboxes to proceed" }
);
export const simCancelFullRequestSchema = z
.object({
cancellationMonth: z
.string()
.regex(/^\d{4}-\d{2}$/, "Cancellation month must be in YYYY-MM format"),
confirmRead: z.boolean(),
confirmCancel: z.boolean(),
alternativeEmail: z.string().email().optional().or(z.literal("")),
comments: z.string().max(1000).optional(),
})
.refine(data => data.confirmRead === true && data.confirmCancel === true, {
message: "You must confirm both checkboxes to proceed",
});
// Top-up request with enhanced details for email
export const simTopUpFullRequestSchema = z.object({
@ -292,7 +291,10 @@ export const simOrderActivationMnpSchema = z.object({
firstnameZenKana: z.string().optional(),
lastnameZenKana: z.string().optional(),
gender: z.string().optional(),
birthday: z.string().regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format").optional(),
birthday: z
.string()
.regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format")
.optional(),
});
export const simOrderActivationAddonsSchema = z.object({
@ -300,42 +302,48 @@ export const simOrderActivationAddonsSchema = z.object({
callWaiting: z.boolean().optional(),
});
export const simOrderActivationRequestSchema = z.object({
planSku: z.string().min(1, "Plan SKU is required"),
simType: simCardTypeSchema,
eid: z.string().min(15, "EID must be at least 15 characters").optional(),
activationType: simActivationTypeSchema,
scheduledAt: z.string().regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format").optional(),
addons: simOrderActivationAddonsSchema.optional(),
mnp: simOrderActivationMnpSchema.optional(),
msisdn: z.string().min(1, "Phone number (msisdn) is required"),
oneTimeAmountJpy: z.number().nonnegative("One-time amount must be non-negative"),
monthlyAmountJpy: z.number().nonnegative("Monthly amount must be non-negative"),
}).refine(
(data) => {
// If simType is eSIM, eid is required
if (data.simType === "eSIM" && (!data.eid || data.eid.length < 15)) {
return false;
export const simOrderActivationRequestSchema = z
.object({
planSku: z.string().min(1, "Plan SKU is required"),
simType: simCardTypeSchema,
eid: z.string().min(15, "EID must be at least 15 characters").optional(),
activationType: simActivationTypeSchema,
scheduledAt: z
.string()
.regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format")
.optional(),
addons: simOrderActivationAddonsSchema.optional(),
mnp: simOrderActivationMnpSchema.optional(),
msisdn: z.string().min(1, "Phone number (msisdn) is required"),
oneTimeAmountJpy: z.number().nonnegative("One-time amount must be non-negative"),
monthlyAmountJpy: z.number().nonnegative("Monthly amount must be non-negative"),
})
.refine(
data => {
// If simType is eSIM, eid is required
if (data.simType === "eSIM" && (!data.eid || data.eid.length < 15)) {
return false;
}
return true;
},
{
message: "EID is required for eSIM and must be at least 15 characters",
path: ["eid"],
}
return true;
},
{
message: "EID is required for eSIM and must be at least 15 characters",
path: ["eid"],
}
).refine(
(data) => {
// If activationType is Scheduled, scheduledAt is required
if (data.activationType === "Scheduled" && !data.scheduledAt) {
return false;
)
.refine(
data => {
// If activationType is Scheduled, scheduledAt is required
if (data.activationType === "Scheduled" && !data.scheduledAt) {
return false;
}
return true;
},
{
message: "Scheduled date is required for Scheduled activation",
path: ["scheduledAt"],
}
return true;
},
{
message: "Scheduled date is required for Scheduled activation",
path: ["scheduledAt"],
}
);
);
export type SimOrderActivationRequest = z.infer<typeof simOrderActivationRequestSchema>;
export type SimOrderActivationMnp = z.infer<typeof simOrderActivationMnpSchema>;

View File

@ -1,6 +1,6 @@
/**
* Subscriptions Domain - Contract
*
*
* Constants and types for the subscriptions domain.
* All validated types are derived from schemas (see schema.ts).
*/
@ -45,4 +45,4 @@ export type {
SubscriptionList,
SubscriptionQueryParams,
SubscriptionQuery,
} from './schema.js';
} from "./schema.js";

View File

@ -1,6 +1,6 @@
/**
* WHMCS Subscriptions Provider - Raw Types
*
*
* Type definitions for the WHMCS subscriptions API contract:
* - Request parameter types (API inputs)
* - Response types (API outputs)
@ -8,30 +8,24 @@
import { z } from "zod";
const normalizeRequiredNumber = z.preprocess(
value => {
if (typeof value === "number") return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : value;
}
return value;
},
z.number()
);
const normalizeRequiredNumber = z.preprocess(value => {
if (typeof value === "number") return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : value;
}
return value;
}, z.number());
const normalizeOptionalNumber = z.preprocess(
value => {
if (value === undefined || value === null || value === "") return undefined;
if (typeof value === "number") return value;
if (typeof value === "string") {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
},
z.number().optional()
);
const normalizeOptionalNumber = z.preprocess(value => {
if (value === undefined || value === null || value === "") return undefined;
if (typeof value === "number") return value;
if (typeof value === "string") {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}, z.number().optional());
const optionalStringField = () =>
z
@ -117,7 +111,7 @@ export const whmcsProductRawSchema = z.object({
ns1: optionalStringField(),
ns2: optionalStringField(),
assignedips: optionalStringField(),
// Pricing
firstpaymentamount: z.union([z.string(), z.number()]).optional(),
amount: z.union([z.string(), z.number()]).optional(),
@ -125,16 +119,16 @@ export const whmcsProductRawSchema = z.object({
billingcycle: z.string().optional(),
paymentmethod: z.string().optional(),
paymentmethodname: z.string().optional(),
// Dates
nextduedate: z.string().optional(),
nextinvoicedate: z.string().optional(),
// Status
status: z.string(),
username: z.string().optional(),
password: z.string().optional(),
// Notes
notes: z.string().optional(),
diskusage: normalizeOptionalNumber,
@ -142,18 +136,20 @@ export const whmcsProductRawSchema = z.object({
bwusage: normalizeOptionalNumber,
bwlimit: normalizeOptionalNumber,
lastupdate: z.string().optional(),
// Custom fields
customfields: whmcsCustomFieldsContainerSchema.optional(),
configoptions: whmcsConfigOptionsContainerSchema.optional(),
// Pricing details
pricing: z.object({
amount: z.union([z.string(), z.number()]).optional(),
currency: z.string().optional(),
currencyprefix: z.string().optional(),
currencysuffix: z.string().optional(),
}).optional(),
pricing: z
.object({
amount: z.union([z.string(), z.number()]).optional(),
currency: z.string().optional(),
currencyprefix: z.string().optional(),
currencysuffix: z.string().optional(),
})
.optional(),
});
export type WhmcsProductRaw = z.infer<typeof whmcsProductRawSchema>;
@ -167,13 +163,15 @@ export type WhmcsCustomField = z.infer<typeof whmcsCustomFieldSchema>;
* WHMCS GetClientsProducts API response schema
*/
const whmcsProductContainerSchema = z.object({
product: z
.preprocess(value => {
product: z.preprocess(
value => {
if (value === null || value === undefined || value === "") {
return undefined;
}
return value;
}, z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional()),
},
z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional()
),
});
export const whmcsProductListResponseSchema = z.object({

View File

@ -15,4 +15,3 @@ export const Salesforce = {
export { SalesforceMapper, SalesforceRaw };
export * from "./salesforce/mapper.js";
export * from "./salesforce/raw.types.js";

View File

@ -4,4 +4,3 @@
export * from "./raw.types.js";
export * from "./mapper.js";

View File

@ -46,9 +46,7 @@ function nowIsoString(): string {
* @param record - Raw Salesforce Case record from SOQL query
* @returns Validated SupportCase domain object
*/
export function transformSalesforceCaseToSupportCase(
record: SalesforceCaseRecord
): SupportCase {
export function transformSalesforceCaseToSupportCase(record: SalesforceCaseRecord): SupportCase {
// Get raw values
const rawStatus = ensureString(record.Status) ?? SALESFORCE_CASE_STATUS.NEW;
const rawPriority = ensureString(record.Priority) ?? SALESFORCE_CASE_PRIORITY.MEDIUM;
@ -167,4 +165,3 @@ export function buildCaseByIdQuery(
LIMIT 1
`.trim();
}

View File

@ -319,4 +319,3 @@ const PRIORITY_TO_SALESFORCE: Record<string, string> = {
export function toSalesforcePriority(displayPriority: string): string {
return PRIORITY_TO_SALESFORCE[displayPriority] ?? SALESFORCE_CASE_PRIORITY.MEDIUM;
}

View File

@ -1,9 +1,5 @@
import { z } from "zod";
import {
SUPPORT_CASE_STATUS,
SUPPORT_CASE_PRIORITY,
SUPPORT_CASE_CATEGORY,
} from "./contract.js";
import { SUPPORT_CASE_STATUS, SUPPORT_CASE_PRIORITY, SUPPORT_CASE_CATEGORY } from "./contract.js";
/**
* Portal status values (mapped from Salesforce Japanese API names)

View File

@ -5,6 +5,7 @@ Utility functions and helpers for the domain layer. This package contains **util
## Purpose
The toolkit provides pure utility functions for common operations like:
- String manipulation
- Date/time formatting
- Currency formatting
@ -43,10 +44,10 @@ Validation determines if input is valid or invalid:
```typescript
// These are VALIDATION functions → Use common/validation.ts
isValidEmail(email) // Returns true/false
isValidUuid(id) // Returns true/false
isValidUrl(url) // Returns true/false
validateUrlOrThrow(url) // Throws if invalid
isValidEmail(email); // Returns true/false
isValidUuid(id); // Returns true/false
isValidUrl(url); // Returns true/false
validateUrlOrThrow(url); // Throws if invalid
```
**Location**: `packages/domain/common/validation.ts`
@ -57,10 +58,10 @@ Utilities transform, extract, or manipulate data:
```typescript
// These are UTILITY functions → Use toolkit
getEmailDomain(email) // Extracts domain from email
normalizeEmail(email) // Lowercases and trims
ensureProtocol(url) // Adds https:// if missing
getHostname(url) // Extracts hostname
getEmailDomain(email); // Extracts domain from email
normalizeEmail(email); // Lowercases and trims
ensureProtocol(url); // Adds https:// if missing
getHostname(url); // Extracts hostname
```
**Location**: `packages/domain/toolkit/`
@ -72,8 +73,8 @@ getHostname(url) // Extracts hostname
```typescript
import { formatCurrency, formatDate } from "@customer-portal/domain/toolkit/formatting";
const price = formatCurrency(1000, "JPY"); // "¥1,000"
const date = formatDate(new Date()); // "2025-10-08"
const price = formatCurrency(1000, "JPY"); // "¥1,000"
const date = formatDate(new Date()); // "2025-10-08"
```
### Type Guards
@ -92,8 +93,8 @@ if (isString(value)) {
```typescript
import { ensureProtocol, getHostname } from "@customer-portal/domain/toolkit/validation/url";
const fullUrl = ensureProtocol("example.com"); // "https://example.com"
const host = getHostname("https://example.com/path"); // "example.com"
const fullUrl = ensureProtocol("example.com"); // "https://example.com"
const host = getHostname("https://example.com/path"); // "example.com"
```
### Email Utilities
@ -101,55 +102,58 @@ const host = getHostname("https://example.com/path"); // "example.com"
```typescript
import { getEmailDomain, normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
const domain = getEmailDomain("user@example.com"); // "example.com"
const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com"
const domain = getEmailDomain("user@example.com"); // "example.com"
const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com"
```
## When to Use What
| Task | Use | Example |
|------|-----|---------|
| Validate email format | `common/validation.ts` | `isValidEmail(email)` |
| Extract email domain | `toolkit/validation/email.ts` | `getEmailDomain(email)` |
| Validate URL format | `common/validation.ts` | `isValidUrl(url)` |
| Add protocol to URL | `toolkit/validation/url.ts` | `ensureProtocol(url)` |
| Format currency | `toolkit/formatting/currency.ts` | `formatCurrency(amount, "JPY")` |
| Format date | `toolkit/formatting/date.ts` | `formatDate(date)` |
| Task | Use | Example |
| --------------------- | -------------------------------- | ------------------------------- |
| Validate email format | `common/validation.ts` | `isValidEmail(email)` |
| Extract email domain | `toolkit/validation/email.ts` | `getEmailDomain(email)` |
| Validate URL format | `common/validation.ts` | `isValidUrl(url)` |
| Add protocol to URL | `toolkit/validation/url.ts` | `ensureProtocol(url)` |
| Format currency | `toolkit/formatting/currency.ts` | `formatCurrency(amount, "JPY")` |
| Format date | `toolkit/formatting/date.ts` | `formatDate(date)` |
## Best Practices
1. **Use validation functions for validation**
```typescript
// ✅ Good
import { isValidEmail } from "@customer-portal/domain/common/validation";
if (!isValidEmail(email)) throw new Error("Invalid email");
// ❌ Bad - don't write custom validation
if (!email.includes("@")) throw new Error("Invalid email");
```
2. **Use utility functions for transformations**
```typescript
// ✅ Good
import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
const clean = normalizeEmail(email);
// ❌ Bad - don't duplicate utility logic
const clean = email.trim().toLowerCase();
```
3. **Don't mix validation and utilities**
```typescript
// ❌ Bad - mixing concerns
function processEmail(email: string) {
if (!email.includes("@")) return null; // Validation
return email.toLowerCase(); // Utility
if (!email.includes("@")) return null; // Validation
return email.toLowerCase(); // Utility
}
// ✅ Good - separate concerns
import { isValidEmail } from "@customer-portal/domain/common/validation";
import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
function processEmail(email: string) {
if (!isValidEmail(email)) return null;
return normalizeEmail(email);
@ -181,4 +185,3 @@ If you're unsure whether something belongs in toolkit or common/validation:
- **Ask**: "Does this transform or extract data?"
- YES → It's a utility → Use `toolkit/`
- NO → Might be validation → Use `common/validation.ts`

View File

@ -1,6 +1,6 @@
/**
* Toolkit - Date Formatting
*
*
* Utilities for formatting dates and times.
*/
@ -17,10 +17,7 @@ export interface DateFormatOptions {
/**
* Format an ISO date string for display
*/
export function formatDate(
isoString: string,
options: DateFormatOptions = {}
): string {
export function formatDate(isoString: string, options: DateFormatOptions = {}): string {
const {
locale = "en-US",
dateStyle = "medium",
@ -31,7 +28,7 @@ export function formatDate(
try {
const date = new Date(isoString);
if (isNaN(date.getTime())) {
return isoString; // Return original if invalid
}
@ -51,10 +48,7 @@ export function formatDate(
/**
* Format a date relative to now (e.g., "2 days ago", "in 3 hours")
*/
export function formatRelativeDate(
isoString: string,
options: { locale?: string } = {}
): string {
export function formatRelativeDate(isoString: string, options: { locale?: string } = {}): string {
const { locale = "en-US" } = options;
try {
@ -90,4 +84,3 @@ export function isValidDate(dateString: string): boolean {
const date = new Date(dateString);
return !isNaN(date.getTime());
}

View File

@ -1,6 +1,6 @@
/**
* Toolkit - Formatting
*
*
* Formatting utilities for currency, dates, phone numbers, etc.
*/
@ -8,4 +8,3 @@ export * from "./currency.js";
export * from "./date.js";
export * from "./phone.js";
export * from "./text.js";

View File

@ -1,6 +1,6 @@
/**
* Toolkit - Phone Number Formatting
*
*
* Utilities for formatting phone numbers.
*/
@ -37,12 +37,12 @@ export function formatPhoneNumber(phone: string): string {
*/
export function normalizePhoneNumber(phone: string, defaultCountryCode = "1"): string {
const digits = phone.replace(/\D/g, "");
// If already has country code, return with +
if (digits.length >= 10 && !digits.startsWith(defaultCountryCode)) {
return `+${digits}`;
}
// Add default country code
return `+${defaultCountryCode}${digits}`;
}
@ -56,7 +56,7 @@ export function formatPhoneForWhmcs(phone: string): string {
// Remove all non-digit characters except leading +
const hasPlus = phone.startsWith("+");
const digits = phone.replace(/\D/g, "");
if (digits.length === 0) {
return phone;
}
@ -65,7 +65,7 @@ export function formatPhoneForWhmcs(phone: string): string {
if (digits.startsWith("81") && digits.length >= 11) {
return `+81.${digits.slice(2)}`;
}
// For US/Canada numbers (10 digits or 11 starting with 1)
if (digits.length === 10) {
return `+1.${digits}`;
@ -88,4 +88,3 @@ export function formatPhoneForWhmcs(phone: string): string {
// Return with + prefix if it had one, otherwise as-is
return hasPlus ? `+${digits}` : digits;
}

View File

@ -1,6 +1,6 @@
/**
* Toolkit - Text Formatting
*
*
* Utilities for text manipulation and formatting.
*/
@ -56,12 +56,11 @@ export function maskString(str: string, visibleStart = 3, visibleEnd = 3, maskCh
if (str.length <= visibleStart + visibleEnd) {
return str;
}
const start = str.slice(0, visibleStart);
const end = str.slice(-visibleEnd);
const maskedLength = str.length - visibleStart - visibleEnd;
const masked = maskChar.repeat(maskedLength);
return `${start}${masked}${end}`;
}

View File

@ -1,6 +1,6 @@
/**
* Domain Toolkit
*
*
* Utility functions and helpers used across all domain packages.
*/
@ -24,4 +24,3 @@ export {
isSuccess,
isError,
} from "./typing/helpers.js";

View File

@ -1,6 +1,6 @@
/**
* Toolkit - Type Assertions
*
*
* Runtime assertion utilities for type safety.
*/
@ -62,4 +62,3 @@ export function assertNumber(
export function assertNever(value: never, message = "Unexpected value"): never {
throw new AssertionError(`${message}: ${JSON.stringify(value)}`);
}

View File

@ -1,6 +1,6 @@
/**
* Toolkit - Type Guards
*
*
* Type guard utilities for runtime type checking.
*/
@ -59,4 +59,3 @@ export function isDefined<T>(value: T | null | undefined): value is T {
export function filterDefined<T>(arr: (T | null | undefined)[]): T[] {
return arr.filter(isDefined);
}

View File

@ -1,10 +1,9 @@
/**
* Toolkit - Typing
*
*
* TypeScript type utilities and runtime type checking.
*/
export * from "./guards.js";
export * from "./assertions.js";
export * from "./helpers.js";

View File

@ -1,6 +1,6 @@
/**
* Toolkit - Email Utilities
*
*
* Email utility functions (extraction, normalization).
* For email validation, use functions from common/validation.ts
*/
@ -19,4 +19,3 @@ export function getEmailDomain(email: string): string | null {
export function normalizeEmail(email: string): string {
return email.trim().toLowerCase();
}

View File

@ -1,6 +1,6 @@
/**
* Toolkit - Validation
*
*
* Validation utilities for common data types.
*/
@ -8,5 +8,3 @@ export * from "./email.js";
export * from "./url.js";
export * from "./string.js";
export * from "./helpers.js";

View File

@ -1,6 +1,6 @@
/**
* Toolkit - String Validation
*
*
* String validation utilities.
*/
@ -45,4 +45,3 @@ export function isAlpha(str: string): boolean {
export function isNumeric(str: string): boolean {
return /^\d+$/.test(str);
}

View File

@ -1,6 +1,6 @@
/**
* Toolkit - URL Utilities
*
*
* URL parsing and manipulation utilities.
* For URL validation, use functions from common/validation.ts
*/
@ -26,4 +26,3 @@ export function getHostname(url: string): string | null {
return null;
}
}

15
pnpm-lock.yaml generated
View File

@ -241,6 +241,9 @@ importers:
tailwindcss:
specifier: ^4.1.17
version: 4.1.17
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.1.17)
typescript:
specifier: "catalog:"
version: 5.9.3
@ -6874,6 +6877,14 @@ packages:
integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==,
}
tailwindcss-animate@1.0.7:
resolution:
{
integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==,
}
peerDependencies:
tailwindcss: ">=3.0.0 || insiders"
tailwindcss@4.1.17:
resolution:
{
@ -11504,6 +11515,10 @@ snapshots:
tailwind-merge@3.4.0: {}
tailwindcss-animate@1.0.7(tailwindcss@4.1.17):
dependencies:
tailwindcss: 4.1.17
tailwindcss@4.1.17: {}
tapable@2.3.0: {}