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:
parent
75dc6ec15d
commit
1b944f57aa
@ -1,4 +1,5 @@
|
|||||||
<!-- 67f8fea5-b6cb-4187-8097-25ccb37e1dcf fa268fdd-dd67-4003-bb94-8236ed95ab44 -->
|
<!-- 67f8fea5-b6cb-4187-8097-25ccb37e1dcf fa268fdd-dd67-4003-bb94-8236ed95ab44 -->
|
||||||
|
|
||||||
# Domain & BFF Clean Architecture Refactoring
|
# Domain & BFF Clean Architecture Refactoring
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@ -173,7 +174,7 @@ const result = this.orderWhmcsMapper.mapOrderItemsToWhmcs(items);
|
|||||||
With:
|
With:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Providers } from '@customer-portal/domain/orders';
|
import { Providers } from "@customer-portal/domain/orders";
|
||||||
const result = Providers.Whmcs.mapFulfillmentOrderItems(items);
|
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] Update orders.module.ts and salesforce.module.ts with new services
|
||||||
- [x] Verify catalog services follow same clean pattern (already correct)
|
- [x] Verify catalog services follow same clean pattern (already correct)
|
||||||
- [x] Update domain README and architecture documentation with clean patterns
|
- [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": [
|
"setup-worktree": ["pnpm install"]
|
||||||
"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
|
## Directory Structure
|
||||||
|
|
||||||
### Development (Monorepo)
|
### Development (Monorepo)
|
||||||
|
|
||||||
```
|
```
|
||||||
/project-root/
|
/project-root/
|
||||||
├── apps/bff/
|
├── apps/bff/
|
||||||
@ -24,6 +25,7 @@ Prisma embeds the schema path into the generated client. We regenerate the clien
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Production (Docker Container)
|
### Production (Docker Container)
|
||||||
|
|
||||||
```
|
```
|
||||||
/app/
|
/app/
|
||||||
├── prisma/
|
├── prisma/
|
||||||
@ -36,12 +38,12 @@ Prisma embeds the schema path into the generated client. We regenerate the clien
|
|||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
| ------------------ | ---------------------------------------------------------- |
|
||||||
| `pnpm db:generate` | Regenerate Prisma client (`--schema=prisma/schema.prisma`) |
|
| `pnpm db:generate` | Regenerate Prisma client (`--schema=prisma/schema.prisma`) |
|
||||||
| `pnpm db:migrate` | Run migrations (dev) 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:studio` | Open Prisma Studio with explicit schema path |
|
||||||
| `pnpm db:reset` | Reset database with explicit schema path |
|
| `pnpm db:reset` | Reset database with explicit schema path |
|
||||||
|
|
||||||
## Binary Targets
|
## Binary Targets
|
||||||
|
|
||||||
|
|||||||
@ -19,4 +19,3 @@ export default defineConfig({
|
|||||||
url: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
|
url: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
export { RateLimitModule } from "./rate-limit.module.js";
|
export { RateLimitModule } from "./rate-limit.module.js";
|
||||||
export { RateLimitGuard } from "./rate-limit.guard.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)
|
* Skip rate limiting for this route (useful when applied at controller level)
|
||||||
*/
|
*/
|
||||||
export const SkipRateLimit = () => SetMetadata(RATE_LIMIT_KEY, { skip: true } as RateLimitOptions);
|
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
|
* 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 handlerName = context.getHandler().name;
|
||||||
const controllerName = context.getClass().name;
|
const controllerName = context.getClass().name;
|
||||||
const cacheKey = `${controllerName}:${handlerName}:${options.limit}:${options.ttl}`;
|
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],
|
exports: [RateLimitGuard],
|
||||||
})
|
})
|
||||||
export class RateLimitModule {}
|
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 { ConfigService } from "@nestjs/config";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
|
|||||||
@ -39,17 +39,32 @@ export class BaseServicesService {
|
|||||||
soql: string,
|
soql: string,
|
||||||
context: string
|
context: string
|
||||||
): Promise<TRecord[]> {
|
): Promise<TRecord[]> {
|
||||||
|
this.logger.debug(`Executing Salesforce query for ${context}`, {
|
||||||
|
soql: soql.replace(/\s+/g, " ").trim(),
|
||||||
|
portalPricebookId: this.portalPriceBookId,
|
||||||
|
portalCategoryField: this.portalCategoryField,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = (await this.sf.query(soql, {
|
const res = (await this.sf.query(soql, {
|
||||||
label: `services:${context.replace(/\s+/g, "_").toLowerCase()}`,
|
label: `services:${context.replace(/\s+/g, "_").toLowerCase()}`,
|
||||||
})) as SalesforceResponse<TRecord>;
|
})) as SalesforceResponse<TRecord>;
|
||||||
return res.records ?? [];
|
const records = res.records ?? [];
|
||||||
} catch (error: unknown) {
|
this.logger.debug(`Query result for ${context}`, {
|
||||||
this.logger.error(`Query failed: ${context}`, {
|
recordCount: records.length,
|
||||||
error: getErrorMessage(error),
|
records: records.map(r => ({
|
||||||
soql,
|
id: r.Id,
|
||||||
context,
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,11 +67,7 @@ export class SimServicesService extends BaseServicesService {
|
|||||||
return this.catalogCache.getCachedServices(
|
return this.catalogCache.getCachedServices(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
async () => {
|
async () => {
|
||||||
const soql = this.buildProductQuery("SIM", "Activation", [
|
const soql = this.buildProductQuery("SIM", "Activation", ["Catalog_Order__c"]);
|
||||||
"Catalog_Order__c",
|
|
||||||
"Auto_Add__c",
|
|
||||||
"Is_Default__c",
|
|
||||||
]);
|
|
||||||
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
"SIM Activation Fees"
|
"SIM Activation Fees"
|
||||||
|
|||||||
@ -55,7 +55,10 @@ export class VpnServicesService extends BaseServicesService {
|
|||||||
return this.catalogCache.getCachedServices(
|
return this.catalogCache.getCachedServices(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
async () => {
|
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>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
"VPN Activation Fees"
|
"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" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <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
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "node ./scripts/dev-prep.mjs",
|
"predev": "node ./scripts/dev-prep.mjs",
|
||||||
"dev": "next dev -p ${NEXT_PORT:-3000}",
|
"dev": "next dev -p ${NEXT_PORT:-3000}",
|
||||||
"build": "next build --webpack",
|
"build": "next build",
|
||||||
"build:turbo": "next build",
|
"build:turbo": "next build",
|
||||||
"build:analyze": "ANALYZE=true next build",
|
"build:analyze": "ANALYZE=true next build",
|
||||||
"analyze": "pnpm run build:analyze",
|
"analyze": "pnpm run build:analyze",
|
||||||
@ -40,6 +40,7 @@
|
|||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
/* Tailwind CSS v4 */
|
/* Tailwind CSS v4 */
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@plugin "tailwindcss-animate";
|
||||||
@import "../styles/tokens.css";
|
@import "../styles/tokens.css";
|
||||||
@import "../styles/utilities.css";
|
@import "../styles/utilities.css";
|
||||||
@import "../styles/responsive.css";
|
@import "../styles/responsive.css";
|
||||||
|
|||||||
@ -17,8 +17,7 @@ const errorMessageVariants = cva("flex items-center gap-1 text-sm", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface ErrorMessageProps
|
interface ErrorMessageProps
|
||||||
extends React.HTMLAttributes<HTMLParagraphElement>,
|
extends React.HTMLAttributes<HTMLParagraphElement>, VariantProps<typeof errorMessageVariants> {
|
||||||
VariantProps<typeof errorMessageVariants> {
|
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export { AgentforceWidget } from "./AgentforceWidget";
|
export { AgentforceWidget } from "./AgentforceWidget";
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export const AuroraBackground = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-bg relative flex h-[100vh] flex-col items-center justify-center bg-zinc-50 text-slate-950 dark:bg-zinc-900",
|
"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}
|
{...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)]`,
|
`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 &&
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,4 +2,3 @@ export { AccountStep } from "./AccountStep";
|
|||||||
export { AddressStep } from "./AddressStep";
|
export { AddressStep } from "./AddressStep";
|
||||||
export { PasswordStep } from "./PasswordStep";
|
export { PasswordStep } from "./PasswordStep";
|
||||||
export { ReviewStep } from "./ReviewStep";
|
export { ReviewStep } from "./ReviewStep";
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,7 @@
|
|||||||
|
|
||||||
import type { OrderDisplayItem } from "@customer-portal/domain/orders";
|
import type { OrderDisplayItem } from "@customer-portal/domain/orders";
|
||||||
|
|
||||||
export {
|
export { buildOrderDisplayItems, categorizeOrderItem } from "@customer-portal/domain/orders";
|
||||||
buildOrderDisplayItems,
|
|
||||||
categorizeOrderItem,
|
|
||||||
} from "@customer-portal/domain/orders";
|
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
OrderDisplayItem,
|
OrderDisplayItem,
|
||||||
@ -20,10 +17,7 @@ export type {
|
|||||||
/**
|
/**
|
||||||
* Summarize order display items for compact display
|
* Summarize order display items for compact display
|
||||||
*/
|
*/
|
||||||
export function summarizeOrderDisplayItems(
|
export function summarizeOrderDisplayItems(items: OrderDisplayItem[], fallback: string): string {
|
||||||
items: OrderDisplayItem[],
|
|
||||||
fallback: string
|
|
||||||
): string {
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export function AddressConfirmation({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed]);
|
}, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed, setAddressConfirmed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
log.info("Address confirmation component mounted");
|
log.info("Address confirmation component mounted");
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { apiClient } from "@/lib/api";
|
import { apiClient } from "@/lib/api";
|
||||||
import type {
|
import type { SimTopUpPricing, SimTopUpPricingPreviewResponse } from "@customer-portal/domain/sim";
|
||||||
SimTopUpPricing,
|
|
||||||
SimTopUpPricingPreviewResponse,
|
|
||||||
} from "@customer-portal/domain/sim";
|
|
||||||
|
|
||||||
interface UseSimTopUpPricingResult {
|
interface UseSimTopUpPricingResult {
|
||||||
pricing: SimTopUpPricing | null;
|
pricing: SimTopUpPricing | null;
|
||||||
@ -24,7 +21,7 @@ export function useSimTopUpPricing(): UseSimTopUpPricingResult {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await apiClient.GET("/api/subscriptions/sim/top-up/pricing");
|
const response = await apiClient.GET("/api/subscriptions/sim/top-up/pricing");
|
||||||
|
|
||||||
if (mounted && response.data) {
|
if (mounted && response.data) {
|
||||||
const data = response.data as { success: boolean; data: SimTopUpPricing };
|
const data = response.data as { success: boolean; data: SimTopUpPricing };
|
||||||
setPricing(data.data);
|
setPricing(data.data);
|
||||||
@ -66,4 +63,3 @@ export function useSimTopUpPricing(): UseSimTopUpPricingResult {
|
|||||||
|
|
||||||
return { pricing, loading, error, calculatePreview };
|
return { pricing, loading, error, calculatePreview };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export function getSimPlanSku(planCode?: string): string | undefined {
|
|||||||
*/
|
*/
|
||||||
export function mapToSimplifiedFormat(planCode?: string): string {
|
export function mapToSimplifiedFormat(planCode?: string): string {
|
||||||
if (!planCode) return "";
|
if (!planCode) return "";
|
||||||
|
|
||||||
// Handle Freebit format (PASI_5G, PASI_25G, etc.)
|
// Handle Freebit format (PASI_5G, PASI_25G, etc.)
|
||||||
if (planCode.startsWith("PASI_")) {
|
if (planCode.startsWith("PASI_")) {
|
||||||
const match = planCode.match(/PASI_(\d+)G/);
|
const match = planCode.match(/PASI_(\d+)G/);
|
||||||
@ -50,13 +50,13 @@ export function mapToSimplifiedFormat(planCode?: string): string {
|
|||||||
return `${match[1]}GB`;
|
return `${match[1]}GB`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle other formats that might end with G or GB
|
// Handle other formats that might end with G or GB
|
||||||
const match = planCode.match(/(\d+)\s*G(?:B)?\b/i);
|
const match = planCode.match(/(\d+)\s*G(?:B)?\b/i);
|
||||||
if (match) {
|
if (match) {
|
||||||
return `${match[1]}GB`;
|
return `${match[1]}GB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return as-is if no pattern matches
|
// Return as-is if no pattern matches
|
||||||
return planCode;
|
return planCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,4 +25,3 @@ export function useCreateCase() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from "./case-presenters";
|
export * from "./case-presenters";
|
||||||
|
|
||||||
|
|||||||
@ -68,15 +68,15 @@ export const JAPAN_PREFECTURES: PrefectureOption[] = [
|
|||||||
*/
|
*/
|
||||||
export function formatJapanesePostalCode(value: string): string {
|
export function formatJapanesePostalCode(value: string): string {
|
||||||
const digits = value.replace(/\D/g, "");
|
const digits = value.replace(/\D/g, "");
|
||||||
|
|
||||||
if (digits.length <= 3) {
|
if (digits.length <= 3) {
|
||||||
return digits;
|
return digits;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (digits.length <= 7) {
|
if (digits.length <= 7) {
|
||||||
return `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
return `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${digits.slice(0, 3)}-${digits.slice(3, 7)}`;
|
return `${digits.slice(0, 3)}-${digits.slice(3, 7)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
# Complete Portainer Guide for Customer Portal
|
# Complete Portainer Guide for Customer Portal
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Creating a Stack in Portainer](#creating-a-stack-in-portainer)
|
1. [Creating a Stack in Portainer](#creating-a-stack-in-portainer)
|
||||||
2. [Repository vs Upload vs Web Editor](#stack-creation-methods)
|
2. [Repository vs Upload vs Web Editor](#stack-creation-methods)
|
||||||
3. [Security Concerns & Best Practices](#security-concerns)
|
3. [Security Concerns & Best Practices](#security-concerns)
|
||||||
@ -22,6 +23,7 @@
|
|||||||
Click **"+ Add stack"** button
|
Click **"+ Add stack"** button
|
||||||
|
|
||||||
You'll see three creation methods:
|
You'll see three creation methods:
|
||||||
|
|
||||||
- **Web editor** - Paste compose file directly
|
- **Web editor** - Paste compose file directly
|
||||||
- **Upload** - Upload a compose file
|
- **Upload** - Upload a compose file
|
||||||
- **Repository** - Pull from Git repository
|
- **Repository** - Pull from Git repository
|
||||||
@ -45,16 +47,19 @@ Click **"Deploy the stack"**
|
|||||||
### Method 1: Web Editor (Simplest)
|
### Method 1: Web Editor (Simplest)
|
||||||
|
|
||||||
**How:**
|
**How:**
|
||||||
|
|
||||||
1. Select "Web editor"
|
1. Select "Web editor"
|
||||||
2. Paste your `docker-compose.yml` content
|
2. Paste your `docker-compose.yml` content
|
||||||
3. Add environment variables manually or load from file
|
3. Add environment variables manually or load from file
|
||||||
|
|
||||||
**Pros:**
|
**Pros:**
|
||||||
|
|
||||||
- ✅ Quick and simple
|
- ✅ Quick and simple
|
||||||
- ✅ No external dependencies
|
- ✅ No external dependencies
|
||||||
- ✅ Full control over content
|
- ✅ Full control over content
|
||||||
|
|
||||||
**Cons:**
|
**Cons:**
|
||||||
|
|
||||||
- ❌ Manual updates required
|
- ❌ Manual updates required
|
||||||
- ❌ No version control
|
- ❌ No version control
|
||||||
- ❌ Easy to make mistakes when editing
|
- ❌ Easy to make mistakes when editing
|
||||||
@ -66,17 +71,20 @@ Click **"Deploy the stack"**
|
|||||||
### Method 2: Upload (Recommended for Your Case)
|
### Method 2: Upload (Recommended for Your Case)
|
||||||
|
|
||||||
**How:**
|
**How:**
|
||||||
|
|
||||||
1. Select "Upload"
|
1. Select "Upload"
|
||||||
2. Upload your `docker-compose.yml` file
|
2. Upload your `docker-compose.yml` file
|
||||||
3. Optionally upload a `.env` file for environment variables
|
3. Optionally upload a `.env` file for environment variables
|
||||||
|
|
||||||
**Pros:**
|
**Pros:**
|
||||||
|
|
||||||
- ✅ Version control on your local machine
|
- ✅ Version control on your local machine
|
||||||
- ✅ Can prepare and test locally
|
- ✅ Can prepare and test locally
|
||||||
- ✅ No external network dependencies
|
- ✅ No external network dependencies
|
||||||
- ✅ Works in air-gapped environments
|
- ✅ Works in air-gapped environments
|
||||||
|
|
||||||
**Cons:**
|
**Cons:**
|
||||||
|
|
||||||
- ❌ Manual upload for each update
|
- ❌ Manual upload for each update
|
||||||
- ❌ Need to manage files locally
|
- ❌ Need to manage files locally
|
||||||
|
|
||||||
@ -87,12 +95,14 @@ Click **"Deploy the stack"**
|
|||||||
### Method 3: Repository (Git Integration)
|
### Method 3: Repository (Git Integration)
|
||||||
|
|
||||||
**How:**
|
**How:**
|
||||||
|
|
||||||
1. Select "Repository"
|
1. Select "Repository"
|
||||||
2. Enter repository URL (GitHub, GitLab, Bitbucket, etc.)
|
2. Enter repository URL (GitHub, GitLab, Bitbucket, etc.)
|
||||||
3. Specify branch and compose file path
|
3. Specify branch and compose file path
|
||||||
4. Add authentication if private repo
|
4. Add authentication if private repo
|
||||||
|
|
||||||
**Example Configuration:**
|
**Example Configuration:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Repository URL: https://github.com/your-org/customer-portal
|
Repository URL: https://github.com/your-org/customer-portal
|
||||||
Reference: main
|
Reference: main
|
||||||
@ -100,16 +110,19 @@ Compose path: docker/portainer/docker-compose.yml
|
|||||||
```
|
```
|
||||||
|
|
||||||
**For Private Repos:**
|
**For Private Repos:**
|
||||||
|
|
||||||
- Use a Personal Access Token (PAT) as password
|
- Use a Personal Access Token (PAT) as password
|
||||||
- Or use deploy keys
|
- Or use deploy keys
|
||||||
|
|
||||||
**Pros:**
|
**Pros:**
|
||||||
|
|
||||||
- ✅ Version controlled
|
- ✅ Version controlled
|
||||||
- ✅ Easy to update (just click "Pull and redeploy")
|
- ✅ Easy to update (just click "Pull and redeploy")
|
||||||
- ✅ Team can review changes via PR
|
- ✅ Team can review changes via PR
|
||||||
- ✅ Audit trail of changes
|
- ✅ Audit trail of changes
|
||||||
|
|
||||||
**Cons:**
|
**Cons:**
|
||||||
|
|
||||||
- ❌ Requires network access to repo
|
- ❌ Requires network access to repo
|
||||||
- ❌ Secrets in repo = security risk
|
- ❌ Secrets in repo = security risk
|
||||||
- ❌ Need to manage repo access tokens
|
- ❌ Need to manage repo access tokens
|
||||||
@ -124,6 +137,7 @@ Compose path: docker/portainer/docker-compose.yml
|
|||||||
**Use: Upload + Environment Variables in Portainer UI**
|
**Use: Upload + Environment Variables in Portainer UI**
|
||||||
|
|
||||||
Why:
|
Why:
|
||||||
|
|
||||||
1. Your compose file rarely changes (it's just orchestration)
|
1. Your compose file rarely changes (it's just orchestration)
|
||||||
2. Sensitive data stays in Portainer, not in Git
|
2. Sensitive data stays in Portainer, not in Git
|
||||||
3. Image updates are done via environment variables
|
3. Image updates are done via environment variables
|
||||||
@ -136,6 +150,7 @@ Why:
|
|||||||
### 🔴 Critical Security Issues
|
### 🔴 Critical Security Issues
|
||||||
|
|
||||||
#### 1. Never Store Secrets in Git
|
#### 1. Never Store Secrets in Git
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ❌ BAD - Secrets in compose file
|
# ❌ BAD - Secrets in compose file
|
||||||
environment:
|
environment:
|
||||||
@ -149,6 +164,7 @@ environment:
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Never Store Secrets in Docker Images
|
#### 2. Never Store Secrets in Docker Images
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
# ❌ BAD - Secrets baked into image
|
# ❌ BAD - Secrets baked into image
|
||||||
ENV JWT_SECRET="my-secret"
|
ENV JWT_SECRET="my-secret"
|
||||||
@ -159,6 +175,7 @@ COPY secrets/ /app/secrets/
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 3. Portainer Access Control
|
#### 3. Portainer Access Control
|
||||||
|
|
||||||
```
|
```
|
||||||
⚠️ Portainer has full Docker access = root on the host
|
⚠️ Portainer has full Docker access = root on the host
|
||||||
|
|
||||||
@ -173,6 +190,7 @@ Best practices:
|
|||||||
### 🟡 Medium Security Concerns
|
### 🟡 Medium Security Concerns
|
||||||
|
|
||||||
#### 4. Environment Variables in Portainer
|
#### 4. Environment Variables in Portainer
|
||||||
|
|
||||||
```
|
```
|
||||||
Portainer stores env vars in its database.
|
Portainer stores env vars in its database.
|
||||||
This is generally safe, but consider:
|
This is generally safe, but consider:
|
||||||
@ -188,6 +206,7 @@ Mitigation:
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 5. Image Trust
|
#### 5. Image Trust
|
||||||
|
|
||||||
```
|
```
|
||||||
⚠️ You're loading .tar files - verify their integrity
|
⚠️ You're loading .tar files - verify their integrity
|
||||||
|
|
||||||
@ -198,6 +217,7 @@ Best practice:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Add to build script:
|
Add to build script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate checksums
|
# Generate checksums
|
||||||
sha256sum portal-frontend.latest.tar > portal-frontend.latest.tar.sha256
|
sha256sum portal-frontend.latest.tar > portal-frontend.latest.tar.sha256
|
||||||
@ -209,6 +229,7 @@ sha256sum -c portal-backend.latest.tar.sha256
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 6. Network Exposure
|
#### 6. Network Exposure
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ❌ BAD - Database exposed to host
|
# ❌ BAD - Database exposed to host
|
||||||
database:
|
database:
|
||||||
@ -225,6 +246,7 @@ database:
|
|||||||
### 🟢 Good Security Practices (Already in Place)
|
### 🟢 Good Security Practices (Already in Place)
|
||||||
|
|
||||||
Your current setup does these right:
|
Your current setup does these right:
|
||||||
|
|
||||||
- ✅ Non-root users in containers
|
- ✅ Non-root users in containers
|
||||||
- ✅ Health checks configured
|
- ✅ Health checks configured
|
||||||
- ✅ Database/Redis not exposed externally
|
- ✅ Database/Redis not exposed externally
|
||||||
@ -252,12 +274,14 @@ watchtower:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Why NOT recommended:**
|
**Why NOT recommended:**
|
||||||
|
|
||||||
- ❌ No control over when updates happen
|
- ❌ No control over when updates happen
|
||||||
- ❌ No rollback mechanism
|
- ❌ No rollback mechanism
|
||||||
- ❌ Can break production unexpectedly
|
- ❌ Can break production unexpectedly
|
||||||
- ❌ Requires images in a registry (not .tar files)
|
- ❌ Requires images in a registry (not .tar files)
|
||||||
|
|
||||||
We've disabled Watchtower in your compose:
|
We've disabled Watchtower in your compose:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
labels:
|
labels:
|
||||||
- "com.centurylinklabs.watchtower.enable=false"
|
- "com.centurylinklabs.watchtower.enable=false"
|
||||||
@ -270,27 +294,32 @@ labels:
|
|||||||
Portainer can expose a webhook URL that triggers stack redeployment.
|
Portainer can expose a webhook URL that triggers stack redeployment.
|
||||||
|
|
||||||
**Setup:**
|
**Setup:**
|
||||||
|
|
||||||
1. Go to Stack → Settings
|
1. Go to Stack → Settings
|
||||||
2. Enable "Webhook"
|
2. Enable "Webhook"
|
||||||
3. Copy the webhook URL
|
3. Copy the webhook URL
|
||||||
|
|
||||||
**Trigger from CI/CD:**
|
**Trigger from CI/CD:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# In your GitHub Actions / GitLab CI
|
# In your GitHub Actions / GitLab CI
|
||||||
curl -X POST "https://your-portainer:9443/api/stacks/webhook/abc123"
|
curl -X POST "https://your-portainer:9443/api/stacks/webhook/abc123"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Workflow:**
|
**Workflow:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Build Images → Push to Registry → Trigger Webhook → Portainer Redeploys
|
Build Images → Push to Registry → Trigger Webhook → Portainer Redeploys
|
||||||
```
|
```
|
||||||
|
|
||||||
**Pros:**
|
**Pros:**
|
||||||
|
|
||||||
- ✅ Controlled updates
|
- ✅ Controlled updates
|
||||||
- ✅ Integrated with CI/CD
|
- ✅ Integrated with CI/CD
|
||||||
- ✅ Can add approval gates
|
- ✅ Can add approval gates
|
||||||
|
|
||||||
**Cons:**
|
**Cons:**
|
||||||
|
|
||||||
- ❌ Requires images in a registry
|
- ❌ Requires images in a registry
|
||||||
- ❌ Webhook URL is a secret
|
- ❌ Webhook URL is a secret
|
||||||
- ❌ Limited rollback options
|
- ❌ 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:**
|
**Make it a one-liner:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# deploy.sh - Run locally
|
# deploy.sh - Run locally
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
@ -339,11 +369,13 @@ echo "✅ Deployed ${TAG}"
|
|||||||
If you want auto-updates, use a registry:
|
If you want auto-updates, use a registry:
|
||||||
|
|
||||||
**Free Options:**
|
**Free Options:**
|
||||||
|
|
||||||
- GitHub Container Registry (ghcr.io) - free for public repos
|
- GitHub Container Registry (ghcr.io) - free for public repos
|
||||||
- GitLab Container Registry - free
|
- GitLab Container Registry - free
|
||||||
- Docker Hub - 1 private repo free
|
- Docker Hub - 1 private repo free
|
||||||
|
|
||||||
**Setup:**
|
**Setup:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build and push
|
# Build and push
|
||||||
./scripts/plesk/build-images.sh --tag v1.2.3 --push ghcr.io/your-org
|
./scripts/plesk/build-images.sh --tag v1.2.3 --push ghcr.io/your-org
|
||||||
@ -377,6 +409,7 @@ services:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
|
|
||||||
1. Build: `./scripts/plesk/build-images.sh --tag 20241201-abc`
|
1. Build: `./scripts/plesk/build-images.sh --tag 20241201-abc`
|
||||||
2. Upload: `scp *.tar server:/path/images/`
|
2. Upload: `scp *.tar server:/path/images/`
|
||||||
3. Load: `docker load -i *.tar`
|
3. Load: `docker load -i *.tar`
|
||||||
@ -404,17 +437,19 @@ services:
|
|||||||
## Quick Reference: Portainer Stack Commands
|
## Quick Reference: Portainer Stack Commands
|
||||||
|
|
||||||
### Via Portainer UI
|
### Via Portainer UI
|
||||||
| Action | Steps |
|
|
||||||
|--------|-------|
|
| Action | Steps |
|
||||||
| Create stack | Stacks → Add stack → Configure → Deploy |
|
| ------------ | -------------------------------------------------- |
|
||||||
| Update stack | Stacks → Select → Editor → Update |
|
| Create stack | Stacks → Add stack → Configure → Deploy |
|
||||||
|
| Update stack | Stacks → Select → Editor → Update |
|
||||||
| Change image | Stacks → Select → Env vars → Change IMAGE → Update |
|
| Change image | Stacks → Select → Env vars → Change IMAGE → Update |
|
||||||
| View logs | Stacks → Select → Container → Logs |
|
| View logs | Stacks → Select → Container → Logs |
|
||||||
| Restart | Stacks → Select → Container → Restart |
|
| Restart | Stacks → Select → Container → Restart |
|
||||||
| Stop | Stacks → Select → Stop |
|
| Stop | Stacks → Select → Stop |
|
||||||
| Delete | Stacks → Select → Delete |
|
| Delete | Stacks → Select → Delete |
|
||||||
|
|
||||||
### Via CLI (on server)
|
### Via CLI (on server)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Navigate to stack directory
|
# Navigate to stack directory
|
||||||
cd /path/to/portal
|
cd /path/to/portal
|
||||||
@ -442,11 +477,10 @@ docker compose --env-file stack.env down -v
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
| Aspect | Recommendation |
|
| Aspect | Recommendation |
|
||||||
|--------|---------------|
|
| ------------------ | ------------------------------------------------------------------ |
|
||||||
| Stack creation | **Upload** method (version control locally, no secrets in git) |
|
| Stack creation | **Upload** method (version control locally, no secrets in git) |
|
||||||
| Secrets management | **Portainer env vars** or **mounted secrets volume** |
|
| Secrets management | **Portainer env vars** or **mounted secrets volume** |
|
||||||
| Image updates | **Manual script** for now, migrate to **registry + webhook** later |
|
| Image updates | **Manual script** for now, migrate to **registry + webhook** later |
|
||||||
| Auto-updates | **Not recommended** for production; use controlled deployments |
|
| Auto-updates | **Not recommended** for production; use controlled deployments |
|
||||||
| Rollback | Keep previous image tags, update env vars to rollback |
|
| Rollback | Keep previous image tags, update env vars to rollback |
|
||||||
|
|
||||||
|
|||||||
@ -39,4 +39,3 @@ volumes:
|
|||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
name: portal_dev
|
name: portal_dev
|
||||||
|
|
||||||
|
|||||||
@ -71,7 +71,10 @@ export default [
|
|||||||
files: [...BFF_TS_FILES, "packages/domain/**/*.ts"],
|
files: [...BFF_TS_FILES, "packages/domain/**/*.ts"],
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/consistent-type-imports": "error",
|
"@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"] }],
|
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -72,18 +72,22 @@ packages/domain/
|
|||||||
## 🎯 Design Principles
|
## 🎯 Design Principles
|
||||||
|
|
||||||
### 1. **Domain-First Organization**
|
### 1. **Domain-First Organization**
|
||||||
|
|
||||||
Each business domain owns its:
|
Each business domain owns its:
|
||||||
|
|
||||||
- **`contract.ts`** - TypeScript interfaces (provider-agnostic)
|
- **`contract.ts`** - TypeScript interfaces (provider-agnostic)
|
||||||
- **`schema.ts`** - Zod validation schemas (runtime safety)
|
- **`schema.ts`** - Zod validation schemas (runtime safety)
|
||||||
- **`providers/`** - Provider-specific adapters (WHMCS, Salesforce, Freebit)
|
- **`providers/`** - Provider-specific adapters (WHMCS, Salesforce, Freebit)
|
||||||
|
|
||||||
### 2. **Single Source of Truth**
|
### 2. **Single Source of Truth**
|
||||||
|
|
||||||
- ✅ All types defined in domain package
|
- ✅ All types defined in domain package
|
||||||
- ✅ All validation schemas in domain package
|
- ✅ All validation schemas in domain package
|
||||||
- ✅ No duplicate type definitions in apps
|
- ✅ No duplicate type definitions in apps
|
||||||
- ✅ Shared between frontend (Next.js) and backend (NestJS)
|
- ✅ Shared between frontend (Next.js) and backend (NestJS)
|
||||||
|
|
||||||
### 3. **Type Safety + Runtime Validation**
|
### 3. **Type Safety + Runtime Validation**
|
||||||
|
|
||||||
- TypeScript provides compile-time type checking
|
- TypeScript provides compile-time type checking
|
||||||
- Zod schemas provide runtime validation
|
- Zod schemas provide runtime validation
|
||||||
- Use `z.infer<typeof schema>` to derive types from schemas
|
- 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**
|
### **API Response Handling**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {
|
import {
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
ApiErrorResponse,
|
ApiErrorResponse,
|
||||||
apiResponseSchema
|
apiResponseSchema,
|
||||||
} from "@customer-portal/domain/common";
|
} from "@customer-portal/domain/common";
|
||||||
|
|
||||||
// Type-safe API responses
|
// Type-safe API responses
|
||||||
const response: ApiResponse<Invoice> = {
|
const response: ApiResponse<Invoice> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: { /* invoice data */ }
|
data: {
|
||||||
|
/* invoice data */
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// With validation
|
// With validation
|
||||||
@ -124,9 +130,9 @@ const validated = apiResponseSchema(invoiceSchema).parse(rawResponse);
|
|||||||
### **Query Parameters with Validation**
|
### **Query Parameters with Validation**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {
|
import {
|
||||||
InvoiceQueryParams,
|
InvoiceQueryParams,
|
||||||
invoiceQueryParamsSchema
|
invoiceQueryParamsSchema
|
||||||
} from "@customer-portal/domain/billing";
|
} from "@customer-portal/domain/billing";
|
||||||
|
|
||||||
// In BFF controller
|
// In BFF controller
|
||||||
@ -172,10 +178,7 @@ function LoginForm() {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { ZodValidationPipe } from "@bff/core/validation";
|
import { ZodValidationPipe } from "@bff/core/validation";
|
||||||
import {
|
import { createOrderRequestSchema, type CreateOrderRequest } from "@customer-portal/domain/orders";
|
||||||
createOrderRequestSchema,
|
|
||||||
type CreateOrderRequest
|
|
||||||
} from "@customer-portal/domain/orders";
|
|
||||||
|
|
||||||
@Controller("orders")
|
@Controller("orders")
|
||||||
export class OrdersController {
|
export class OrdersController {
|
||||||
@ -194,39 +197,39 @@ export class OrdersController {
|
|||||||
|
|
||||||
### **API Responses**
|
### **API Responses**
|
||||||
|
|
||||||
| Schema | Description |
|
| Schema | Description |
|
||||||
|--------|-------------|
|
| -------------------------------------- | ------------------------------------ |
|
||||||
| `apiSuccessResponseSchema(dataSchema)` | Successful API response wrapper |
|
| `apiSuccessResponseSchema(dataSchema)` | Successful API response wrapper |
|
||||||
| `apiErrorResponseSchema` | Error API response with code/message |
|
| `apiErrorResponseSchema` | Error API response with code/message |
|
||||||
| `apiResponseSchema(dataSchema)` | Discriminated union of success/error |
|
| `apiResponseSchema(dataSchema)` | Discriminated union of success/error |
|
||||||
|
|
||||||
### **Pagination & Queries**
|
### **Pagination & Queries**
|
||||||
|
|
||||||
| Schema | Description |
|
| Schema | Description |
|
||||||
|--------|-------------|
|
| ------------------------------------- | ------------------------------ |
|
||||||
| `paginationParamsSchema` | Page, limit, offset parameters |
|
| `paginationParamsSchema` | Page, limit, offset parameters |
|
||||||
| `paginatedResponseSchema(itemSchema)` | Paginated list response |
|
| `paginatedResponseSchema(itemSchema)` | Paginated list response |
|
||||||
| `filterParamsSchema` | Search, sortBy, sortOrder |
|
| `filterParamsSchema` | Search, sortBy, sortOrder |
|
||||||
| `queryParamsSchema` | Combined pagination + filters |
|
| `queryParamsSchema` | Combined pagination + filters |
|
||||||
|
|
||||||
### **Domain-Specific Query Params**
|
### **Domain-Specific Query Params**
|
||||||
|
|
||||||
| Schema | Description |
|
| Schema | Description |
|
||||||
|--------|-------------|
|
| ------------------------------- | -------------------------------------- |
|
||||||
| `invoiceQueryParamsSchema` | Invoice list filtering (status, dates) |
|
| `invoiceQueryParamsSchema` | Invoice list filtering (status, dates) |
|
||||||
| `subscriptionQueryParamsSchema` | Subscription filtering (status, type) |
|
| `subscriptionQueryParamsSchema` | Subscription filtering (status, type) |
|
||||||
| `orderQueryParamsSchema` | Order filtering (status, orderType) |
|
| `orderQueryParamsSchema` | Order filtering (status, orderType) |
|
||||||
|
|
||||||
### **Validation Primitives**
|
### **Validation Primitives**
|
||||||
|
|
||||||
| Schema | Description |
|
| Schema | Description |
|
||||||
|--------|-------------|
|
| ----------------- | ------------------------------------------------------- |
|
||||||
| `emailSchema` | Email validation (lowercase, trimmed) |
|
| `emailSchema` | Email validation (lowercase, trimmed) |
|
||||||
| `passwordSchema` | Strong password (8+ chars, mixed case, number, special) |
|
| `passwordSchema` | Strong password (8+ chars, mixed case, number, special) |
|
||||||
| `nameSchema` | Name validation (1-100 chars) |
|
| `nameSchema` | Name validation (1-100 chars) |
|
||||||
| `phoneSchema` | Phone number validation |
|
| `phoneSchema` | Phone number validation |
|
||||||
| `timestampSchema` | ISO datetime string |
|
| `timestampSchema` | ISO datetime string |
|
||||||
| `dateSchema` | ISO date string |
|
| `dateSchema` | ISO date string |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -281,11 +284,11 @@ export * from "./schema";
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { ZodValidationPipe } from "@bff/core/validation";
|
import { ZodValidationPipe } from "@bff/core/validation";
|
||||||
import {
|
import {
|
||||||
myEntitySchema,
|
myEntitySchema,
|
||||||
myEntityQueryParamsSchema,
|
myEntityQueryParamsSchema,
|
||||||
type MyEntity,
|
type MyEntity,
|
||||||
type MyEntityQueryParams
|
type MyEntityQueryParams,
|
||||||
} from "@customer-portal/domain/my-domain";
|
} from "@customer-portal/domain/my-domain";
|
||||||
|
|
||||||
@Controller("my-entities")
|
@Controller("my-entities")
|
||||||
@ -370,13 +373,15 @@ export const paginationSchema = z.object({
|
|||||||
### 4. **Use Refinements for Complex Validation**
|
### 4. **Use Refinements for Complex Validation**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const simActivationSchema = z.object({
|
export const simActivationSchema = z
|
||||||
simType: z.enum(["eSIM", "Physical SIM"]),
|
.object({
|
||||||
eid: z.string().optional(),
|
simType: z.enum(["eSIM", "Physical SIM"]),
|
||||||
}).refine(
|
eid: z.string().optional(),
|
||||||
(data) => data.simType !== "eSIM" || (data.eid && data.eid.length >= 15),
|
})
|
||||||
{ message: "EID required for eSIM", path: ["eid"] }
|
.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
|
**Maintained by**: Customer Portal Team
|
||||||
**Last Updated**: October 2025
|
**Last Updated**: October 2025
|
||||||
|
|
||||||
|
|||||||
@ -11,4 +11,3 @@ type SignupRequestInput = z.input<typeof signupRequestSchema>;
|
|||||||
export function buildSignupRequest(input: SignupRequestInput) {
|
export function buildSignupRequest(input: SignupRequestInput) {
|
||||||
return signupRequestSchema.parse(input);
|
return signupRequestSchema.parse(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Billing Domain - Contract
|
* Billing Domain - Contract
|
||||||
*
|
*
|
||||||
* Constants and types for the billing domain.
|
* Constants and types for the billing domain.
|
||||||
* All validated types are derived from schemas (see schema.ts).
|
* All validated types are derived from schemas (see schema.ts).
|
||||||
*/
|
*/
|
||||||
@ -35,5 +35,4 @@ export type {
|
|||||||
BillingSummary,
|
BillingSummary,
|
||||||
InvoiceQueryParams,
|
InvoiceQueryParams,
|
||||||
InvoiceListQuery,
|
InvoiceListQuery,
|
||||||
} from './schema.js';
|
} from "./schema.js";
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Billing Domain
|
* Billing Domain
|
||||||
*
|
*
|
||||||
* Exports all billing-related contracts, schemas, and provider mappers.
|
* Exports all billing-related contracts, schemas, and provider mappers.
|
||||||
*
|
*
|
||||||
* Types are derived from Zod schemas (Schema-First Approach)
|
* Types are derived from Zod schemas (Schema-First Approach)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export type {
|
|||||||
BillingSummary,
|
BillingSummary,
|
||||||
InvoiceQueryParams,
|
InvoiceQueryParams,
|
||||||
InvoiceListQuery,
|
InvoiceListQuery,
|
||||||
} from './schema.js';
|
} from "./schema.js";
|
||||||
|
|
||||||
// Provider adapters
|
// Provider adapters
|
||||||
export * as Providers from "./providers/index.js";
|
export * as Providers from "./providers/index.js";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* WHMCS Billing Provider - Mapper
|
* WHMCS Billing Provider - Mapper
|
||||||
*
|
*
|
||||||
* Transforms raw WHMCS invoice data into normalized billing domain types.
|
* 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 parsed = whmcsInvoiceItemsRawSchema.parse(rawItems);
|
||||||
const itemArray = Array.isArray(parsed.item) ? parsed.item : [parsed.item];
|
const itemArray = Array.isArray(parsed.item) ? parsed.item : [parsed.item];
|
||||||
|
|
||||||
return itemArray.map(item => ({
|
return itemArray.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
@ -83,9 +83,7 @@ export function transformWhmcsInvoice(
|
|||||||
|
|
||||||
const currency = whmcsInvoice.currencycode || options.defaultCurrencyCode || "JPY";
|
const currency = whmcsInvoice.currencycode || options.defaultCurrencyCode || "JPY";
|
||||||
const currencySymbol =
|
const currencySymbol =
|
||||||
whmcsInvoice.currencyprefix ||
|
whmcsInvoice.currencyprefix || whmcsInvoice.currencysuffix || options.defaultCurrencySymbol;
|
||||||
whmcsInvoice.currencysuffix ||
|
|
||||||
options.defaultCurrencySymbol;
|
|
||||||
|
|
||||||
// Transform to domain model
|
// Transform to domain model
|
||||||
const invoice: Invoice = {
|
const invoice: Invoice = {
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* WHMCS Billing Provider - Raw Types
|
* WHMCS Billing Provider - Raw Types
|
||||||
*
|
*
|
||||||
* Type definitions for the WHMCS billing API contract:
|
* Type definitions for the WHMCS billing API contract:
|
||||||
* - Request parameter types (API inputs)
|
* - Request parameter types (API inputs)
|
||||||
* - Response types (API outputs)
|
* - Response types (API outputs)
|
||||||
*
|
*
|
||||||
* These represent the exact structure used by WHMCS APIs.
|
* 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 GetCurrencies API response schema
|
||||||
*
|
*
|
||||||
* WHMCS can return currencies in different formats:
|
* WHMCS can return currencies in different formats:
|
||||||
* 1. Nested format: { currencies: { currency: [...] } }
|
* 1. Nested format: { currencies: { currency: [...] } }
|
||||||
* 2. Flat format: currencies[currency][0][id], currencies[currency][0][code], etc.
|
* 2. Flat format: currencies[currency][0][id], currencies[currency][0][code], etc.
|
||||||
* 3. Missing result field in some cases
|
* 3. Missing result field in some cases
|
||||||
*/
|
*/
|
||||||
export const whmcsCurrenciesResponseSchema = z.object({
|
export const whmcsCurrenciesResponseSchema = z
|
||||||
result: z.enum(["success", "error"]).optional(),
|
.object({
|
||||||
totalresults: z.string().transform(val => parseInt(val, 10)).or(z.number()).optional(),
|
result: z.enum(["success", "error"]).optional(),
|
||||||
currencies: z.object({
|
totalresults: z
|
||||||
currency: z.array(whmcsCurrencySchema).or(whmcsCurrencySchema),
|
.string()
|
||||||
}).optional(),
|
.transform(val => parseInt(val, 10))
|
||||||
// Allow any additional flat currency keys for flat format
|
.or(z.number())
|
||||||
}).catchall(z.string().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>;
|
export type WhmcsCurrenciesResponse = z.infer<typeof whmcsCurrenciesResponseSchema>;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Billing Domain - Schemas
|
* Billing Domain - Schemas
|
||||||
*
|
*
|
||||||
* Zod validation schemas for billing domain types.
|
* Zod validation schemas for billing domain types.
|
||||||
* Used for runtime validation of data from any source.
|
* 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
|
// Re-export provider types for convenience
|
||||||
export type { WhmcsResponse, WhmcsErrorResponse } from "./providers/whmcs.js";
|
export type { WhmcsResponse, WhmcsErrorResponse } from "./providers/whmcs.js";
|
||||||
export type { SalesforceResponse } from "./providers/salesforce.js";
|
export type { SalesforceResponse } from "./providers/salesforce.js";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Common Provider Types
|
* Common Provider Types
|
||||||
*
|
*
|
||||||
* Generic provider-specific response structures used across multiple domains.
|
* Generic provider-specific response structures used across multiple domains.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Common Salesforce Provider Types
|
* Common Salesforce Provider Types
|
||||||
*
|
*
|
||||||
* Generic Salesforce API response structures used across multiple domains.
|
* 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
|
* Generic type for Salesforce query results derived from schema
|
||||||
* All SOQL queries return this structure regardless of SObject type
|
* All SOQL queries return this structure regardless of SObject type
|
||||||
*
|
*
|
||||||
* Usage: SalesforceResponse<SalesforceOrderRecord>
|
* Usage: SalesforceResponse<SalesforceOrderRecord>
|
||||||
*/
|
*/
|
||||||
export type SalesforceResponse<TRecord> = Omit<SalesforceResponseBase, 'records'> & {
|
export type SalesforceResponse<TRecord> = Omit<SalesforceResponseBase, "records"> & {
|
||||||
records: TRecord[];
|
records: TRecord[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,4 +39,3 @@ export const salesforceResponseSchema = <TRecord extends z.ZodTypeAny>(recordSch
|
|||||||
salesforceResponseBaseSchema.extend({
|
salesforceResponseBaseSchema.extend({
|
||||||
records: z.array(recordSchema),
|
records: z.array(recordSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from "./raw.types.js";
|
export * from "./raw.types.js";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Common Salesforce Provider Types
|
* Common Salesforce Provider Types
|
||||||
*
|
*
|
||||||
* Generic Salesforce API response structures used across multiple domains.
|
* 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
|
* Generic type for Salesforce query results
|
||||||
* All SOQL queries return this structure regardless of SObject type
|
* All SOQL queries return this structure regardless of SObject type
|
||||||
*
|
*
|
||||||
* Usage: SalesforceQueryResult<SalesforceOrderRecord>
|
* Usage: SalesforceQueryResult<SalesforceOrderRecord>
|
||||||
*/
|
*/
|
||||||
export interface SalesforceQueryResult<TRecord = unknown> {
|
export interface SalesforceQueryResult<TRecord = unknown> {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Common WHMCS Provider Types
|
* Common WHMCS Provider Types
|
||||||
*
|
*
|
||||||
* Generic WHMCS API response structures used across multiple domains.
|
* 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
|
* Generic type for WHMCS API responses derived from schema
|
||||||
* All WHMCS API endpoints return this structure
|
* All WHMCS API endpoints return this structure
|
||||||
*
|
*
|
||||||
* Usage: WhmcsResponse<InvoiceData>
|
* Usage: WhmcsResponse<InvoiceData>
|
||||||
*/
|
*/
|
||||||
export type WhmcsResponse<T> = WhmcsResponseBase & {
|
export type WhmcsResponse<T> = WhmcsResponseBase & {
|
||||||
@ -53,4 +53,3 @@ export const whmcsErrorResponseSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type WhmcsErrorResponse = z.infer<typeof whmcsErrorResponseSchema>;
|
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(/[0-9]/, "Password must contain at least one number")
|
||||||
.regex(/[^A-Za-z0-9]/, "Password must contain at least one special character");
|
.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
|
export const phoneSchema = z
|
||||||
.string()
|
.string()
|
||||||
@ -78,7 +82,10 @@ export const nonEmptyStringSchema = z.string().min(1, "Value cannot be empty").t
|
|||||||
/**
|
/**
|
||||||
* Schema for validating SOQL field names
|
* 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
|
// API Response Schemas
|
||||||
@ -111,10 +118,7 @@ export const apiErrorResponseSchema = z.object({
|
|||||||
* Usage: apiResponseSchema(yourDataSchema)
|
* Usage: apiResponseSchema(yourDataSchema)
|
||||||
*/
|
*/
|
||||||
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
|
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
|
||||||
z.discriminatedUnion("success", [
|
z.discriminatedUnion("success", [apiSuccessResponseSchema(dataSchema), apiErrorResponseSchema]);
|
||||||
apiSuccessResponseSchema(dataSchema),
|
|
||||||
apiErrorResponseSchema,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Pagination Schemas
|
// Pagination Schemas
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Common Domain - Types
|
* Common Domain - Types
|
||||||
*
|
*
|
||||||
* Shared utility types and branded types used across all domains.
|
* Shared utility types and branded types used across all domains.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Common Domain - Validation Utilities
|
* Common Domain - Validation Utilities
|
||||||
*
|
*
|
||||||
* Generic validation functions used across all domains.
|
* Generic validation functions used across all domains.
|
||||||
* These are pure functions with no infrastructure dependencies.
|
* 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
|
* Normalize and validate an email address
|
||||||
*
|
*
|
||||||
* This is a convenience wrapper that throws on invalid input.
|
* This is a convenience wrapper that throws on invalid input.
|
||||||
* For validation without throwing, use the emailSchema directly with .safeParse()
|
* For validation without throwing, use the emailSchema directly with .safeParse()
|
||||||
*
|
*
|
||||||
* @throws Error if email format is invalid
|
* @throws Error if email format is invalid
|
||||||
*/
|
*/
|
||||||
export function normalizeAndValidateEmail(email: string): string {
|
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);
|
return emailValidationSchema.parse(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a UUID (v4)
|
* Validate a UUID (v4)
|
||||||
*
|
*
|
||||||
* This is a convenience wrapper that throws on invalid input.
|
* This is a convenience wrapper that throws on invalid input.
|
||||||
* For validation without throwing, use the uuidSchema directly with .safeParse()
|
* For validation without throwing, use the uuidSchema directly with .safeParse()
|
||||||
*
|
*
|
||||||
* @throws Error if UUID format is invalid
|
* @throws Error if UUID format is invalid
|
||||||
*/
|
*/
|
||||||
export function validateUuidV4OrThrow(id: string): string {
|
export function validateUuidV4OrThrow(id: string): string {
|
||||||
@ -74,10 +77,10 @@ export const urlSchema = z.string().url();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a URL
|
* Validate a URL
|
||||||
*
|
*
|
||||||
* This is a convenience wrapper that throws on invalid input.
|
* This is a convenience wrapper that throws on invalid input.
|
||||||
* For validation without throwing, use the urlSchema directly with .safeParse()
|
* For validation without throwing, use the urlSchema directly with .safeParse()
|
||||||
*
|
*
|
||||||
* @throws Error if URL format is invalid
|
* @throws Error if URL format is invalid
|
||||||
*/
|
*/
|
||||||
export function validateUrlOrThrow(url: string): string {
|
export function validateUrlOrThrow(url: string): string {
|
||||||
@ -90,7 +93,7 @@ export function validateUrlOrThrow(url: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a URL (non-throwing)
|
* Validate a URL (non-throwing)
|
||||||
*
|
*
|
||||||
* Returns validation result with errors if any.
|
* Returns validation result with errors if any.
|
||||||
* Prefer using urlSchema.safeParse() directly for more control.
|
* Prefer using urlSchema.safeParse() directly for more control.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Customer Domain - Providers
|
* Customer Domain - Providers
|
||||||
*
|
*
|
||||||
* Providers handle mapping from external systems to domain types:
|
* Providers handle mapping from external systems to domain types:
|
||||||
* - Portal: Prisma (portal DB) → UserAuth
|
* - Portal: Prisma (portal DB) → UserAuth
|
||||||
* - Whmcs: WHMCS API → WhmcsClient
|
* - Whmcs: WHMCS API → WhmcsClient
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Portal Provider
|
* Portal Provider
|
||||||
*
|
*
|
||||||
* Handles mapping from Prisma (portal database) to UserAuth domain type
|
* Handles mapping from Prisma (portal database) to UserAuth domain type
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./mapper.js";
|
export * from "./mapper.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Portal Provider - Mapper
|
* Portal Provider - Mapper
|
||||||
*
|
*
|
||||||
* Maps Prisma user data to UserAuth domain type using schema validation
|
* 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
|
* Maps raw Prisma user data to UserAuth domain type
|
||||||
*
|
*
|
||||||
* Uses schema validation for runtime type safety
|
* Uses schema validation for runtime type safety
|
||||||
*
|
*
|
||||||
* @param raw - Raw Prisma user data from portal database
|
* @param raw - Raw Prisma user data from portal database
|
||||||
* @returns Validated UserAuth with only authentication state
|
* @returns Validated UserAuth with only authentication state
|
||||||
*/
|
*/
|
||||||
@ -28,4 +28,3 @@ export function mapPrismaUserToUserAuth(raw: PrismaUserRaw): UserAuth {
|
|||||||
updatedAt: raw.updatedAt.toISOString(),
|
updatedAt: raw.updatedAt.toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Portal Provider - Raw Types
|
* Portal Provider - Raw Types
|
||||||
*
|
*
|
||||||
* Raw Prisma user data interface.
|
* Raw Prisma user data interface.
|
||||||
* Domain doesn't depend on @prisma/client directly.
|
* Domain doesn't depend on @prisma/client directly.
|
||||||
*/
|
*/
|
||||||
@ -24,4 +24,3 @@ export interface PrismaUserRaw {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* WHMCS Provider - Mapper
|
* WHMCS Provider - Mapper
|
||||||
*
|
*
|
||||||
* Maps WHMCS API responses to domain types.
|
* Maps WHMCS API responses to domain types.
|
||||||
* Minimal transformation - validates and normalizes only address structure.
|
* 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
|
* Transform raw WHMCS client to domain WhmcsClient
|
||||||
*
|
*
|
||||||
* Keeps raw WHMCS field names, only normalizes:
|
* Keeps raw WHMCS field names, only normalizes:
|
||||||
* - Address structure to domain Address type
|
* - Address structure to domain Address type
|
||||||
* - Type coercions (strings to numbers/booleans)
|
* - Type coercions (strings to numbers/booleans)
|
||||||
|
|||||||
@ -3,27 +3,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type {
|
import type { CreateMappingRequest, UpdateMappingRequest, UserIdMapping } from "./contract.js";
|
||||||
CreateMappingRequest,
|
|
||||||
UpdateMappingRequest,
|
|
||||||
UserIdMapping,
|
|
||||||
} from "./contract.js";
|
|
||||||
|
|
||||||
export const createMappingRequestSchema: z.ZodType<CreateMappingRequest> = z.object({
|
export const createMappingRequestSchema: z.ZodType<CreateMappingRequest> = z.object({
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
whmcsClientId: z.number().int().positive(),
|
whmcsClientId: z.number().int().positive(),
|
||||||
sfAccountId: z
|
sfAccountId: z.string().min(1, "Salesforce account ID must be at least 1 character").optional(),
|
||||||
.string()
|
|
||||||
.min(1, "Salesforce account ID must be at least 1 character")
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateMappingRequestSchema: z.ZodType<UpdateMappingRequest> = z.object({
|
export const updateMappingRequestSchema: z.ZodType<UpdateMappingRequest> = z.object({
|
||||||
whmcsClientId: z.number().int().positive().optional(),
|
whmcsClientId: z.number().int().positive().optional(),
|
||||||
sfAccountId: z
|
sfAccountId: z.string().min(1, "Salesforce account ID must be at least 1 character").optional(),
|
||||||
.string()
|
|
||||||
.min(1, "Salesforce account ID must be at least 1 character")
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userIdMappingSchema: z.ZodType<UserIdMapping> = z.object({
|
export const userIdMappingSchema: z.ZodType<UserIdMapping> = z.object({
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* ID Mapping Domain - Validation
|
* ID Mapping Domain - Validation
|
||||||
*
|
*
|
||||||
* Pure business validation functions for ID mappings.
|
* Pure business validation functions for ID mappings.
|
||||||
* These functions contain no infrastructure dependencies (no DB, no HTTP, no logging).
|
* 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
|
* Validate no conflicts exist with existing mappings
|
||||||
* Business rule: Each userId, whmcsClientId should be unique
|
* Business rule: Each userId, whmcsClientId should be unique
|
||||||
*
|
*
|
||||||
* Note: This assumes the request has already been validated by schema.
|
* Note: This assumes the request has already been validated by schema.
|
||||||
* Use createMappingRequestSchema.parse() before calling this function.
|
* Use createMappingRequestSchema.parse() before calling this function.
|
||||||
*/
|
*/
|
||||||
@ -72,11 +72,13 @@ export function validateNoConflicts(
|
|||||||
/**
|
/**
|
||||||
* Validate deletion constraints
|
* Validate deletion constraints
|
||||||
* Business rule: Warn about data access impacts
|
* Business rule: Warn about data access impacts
|
||||||
*
|
*
|
||||||
* Note: This assumes the mapping has already been validated.
|
* Note: This assumes the mapping has already been validated.
|
||||||
* This function adds business warnings about the impact of deletion.
|
* 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 errors: string[] = [];
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
|
|
||||||
@ -85,9 +87,7 @@ export function validateDeletion(mapping: UserIdMapping | null | undefined): Map
|
|||||||
return { isValid: false, errors, warnings };
|
return { isValid: false, errors, warnings };
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings.push(
|
warnings.push("Deleting this mapping will prevent access to WHMCS/Salesforce data for this user");
|
||||||
"Deleting this mapping will prevent access to WHMCS/Salesforce data for this user"
|
|
||||||
);
|
|
||||||
if (mapping.sfAccountId) {
|
if (mapping.sfAccountId) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
"This mapping includes Salesforce integration - deletion will affect case management"
|
"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
|
* Sanitize and normalize a create mapping request
|
||||||
*
|
*
|
||||||
* Note: This performs basic string trimming before validation.
|
* Note: This performs basic string trimming before validation.
|
||||||
* The schema handles validation; this is purely for data cleanup.
|
* 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
|
* Sanitize and normalize an update mapping request
|
||||||
*
|
*
|
||||||
* Note: This performs basic string trimming before validation.
|
* Note: This performs basic string trimming before validation.
|
||||||
* The schema handles validation; this is purely for data cleanup.
|
* The schema handles validation; this is purely for data cleanup.
|
||||||
*/
|
*/
|
||||||
@ -130,4 +130,3 @@ export function sanitizeUpdateRequest(request: UpdateMappingRequest): UpdateMapp
|
|||||||
|
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Orders Domain - Checkout Types
|
* Orders Domain - Checkout Types
|
||||||
*
|
*
|
||||||
* Minimal type definitions for checkout flow.
|
* Minimal type definitions for checkout flow.
|
||||||
* Frontend handles its own URL param serialization.
|
* Frontend handles its own URL param serialization.
|
||||||
*/
|
*/
|
||||||
@ -8,5 +8,5 @@
|
|||||||
// This file is intentionally minimal after cleanup.
|
// This file is intentionally minimal after cleanup.
|
||||||
// The build/derive/normalize functions were removed as they were
|
// The build/derive/normalize functions were removed as they were
|
||||||
// unnecessary abstractions that should be handled by the frontend.
|
// unnecessary abstractions that should be handled by the frontend.
|
||||||
//
|
//
|
||||||
// See CLEANUP_PROPOSAL_NORMALIZERS.md for details.
|
// See CLEANUP_PROPOSAL_NORMALIZERS.md for details.
|
||||||
|
|||||||
@ -64,7 +64,10 @@ const BILLING_CYCLE_ALIASES: Record<string, BillingCycle> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const normalizeBillingCycleKey = (value: string): string =>
|
const normalizeBillingCycleKey = (value: string): string =>
|
||||||
value.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
value
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[\s_-]+/g, "");
|
||||||
|
|
||||||
const DEFAULT_BILLING_CYCLE: BillingCycle = "monthly";
|
const DEFAULT_BILLING_CYCLE: BillingCycle = "monthly";
|
||||||
|
|
||||||
@ -337,7 +340,7 @@ export function buildOrderDisplayItems(
|
|||||||
primaryCategory: category,
|
primaryCategory: category,
|
||||||
categories: [category],
|
categories: [category],
|
||||||
charges,
|
charges,
|
||||||
included: charges.every((charge) => charge.amount <= 0),
|
included: charges.every(charge => charge.amount <= 0),
|
||||||
sourceItems: [item],
|
sourceItems: [item],
|
||||||
isBundle: Boolean(item.isBundledAddon),
|
isBundle: Boolean(item.isBundledAddon),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Orders Domain
|
* Orders Domain
|
||||||
*
|
*
|
||||||
* Exports all order-related contracts, schemas, and provider mappers.
|
* Exports all order-related contracts, schemas, and provider mappers.
|
||||||
*
|
*
|
||||||
* Types are derived from Zod schemas (Schema-First Approach)
|
* Types are derived from Zod schemas (Schema-First Approach)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Business types and constants
|
// Business types and constants
|
||||||
export {
|
export {
|
||||||
type OrderCreationType,
|
type OrderCreationType,
|
||||||
type OrderStatus,
|
type OrderStatus,
|
||||||
type OrderType,
|
type OrderType,
|
||||||
type OrderTypeValue,
|
type OrderTypeValue,
|
||||||
type UserMapping,
|
type UserMapping,
|
||||||
// Checkout types
|
// Checkout types
|
||||||
@ -79,7 +79,7 @@ export type {
|
|||||||
OrderDisplayItemCategory,
|
OrderDisplayItemCategory,
|
||||||
OrderDisplayItemCharge,
|
OrderDisplayItemCharge,
|
||||||
OrderDisplayItemChargeKind,
|
OrderDisplayItemChargeKind,
|
||||||
} from './schema.js';
|
} from "./schema.js";
|
||||||
|
|
||||||
// Provider adapters
|
// Provider adapters
|
||||||
export * as Providers from "./providers/index.js";
|
export * as Providers from "./providers/index.js";
|
||||||
|
|||||||
@ -21,12 +21,7 @@ export const Salesforce = {
|
|||||||
fieldMap: SalesforceFieldMap,
|
fieldMap: SalesforceFieldMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export { WhmcsMapper, WhmcsRaw, SalesforceMapper, SalesforceRaw };
|
||||||
WhmcsMapper,
|
|
||||||
WhmcsRaw,
|
|
||||||
SalesforceMapper,
|
|
||||||
SalesforceRaw,
|
|
||||||
};
|
|
||||||
export * from "./whmcs/mapper.js";
|
export * from "./whmcs/mapper.js";
|
||||||
export * from "./whmcs/raw.types.js";
|
export * from "./whmcs/raw.types.js";
|
||||||
export * from "./salesforce/mapper.js";
|
export * from "./salesforce/mapper.js";
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export function createOrderRequest(payload: {
|
|||||||
/**
|
/**
|
||||||
* Transform CheckoutCart into CreateOrderRequest
|
* Transform CheckoutCart into CreateOrderRequest
|
||||||
* Handles SKU extraction, validation, and payload formatting
|
* Handles SKU extraction, validation, and payload formatting
|
||||||
*
|
*
|
||||||
* @throws Error if no products are selected
|
* @throws Error if no products are selected
|
||||||
*/
|
*/
|
||||||
export function prepareOrderFromCart(
|
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
|
// Note: Zod validation of the final structure should happen at the boundary or via schema.parse
|
||||||
// This function focuses on the structural transformation logic.
|
// This function focuses on the structural transformation logic.
|
||||||
|
|
||||||
const orderData: CreateOrderRequest = {
|
const orderData: CreateOrderRequest = {
|
||||||
orderType,
|
orderType,
|
||||||
skus: uniqueSkus,
|
skus: uniqueSkus,
|
||||||
...(Object.keys(cart.configuration).length > 0
|
...(Object.keys(cart.configuration).length > 0 ? { configurations: cart.configuration } : {}),
|
||||||
? { configurations: cart.configuration }
|
|
||||||
: {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return orderData;
|
return orderData;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Payments Domain - Contract
|
* Payments Domain - Contract
|
||||||
*
|
*
|
||||||
* Constants and types for the payments domain.
|
* Constants and types for the payments domain.
|
||||||
* All validated types are derived from schemas (see schema.ts).
|
* All validated types are derived from schemas (see schema.ts).
|
||||||
*/
|
*/
|
||||||
@ -53,4 +53,4 @@ export type {
|
|||||||
PaymentGatewayType,
|
PaymentGatewayType,
|
||||||
PaymentGateway,
|
PaymentGateway,
|
||||||
PaymentGatewayList,
|
PaymentGatewayList,
|
||||||
} from './schema.js';
|
} from "./schema.js";
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Payments Domain
|
* Payments Domain
|
||||||
*
|
*
|
||||||
* Exports all payment-related contracts, schemas, and provider mappers.
|
* Exports all payment-related contracts, schemas, and provider mappers.
|
||||||
*
|
*
|
||||||
* Types are derived from Zod schemas (Schema-First Approach)
|
* Types are derived from Zod schemas (Schema-First Approach)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ export type {
|
|||||||
PaymentGatewayType,
|
PaymentGatewayType,
|
||||||
PaymentGateway,
|
PaymentGateway,
|
||||||
PaymentGatewayList,
|
PaymentGatewayList,
|
||||||
} from './schema.js';
|
} from "./schema.js";
|
||||||
|
|
||||||
// Provider adapters
|
// Provider adapters
|
||||||
export * as Providers from "./providers/index.js";
|
export * as Providers from "./providers/index.js";
|
||||||
|
|||||||
@ -77,4 +77,3 @@ export function transformWhmcsPaymentGateway(raw: unknown): PaymentGateway {
|
|||||||
|
|
||||||
return paymentGatewaySchema.parse(gateway);
|
return paymentGatewaySchema.parse(gateway);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,4 +4,3 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * as Whmcs from "./whmcs/index.js";
|
export * as Whmcs from "./whmcs/index.js";
|
||||||
|
|
||||||
|
|||||||
@ -4,4 +4,3 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./utils.js";
|
export * from "./utils.js";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* SIM Domain - Contract
|
* SIM Domain - Contract
|
||||||
*
|
*
|
||||||
* Constants and types for the SIM domain.
|
* Constants and types for the SIM domain.
|
||||||
* All validated types are derived from schemas (see schema.ts).
|
* All validated types are derived from schemas (see schema.ts).
|
||||||
*/
|
*/
|
||||||
@ -64,4 +64,4 @@ export type {
|
|||||||
SimOrderActivationRequest,
|
SimOrderActivationRequest,
|
||||||
SimOrderActivationMnp,
|
SimOrderActivationMnp,
|
||||||
SimOrderActivationAddons,
|
SimOrderActivationAddons,
|
||||||
} from './schema.js';
|
} from "./schema.js";
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* SIM Domain
|
* SIM Domain
|
||||||
*
|
*
|
||||||
* Exports all SIM-related contracts, schemas, and provider mappers.
|
* Exports all SIM-related contracts, schemas, and provider mappers.
|
||||||
*
|
*
|
||||||
* Types are derived from Zod schemas (Schema-First Approach)
|
* Types are derived from Zod schemas (Schema-First Approach)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ export type {
|
|||||||
SimTopUpPricing,
|
SimTopUpPricing,
|
||||||
SimTopUpPricingPreviewRequest,
|
SimTopUpPricingPreviewRequest,
|
||||||
SimTopUpPricingPreviewResponse,
|
SimTopUpPricingPreviewResponse,
|
||||||
} from './schema.js';
|
} from "./schema.js";
|
||||||
export type { SimPlanCode } from "./contract.js";
|
export type { SimPlanCode } from "./contract.js";
|
||||||
export type { SimPlanOption, SimFeatureToggleSnapshot } from "./helpers.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 CancelPlanResponse = ReturnType<typeof Mapper.transformFreebitCancelPlanResponse>;
|
||||||
export type CancelAccountResponse = ReturnType<typeof Mapper.transformFreebitCancelAccountResponse>;
|
export type CancelAccountResponse = ReturnType<typeof Mapper.transformFreebitCancelAccountResponse>;
|
||||||
export type EsimReissueResponse = ReturnType<typeof Mapper.transformFreebitEsimReissueResponse>;
|
export type EsimReissueResponse = ReturnType<typeof Mapper.transformFreebitEsimReissueResponse>;
|
||||||
export type EsimAddAccountResponse = ReturnType<typeof Mapper.transformFreebitEsimAddAccountResponse>;
|
export type EsimAddAccountResponse = ReturnType<
|
||||||
export type EsimActivationResponse = ReturnType<typeof Mapper.transformFreebitEsimActivationResponse>;
|
typeof Mapper.transformFreebitEsimAddAccountResponse
|
||||||
|
>;
|
||||||
|
export type EsimActivationResponse = ReturnType<
|
||||||
|
typeof Mapper.transformFreebitEsimActivationResponse
|
||||||
|
>;
|
||||||
export type AuthResponse = ReturnType<typeof Mapper.transformFreebitAuthResponse>;
|
export type AuthResponse = ReturnType<typeof Mapper.transformFreebitAuthResponse>;
|
||||||
|
|
||||||
export * from "./mapper.js";
|
export * from "./mapper.js";
|
||||||
|
|||||||
@ -48,11 +48,13 @@ export const freebitTrafficInfoRawSchema = z.object({
|
|||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
account: z.union([z.string(), z.number()]).optional(),
|
account: z.union([z.string(), z.number()]).optional(),
|
||||||
traffic: z.object({
|
traffic: z
|
||||||
today: z.union([z.string(), z.number()]).optional(),
|
.object({
|
||||||
inRecentDays: z.union([z.string(), z.number()]).optional(),
|
today: z.union([z.string(), z.number()]).optional(),
|
||||||
blackList: z.union([z.string(), z.number()]).optional(),
|
inRecentDays: z.union([z.string(), z.number()]).optional(),
|
||||||
}).optional(),
|
blackList: z.union([z.string(), z.number()]).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
export const freebitTopUpRawSchema = z.object({
|
export const freebitTopUpRawSchema = z.object({
|
||||||
resultCode: z.string().optional(),
|
resultCode: z.string().optional(),
|
||||||
@ -154,14 +156,16 @@ export const freebitQuotaHistoryRawSchema = z.object({
|
|||||||
account: z.union([z.string(), z.number()]).optional(),
|
account: z.union([z.string(), z.number()]).optional(),
|
||||||
total: z.union([z.string(), z.number()]).optional(),
|
total: z.union([z.string(), z.number()]).optional(),
|
||||||
count: z.union([z.string(), z.number()]).optional(),
|
count: z.union([z.string(), z.number()]).optional(),
|
||||||
quotaHistory: z.array(
|
quotaHistory: z
|
||||||
z.object({
|
.array(
|
||||||
addQuotaKb: z.union([z.string(), z.number()]).optional(),
|
z.object({
|
||||||
addDate: z.string().optional(),
|
addQuotaKb: z.union([z.string(), z.number()]).optional(),
|
||||||
expireDate: z.string().optional(),
|
addDate: z.string().optional(),
|
||||||
campaignCode: z.string().optional(),
|
expireDate: z.string().optional(),
|
||||||
})
|
campaignCode: z.string().optional(),
|
||||||
).optional(),
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FreebitQuotaHistoryRaw = z.infer<typeof freebitQuotaHistoryRawSchema>;
|
export type FreebitQuotaHistoryRaw = z.infer<typeof freebitQuotaHistoryRawSchema>;
|
||||||
@ -178,4 +182,3 @@ export const freebitAuthResponseRawSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type FreebitAuthResponseRaw = z.infer<typeof freebitAuthResponseRawSchema>;
|
export type FreebitAuthResponseRaw = z.infer<typeof freebitAuthResponseRawSchema>;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* SIM Domain - Freebit Provider Request Schemas
|
* SIM Domain - Freebit Provider Request Schemas
|
||||||
*
|
*
|
||||||
* Zod schemas for all Freebit API request payloads.
|
* 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"),
|
account: z.string().min(1, "Account is required"),
|
||||||
eid: z.string().min(1, "EID is required"),
|
eid: z.string().min(1, "EID is required"),
|
||||||
addKind: z.enum(["N", "R"]).default("N"),
|
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(),
|
planCode: z.string().optional(),
|
||||||
contractLine: z.enum(["4G", "5G"]).optional(),
|
contractLine: z.enum(["4G", "5G"]).optional(),
|
||||||
mnp: freebitEsimMnpSchema.optional(),
|
mnp: freebitEsimMnpSchema.optional(),
|
||||||
@ -178,7 +181,10 @@ export const freebitEsimIdentitySchema = z.object({
|
|||||||
firstnameZenKana: z.string().optional(),
|
firstnameZenKana: z.string().optional(),
|
||||||
lastnameZenKana: z.string().optional(),
|
lastnameZenKana: z.string().optional(),
|
||||||
gender: z.enum(["M", "F"]).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"),
|
simkind: z.enum(["esim", "psim"]).default("esim"),
|
||||||
planCode: z.string().optional(),
|
planCode: z.string().optional(),
|
||||||
contractLine: z.enum(["4G", "5G"]).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(),
|
mnp: freebitEsimMnpSchema.optional(),
|
||||||
// Identity fields (flattened for API)
|
// Identity fields (flattened for API)
|
||||||
firstnameKanji: z.string().optional(),
|
firstnameKanji: z.string().optional(),
|
||||||
@ -202,7 +211,10 @@ export const freebitEsimActivationRequestSchema = z.object({
|
|||||||
firstnameZenKana: z.string().optional(),
|
firstnameZenKana: z.string().optional(),
|
||||||
lastnameZenKana: z.string().optional(),
|
lastnameZenKana: z.string().optional(),
|
||||||
gender: z.enum(["M", "F"]).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
|
// Additional fields for reissue/exchange
|
||||||
masterAccount: z.string().optional(),
|
masterAccount: z.string().optional(),
|
||||||
masterPassword: z.string().optional(),
|
masterPassword: z.string().optional(),
|
||||||
@ -224,10 +236,12 @@ export const freebitEsimActivationResponseSchema = z.object({
|
|||||||
resultCode: z.string(),
|
resultCode: z.string(),
|
||||||
resultMessage: z.string().optional(),
|
resultMessage: z.string().optional(),
|
||||||
data: z.unknown().optional(),
|
data: z.unknown().optional(),
|
||||||
status: z.object({
|
status: z
|
||||||
statusCode: z.union([z.string(), z.number()]),
|
.object({
|
||||||
message: z.string(),
|
statusCode: z.union([z.string(), z.number()]),
|
||||||
}).optional(),
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
message: z.string().optional(),
|
message: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -241,7 +255,10 @@ export const freebitEsimActivationParamsSchema = z.object({
|
|||||||
planCode: z.string().optional(),
|
planCode: z.string().optional(),
|
||||||
contractLine: z.enum(["4G", "5G"]).optional(),
|
contractLine: z.enum(["4G", "5G"]).optional(),
|
||||||
aladinOperated: z.enum(["10", "20"]).default("10"),
|
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(),
|
mnp: freebitEsimMnpSchema.optional(),
|
||||||
identity: freebitEsimIdentitySchema.optional(),
|
identity: freebitEsimIdentitySchema.optional(),
|
||||||
});
|
});
|
||||||
@ -271,4 +288,3 @@ export type FreebitQuotaHistoryResponse = z.infer<typeof freebitQuotaHistoryResp
|
|||||||
export type FreebitEsimAddAccountRequest = z.infer<typeof freebitEsimAddAccountRequestSchema>;
|
export type FreebitEsimAddAccountRequest = z.infer<typeof freebitEsimAddAccountRequestSchema>;
|
||||||
export type FreebitAuthRequest = z.infer<typeof freebitAuthRequestSchema>;
|
export type FreebitAuthRequest = z.infer<typeof freebitAuthRequestSchema>;
|
||||||
export type FreebitCancelAccountRequest = z.infer<typeof freebitCancelAccountRequestSchema>;
|
export type FreebitCancelAccountRequest = z.infer<typeof freebitCancelAccountRequestSchema>;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Freebit Provider Utilities
|
* Freebit Provider Utilities
|
||||||
*
|
*
|
||||||
* Provider-specific utilities for Freebit SIM API integration
|
* Provider-specific utilities for Freebit SIM API integration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -9,7 +9,7 @@
|
|||||||
* Removes all non-digit characters from account string
|
* Removes all non-digit characters from account string
|
||||||
*/
|
*/
|
||||||
export function normalizeAccount(account: string): 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 {
|
export function formatDateForApi(date: Date): string {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
return `${year}${month}${day}`;
|
return `${year}${month}${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,11 +36,10 @@ export function formatDateForApi(date: Date): string {
|
|||||||
*/
|
*/
|
||||||
export function parseDateFromApi(dateString: string): Date | null {
|
export function parseDateFromApi(dateString: string): Date | null {
|
||||||
if (!/^\d{8}$/.test(dateString)) return null;
|
if (!/^\d{8}$/.test(dateString)) return null;
|
||||||
|
|
||||||
const year = parseInt(dateString.substring(0, 4), 10);
|
const year = parseInt(dateString.substring(0, 4), 10);
|
||||||
const month = parseInt(dateString.substring(4, 6), 10) - 1; // Month is 0-indexed
|
const month = parseInt(dateString.substring(4, 6), 10) - 1; // Month is 0-indexed
|
||||||
const day = parseInt(dateString.substring(6, 8), 10);
|
const day = parseInt(dateString.substring(6, 8), 10);
|
||||||
|
|
||||||
return new Date(year, month, day);
|
return new Date(year, month, day);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -138,12 +138,8 @@ export const simCancelRequestSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const simTopUpHistoryRequestSchema = z.object({
|
export const simTopUpHistoryRequestSchema = z.object({
|
||||||
fromDate: z
|
fromDate: z.string().regex(/^\d{8}$/, "From date must be in YYYYMMDD format"),
|
||||||
.string()
|
toDate: z.string().regex(/^\d{8}$/, "To date must be in YYYYMMDD format"),
|
||||||
.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({
|
export const simFeaturesUpdateRequestSchema = z.object({
|
||||||
@ -162,16 +158,19 @@ export const simReissueRequestSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Enhanced cancellation request with more details
|
// Enhanced cancellation request with more details
|
||||||
export const simCancelFullRequestSchema = z.object({
|
export const simCancelFullRequestSchema = z
|
||||||
cancellationMonth: z.string().regex(/^\d{4}-\d{2}$/, "Cancellation month must be in YYYY-MM format"),
|
.object({
|
||||||
confirmRead: z.boolean(),
|
cancellationMonth: z
|
||||||
confirmCancel: z.boolean(),
|
.string()
|
||||||
alternativeEmail: z.string().email().optional().or(z.literal("")),
|
.regex(/^\d{4}-\d{2}$/, "Cancellation month must be in YYYY-MM format"),
|
||||||
comments: z.string().max(1000).optional(),
|
confirmRead: z.boolean(),
|
||||||
}).refine(
|
confirmCancel: z.boolean(),
|
||||||
(data) => data.confirmRead === true && data.confirmCancel === true,
|
alternativeEmail: z.string().email().optional().or(z.literal("")),
|
||||||
{ message: "You must confirm both checkboxes to proceed" }
|
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
|
// Top-up request with enhanced details for email
|
||||||
export const simTopUpFullRequestSchema = z.object({
|
export const simTopUpFullRequestSchema = z.object({
|
||||||
@ -292,7 +291,10 @@ export const simOrderActivationMnpSchema = z.object({
|
|||||||
firstnameZenKana: z.string().optional(),
|
firstnameZenKana: z.string().optional(),
|
||||||
lastnameZenKana: z.string().optional(),
|
lastnameZenKana: z.string().optional(),
|
||||||
gender: 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({
|
export const simOrderActivationAddonsSchema = z.object({
|
||||||
@ -300,42 +302,48 @@ export const simOrderActivationAddonsSchema = z.object({
|
|||||||
callWaiting: z.boolean().optional(),
|
callWaiting: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const simOrderActivationRequestSchema = z.object({
|
export const simOrderActivationRequestSchema = z
|
||||||
planSku: z.string().min(1, "Plan SKU is required"),
|
.object({
|
||||||
simType: simCardTypeSchema,
|
planSku: z.string().min(1, "Plan SKU is required"),
|
||||||
eid: z.string().min(15, "EID must be at least 15 characters").optional(),
|
simType: simCardTypeSchema,
|
||||||
activationType: simActivationTypeSchema,
|
eid: z.string().min(15, "EID must be at least 15 characters").optional(),
|
||||||
scheduledAt: z.string().regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format").optional(),
|
activationType: simActivationTypeSchema,
|
||||||
addons: simOrderActivationAddonsSchema.optional(),
|
scheduledAt: z
|
||||||
mnp: simOrderActivationMnpSchema.optional(),
|
.string()
|
||||||
msisdn: z.string().min(1, "Phone number (msisdn) is required"),
|
.regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format")
|
||||||
oneTimeAmountJpy: z.number().nonnegative("One-time amount must be non-negative"),
|
.optional(),
|
||||||
monthlyAmountJpy: z.number().nonnegative("Monthly amount must be non-negative"),
|
addons: simOrderActivationAddonsSchema.optional(),
|
||||||
}).refine(
|
mnp: simOrderActivationMnpSchema.optional(),
|
||||||
(data) => {
|
msisdn: z.string().min(1, "Phone number (msisdn) is required"),
|
||||||
// If simType is eSIM, eid is required
|
oneTimeAmountJpy: z.number().nonnegative("One-time amount must be non-negative"),
|
||||||
if (data.simType === "eSIM" && (!data.eid || data.eid.length < 15)) {
|
monthlyAmountJpy: z.number().nonnegative("Monthly amount must be non-negative"),
|
||||||
return false;
|
})
|
||||||
|
.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;
|
)
|
||||||
},
|
.refine(
|
||||||
{
|
data => {
|
||||||
message: "EID is required for eSIM and must be at least 15 characters",
|
// If activationType is Scheduled, scheduledAt is required
|
||||||
path: ["eid"],
|
if (data.activationType === "Scheduled" && !data.scheduledAt) {
|
||||||
}
|
return false;
|
||||||
).refine(
|
}
|
||||||
(data) => {
|
return true;
|
||||||
// If activationType is Scheduled, scheduledAt is required
|
},
|
||||||
if (data.activationType === "Scheduled" && !data.scheduledAt) {
|
{
|
||||||
return false;
|
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 SimOrderActivationRequest = z.infer<typeof simOrderActivationRequestSchema>;
|
||||||
export type SimOrderActivationMnp = z.infer<typeof simOrderActivationMnpSchema>;
|
export type SimOrderActivationMnp = z.infer<typeof simOrderActivationMnpSchema>;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Subscriptions Domain - Contract
|
* Subscriptions Domain - Contract
|
||||||
*
|
*
|
||||||
* Constants and types for the subscriptions domain.
|
* Constants and types for the subscriptions domain.
|
||||||
* All validated types are derived from schemas (see schema.ts).
|
* All validated types are derived from schemas (see schema.ts).
|
||||||
*/
|
*/
|
||||||
@ -45,4 +45,4 @@ export type {
|
|||||||
SubscriptionList,
|
SubscriptionList,
|
||||||
SubscriptionQueryParams,
|
SubscriptionQueryParams,
|
||||||
SubscriptionQuery,
|
SubscriptionQuery,
|
||||||
} from './schema.js';
|
} from "./schema.js";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* WHMCS Subscriptions Provider - Raw Types
|
* WHMCS Subscriptions Provider - Raw Types
|
||||||
*
|
*
|
||||||
* Type definitions for the WHMCS subscriptions API contract:
|
* Type definitions for the WHMCS subscriptions API contract:
|
||||||
* - Request parameter types (API inputs)
|
* - Request parameter types (API inputs)
|
||||||
* - Response types (API outputs)
|
* - Response types (API outputs)
|
||||||
@ -8,30 +8,24 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const normalizeRequiredNumber = z.preprocess(
|
const normalizeRequiredNumber = z.preprocess(value => {
|
||||||
value => {
|
if (typeof value === "number") return value;
|
||||||
if (typeof value === "number") return value;
|
if (typeof value === "string" && value.trim().length > 0) {
|
||||||
if (typeof value === "string" && value.trim().length > 0) {
|
const parsed = Number(value);
|
||||||
const parsed = Number(value);
|
return Number.isFinite(parsed) ? parsed : value;
|
||||||
return Number.isFinite(parsed) ? parsed : value;
|
}
|
||||||
}
|
return value;
|
||||||
return value;
|
}, z.number());
|
||||||
},
|
|
||||||
z.number()
|
|
||||||
);
|
|
||||||
|
|
||||||
const normalizeOptionalNumber = z.preprocess(
|
const normalizeOptionalNumber = z.preprocess(value => {
|
||||||
value => {
|
if (value === undefined || value === null || value === "") return undefined;
|
||||||
if (value === undefined || value === null || value === "") return undefined;
|
if (typeof value === "number") return value;
|
||||||
if (typeof value === "number") return value;
|
if (typeof value === "string") {
|
||||||
if (typeof value === "string") {
|
const parsed = Number(value);
|
||||||
const parsed = Number(value);
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
}
|
||||||
}
|
return undefined;
|
||||||
return undefined;
|
}, z.number().optional());
|
||||||
},
|
|
||||||
z.number().optional()
|
|
||||||
);
|
|
||||||
|
|
||||||
const optionalStringField = () =>
|
const optionalStringField = () =>
|
||||||
z
|
z
|
||||||
@ -117,7 +111,7 @@ export const whmcsProductRawSchema = z.object({
|
|||||||
ns1: optionalStringField(),
|
ns1: optionalStringField(),
|
||||||
ns2: optionalStringField(),
|
ns2: optionalStringField(),
|
||||||
assignedips: optionalStringField(),
|
assignedips: optionalStringField(),
|
||||||
|
|
||||||
// Pricing
|
// Pricing
|
||||||
firstpaymentamount: z.union([z.string(), z.number()]).optional(),
|
firstpaymentamount: z.union([z.string(), z.number()]).optional(),
|
||||||
amount: 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(),
|
billingcycle: z.string().optional(),
|
||||||
paymentmethod: z.string().optional(),
|
paymentmethod: z.string().optional(),
|
||||||
paymentmethodname: z.string().optional(),
|
paymentmethodname: z.string().optional(),
|
||||||
|
|
||||||
// Dates
|
// Dates
|
||||||
nextduedate: z.string().optional(),
|
nextduedate: z.string().optional(),
|
||||||
nextinvoicedate: z.string().optional(),
|
nextinvoicedate: z.string().optional(),
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
|
|
||||||
// Notes
|
// Notes
|
||||||
notes: z.string().optional(),
|
notes: z.string().optional(),
|
||||||
diskusage: normalizeOptionalNumber,
|
diskusage: normalizeOptionalNumber,
|
||||||
@ -142,18 +136,20 @@ export const whmcsProductRawSchema = z.object({
|
|||||||
bwusage: normalizeOptionalNumber,
|
bwusage: normalizeOptionalNumber,
|
||||||
bwlimit: normalizeOptionalNumber,
|
bwlimit: normalizeOptionalNumber,
|
||||||
lastupdate: z.string().optional(),
|
lastupdate: z.string().optional(),
|
||||||
|
|
||||||
// Custom fields
|
// Custom fields
|
||||||
customfields: whmcsCustomFieldsContainerSchema.optional(),
|
customfields: whmcsCustomFieldsContainerSchema.optional(),
|
||||||
configoptions: whmcsConfigOptionsContainerSchema.optional(),
|
configoptions: whmcsConfigOptionsContainerSchema.optional(),
|
||||||
|
|
||||||
// Pricing details
|
// Pricing details
|
||||||
pricing: z.object({
|
pricing: z
|
||||||
amount: z.union([z.string(), z.number()]).optional(),
|
.object({
|
||||||
currency: z.string().optional(),
|
amount: z.union([z.string(), z.number()]).optional(),
|
||||||
currencyprefix: z.string().optional(),
|
currency: z.string().optional(),
|
||||||
currencysuffix: z.string().optional(),
|
currencyprefix: z.string().optional(),
|
||||||
}).optional(),
|
currencysuffix: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type WhmcsProductRaw = z.infer<typeof whmcsProductRawSchema>;
|
export type WhmcsProductRaw = z.infer<typeof whmcsProductRawSchema>;
|
||||||
@ -167,13 +163,15 @@ export type WhmcsCustomField = z.infer<typeof whmcsCustomFieldSchema>;
|
|||||||
* WHMCS GetClientsProducts API response schema
|
* WHMCS GetClientsProducts API response schema
|
||||||
*/
|
*/
|
||||||
const whmcsProductContainerSchema = z.object({
|
const whmcsProductContainerSchema = z.object({
|
||||||
product: z
|
product: z.preprocess(
|
||||||
.preprocess(value => {
|
value => {
|
||||||
if (value === null || value === undefined || value === "") {
|
if (value === null || value === undefined || value === "") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}, z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional()),
|
},
|
||||||
|
z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional()
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const whmcsProductListResponseSchema = z.object({
|
export const whmcsProductListResponseSchema = z.object({
|
||||||
|
|||||||
@ -15,4 +15,3 @@ export const Salesforce = {
|
|||||||
export { SalesforceMapper, SalesforceRaw };
|
export { SalesforceMapper, SalesforceRaw };
|
||||||
export * from "./salesforce/mapper.js";
|
export * from "./salesforce/mapper.js";
|
||||||
export * from "./salesforce/raw.types.js";
|
export * from "./salesforce/raw.types.js";
|
||||||
|
|
||||||
|
|||||||
@ -4,4 +4,3 @@
|
|||||||
|
|
||||||
export * from "./raw.types.js";
|
export * from "./raw.types.js";
|
||||||
export * from "./mapper.js";
|
export * from "./mapper.js";
|
||||||
|
|
||||||
|
|||||||
@ -46,9 +46,7 @@ function nowIsoString(): string {
|
|||||||
* @param record - Raw Salesforce Case record from SOQL query
|
* @param record - Raw Salesforce Case record from SOQL query
|
||||||
* @returns Validated SupportCase domain object
|
* @returns Validated SupportCase domain object
|
||||||
*/
|
*/
|
||||||
export function transformSalesforceCaseToSupportCase(
|
export function transformSalesforceCaseToSupportCase(record: SalesforceCaseRecord): SupportCase {
|
||||||
record: SalesforceCaseRecord
|
|
||||||
): SupportCase {
|
|
||||||
// Get raw values
|
// Get raw values
|
||||||
const rawStatus = ensureString(record.Status) ?? SALESFORCE_CASE_STATUS.NEW;
|
const rawStatus = ensureString(record.Status) ?? SALESFORCE_CASE_STATUS.NEW;
|
||||||
const rawPriority = ensureString(record.Priority) ?? SALESFORCE_CASE_PRIORITY.MEDIUM;
|
const rawPriority = ensureString(record.Priority) ?? SALESFORCE_CASE_PRIORITY.MEDIUM;
|
||||||
@ -167,4 +165,3 @@ export function buildCaseByIdQuery(
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -319,4 +319,3 @@ const PRIORITY_TO_SALESFORCE: Record<string, string> = {
|
|||||||
export function toSalesforcePriority(displayPriority: string): string {
|
export function toSalesforcePriority(displayPriority: string): string {
|
||||||
return PRIORITY_TO_SALESFORCE[displayPriority] ?? SALESFORCE_CASE_PRIORITY.MEDIUM;
|
return PRIORITY_TO_SALESFORCE[displayPriority] ?? SALESFORCE_CASE_PRIORITY.MEDIUM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { SUPPORT_CASE_STATUS, SUPPORT_CASE_PRIORITY, SUPPORT_CASE_CATEGORY } from "./contract.js";
|
||||||
SUPPORT_CASE_STATUS,
|
|
||||||
SUPPORT_CASE_PRIORITY,
|
|
||||||
SUPPORT_CASE_CATEGORY,
|
|
||||||
} from "./contract.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Portal status values (mapped from Salesforce Japanese API names)
|
* 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
|
## Purpose
|
||||||
|
|
||||||
The toolkit provides pure utility functions for common operations like:
|
The toolkit provides pure utility functions for common operations like:
|
||||||
|
|
||||||
- String manipulation
|
- String manipulation
|
||||||
- Date/time formatting
|
- Date/time formatting
|
||||||
- Currency formatting
|
- Currency formatting
|
||||||
@ -43,10 +44,10 @@ Validation determines if input is valid or invalid:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// These are VALIDATION functions → Use common/validation.ts
|
// These are VALIDATION functions → Use common/validation.ts
|
||||||
isValidEmail(email) // Returns true/false
|
isValidEmail(email); // Returns true/false
|
||||||
isValidUuid(id) // Returns true/false
|
isValidUuid(id); // Returns true/false
|
||||||
isValidUrl(url) // Returns true/false
|
isValidUrl(url); // Returns true/false
|
||||||
validateUrlOrThrow(url) // Throws if invalid
|
validateUrlOrThrow(url); // Throws if invalid
|
||||||
```
|
```
|
||||||
|
|
||||||
**Location**: `packages/domain/common/validation.ts`
|
**Location**: `packages/domain/common/validation.ts`
|
||||||
@ -57,10 +58,10 @@ Utilities transform, extract, or manipulate data:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// These are UTILITY functions → Use toolkit
|
// These are UTILITY functions → Use toolkit
|
||||||
getEmailDomain(email) // Extracts domain from email
|
getEmailDomain(email); // Extracts domain from email
|
||||||
normalizeEmail(email) // Lowercases and trims
|
normalizeEmail(email); // Lowercases and trims
|
||||||
ensureProtocol(url) // Adds https:// if missing
|
ensureProtocol(url); // Adds https:// if missing
|
||||||
getHostname(url) // Extracts hostname
|
getHostname(url); // Extracts hostname
|
||||||
```
|
```
|
||||||
|
|
||||||
**Location**: `packages/domain/toolkit/`
|
**Location**: `packages/domain/toolkit/`
|
||||||
@ -72,8 +73,8 @@ getHostname(url) // Extracts hostname
|
|||||||
```typescript
|
```typescript
|
||||||
import { formatCurrency, formatDate } from "@customer-portal/domain/toolkit/formatting";
|
import { formatCurrency, formatDate } from "@customer-portal/domain/toolkit/formatting";
|
||||||
|
|
||||||
const price = formatCurrency(1000, "JPY"); // "¥1,000"
|
const price = formatCurrency(1000, "JPY"); // "¥1,000"
|
||||||
const date = formatDate(new Date()); // "2025-10-08"
|
const date = formatDate(new Date()); // "2025-10-08"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Type Guards
|
### Type Guards
|
||||||
@ -92,8 +93,8 @@ if (isString(value)) {
|
|||||||
```typescript
|
```typescript
|
||||||
import { ensureProtocol, getHostname } from "@customer-portal/domain/toolkit/validation/url";
|
import { ensureProtocol, getHostname } from "@customer-portal/domain/toolkit/validation/url";
|
||||||
|
|
||||||
const fullUrl = ensureProtocol("example.com"); // "https://example.com"
|
const fullUrl = ensureProtocol("example.com"); // "https://example.com"
|
||||||
const host = getHostname("https://example.com/path"); // "example.com"
|
const host = getHostname("https://example.com/path"); // "example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Email Utilities
|
### Email Utilities
|
||||||
@ -101,55 +102,58 @@ const host = getHostname("https://example.com/path"); // "example.com"
|
|||||||
```typescript
|
```typescript
|
||||||
import { getEmailDomain, normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
|
import { getEmailDomain, normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
|
||||||
|
|
||||||
const domain = getEmailDomain("user@example.com"); // "example.com"
|
const domain = getEmailDomain("user@example.com"); // "example.com"
|
||||||
const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com"
|
const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
## When to Use What
|
## When to Use What
|
||||||
|
|
||||||
| Task | Use | Example |
|
| Task | Use | Example |
|
||||||
|------|-----|---------|
|
| --------------------- | -------------------------------- | ------------------------------- |
|
||||||
| Validate email format | `common/validation.ts` | `isValidEmail(email)` |
|
| Validate email format | `common/validation.ts` | `isValidEmail(email)` |
|
||||||
| Extract email domain | `toolkit/validation/email.ts` | `getEmailDomain(email)` |
|
| Extract email domain | `toolkit/validation/email.ts` | `getEmailDomain(email)` |
|
||||||
| Validate URL format | `common/validation.ts` | `isValidUrl(url)` |
|
| Validate URL format | `common/validation.ts` | `isValidUrl(url)` |
|
||||||
| Add protocol to URL | `toolkit/validation/url.ts` | `ensureProtocol(url)` |
|
| Add protocol to URL | `toolkit/validation/url.ts` | `ensureProtocol(url)` |
|
||||||
| Format currency | `toolkit/formatting/currency.ts` | `formatCurrency(amount, "JPY")` |
|
| Format currency | `toolkit/formatting/currency.ts` | `formatCurrency(amount, "JPY")` |
|
||||||
| Format date | `toolkit/formatting/date.ts` | `formatDate(date)` |
|
| Format date | `toolkit/formatting/date.ts` | `formatDate(date)` |
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Use validation functions for validation**
|
1. **Use validation functions for validation**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ Good
|
// ✅ Good
|
||||||
import { isValidEmail } from "@customer-portal/domain/common/validation";
|
import { isValidEmail } from "@customer-portal/domain/common/validation";
|
||||||
if (!isValidEmail(email)) throw new Error("Invalid email");
|
if (!isValidEmail(email)) throw new Error("Invalid email");
|
||||||
|
|
||||||
// ❌ Bad - don't write custom validation
|
// ❌ Bad - don't write custom validation
|
||||||
if (!email.includes("@")) throw new Error("Invalid email");
|
if (!email.includes("@")) throw new Error("Invalid email");
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Use utility functions for transformations**
|
2. **Use utility functions for transformations**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ Good
|
// ✅ Good
|
||||||
import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
|
import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
|
||||||
const clean = normalizeEmail(email);
|
const clean = normalizeEmail(email);
|
||||||
|
|
||||||
// ❌ Bad - don't duplicate utility logic
|
// ❌ Bad - don't duplicate utility logic
|
||||||
const clean = email.trim().toLowerCase();
|
const clean = email.trim().toLowerCase();
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Don't mix validation and utilities**
|
3. **Don't mix validation and utilities**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ Bad - mixing concerns
|
// ❌ Bad - mixing concerns
|
||||||
function processEmail(email: string) {
|
function processEmail(email: string) {
|
||||||
if (!email.includes("@")) return null; // Validation
|
if (!email.includes("@")) return null; // Validation
|
||||||
return email.toLowerCase(); // Utility
|
return email.toLowerCase(); // Utility
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Good - separate concerns
|
// ✅ Good - separate concerns
|
||||||
import { isValidEmail } from "@customer-portal/domain/common/validation";
|
import { isValidEmail } from "@customer-portal/domain/common/validation";
|
||||||
import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
|
import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
|
||||||
|
|
||||||
function processEmail(email: string) {
|
function processEmail(email: string) {
|
||||||
if (!isValidEmail(email)) return null;
|
if (!isValidEmail(email)) return null;
|
||||||
return normalizeEmail(email);
|
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?"
|
- **Ask**: "Does this transform or extract data?"
|
||||||
- YES → It's a utility → Use `toolkit/`
|
- YES → It's a utility → Use `toolkit/`
|
||||||
- NO → Might be validation → Use `common/validation.ts`
|
- NO → Might be validation → Use `common/validation.ts`
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - Date Formatting
|
* Toolkit - Date Formatting
|
||||||
*
|
*
|
||||||
* Utilities for formatting dates and times.
|
* Utilities for formatting dates and times.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -17,10 +17,7 @@ export interface DateFormatOptions {
|
|||||||
/**
|
/**
|
||||||
* Format an ISO date string for display
|
* Format an ISO date string for display
|
||||||
*/
|
*/
|
||||||
export function formatDate(
|
export function formatDate(isoString: string, options: DateFormatOptions = {}): string {
|
||||||
isoString: string,
|
|
||||||
options: DateFormatOptions = {}
|
|
||||||
): string {
|
|
||||||
const {
|
const {
|
||||||
locale = "en-US",
|
locale = "en-US",
|
||||||
dateStyle = "medium",
|
dateStyle = "medium",
|
||||||
@ -31,7 +28,7 @@ export function formatDate(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const date = new Date(isoString);
|
const date = new Date(isoString);
|
||||||
|
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
return isoString; // Return original if invalid
|
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")
|
* Format a date relative to now (e.g., "2 days ago", "in 3 hours")
|
||||||
*/
|
*/
|
||||||
export function formatRelativeDate(
|
export function formatRelativeDate(isoString: string, options: { locale?: string } = {}): string {
|
||||||
isoString: string,
|
|
||||||
options: { locale?: string } = {}
|
|
||||||
): string {
|
|
||||||
const { locale = "en-US" } = options;
|
const { locale = "en-US" } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -90,4 +84,3 @@ export function isValidDate(dateString: string): boolean {
|
|||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return !isNaN(date.getTime());
|
return !isNaN(date.getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - Formatting
|
* Toolkit - Formatting
|
||||||
*
|
*
|
||||||
* Formatting utilities for currency, dates, phone numbers, etc.
|
* Formatting utilities for currency, dates, phone numbers, etc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -8,4 +8,3 @@ export * from "./currency.js";
|
|||||||
export * from "./date.js";
|
export * from "./date.js";
|
||||||
export * from "./phone.js";
|
export * from "./phone.js";
|
||||||
export * from "./text.js";
|
export * from "./text.js";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - Phone Number Formatting
|
* Toolkit - Phone Number Formatting
|
||||||
*
|
*
|
||||||
* Utilities for formatting phone numbers.
|
* Utilities for formatting phone numbers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -37,12 +37,12 @@ export function formatPhoneNumber(phone: string): string {
|
|||||||
*/
|
*/
|
||||||
export function normalizePhoneNumber(phone: string, defaultCountryCode = "1"): string {
|
export function normalizePhoneNumber(phone: string, defaultCountryCode = "1"): string {
|
||||||
const digits = phone.replace(/\D/g, "");
|
const digits = phone.replace(/\D/g, "");
|
||||||
|
|
||||||
// If already has country code, return with +
|
// If already has country code, return with +
|
||||||
if (digits.length >= 10 && !digits.startsWith(defaultCountryCode)) {
|
if (digits.length >= 10 && !digits.startsWith(defaultCountryCode)) {
|
||||||
return `+${digits}`;
|
return `+${digits}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add default country code
|
// Add default country code
|
||||||
return `+${defaultCountryCode}${digits}`;
|
return `+${defaultCountryCode}${digits}`;
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@ export function formatPhoneForWhmcs(phone: string): string {
|
|||||||
// Remove all non-digit characters except leading +
|
// Remove all non-digit characters except leading +
|
||||||
const hasPlus = phone.startsWith("+");
|
const hasPlus = phone.startsWith("+");
|
||||||
const digits = phone.replace(/\D/g, "");
|
const digits = phone.replace(/\D/g, "");
|
||||||
|
|
||||||
if (digits.length === 0) {
|
if (digits.length === 0) {
|
||||||
return phone;
|
return phone;
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ export function formatPhoneForWhmcs(phone: string): string {
|
|||||||
if (digits.startsWith("81") && digits.length >= 11) {
|
if (digits.startsWith("81") && digits.length >= 11) {
|
||||||
return `+81.${digits.slice(2)}`;
|
return `+81.${digits.slice(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For US/Canada numbers (10 digits or 11 starting with 1)
|
// For US/Canada numbers (10 digits or 11 starting with 1)
|
||||||
if (digits.length === 10) {
|
if (digits.length === 10) {
|
||||||
return `+1.${digits}`;
|
return `+1.${digits}`;
|
||||||
@ -88,4 +88,3 @@ export function formatPhoneForWhmcs(phone: string): string {
|
|||||||
// Return with + prefix if it had one, otherwise as-is
|
// Return with + prefix if it had one, otherwise as-is
|
||||||
return hasPlus ? `+${digits}` : digits;
|
return hasPlus ? `+${digits}` : digits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - Text Formatting
|
* Toolkit - Text Formatting
|
||||||
*
|
*
|
||||||
* Utilities for text manipulation and 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) {
|
if (str.length <= visibleStart + visibleEnd) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = str.slice(0, visibleStart);
|
const start = str.slice(0, visibleStart);
|
||||||
const end = str.slice(-visibleEnd);
|
const end = str.slice(-visibleEnd);
|
||||||
const maskedLength = str.length - visibleStart - visibleEnd;
|
const maskedLength = str.length - visibleStart - visibleEnd;
|
||||||
const masked = maskChar.repeat(maskedLength);
|
const masked = maskChar.repeat(maskedLength);
|
||||||
|
|
||||||
return `${start}${masked}${end}`;
|
return `${start}${masked}${end}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Domain Toolkit
|
* Domain Toolkit
|
||||||
*
|
*
|
||||||
* Utility functions and helpers used across all domain packages.
|
* Utility functions and helpers used across all domain packages.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -24,4 +24,3 @@ export {
|
|||||||
isSuccess,
|
isSuccess,
|
||||||
isError,
|
isError,
|
||||||
} from "./typing/helpers.js";
|
} from "./typing/helpers.js";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - Type Assertions
|
* Toolkit - Type Assertions
|
||||||
*
|
*
|
||||||
* Runtime assertion utilities for type safety.
|
* Runtime assertion utilities for type safety.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -62,4 +62,3 @@ export function assertNumber(
|
|||||||
export function assertNever(value: never, message = "Unexpected value"): never {
|
export function assertNever(value: never, message = "Unexpected value"): never {
|
||||||
throw new AssertionError(`${message}: ${JSON.stringify(value)}`);
|
throw new AssertionError(`${message}: ${JSON.stringify(value)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - Type Guards
|
* Toolkit - Type Guards
|
||||||
*
|
*
|
||||||
* Type guard utilities for runtime type checking.
|
* 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[] {
|
export function filterDefined<T>(arr: (T | null | undefined)[]): T[] {
|
||||||
return arr.filter(isDefined);
|
return arr.filter(isDefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - Typing
|
* Toolkit - Typing
|
||||||
*
|
*
|
||||||
* TypeScript type utilities and runtime type checking.
|
* TypeScript type utilities and runtime type checking.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./guards.js";
|
export * from "./guards.js";
|
||||||
export * from "./assertions.js";
|
export * from "./assertions.js";
|
||||||
export * from "./helpers.js";
|
export * from "./helpers.js";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - Email Utilities
|
* Toolkit - Email Utilities
|
||||||
*
|
*
|
||||||
* Email utility functions (extraction, normalization).
|
* Email utility functions (extraction, normalization).
|
||||||
* For email validation, use functions from common/validation.ts
|
* 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 {
|
export function normalizeEmail(email: string): string {
|
||||||
return email.trim().toLowerCase();
|
return email.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - Validation
|
* Toolkit - Validation
|
||||||
*
|
*
|
||||||
* Validation utilities for common data types.
|
* Validation utilities for common data types.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -8,5 +8,3 @@ export * from "./email.js";
|
|||||||
export * from "./url.js";
|
export * from "./url.js";
|
||||||
export * from "./string.js";
|
export * from "./string.js";
|
||||||
export * from "./helpers.js";
|
export * from "./helpers.js";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - String Validation
|
* Toolkit - String Validation
|
||||||
*
|
*
|
||||||
* String validation utilities.
|
* String validation utilities.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -45,4 +45,3 @@ export function isAlpha(str: string): boolean {
|
|||||||
export function isNumeric(str: string): boolean {
|
export function isNumeric(str: string): boolean {
|
||||||
return /^\d+$/.test(str);
|
return /^\d+$/.test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Toolkit - URL Utilities
|
* Toolkit - URL Utilities
|
||||||
*
|
*
|
||||||
* URL parsing and manipulation utilities.
|
* URL parsing and manipulation utilities.
|
||||||
* For URL validation, use functions from common/validation.ts
|
* For URL validation, use functions from common/validation.ts
|
||||||
*/
|
*/
|
||||||
@ -26,4 +26,3 @@ export function getHostname(url: string): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@ -241,6 +241,9 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.17
|
specifier: ^4.1.17
|
||||||
version: 4.1.17
|
version: 4.1.17
|
||||||
|
tailwindcss-animate:
|
||||||
|
specifier: ^1.0.7
|
||||||
|
version: 1.0.7(tailwindcss@4.1.17)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: "catalog:"
|
specifier: "catalog:"
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@ -6874,6 +6877,14 @@ packages:
|
|||||||
integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==,
|
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:
|
tailwindcss@4.1.17:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -11504,6 +11515,10 @@ snapshots:
|
|||||||
|
|
||||||
tailwind-merge@3.4.0: {}
|
tailwind-merge@3.4.0: {}
|
||||||
|
|
||||||
|
tailwindcss-animate@1.0.7(tailwindcss@4.1.17):
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 4.1.17
|
||||||
|
|
||||||
tailwindcss@4.1.17: {}
|
tailwindcss@4.1.17: {}
|
||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user