commit
b8da286fce
@ -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
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
{
|
||||
"setup-worktree": [
|
||||
"pnpm install"
|
||||
]
|
||||
"setup-worktree": ["pnpm install"]
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
"./config/prettier.config.js"
|
||||
"./config/prettier.config.js"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -19,4 +19,3 @@ export default defineConfig({
|
||||
url: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -26,4 +26,3 @@ import { RateLimitGuard } from "./rate-limit.guard.js";
|
||||
exports: [RateLimitGuard],
|
||||
})
|
||||
export class RateLimitModule {}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -47,8 +47,6 @@ export class BaseServicesService {
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Query failed: ${context}`, {
|
||||
error: getErrorMessage(error),
|
||||
soql,
|
||||
context,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
2
apps/portal/next-env.d.ts
vendored
2
apps/portal/next-env.d.ts
vendored
@ -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.
|
||||
|
||||
@ -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:"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* Tailwind CSS v4 */
|
||||
@import "tailwindcss";
|
||||
@plugin "tailwindcss-animate";
|
||||
@import "../styles/tokens.css";
|
||||
@import "../styles/utilities.css";
|
||||
@import "../styles/responsive.css";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export { AgentforceWidget } from "./AgentforceWidget";
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -2,4 +2,3 @@ export { AccountStep } from "./AccountStep";
|
||||
export { AddressStep } from "./AddressStep";
|
||||
export { PasswordStep } from "./PasswordStep";
|
||||
export { ReviewStep } from "./ReviewStep";
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ export function AddressConfirmation({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed]);
|
||||
}, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed, setAddressConfirmed]);
|
||||
|
||||
useEffect(() => {
|
||||
log.info("Address confirmation component mounted");
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -25,4 +25,3 @@ export function useCreateCase() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from "./case-presenters";
|
||||
|
||||
|
||||
@ -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)}`;
|
||||
}
|
||||
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -39,4 +39,3 @@ volumes:
|
||||
networks:
|
||||
default:
|
||||
name: portal_dev
|
||||
|
||||
|
||||
@ -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"] }],
|
||||
},
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -11,4 +11,3 @@ type SignupRequestInput = z.input<typeof signupRequestSchema>;
|
||||
export function buildSignupRequest(input: SignupRequestInput) {
|
||||
return signupRequestSchema.parse(input);
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Billing Domain - Schemas
|
||||
*
|
||||
*
|
||||
* Zod validation schemas for billing domain types.
|
||||
* Used for runtime validation of data from any source.
|
||||
*/
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Common Provider Types
|
||||
*
|
||||
*
|
||||
* Generic provider-specific response structures used across multiple domains.
|
||||
*/
|
||||
|
||||
|
||||
@ -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),
|
||||
});
|
||||
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from "./raw.types.js";
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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>;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Common Domain - Types
|
||||
*
|
||||
*
|
||||
* Shared utility types and branded types used across all domains.
|
||||
*/
|
||||
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
/**
|
||||
* Portal Provider
|
||||
*
|
||||
*
|
||||
* Handles mapping from Prisma (portal database) to UserAuth domain type
|
||||
*/
|
||||
|
||||
export * from "./mapper.js";
|
||||
export * from "./types.js";
|
||||
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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),
|
||||
};
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -77,4 +77,3 @@ export function transformWhmcsPaymentGateway(raw: unknown): PaymentGateway {
|
||||
|
||||
return paymentGatewaySchema.parse(gateway);
|
||||
}
|
||||
|
||||
|
||||
@ -4,4 +4,3 @@
|
||||
*/
|
||||
|
||||
export * as Whmcs from "./whmcs/index.js";
|
||||
|
||||
|
||||
@ -4,4 +4,3 @@
|
||||
*/
|
||||
|
||||
export * from "./utils.js";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -138,4 +138,3 @@ export const SIM_MANAGEMENT_FLOW: ManagementFlow = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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>;
|
||||
|
||||
|
||||
@ -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>;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -15,4 +15,3 @@ export const Salesforce = {
|
||||
export { SalesforceMapper, SalesforceRaw };
|
||||
export * from "./salesforce/mapper.js";
|
||||
export * from "./salesforce/raw.types.js";
|
||||
|
||||
|
||||
@ -4,4 +4,3 @@
|
||||
|
||||
export * from "./raw.types.js";
|
||||
export * from "./mapper.js";
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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)}`);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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
15
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user