From 1b944f57aa8850a4948c2cd44396533038ccefb6 Mon Sep 17 00:00:00 2001 From: barsa Date: Thu, 25 Dec 2025 17:30:02 +0900 Subject: [PATCH] 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. --- .cursor/plans/d-67f8fea5.plan.md | 5 +- .cursor/worktrees.json | 4 +- .prettierrc | 2 +- apps/bff/prisma/README.md | 12 +- apps/bff/prisma/prisma.config.ts | 1 - apps/bff/src/core/rate-limiting/index.ts | 8 +- .../rate-limiting/rate-limit.decorator.ts | 1 - .../core/rate-limiting/rate-limit.guard.ts | 6 +- .../core/rate-limiting/rate-limit.module.ts | 1 - .../rate-limiting/auth-rate-limit.service.ts | 8 +- .../services/base-services.service.ts | 27 ++++- .../services/services/sim-services.service.ts | 6 +- .../services/services/vpn-services.service.ts | 5 +- apps/portal/next-env.d.ts | 2 +- apps/portal/package.json | 3 +- apps/portal/src/app/globals.css | 1 + .../src/components/atoms/error-message.tsx | 3 +- .../organisms/AgentforceWidget/index.ts | 1 - .../ui/shadcn-io/aurora-background/index.tsx | 4 +- .../auth/components/SignupForm/steps/index.ts | 1 - .../features/orders/utils/order-display.ts | 10 +- .../components/base/AddressConfirmation.tsx | 2 +- .../hooks/useSimTopUpPricing.ts | 8 +- .../src/features/sim-management/utils/plan.ts | 6 +- .../features/support/hooks/useCreateCase.ts | 1 - .../src/features/support/utils/index.ts | 1 - .../src/lib/constants/japan-prefectures.ts | 6 +- docker/Prod - Portainer/PORTAINER-GUIDE.md | 66 ++++++++--- docker/dev/docker-compose.yml | 1 - eslint.config.mjs | 5 +- packages/domain/README.md | 98 ++++++++-------- packages/domain/auth/helpers.ts | 1 - packages/domain/billing/contract.ts | 5 +- packages/domain/billing/index.ts | 6 +- .../domain/billing/providers/whmcs/mapper.ts | 8 +- .../billing/providers/whmcs/raw.types.ts | 30 +++-- packages/domain/billing/schema.ts | 2 +- packages/domain/common/index.ts | 1 - packages/domain/common/providers/index.ts | 2 +- .../domain/common/providers/salesforce.ts | 7 +- .../common/providers/salesforce/index.ts | 1 - .../common/providers/salesforce/raw.types.ts | 4 +- packages/domain/common/providers/whmcs.ts | 5 +- packages/domain/common/schema.ts | 16 ++- packages/domain/common/types.ts | 2 +- packages/domain/common/validation.ts | 21 ++-- packages/domain/customer/providers/index.ts | 2 +- .../domain/customer/providers/portal/index.ts | 3 +- .../customer/providers/portal/mapper.ts | 7 +- .../domain/customer/providers/portal/types.ts | 3 +- .../domain/customer/providers/whmcs/mapper.ts | 4 +- packages/domain/mappings/schema.ts | 16 +-- packages/domain/mappings/validation.ts | 19 ++- packages/domain/orders/checkout.ts | 4 +- packages/domain/orders/helpers.ts | 7 +- packages/domain/orders/index.ts | 14 +-- packages/domain/orders/providers/index.ts | 7 +- packages/domain/orders/utils.ts | 8 +- packages/domain/payments/contract.ts | 4 +- packages/domain/payments/index.ts | 6 +- .../domain/payments/providers/whmcs/mapper.ts | 1 - packages/domain/providers/index.ts | 1 - packages/domain/providers/whmcs/index.ts | 1 - packages/domain/sim/contract.ts | 4 +- packages/domain/sim/index.ts | 6 +- packages/domain/sim/lifecycle.ts | 1 - .../domain/sim/providers/freebit/index.ts | 8 +- .../domain/sim/providers/freebit/raw.types.ts | 31 ++--- .../domain/sim/providers/freebit/requests.ts | 38 ++++-- .../domain/sim/providers/freebit/utils.ts | 13 +-- packages/domain/sim/schema.ts | 110 ++++++++++-------- packages/domain/subscriptions/contract.ts | 4 +- .../providers/whmcs/raw.types.ts | 76 ++++++------ packages/domain/support/providers/index.ts | 1 - .../support/providers/salesforce/index.ts | 1 - .../support/providers/salesforce/mapper.ts | 5 +- .../support/providers/salesforce/raw.types.ts | 1 - packages/domain/support/schema.ts | 6 +- packages/domain/toolkit/README.md | 61 +++++----- packages/domain/toolkit/formatting/date.ts | 15 +-- packages/domain/toolkit/formatting/index.ts | 3 +- packages/domain/toolkit/formatting/phone.ts | 11 +- packages/domain/toolkit/formatting/text.ts | 7 +- packages/domain/toolkit/index.ts | 3 +- packages/domain/toolkit/typing/assertions.ts | 3 +- packages/domain/toolkit/typing/guards.ts | 3 +- packages/domain/toolkit/typing/index.ts | 3 +- packages/domain/toolkit/validation/email.ts | 3 +- packages/domain/toolkit/validation/index.ts | 4 +- packages/domain/toolkit/validation/string.ts | 3 +- packages/domain/toolkit/validation/url.ts | 3 +- pnpm-lock.yaml | 15 +++ 92 files changed, 509 insertions(+), 456 deletions(-) diff --git a/.cursor/plans/d-67f8fea5.plan.md b/.cursor/plans/d-67f8fea5.plan.md index 7bfa7b6f..9ba89a1b 100644 --- a/.cursor/plans/d-67f8fea5.plan.md +++ b/.cursor/plans/d-67f8fea5.plan.md @@ -1,4 +1,5 @@ + # Domain & BFF Clean Architecture Refactoring ## Overview @@ -173,7 +174,7 @@ const result = this.orderWhmcsMapper.mapOrderItemsToWhmcs(items); With: ```typescript -import { Providers } from '@customer-portal/domain/orders'; +import { Providers } from "@customer-portal/domain/orders"; const result = Providers.Whmcs.mapFulfillmentOrderItems(items); ``` @@ -346,4 +347,4 @@ Flow: Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directl - [x] Update orders.module.ts and salesforce.module.ts with new services - [x] Verify catalog services follow same clean pattern (already correct) - [x] Update domain README and architecture documentation with clean patterns -- [x] Test order creation and fulfillment flows end-to-end \ No newline at end of file +- [x] Test order creation and fulfillment flows end-to-end diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json index e57556bc..bf9e730d 100644 --- a/.cursor/worktrees.json +++ b/.cursor/worktrees.json @@ -1,5 +1,3 @@ { - "setup-worktree": [ - "pnpm install" - ] + "setup-worktree": ["pnpm install"] } diff --git a/.prettierrc b/.prettierrc index 181386dd..58d41914 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1 +1 @@ -"./config/prettier.config.js" \ No newline at end of file +"./config/prettier.config.js" diff --git a/apps/bff/prisma/README.md b/apps/bff/prisma/README.md index e51c39c7..5595637d 100644 --- a/apps/bff/prisma/README.md +++ b/apps/bff/prisma/README.md @@ -13,6 +13,7 @@ Prisma embeds the schema path into the generated client. We regenerate the clien ## Directory Structure ### Development (Monorepo) + ``` /project-root/ ├── apps/bff/ @@ -24,6 +25,7 @@ Prisma embeds the schema path into the generated client. We regenerate the clien ``` ### Production (Docker Container) + ``` /app/ ├── prisma/ @@ -36,12 +38,12 @@ Prisma embeds the schema path into the generated client. We regenerate the clien ## Commands -| Command | Description | -|---------|-------------| +| Command | Description | +| ------------------ | ---------------------------------------------------------- | | `pnpm db:generate` | Regenerate Prisma client (`--schema=prisma/schema.prisma`) | -| `pnpm db:migrate` | Run migrations (dev) with explicit schema path | -| `pnpm db:studio` | Open Prisma Studio with explicit schema path | -| `pnpm db:reset` | Reset database with explicit schema path | +| `pnpm db:migrate` | Run migrations (dev) with explicit schema path | +| `pnpm db:studio` | Open Prisma Studio with explicit schema path | +| `pnpm db:reset` | Reset database with explicit schema path | ## Binary Targets diff --git a/apps/bff/prisma/prisma.config.ts b/apps/bff/prisma/prisma.config.ts index cb1cf50c..a2bde8bf 100644 --- a/apps/bff/prisma/prisma.config.ts +++ b/apps/bff/prisma/prisma.config.ts @@ -19,4 +19,3 @@ export default defineConfig({ url: process.env.DATABASE_URL || DEFAULT_DATABASE_URL, }, }); - diff --git a/apps/bff/src/core/rate-limiting/index.ts b/apps/bff/src/core/rate-limiting/index.ts index b659693d..bbc1be5d 100644 --- a/apps/bff/src/core/rate-limiting/index.ts +++ b/apps/bff/src/core/rate-limiting/index.ts @@ -1,4 +1,8 @@ export { RateLimitModule } from "./rate-limit.module.js"; export { RateLimitGuard } from "./rate-limit.guard.js"; -export { RateLimit, SkipRateLimit, type RateLimitOptions, RATE_LIMIT_KEY } from "./rate-limit.decorator.js"; - +export { + RateLimit, + SkipRateLimit, + type RateLimitOptions, + RATE_LIMIT_KEY, +} from "./rate-limit.decorator.js"; diff --git a/apps/bff/src/core/rate-limiting/rate-limit.decorator.ts b/apps/bff/src/core/rate-limiting/rate-limit.decorator.ts index 99faeb74..a9abc6b7 100644 --- a/apps/bff/src/core/rate-limiting/rate-limit.decorator.ts +++ b/apps/bff/src/core/rate-limiting/rate-limit.decorator.ts @@ -41,4 +41,3 @@ export const RateLimit = (options: RateLimitOptions) => SetMetadata(RATE_LIMIT_K * Skip rate limiting for this route (useful when applied at controller level) */ export const SkipRateLimit = () => SetMetadata(RATE_LIMIT_KEY, { skip: true } as RateLimitOptions); - diff --git a/apps/bff/src/core/rate-limiting/rate-limit.guard.ts b/apps/bff/src/core/rate-limiting/rate-limit.guard.ts index 37fd6421..ffb58f13 100644 --- a/apps/bff/src/core/rate-limiting/rate-limit.guard.ts +++ b/apps/bff/src/core/rate-limiting/rate-limit.guard.ts @@ -124,7 +124,10 @@ export class RateLimitGuard implements CanActivate { /** * Get or create a rate limiter for the given options */ - private getOrCreateLimiter(options: RateLimitOptions, context: ExecutionContext): RateLimiterRedis { + private getOrCreateLimiter( + options: RateLimitOptions, + context: ExecutionContext + ): RateLimiterRedis { const handlerName = context.getHandler().name; const controllerName = context.getClass().name; const cacheKey = `${controllerName}:${handlerName}:${options.limit}:${options.ttl}`; @@ -163,4 +166,3 @@ export class RateLimitGuard implements CanActivate { } } } - diff --git a/apps/bff/src/core/rate-limiting/rate-limit.module.ts b/apps/bff/src/core/rate-limiting/rate-limit.module.ts index 1ceecc15..94d1015d 100644 --- a/apps/bff/src/core/rate-limiting/rate-limit.module.ts +++ b/apps/bff/src/core/rate-limiting/rate-limit.module.ts @@ -26,4 +26,3 @@ import { RateLimitGuard } from "./rate-limit.guard.js"; exports: [RateLimitGuard], }) export class RateLimitModule {} - diff --git a/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts b/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts index 0b6b64d0..9973093d 100644 --- a/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts +++ b/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts @@ -1,4 +1,10 @@ -import { Inject, Injectable, InternalServerErrorException, HttpException, HttpStatus } from "@nestjs/common"; +import { + Inject, + Injectable, + InternalServerErrorException, + HttpException, + HttpStatus, +} from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Logger } from "nestjs-pino"; import type { Request } from "express"; diff --git a/apps/bff/src/modules/services/services/base-services.service.ts b/apps/bff/src/modules/services/services/base-services.service.ts index 259d5ef6..31b920e4 100644 --- a/apps/bff/src/modules/services/services/base-services.service.ts +++ b/apps/bff/src/modules/services/services/base-services.service.ts @@ -39,17 +39,32 @@ export class BaseServicesService { soql: string, context: string ): Promise { + this.logger.debug(`Executing Salesforce query for ${context}`, { + soql: soql.replace(/\s+/g, " ").trim(), + portalPricebookId: this.portalPriceBookId, + portalCategoryField: this.portalCategoryField, + }); + try { const res = (await this.sf.query(soql, { label: `services:${context.replace(/\s+/g, "_").toLowerCase()}`, })) as SalesforceResponse; - return res.records ?? []; - } catch (error: unknown) { - this.logger.error(`Query failed: ${context}`, { - error: getErrorMessage(error), - soql, - context, + const records = res.records ?? []; + this.logger.debug(`Query result for ${context}`, { + recordCount: records.length, + records: records.map(r => ({ + id: r.Id, + name: r.Name, + sku: r.StockKeepingUnit, + itemClass: r.Item_Class__c, + hasPricebookEntries: Boolean(r.PricebookEntries?.records?.length), + })), }); + return records; + } catch (error: unknown) { + const errorMessage = getErrorMessage(error); + this.logger.error(`Query failed: ${context} - ${errorMessage}`); + this.logger.error(`Failed SOQL: ${soql.replace(/\s+/g, " ").trim()}`); return []; } } diff --git a/apps/bff/src/modules/services/services/sim-services.service.ts b/apps/bff/src/modules/services/services/sim-services.service.ts index 1d3cd450..a1f150aa 100644 --- a/apps/bff/src/modules/services/services/sim-services.service.ts +++ b/apps/bff/src/modules/services/services/sim-services.service.ts @@ -67,11 +67,7 @@ export class SimServicesService extends BaseServicesService { return this.catalogCache.getCachedServices( cacheKey, async () => { - const soql = this.buildProductQuery("SIM", "Activation", [ - "Catalog_Order__c", - "Auto_Add__c", - "Is_Default__c", - ]); + const soql = this.buildProductQuery("SIM", "Activation", ["Catalog_Order__c"]); const records = await this.executeQuery( soql, "SIM Activation Fees" diff --git a/apps/bff/src/modules/services/services/vpn-services.service.ts b/apps/bff/src/modules/services/services/vpn-services.service.ts index 8b795ff1..3ccda343 100644 --- a/apps/bff/src/modules/services/services/vpn-services.service.ts +++ b/apps/bff/src/modules/services/services/vpn-services.service.ts @@ -55,7 +55,10 @@ export class VpnServicesService extends BaseServicesService { return this.catalogCache.getCachedServices( cacheKey, async () => { - const soql = this.buildProductQuery("VPN", "Activation", ["VPN_Region__c"]); + const soql = this.buildProductQuery("VPN", "Activation", [ + "VPN_Region__c", + "Catalog_Order__c", + ]); const records = await this.executeQuery( soql, "VPN Activation Fees" diff --git a/apps/portal/next-env.d.ts b/apps/portal/next-env.d.ts index c4b7818f..9edff1c7 100644 --- a/apps/portal/next-env.d.ts +++ b/apps/portal/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/portal/package.json b/apps/portal/package.json index d3288faa..43d1559d 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -5,7 +5,7 @@ "scripts": { "predev": "node ./scripts/dev-prep.mjs", "dev": "next dev -p ${NEXT_PORT:-3000}", - "build": "next build --webpack", + "build": "next build", "build:turbo": "next build", "build:analyze": "ANALYZE=true next build", "analyze": "pnpm run build:analyze", @@ -40,6 +40,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "tailwindcss": "^4.1.17", + "tailwindcss-animate": "^1.0.7", "typescript": "catalog:" } } diff --git a/apps/portal/src/app/globals.css b/apps/portal/src/app/globals.css index a276c5b8..5c838804 100644 --- a/apps/portal/src/app/globals.css +++ b/apps/portal/src/app/globals.css @@ -1,5 +1,6 @@ /* Tailwind CSS v4 */ @import "tailwindcss"; +@plugin "tailwindcss-animate"; @import "../styles/tokens.css"; @import "../styles/utilities.css"; @import "../styles/responsive.css"; diff --git a/apps/portal/src/components/atoms/error-message.tsx b/apps/portal/src/components/atoms/error-message.tsx index cb5cfc0c..9adf555e 100644 --- a/apps/portal/src/components/atoms/error-message.tsx +++ b/apps/portal/src/components/atoms/error-message.tsx @@ -17,8 +17,7 @@ const errorMessageVariants = cva("flex items-center gap-1 text-sm", { }); interface ErrorMessageProps - extends React.HTMLAttributes, - VariantProps { + extends React.HTMLAttributes, VariantProps { showIcon?: boolean; } diff --git a/apps/portal/src/components/organisms/AgentforceWidget/index.ts b/apps/portal/src/components/organisms/AgentforceWidget/index.ts index 55daca7a..b76d970c 100644 --- a/apps/portal/src/components/organisms/AgentforceWidget/index.ts +++ b/apps/portal/src/components/organisms/AgentforceWidget/index.ts @@ -1,2 +1 @@ export { AgentforceWidget } from "./AgentforceWidget"; - diff --git a/apps/portal/src/components/ui/shadcn-io/aurora-background/index.tsx b/apps/portal/src/components/ui/shadcn-io/aurora-background/index.tsx index 09c8c1be..ce6da896 100644 --- a/apps/portal/src/components/ui/shadcn-io/aurora-background/index.tsx +++ b/apps/portal/src/components/ui/shadcn-io/aurora-background/index.tsx @@ -19,7 +19,7 @@ export const AuroraBackground = ({
@@ -51,7 +51,7 @@ export const AuroraBackground = ({ `after:animate-aurora pointer-events-none absolute -inset-[10px] [background-image:var(--white-gradient),var(--aurora)] [background-size:300%,_200%] [background-position:50%_50%,50%_50%] opacity-50 blur-[10px] invert filter will-change-transform [--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)] [--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)] [--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)] after:absolute after:inset-0 after:[background-image:var(--white-gradient),var(--aurora)] after:[background-size:200%,_100%] after:[background-attachment:fixed] after:mix-blend-difference after:content-[""] dark:[background-image:var(--dark-gradient),var(--aurora)] dark:invert-0 after:dark:[background-image:var(--dark-gradient),var(--aurora)]`, showRadialGradient && - `[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`, + `[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]` )} >
diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/index.ts b/apps/portal/src/features/auth/components/SignupForm/steps/index.ts index bcfc995d..7bf590d7 100644 --- a/apps/portal/src/features/auth/components/SignupForm/steps/index.ts +++ b/apps/portal/src/features/auth/components/SignupForm/steps/index.ts @@ -2,4 +2,3 @@ export { AccountStep } from "./AccountStep"; export { AddressStep } from "./AddressStep"; export { PasswordStep } from "./PasswordStep"; export { ReviewStep } from "./ReviewStep"; - diff --git a/apps/portal/src/features/orders/utils/order-display.ts b/apps/portal/src/features/orders/utils/order-display.ts index 4e2db651..d1e33686 100644 --- a/apps/portal/src/features/orders/utils/order-display.ts +++ b/apps/portal/src/features/orders/utils/order-display.ts @@ -5,10 +5,7 @@ import type { OrderDisplayItem } from "@customer-portal/domain/orders"; -export { - buildOrderDisplayItems, - categorizeOrderItem, -} from "@customer-portal/domain/orders"; +export { buildOrderDisplayItems, categorizeOrderItem } from "@customer-portal/domain/orders"; export type { OrderDisplayItem, @@ -20,10 +17,7 @@ export type { /** * Summarize order display items for compact display */ -export function summarizeOrderDisplayItems( - items: OrderDisplayItem[], - fallback: string -): string { +export function summarizeOrderDisplayItems(items: OrderDisplayItem[], fallback: string): string { if (items.length === 0) { return fallback; } diff --git a/apps/portal/src/features/services/components/base/AddressConfirmation.tsx b/apps/portal/src/features/services/components/base/AddressConfirmation.tsx index 6de19600..4b9bd121 100644 --- a/apps/portal/src/features/services/components/base/AddressConfirmation.tsx +++ b/apps/portal/src/features/services/components/base/AddressConfirmation.tsx @@ -93,7 +93,7 @@ export function AddressConfirmation({ } finally { setLoading(false); } - }, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed]); + }, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed, setAddressConfirmed]); useEffect(() => { log.info("Address confirmation component mounted"); diff --git a/apps/portal/src/features/sim-management/hooks/useSimTopUpPricing.ts b/apps/portal/src/features/sim-management/hooks/useSimTopUpPricing.ts index e96f4960..8b894a12 100644 --- a/apps/portal/src/features/sim-management/hooks/useSimTopUpPricing.ts +++ b/apps/portal/src/features/sim-management/hooks/useSimTopUpPricing.ts @@ -1,9 +1,6 @@ import { useEffect, useState } from "react"; import { apiClient } from "@/lib/api"; -import type { - SimTopUpPricing, - SimTopUpPricingPreviewResponse, -} from "@customer-portal/domain/sim"; +import type { SimTopUpPricing, SimTopUpPricingPreviewResponse } from "@customer-portal/domain/sim"; interface UseSimTopUpPricingResult { pricing: SimTopUpPricing | null; @@ -24,7 +21,7 @@ export function useSimTopUpPricing(): UseSimTopUpPricingResult { try { setLoading(true); const response = await apiClient.GET("/api/subscriptions/sim/top-up/pricing"); - + if (mounted && response.data) { const data = response.data as { success: boolean; data: SimTopUpPricing }; setPricing(data.data); @@ -66,4 +63,3 @@ export function useSimTopUpPricing(): UseSimTopUpPricingResult { return { pricing, loading, error, calculatePreview }; } - diff --git a/apps/portal/src/features/sim-management/utils/plan.ts b/apps/portal/src/features/sim-management/utils/plan.ts index f3978053..836db7a5 100644 --- a/apps/portal/src/features/sim-management/utils/plan.ts +++ b/apps/portal/src/features/sim-management/utils/plan.ts @@ -42,7 +42,7 @@ export function getSimPlanSku(planCode?: string): string | undefined { */ export function mapToSimplifiedFormat(planCode?: string): string { if (!planCode) return ""; - + // Handle Freebit format (PASI_5G, PASI_25G, etc.) if (planCode.startsWith("PASI_")) { const match = planCode.match(/PASI_(\d+)G/); @@ -50,13 +50,13 @@ export function mapToSimplifiedFormat(planCode?: string): string { return `${match[1]}GB`; } } - + // Handle other formats that might end with G or GB const match = planCode.match(/(\d+)\s*G(?:B)?\b/i); if (match) { return `${match[1]}GB`; } - + // Return as-is if no pattern matches return planCode; } diff --git a/apps/portal/src/features/support/hooks/useCreateCase.ts b/apps/portal/src/features/support/hooks/useCreateCase.ts index 25cb6329..e1892496 100644 --- a/apps/portal/src/features/support/hooks/useCreateCase.ts +++ b/apps/portal/src/features/support/hooks/useCreateCase.ts @@ -25,4 +25,3 @@ export function useCreateCase() { }, }); } - diff --git a/apps/portal/src/features/support/utils/index.ts b/apps/portal/src/features/support/utils/index.ts index 9c9de8bf..f1b8bd82 100644 --- a/apps/portal/src/features/support/utils/index.ts +++ b/apps/portal/src/features/support/utils/index.ts @@ -1,2 +1 @@ export * from "./case-presenters"; - diff --git a/apps/portal/src/lib/constants/japan-prefectures.ts b/apps/portal/src/lib/constants/japan-prefectures.ts index d6c0f66d..c7a5aa46 100644 --- a/apps/portal/src/lib/constants/japan-prefectures.ts +++ b/apps/portal/src/lib/constants/japan-prefectures.ts @@ -68,15 +68,15 @@ export const JAPAN_PREFECTURES: PrefectureOption[] = [ */ export function formatJapanesePostalCode(value: string): string { const digits = value.replace(/\D/g, ""); - + if (digits.length <= 3) { return digits; } - + if (digits.length <= 7) { return `${digits.slice(0, 3)}-${digits.slice(3)}`; } - + return `${digits.slice(0, 3)}-${digits.slice(3, 7)}`; } diff --git a/docker/Prod - Portainer/PORTAINER-GUIDE.md b/docker/Prod - Portainer/PORTAINER-GUIDE.md index 87420ae0..514279f1 100644 --- a/docker/Prod - Portainer/PORTAINER-GUIDE.md +++ b/docker/Prod - Portainer/PORTAINER-GUIDE.md @@ -1,6 +1,7 @@ # Complete Portainer Guide for Customer Portal ## Table of Contents + 1. [Creating a Stack in Portainer](#creating-a-stack-in-portainer) 2. [Repository vs Upload vs Web Editor](#stack-creation-methods) 3. [Security Concerns & Best Practices](#security-concerns) @@ -22,6 +23,7 @@ Click **"+ Add stack"** button You'll see three creation methods: + - **Web editor** - Paste compose file directly - **Upload** - Upload a compose file - **Repository** - Pull from Git repository @@ -45,16 +47,19 @@ Click **"Deploy the stack"** ### Method 1: Web Editor (Simplest) **How:** + 1. Select "Web editor" 2. Paste your `docker-compose.yml` content 3. Add environment variables manually or load from file **Pros:** + - ✅ Quick and simple - ✅ No external dependencies - ✅ Full control over content **Cons:** + - ❌ Manual updates required - ❌ No version control - ❌ Easy to make mistakes when editing @@ -66,17 +71,20 @@ Click **"Deploy the stack"** ### Method 2: Upload (Recommended for Your Case) **How:** + 1. Select "Upload" 2. Upload your `docker-compose.yml` file 3. Optionally upload a `.env` file for environment variables **Pros:** + - ✅ Version control on your local machine - ✅ Can prepare and test locally - ✅ No external network dependencies - ✅ Works in air-gapped environments **Cons:** + - ❌ Manual upload for each update - ❌ Need to manage files locally @@ -87,12 +95,14 @@ Click **"Deploy the stack"** ### Method 3: Repository (Git Integration) **How:** + 1. Select "Repository" 2. Enter repository URL (GitHub, GitLab, Bitbucket, etc.) 3. Specify branch and compose file path 4. Add authentication if private repo **Example Configuration:** + ``` Repository URL: https://github.com/your-org/customer-portal Reference: main @@ -100,16 +110,19 @@ Compose path: docker/portainer/docker-compose.yml ``` **For Private Repos:** + - Use a Personal Access Token (PAT) as password - Or use deploy keys **Pros:** + - ✅ Version controlled - ✅ Easy to update (just click "Pull and redeploy") - ✅ Team can review changes via PR - ✅ Audit trail of changes **Cons:** + - ❌ Requires network access to repo - ❌ Secrets in repo = security risk - ❌ Need to manage repo access tokens @@ -124,6 +137,7 @@ Compose path: docker/portainer/docker-compose.yml **Use: Upload + Environment Variables in Portainer UI** Why: + 1. Your compose file rarely changes (it's just orchestration) 2. Sensitive data stays in Portainer, not in Git 3. Image updates are done via environment variables @@ -136,6 +150,7 @@ Why: ### 🔴 Critical Security Issues #### 1. Never Store Secrets in Git + ```yaml # ❌ BAD - Secrets in compose file environment: @@ -149,6 +164,7 @@ environment: ``` #### 2. Never Store Secrets in Docker Images + ```dockerfile # ❌ BAD - Secrets baked into image ENV JWT_SECRET="my-secret" @@ -159,6 +175,7 @@ COPY secrets/ /app/secrets/ ``` #### 3. Portainer Access Control + ``` ⚠️ Portainer has full Docker access = root on the host @@ -173,6 +190,7 @@ Best practices: ### 🟡 Medium Security Concerns #### 4. Environment Variables in Portainer + ``` Portainer stores env vars in its database. This is generally safe, but consider: @@ -188,6 +206,7 @@ Mitigation: ``` #### 5. Image Trust + ``` ⚠️ You're loading .tar files - verify their integrity @@ -198,6 +217,7 @@ Best practice: ``` Add to build script: + ```bash # Generate checksums sha256sum portal-frontend.latest.tar > portal-frontend.latest.tar.sha256 @@ -209,6 +229,7 @@ sha256sum -c portal-backend.latest.tar.sha256 ``` #### 6. Network Exposure + ```yaml # ❌ BAD - Database exposed to host database: @@ -225,6 +246,7 @@ database: ### 🟢 Good Security Practices (Already in Place) Your current setup does these right: + - ✅ Non-root users in containers - ✅ Health checks configured - ✅ Database/Redis not exposed externally @@ -252,12 +274,14 @@ watchtower: ``` **Why NOT recommended:** + - ❌ No control over when updates happen - ❌ No rollback mechanism - ❌ Can break production unexpectedly - ❌ Requires images in a registry (not .tar files) We've disabled Watchtower in your compose: + ```yaml labels: - "com.centurylinklabs.watchtower.enable=false" @@ -270,27 +294,32 @@ labels: Portainer can expose a webhook URL that triggers stack redeployment. **Setup:** + 1. Go to Stack → Settings 2. Enable "Webhook" 3. Copy the webhook URL **Trigger from CI/CD:** + ```bash # In your GitHub Actions / GitLab CI curl -X POST "https://your-portainer:9443/api/stacks/webhook/abc123" ``` **Workflow:** + ``` Build Images → Push to Registry → Trigger Webhook → Portainer Redeploys ``` **Pros:** + - ✅ Controlled updates - ✅ Integrated with CI/CD - ✅ Can add approval gates **Cons:** + - ❌ Requires images in a registry - ❌ Webhook URL is a secret - ❌ Limited rollback options @@ -313,6 +342,7 @@ ssh user@server "cd /path/to/portal && ./update-stack.sh v1.2.3" ``` **Make it a one-liner:** + ```bash # deploy.sh - Run locally #!/bin/bash @@ -339,11 +369,13 @@ echo "✅ Deployed ${TAG}" If you want auto-updates, use a registry: **Free Options:** + - GitHub Container Registry (ghcr.io) - free for public repos - GitLab Container Registry - free - Docker Hub - 1 private repo free **Setup:** + ```bash # Build and push ./scripts/plesk/build-images.sh --tag v1.2.3 --push ghcr.io/your-org @@ -377,6 +409,7 @@ services: ``` **Steps:** + 1. Build: `./scripts/plesk/build-images.sh --tag 20241201-abc` 2. Upload: `scp *.tar server:/path/images/` 3. Load: `docker load -i *.tar` @@ -404,17 +437,19 @@ services: ## Quick Reference: Portainer Stack Commands ### Via Portainer UI -| Action | Steps | -|--------|-------| -| Create stack | Stacks → Add stack → Configure → Deploy | -| Update stack | Stacks → Select → Editor → Update | + +| Action | Steps | +| ------------ | -------------------------------------------------- | +| Create stack | Stacks → Add stack → Configure → Deploy | +| Update stack | Stacks → Select → Editor → Update | | Change image | Stacks → Select → Env vars → Change IMAGE → Update | -| View logs | Stacks → Select → Container → Logs | -| Restart | Stacks → Select → Container → Restart | -| Stop | Stacks → Select → Stop | -| Delete | Stacks → Select → Delete | +| View logs | Stacks → Select → Container → Logs | +| Restart | Stacks → Select → Container → Restart | +| Stop | Stacks → Select → Stop | +| Delete | Stacks → Select → Delete | ### Via CLI (on server) + ```bash # Navigate to stack directory cd /path/to/portal @@ -442,11 +477,10 @@ docker compose --env-file stack.env down -v ## Summary -| Aspect | Recommendation | -|--------|---------------| -| Stack creation | **Upload** method (version control locally, no secrets in git) | -| Secrets management | **Portainer env vars** or **mounted secrets volume** | -| Image updates | **Manual script** for now, migrate to **registry + webhook** later | -| Auto-updates | **Not recommended** for production; use controlled deployments | -| Rollback | Keep previous image tags, update env vars to rollback | - +| Aspect | Recommendation | +| ------------------ | ------------------------------------------------------------------ | +| Stack creation | **Upload** method (version control locally, no secrets in git) | +| Secrets management | **Portainer env vars** or **mounted secrets volume** | +| Image updates | **Manual script** for now, migrate to **registry + webhook** later | +| Auto-updates | **Not recommended** for production; use controlled deployments | +| Rollback | Keep previous image tags, update env vars to rollback | diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index 673145f7..ad58c67c 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -39,4 +39,3 @@ volumes: networks: default: name: portal_dev - diff --git a/eslint.config.mjs b/eslint.config.mjs index 6c09583e..83b671f4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -71,7 +71,10 @@ export default [ files: [...BFF_TS_FILES, "packages/domain/**/*.ts"], rules: { "@typescript-eslint/consistent-type-imports": "error", - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], "no-console": ["warn", { allow: ["warn", "error"] }], }, }, diff --git a/packages/domain/README.md b/packages/domain/README.md index 59686c6f..381bfe15 100644 --- a/packages/domain/README.md +++ b/packages/domain/README.md @@ -72,18 +72,22 @@ packages/domain/ ## 🎯 Design Principles ### 1. **Domain-First Organization** + Each business domain owns its: + - **`contract.ts`** - TypeScript interfaces (provider-agnostic) - **`schema.ts`** - Zod validation schemas (runtime safety) - **`providers/`** - Provider-specific adapters (WHMCS, Salesforce, Freebit) ### 2. **Single Source of Truth** + - ✅ All types defined in domain package - ✅ All validation schemas in domain package - ✅ No duplicate type definitions in apps - ✅ Shared between frontend (Next.js) and backend (NestJS) ### 3. **Type Safety + Runtime Validation** + - TypeScript provides compile-time type checking - Zod schemas provide runtime validation - Use `z.infer` to derive types from schemas @@ -104,17 +108,19 @@ import { ApiResponse, PaginationParams } from "@customer-portal/domain/common"; ### **API Response Handling** ```typescript -import { - ApiResponse, - ApiSuccessResponse, +import { + ApiResponse, + ApiSuccessResponse, ApiErrorResponse, - apiResponseSchema + apiResponseSchema, } from "@customer-portal/domain/common"; // Type-safe API responses const response: ApiResponse = { success: true, - data: { /* invoice data */ } + data: { + /* invoice data */ + }, }; // With validation @@ -124,9 +130,9 @@ const validated = apiResponseSchema(invoiceSchema).parse(rawResponse); ### **Query Parameters with Validation** ```typescript -import { - InvoiceQueryParams, - invoiceQueryParamsSchema +import { + InvoiceQueryParams, + invoiceQueryParamsSchema } from "@customer-portal/domain/billing"; // In BFF controller @@ -172,10 +178,7 @@ function LoginForm() { ```typescript import { ZodValidationPipe } from "@bff/core/validation"; -import { - createOrderRequestSchema, - type CreateOrderRequest -} from "@customer-portal/domain/orders"; +import { createOrderRequestSchema, type CreateOrderRequest } from "@customer-portal/domain/orders"; @Controller("orders") export class OrdersController { @@ -194,39 +197,39 @@ export class OrdersController { ### **API Responses** -| Schema | Description | -|--------|-------------| -| `apiSuccessResponseSchema(dataSchema)` | Successful API response wrapper | -| `apiErrorResponseSchema` | Error API response with code/message | -| `apiResponseSchema(dataSchema)` | Discriminated union of success/error | +| Schema | Description | +| -------------------------------------- | ------------------------------------ | +| `apiSuccessResponseSchema(dataSchema)` | Successful API response wrapper | +| `apiErrorResponseSchema` | Error API response with code/message | +| `apiResponseSchema(dataSchema)` | Discriminated union of success/error | ### **Pagination & Queries** -| Schema | Description | -|--------|-------------| -| `paginationParamsSchema` | Page, limit, offset parameters | -| `paginatedResponseSchema(itemSchema)` | Paginated list response | -| `filterParamsSchema` | Search, sortBy, sortOrder | -| `queryParamsSchema` | Combined pagination + filters | +| Schema | Description | +| ------------------------------------- | ------------------------------ | +| `paginationParamsSchema` | Page, limit, offset parameters | +| `paginatedResponseSchema(itemSchema)` | Paginated list response | +| `filterParamsSchema` | Search, sortBy, sortOrder | +| `queryParamsSchema` | Combined pagination + filters | ### **Domain-Specific Query Params** -| Schema | Description | -|--------|-------------| -| `invoiceQueryParamsSchema` | Invoice list filtering (status, dates) | -| `subscriptionQueryParamsSchema` | Subscription filtering (status, type) | -| `orderQueryParamsSchema` | Order filtering (status, orderType) | +| Schema | Description | +| ------------------------------- | -------------------------------------- | +| `invoiceQueryParamsSchema` | Invoice list filtering (status, dates) | +| `subscriptionQueryParamsSchema` | Subscription filtering (status, type) | +| `orderQueryParamsSchema` | Order filtering (status, orderType) | ### **Validation Primitives** -| Schema | Description | -|--------|-------------| -| `emailSchema` | Email validation (lowercase, trimmed) | -| `passwordSchema` | Strong password (8+ chars, mixed case, number, special) | -| `nameSchema` | Name validation (1-100 chars) | -| `phoneSchema` | Phone number validation | -| `timestampSchema` | ISO datetime string | -| `dateSchema` | ISO date string | +| Schema | Description | +| ----------------- | ------------------------------------------------------- | +| `emailSchema` | Email validation (lowercase, trimmed) | +| `passwordSchema` | Strong password (8+ chars, mixed case, number, special) | +| `nameSchema` | Name validation (1-100 chars) | +| `phoneSchema` | Phone number validation | +| `timestampSchema` | ISO datetime string | +| `dateSchema` | ISO date string | --- @@ -281,11 +284,11 @@ export * from "./schema"; ```typescript import { ZodValidationPipe } from "@bff/core/validation"; -import { - myEntitySchema, +import { + myEntitySchema, myEntityQueryParamsSchema, type MyEntity, - type MyEntityQueryParams + type MyEntityQueryParams, } from "@customer-portal/domain/my-domain"; @Controller("my-entities") @@ -370,13 +373,15 @@ export const paginationSchema = z.object({ ### 4. **Use Refinements for Complex Validation** ```typescript -export const simActivationSchema = z.object({ - simType: z.enum(["eSIM", "Physical SIM"]), - eid: z.string().optional(), -}).refine( - (data) => data.simType !== "eSIM" || (data.eid && data.eid.length >= 15), - { message: "EID required for eSIM", path: ["eid"] } -); +export const simActivationSchema = z + .object({ + simType: z.enum(["eSIM", "Physical SIM"]), + eid: z.string().optional(), + }) + .refine(data => data.simType !== "eSIM" || (data.eid && data.eid.length >= 15), { + message: "EID required for eSIM", + path: ["eid"], + }); ``` --- @@ -428,4 +433,3 @@ When adding new types or schemas: **Maintained by**: Customer Portal Team **Last Updated**: October 2025 - diff --git a/packages/domain/auth/helpers.ts b/packages/domain/auth/helpers.ts index dc66dfd9..d27a31ad 100644 --- a/packages/domain/auth/helpers.ts +++ b/packages/domain/auth/helpers.ts @@ -11,4 +11,3 @@ type SignupRequestInput = z.input; export function buildSignupRequest(input: SignupRequestInput) { return signupRequestSchema.parse(input); } - diff --git a/packages/domain/billing/contract.ts b/packages/domain/billing/contract.ts index a2d689ba..642de7d3 100644 --- a/packages/domain/billing/contract.ts +++ b/packages/domain/billing/contract.ts @@ -1,6 +1,6 @@ /** * Billing Domain - Contract - * + * * Constants and types for the billing domain. * All validated types are derived from schemas (see schema.ts). */ @@ -35,5 +35,4 @@ export type { BillingSummary, InvoiceQueryParams, InvoiceListQuery, -} from './schema.js'; - +} from "./schema.js"; diff --git a/packages/domain/billing/index.ts b/packages/domain/billing/index.ts index f9e8e87d..bb7cd8fb 100644 --- a/packages/domain/billing/index.ts +++ b/packages/domain/billing/index.ts @@ -1,8 +1,8 @@ /** * Billing Domain - * + * * Exports all billing-related contracts, schemas, and provider mappers. - * + * * Types are derived from Zod schemas (Schema-First Approach) */ @@ -25,7 +25,7 @@ export type { BillingSummary, InvoiceQueryParams, InvoiceListQuery, -} from './schema.js'; +} from "./schema.js"; // Provider adapters export * as Providers from "./providers/index.js"; diff --git a/packages/domain/billing/providers/whmcs/mapper.ts b/packages/domain/billing/providers/whmcs/mapper.ts index 24e16636..fabb9fe0 100644 --- a/packages/domain/billing/providers/whmcs/mapper.ts +++ b/packages/domain/billing/providers/whmcs/mapper.ts @@ -1,6 +1,6 @@ /** * WHMCS Billing Provider - Mapper - * + * * Transforms raw WHMCS invoice data into normalized billing domain types. */ @@ -53,7 +53,7 @@ function mapItems(rawItems: unknown): InvoiceItem[] { const parsed = whmcsInvoiceItemsRawSchema.parse(rawItems); const itemArray = Array.isArray(parsed.item) ? parsed.item : [parsed.item]; - + return itemArray.map(item => ({ id: item.id, description: item.description, @@ -83,9 +83,7 @@ export function transformWhmcsInvoice( const currency = whmcsInvoice.currencycode || options.defaultCurrencyCode || "JPY"; const currencySymbol = - whmcsInvoice.currencyprefix || - whmcsInvoice.currencysuffix || - options.defaultCurrencySymbol; + whmcsInvoice.currencyprefix || whmcsInvoice.currencysuffix || options.defaultCurrencySymbol; // Transform to domain model const invoice: Invoice = { diff --git a/packages/domain/billing/providers/whmcs/raw.types.ts b/packages/domain/billing/providers/whmcs/raw.types.ts index c5f99475..c97cb040 100644 --- a/packages/domain/billing/providers/whmcs/raw.types.ts +++ b/packages/domain/billing/providers/whmcs/raw.types.ts @@ -1,10 +1,10 @@ /** * WHMCS Billing Provider - Raw Types - * + * * Type definitions for the WHMCS billing API contract: * - Request parameter types (API inputs) * - Response types (API outputs) - * + * * These represent the exact structure used by WHMCS APIs. */ @@ -268,19 +268,27 @@ export type WhmcsCurrency = z.infer; /** * WHMCS GetCurrencies API response schema - * + * * WHMCS can return currencies in different formats: * 1. Nested format: { currencies: { currency: [...] } } * 2. Flat format: currencies[currency][0][id], currencies[currency][0][code], etc. * 3. Missing result field in some cases */ -export const whmcsCurrenciesResponseSchema = z.object({ - result: z.enum(["success", "error"]).optional(), - totalresults: z.string().transform(val => parseInt(val, 10)).or(z.number()).optional(), - currencies: z.object({ - currency: z.array(whmcsCurrencySchema).or(whmcsCurrencySchema), - }).optional(), - // Allow any additional flat currency keys for flat format -}).catchall(z.string().or(z.number())); +export const whmcsCurrenciesResponseSchema = z + .object({ + result: z.enum(["success", "error"]).optional(), + totalresults: z + .string() + .transform(val => parseInt(val, 10)) + .or(z.number()) + .optional(), + currencies: z + .object({ + currency: z.array(whmcsCurrencySchema).or(whmcsCurrencySchema), + }) + .optional(), + // Allow any additional flat currency keys for flat format + }) + .catchall(z.string().or(z.number())); export type WhmcsCurrenciesResponse = z.infer; diff --git a/packages/domain/billing/schema.ts b/packages/domain/billing/schema.ts index 29f68d10..a88624ff 100644 --- a/packages/domain/billing/schema.ts +++ b/packages/domain/billing/schema.ts @@ -1,6 +1,6 @@ /** * Billing Domain - Schemas - * + * * Zod validation schemas for billing domain types. * Used for runtime validation of data from any source. */ diff --git a/packages/domain/common/index.ts b/packages/domain/common/index.ts index 52eeb177..3cc371aa 100644 --- a/packages/domain/common/index.ts +++ b/packages/domain/common/index.ts @@ -15,4 +15,3 @@ export * as CommonProviders from "./providers/index.js"; // Re-export provider types for convenience export type { WhmcsResponse, WhmcsErrorResponse } from "./providers/whmcs.js"; export type { SalesforceResponse } from "./providers/salesforce.js"; - diff --git a/packages/domain/common/providers/index.ts b/packages/domain/common/providers/index.ts index 5d6247fe..d1e03d72 100644 --- a/packages/domain/common/providers/index.ts +++ b/packages/domain/common/providers/index.ts @@ -1,6 +1,6 @@ /** * Common Provider Types - * + * * Generic provider-specific response structures used across multiple domains. */ diff --git a/packages/domain/common/providers/salesforce.ts b/packages/domain/common/providers/salesforce.ts index b074bf3c..fb4537bf 100644 --- a/packages/domain/common/providers/salesforce.ts +++ b/packages/domain/common/providers/salesforce.ts @@ -1,6 +1,6 @@ /** * Common Salesforce Provider Types - * + * * Generic Salesforce API response structures used across multiple domains. */ @@ -24,10 +24,10 @@ type SalesforceResponseBase = z.infer; /** * Generic type for Salesforce query results derived from schema * All SOQL queries return this structure regardless of SObject type - * + * * Usage: SalesforceResponse */ -export type SalesforceResponse = Omit & { +export type SalesforceResponse = Omit & { records: TRecord[]; }; @@ -39,4 +39,3 @@ export const salesforceResponseSchema = (recordSch salesforceResponseBaseSchema.extend({ records: z.array(recordSchema), }); - diff --git a/packages/domain/common/providers/salesforce/index.ts b/packages/domain/common/providers/salesforce/index.ts index 3f41fad4..53ce7f60 100644 --- a/packages/domain/common/providers/salesforce/index.ts +++ b/packages/domain/common/providers/salesforce/index.ts @@ -1,2 +1 @@ export * from "./raw.types.js"; - diff --git a/packages/domain/common/providers/salesforce/raw.types.ts b/packages/domain/common/providers/salesforce/raw.types.ts index 682dafac..aa815ae7 100644 --- a/packages/domain/common/providers/salesforce/raw.types.ts +++ b/packages/domain/common/providers/salesforce/raw.types.ts @@ -1,6 +1,6 @@ /** * Common Salesforce Provider Types - * + * * Generic Salesforce API response structures used across multiple domains. */ @@ -24,7 +24,7 @@ export const salesforceQueryResultSchema = (record /** * Generic type for Salesforce query results * All SOQL queries return this structure regardless of SObject type - * + * * Usage: SalesforceQueryResult */ export interface SalesforceQueryResult { diff --git a/packages/domain/common/providers/whmcs.ts b/packages/domain/common/providers/whmcs.ts index 97704dd1..4e65a5bc 100644 --- a/packages/domain/common/providers/whmcs.ts +++ b/packages/domain/common/providers/whmcs.ts @@ -1,6 +1,6 @@ /** * Common WHMCS Provider Types - * + * * Generic WHMCS API response structures used across multiple domains. */ @@ -23,7 +23,7 @@ type WhmcsResponseBase = z.infer; /** * Generic type for WHMCS API responses derived from schema * All WHMCS API endpoints return this structure - * + * * Usage: WhmcsResponse */ export type WhmcsResponse = WhmcsResponseBase & { @@ -53,4 +53,3 @@ export const whmcsErrorResponseSchema = z.object({ }); export type WhmcsErrorResponse = z.infer; - diff --git a/packages/domain/common/schema.ts b/packages/domain/common/schema.ts index e8385a3c..82da0f41 100644 --- a/packages/domain/common/schema.ts +++ b/packages/domain/common/schema.ts @@ -20,7 +20,11 @@ export const passwordSchema = z .regex(/[0-9]/, "Password must contain at least one number") .regex(/[^A-Za-z0-9]/, "Password must contain at least one special character"); -export const nameSchema = z.string().min(1, "Name is required").max(100, "Name must be less than 100 characters").trim(); +export const nameSchema = z + .string() + .min(1, "Name is required") + .max(100, "Name must be less than 100 characters") + .trim(); export const phoneSchema = z .string() @@ -78,7 +82,10 @@ export const nonEmptyStringSchema = z.string().min(1, "Value cannot be empty").t /** * Schema for validating SOQL field names */ -export const soqlFieldNameSchema = z.string().trim().regex(/^[A-Za-z0-9_.]+$/, "Invalid SOQL field name"); +export const soqlFieldNameSchema = z + .string() + .trim() + .regex(/^[A-Za-z0-9_.]+$/, "Invalid SOQL field name"); // ============================================================================ // API Response Schemas @@ -111,10 +118,7 @@ export const apiErrorResponseSchema = z.object({ * Usage: apiResponseSchema(yourDataSchema) */ export const apiResponseSchema = (dataSchema: T) => - z.discriminatedUnion("success", [ - apiSuccessResponseSchema(dataSchema), - apiErrorResponseSchema, - ]); + z.discriminatedUnion("success", [apiSuccessResponseSchema(dataSchema), apiErrorResponseSchema]); // ============================================================================ // Pagination Schemas diff --git a/packages/domain/common/types.ts b/packages/domain/common/types.ts index f9adc648..a6720d7e 100644 --- a/packages/domain/common/types.ts +++ b/packages/domain/common/types.ts @@ -1,6 +1,6 @@ /** * Common Domain - Types - * + * * Shared utility types and branded types used across all domains. */ diff --git a/packages/domain/common/validation.ts b/packages/domain/common/validation.ts index 84d4c522..68391536 100644 --- a/packages/domain/common/validation.ts +++ b/packages/domain/common/validation.ts @@ -1,6 +1,6 @@ /** * Common Domain - Validation Utilities - * + * * Generic validation functions used across all domains. * These are pure functions with no infrastructure dependencies. */ @@ -26,23 +26,26 @@ export const customerNumberSchema = z.string().min(1, "Customer number is requir /** * Normalize and validate an email address - * + * * This is a convenience wrapper that throws on invalid input. * For validation without throwing, use the emailSchema directly with .safeParse() - * + * * @throws Error if email format is invalid */ export function normalizeAndValidateEmail(email: string): string { - const emailValidationSchema = z.string().email().transform(e => e.toLowerCase().trim()); + const emailValidationSchema = z + .string() + .email() + .transform(e => e.toLowerCase().trim()); return emailValidationSchema.parse(email); } /** * Validate a UUID (v4) - * + * * This is a convenience wrapper that throws on invalid input. * For validation without throwing, use the uuidSchema directly with .safeParse() - * + * * @throws Error if UUID format is invalid */ export function validateUuidV4OrThrow(id: string): string { @@ -74,10 +77,10 @@ export const urlSchema = z.string().url(); /** * Validate a URL - * + * * This is a convenience wrapper that throws on invalid input. * For validation without throwing, use the urlSchema directly with .safeParse() - * + * * @throws Error if URL format is invalid */ export function validateUrlOrThrow(url: string): string { @@ -90,7 +93,7 @@ export function validateUrlOrThrow(url: string): string { /** * Validate a URL (non-throwing) - * + * * Returns validation result with errors if any. * Prefer using urlSchema.safeParse() directly for more control. */ diff --git a/packages/domain/customer/providers/index.ts b/packages/domain/customer/providers/index.ts index 5f2ba815..a552f7be 100644 --- a/packages/domain/customer/providers/index.ts +++ b/packages/domain/customer/providers/index.ts @@ -1,6 +1,6 @@ /** * Customer Domain - Providers - * + * * Providers handle mapping from external systems to domain types: * - Portal: Prisma (portal DB) → UserAuth * - Whmcs: WHMCS API → WhmcsClient diff --git a/packages/domain/customer/providers/portal/index.ts b/packages/domain/customer/providers/portal/index.ts index 0187c163..9ffdb486 100644 --- a/packages/domain/customer/providers/portal/index.ts +++ b/packages/domain/customer/providers/portal/index.ts @@ -1,9 +1,8 @@ /** * Portal Provider - * + * * Handles mapping from Prisma (portal database) to UserAuth domain type */ export * from "./mapper.js"; export * from "./types.js"; - diff --git a/packages/domain/customer/providers/portal/mapper.ts b/packages/domain/customer/providers/portal/mapper.ts index d55fca00..17191d54 100644 --- a/packages/domain/customer/providers/portal/mapper.ts +++ b/packages/domain/customer/providers/portal/mapper.ts @@ -1,6 +1,6 @@ /** * Portal Provider - Mapper - * + * * Maps Prisma user data to UserAuth domain type using schema validation */ @@ -10,9 +10,9 @@ import type { UserAuth } from "../../schema.js"; /** * Maps raw Prisma user data to UserAuth domain type - * + * * Uses schema validation for runtime type safety - * + * * @param raw - Raw Prisma user data from portal database * @returns Validated UserAuth with only authentication state */ @@ -28,4 +28,3 @@ export function mapPrismaUserToUserAuth(raw: PrismaUserRaw): UserAuth { updatedAt: raw.updatedAt.toISOString(), }); } - diff --git a/packages/domain/customer/providers/portal/types.ts b/packages/domain/customer/providers/portal/types.ts index b60d1ef6..a1b627a8 100644 --- a/packages/domain/customer/providers/portal/types.ts +++ b/packages/domain/customer/providers/portal/types.ts @@ -1,6 +1,6 @@ /** * Portal Provider - Raw Types - * + * * Raw Prisma user data interface. * Domain doesn't depend on @prisma/client directly. */ @@ -24,4 +24,3 @@ export interface PrismaUserRaw { createdAt: Date; updatedAt: Date; } - diff --git a/packages/domain/customer/providers/whmcs/mapper.ts b/packages/domain/customer/providers/whmcs/mapper.ts index 85802a6e..bd9d73a7 100644 --- a/packages/domain/customer/providers/whmcs/mapper.ts +++ b/packages/domain/customer/providers/whmcs/mapper.ts @@ -1,6 +1,6 @@ /** * WHMCS Provider - Mapper - * + * * Maps WHMCS API responses to domain types. * Minimal transformation - validates and normalizes only address structure. */ @@ -32,7 +32,7 @@ export function transformWhmcsClientResponse(response: unknown): WhmcsClient { /** * Transform raw WHMCS client to domain WhmcsClient - * + * * Keeps raw WHMCS field names, only normalizes: * - Address structure to domain Address type * - Type coercions (strings to numbers/booleans) diff --git a/packages/domain/mappings/schema.ts b/packages/domain/mappings/schema.ts index 30c6defc..ba4a3994 100644 --- a/packages/domain/mappings/schema.ts +++ b/packages/domain/mappings/schema.ts @@ -3,27 +3,17 @@ */ import { z } from "zod"; -import type { - CreateMappingRequest, - UpdateMappingRequest, - UserIdMapping, -} from "./contract.js"; +import type { CreateMappingRequest, UpdateMappingRequest, UserIdMapping } from "./contract.js"; export const createMappingRequestSchema: z.ZodType = z.object({ userId: z.string().uuid(), whmcsClientId: z.number().int().positive(), - sfAccountId: z - .string() - .min(1, "Salesforce account ID must be at least 1 character") - .optional(), + sfAccountId: z.string().min(1, "Salesforce account ID must be at least 1 character").optional(), }); export const updateMappingRequestSchema: z.ZodType = z.object({ whmcsClientId: z.number().int().positive().optional(), - sfAccountId: z - .string() - .min(1, "Salesforce account ID must be at least 1 character") - .optional(), + sfAccountId: z.string().min(1, "Salesforce account ID must be at least 1 character").optional(), }); export const userIdMappingSchema: z.ZodType = z.object({ diff --git a/packages/domain/mappings/validation.ts b/packages/domain/mappings/validation.ts index 4160091e..bde7e192 100644 --- a/packages/domain/mappings/validation.ts +++ b/packages/domain/mappings/validation.ts @@ -1,6 +1,6 @@ /** * ID Mapping Domain - Validation - * + * * Pure business validation functions for ID mappings. * These functions contain no infrastructure dependencies (no DB, no HTTP, no logging). */ @@ -33,7 +33,7 @@ export function checkMappingCompleteness(request: CreateMappingRequest | UserIdM /** * Validate no conflicts exist with existing mappings * Business rule: Each userId, whmcsClientId should be unique - * + * * Note: This assumes the request has already been validated by schema. * Use createMappingRequestSchema.parse() before calling this function. */ @@ -72,11 +72,13 @@ export function validateNoConflicts( /** * Validate deletion constraints * Business rule: Warn about data access impacts - * + * * Note: This assumes the mapping has already been validated. * This function adds business warnings about the impact of deletion. */ -export function validateDeletion(mapping: UserIdMapping | null | undefined): MappingValidationResult { +export function validateDeletion( + mapping: UserIdMapping | null | undefined +): MappingValidationResult { const errors: string[] = []; const warnings: string[] = []; @@ -85,9 +87,7 @@ export function validateDeletion(mapping: UserIdMapping | null | undefined): Map return { isValid: false, errors, warnings }; } - warnings.push( - "Deleting this mapping will prevent access to WHMCS/Salesforce data for this user" - ); + warnings.push("Deleting this mapping will prevent access to WHMCS/Salesforce data for this user"); if (mapping.sfAccountId) { warnings.push( "This mapping includes Salesforce integration - deletion will affect case management" @@ -99,7 +99,7 @@ export function validateDeletion(mapping: UserIdMapping | null | undefined): Map /** * Sanitize and normalize a create mapping request - * + * * Note: This performs basic string trimming before validation. * The schema handles validation; this is purely for data cleanup. */ @@ -113,7 +113,7 @@ export function sanitizeCreateRequest(request: CreateMappingRequest): CreateMapp /** * Sanitize and normalize an update mapping request - * + * * Note: This performs basic string trimming before validation. * The schema handles validation; this is purely for data cleanup. */ @@ -130,4 +130,3 @@ export function sanitizeUpdateRequest(request: UpdateMappingRequest): UpdateMapp return sanitized; } - diff --git a/packages/domain/orders/checkout.ts b/packages/domain/orders/checkout.ts index d250e10c..b2477c13 100644 --- a/packages/domain/orders/checkout.ts +++ b/packages/domain/orders/checkout.ts @@ -1,6 +1,6 @@ /** * Orders Domain - Checkout Types - * + * * Minimal type definitions for checkout flow. * Frontend handles its own URL param serialization. */ @@ -8,5 +8,5 @@ // This file is intentionally minimal after cleanup. // The build/derive/normalize functions were removed as they were // unnecessary abstractions that should be handled by the frontend. -// +// // See CLEANUP_PROPOSAL_NORMALIZERS.md for details. diff --git a/packages/domain/orders/helpers.ts b/packages/domain/orders/helpers.ts index 68b024b7..02bfc784 100644 --- a/packages/domain/orders/helpers.ts +++ b/packages/domain/orders/helpers.ts @@ -64,7 +64,10 @@ const BILLING_CYCLE_ALIASES: Record = { }; const normalizeBillingCycleKey = (value: string): string => - value.trim().toLowerCase().replace(/[\s_-]+/g, ""); + value + .trim() + .toLowerCase() + .replace(/[\s_-]+/g, ""); const DEFAULT_BILLING_CYCLE: BillingCycle = "monthly"; @@ -337,7 +340,7 @@ export function buildOrderDisplayItems( primaryCategory: category, categories: [category], charges, - included: charges.every((charge) => charge.amount <= 0), + included: charges.every(charge => charge.amount <= 0), sourceItems: [item], isBundle: Boolean(item.isBundledAddon), }; diff --git a/packages/domain/orders/index.ts b/packages/domain/orders/index.ts index 2e50b097..82602531 100644 --- a/packages/domain/orders/index.ts +++ b/packages/domain/orders/index.ts @@ -1,16 +1,16 @@ /** * Orders Domain - * + * * Exports all order-related contracts, schemas, and provider mappers. - * + * * Types are derived from Zod schemas (Schema-First Approach) */ // Business types and constants -export { - type OrderCreationType, - type OrderStatus, - type OrderType, +export { + type OrderCreationType, + type OrderStatus, + type OrderType, type OrderTypeValue, type UserMapping, // Checkout types @@ -79,7 +79,7 @@ export type { OrderDisplayItemCategory, OrderDisplayItemCharge, OrderDisplayItemChargeKind, -} from './schema.js'; +} from "./schema.js"; // Provider adapters export * as Providers from "./providers/index.js"; diff --git a/packages/domain/orders/providers/index.ts b/packages/domain/orders/providers/index.ts index 13ba79ad..00084bb6 100644 --- a/packages/domain/orders/providers/index.ts +++ b/packages/domain/orders/providers/index.ts @@ -21,12 +21,7 @@ export const Salesforce = { fieldMap: SalesforceFieldMap, }; -export { - WhmcsMapper, - WhmcsRaw, - SalesforceMapper, - SalesforceRaw, -}; +export { WhmcsMapper, WhmcsRaw, SalesforceMapper, SalesforceRaw }; export * from "./whmcs/mapper.js"; export * from "./whmcs/raw.types.js"; export * from "./salesforce/mapper.js"; diff --git a/packages/domain/orders/utils.ts b/packages/domain/orders/utils.ts index 7445e747..4e3722f5 100644 --- a/packages/domain/orders/utils.ts +++ b/packages/domain/orders/utils.ts @@ -44,7 +44,7 @@ export function createOrderRequest(payload: { /** * Transform CheckoutCart into CreateOrderRequest * Handles SKU extraction, validation, and payload formatting - * + * * @throws Error if no products are selected */ export function prepareOrderFromCart( @@ -65,13 +65,11 @@ export function prepareOrderFromCart( // Note: Zod validation of the final structure should happen at the boundary or via schema.parse // This function focuses on the structural transformation logic. - + const orderData: CreateOrderRequest = { orderType, skus: uniqueSkus, - ...(Object.keys(cart.configuration).length > 0 - ? { configurations: cart.configuration } - : {}), + ...(Object.keys(cart.configuration).length > 0 ? { configurations: cart.configuration } : {}), }; return orderData; diff --git a/packages/domain/payments/contract.ts b/packages/domain/payments/contract.ts index 57e8f482..e9a3dbc3 100644 --- a/packages/domain/payments/contract.ts +++ b/packages/domain/payments/contract.ts @@ -1,6 +1,6 @@ /** * Payments Domain - Contract - * + * * Constants and types for the payments domain. * All validated types are derived from schemas (see schema.ts). */ @@ -53,4 +53,4 @@ export type { PaymentGatewayType, PaymentGateway, PaymentGatewayList, -} from './schema.js'; +} from "./schema.js"; diff --git a/packages/domain/payments/index.ts b/packages/domain/payments/index.ts index 76ba14ed..4485a6b5 100644 --- a/packages/domain/payments/index.ts +++ b/packages/domain/payments/index.ts @@ -1,8 +1,8 @@ /** * Payments Domain - * + * * Exports all payment-related contracts, schemas, and provider mappers. - * + * * Types are derived from Zod schemas (Schema-First Approach) */ @@ -20,7 +20,7 @@ export type { PaymentGatewayType, PaymentGateway, PaymentGatewayList, -} from './schema.js'; +} from "./schema.js"; // Provider adapters export * as Providers from "./providers/index.js"; diff --git a/packages/domain/payments/providers/whmcs/mapper.ts b/packages/domain/payments/providers/whmcs/mapper.ts index d037b26f..dc7ed13d 100644 --- a/packages/domain/payments/providers/whmcs/mapper.ts +++ b/packages/domain/payments/providers/whmcs/mapper.ts @@ -77,4 +77,3 @@ export function transformWhmcsPaymentGateway(raw: unknown): PaymentGateway { return paymentGatewaySchema.parse(gateway); } - diff --git a/packages/domain/providers/index.ts b/packages/domain/providers/index.ts index 70387966..07d2684b 100644 --- a/packages/domain/providers/index.ts +++ b/packages/domain/providers/index.ts @@ -4,4 +4,3 @@ */ export * as Whmcs from "./whmcs/index.js"; - diff --git a/packages/domain/providers/whmcs/index.ts b/packages/domain/providers/whmcs/index.ts index ebe42dc1..0da6c219 100644 --- a/packages/domain/providers/whmcs/index.ts +++ b/packages/domain/providers/whmcs/index.ts @@ -4,4 +4,3 @@ */ export * from "./utils.js"; - diff --git a/packages/domain/sim/contract.ts b/packages/domain/sim/contract.ts index 0111a874..6ff7ebf9 100644 --- a/packages/domain/sim/contract.ts +++ b/packages/domain/sim/contract.ts @@ -1,6 +1,6 @@ /** * SIM Domain - Contract - * + * * Constants and types for the SIM domain. * All validated types are derived from schemas (see schema.ts). */ @@ -64,4 +64,4 @@ export type { SimOrderActivationRequest, SimOrderActivationMnp, SimOrderActivationAddons, -} from './schema.js'; +} from "./schema.js"; diff --git a/packages/domain/sim/index.ts b/packages/domain/sim/index.ts index 7506ac24..36a0385f 100644 --- a/packages/domain/sim/index.ts +++ b/packages/domain/sim/index.ts @@ -1,8 +1,8 @@ /** * SIM Domain - * + * * Exports all SIM-related contracts, schemas, and provider mappers. - * + * * Types are derived from Zod schemas (Schema-First Approach) */ @@ -59,7 +59,7 @@ export type { SimTopUpPricing, SimTopUpPricingPreviewRequest, SimTopUpPricingPreviewResponse, -} from './schema.js'; +} from "./schema.js"; export type { SimPlanCode } from "./contract.js"; export type { SimPlanOption, SimFeatureToggleSnapshot } from "./helpers.js"; diff --git a/packages/domain/sim/lifecycle.ts b/packages/domain/sim/lifecycle.ts index e9ad5c1f..0ebc940f 100644 --- a/packages/domain/sim/lifecycle.ts +++ b/packages/domain/sim/lifecycle.ts @@ -138,4 +138,3 @@ export const SIM_MANAGEMENT_FLOW: ManagementFlow = { }, ], }; - diff --git a/packages/domain/sim/providers/freebit/index.ts b/packages/domain/sim/providers/freebit/index.ts index c6992c04..fab9153e 100644 --- a/packages/domain/sim/providers/freebit/index.ts +++ b/packages/domain/sim/providers/freebit/index.ts @@ -47,8 +47,12 @@ export type PlanChangeResponse = ReturnType; export type CancelAccountResponse = ReturnType; export type EsimReissueResponse = ReturnType; -export type EsimAddAccountResponse = ReturnType; -export type EsimActivationResponse = ReturnType; +export type EsimAddAccountResponse = ReturnType< + typeof Mapper.transformFreebitEsimAddAccountResponse +>; +export type EsimActivationResponse = ReturnType< + typeof Mapper.transformFreebitEsimActivationResponse +>; export type AuthResponse = ReturnType; export * from "./mapper.js"; diff --git a/packages/domain/sim/providers/freebit/raw.types.ts b/packages/domain/sim/providers/freebit/raw.types.ts index e72a2ef8..55724926 100644 --- a/packages/domain/sim/providers/freebit/raw.types.ts +++ b/packages/domain/sim/providers/freebit/raw.types.ts @@ -48,11 +48,13 @@ export const freebitTrafficInfoRawSchema = z.object({ }) .optional(), account: z.union([z.string(), z.number()]).optional(), - traffic: z.object({ - today: z.union([z.string(), z.number()]).optional(), - inRecentDays: z.union([z.string(), z.number()]).optional(), - blackList: z.union([z.string(), z.number()]).optional(), - }).optional(), + traffic: z + .object({ + today: z.union([z.string(), z.number()]).optional(), + inRecentDays: z.union([z.string(), z.number()]).optional(), + blackList: z.union([z.string(), z.number()]).optional(), + }) + .optional(), }); export const freebitTopUpRawSchema = z.object({ resultCode: z.string().optional(), @@ -154,14 +156,16 @@ export const freebitQuotaHistoryRawSchema = z.object({ account: z.union([z.string(), z.number()]).optional(), total: z.union([z.string(), z.number()]).optional(), count: z.union([z.string(), z.number()]).optional(), - quotaHistory: z.array( - z.object({ - addQuotaKb: z.union([z.string(), z.number()]).optional(), - addDate: z.string().optional(), - expireDate: z.string().optional(), - campaignCode: z.string().optional(), - }) - ).optional(), + quotaHistory: z + .array( + z.object({ + addQuotaKb: z.union([z.string(), z.number()]).optional(), + addDate: z.string().optional(), + expireDate: z.string().optional(), + campaignCode: z.string().optional(), + }) + ) + .optional(), }); export type FreebitQuotaHistoryRaw = z.infer; @@ -178,4 +182,3 @@ export const freebitAuthResponseRawSchema = z.object({ }); export type FreebitAuthResponseRaw = z.infer; - diff --git a/packages/domain/sim/providers/freebit/requests.ts b/packages/domain/sim/providers/freebit/requests.ts index 153adf80..29e88791 100644 --- a/packages/domain/sim/providers/freebit/requests.ts +++ b/packages/domain/sim/providers/freebit/requests.ts @@ -1,6 +1,6 @@ /** * SIM Domain - Freebit Provider Request Schemas - * + * * Zod schemas for all Freebit API request payloads. */ @@ -135,7 +135,10 @@ export const freebitEsimAddAccountRequestSchema = z.object({ account: z.string().min(1, "Account is required"), eid: z.string().min(1, "EID is required"), addKind: z.enum(["N", "R"]).default("N"), - shipDate: z.string().regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format").optional(), + shipDate: z + .string() + .regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format") + .optional(), planCode: z.string().optional(), contractLine: z.enum(["4G", "5G"]).optional(), mnp: freebitEsimMnpSchema.optional(), @@ -178,7 +181,10 @@ export const freebitEsimIdentitySchema = z.object({ firstnameZenKana: z.string().optional(), lastnameZenKana: z.string().optional(), gender: z.enum(["M", "F"]).optional(), - birthday: z.string().regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format").optional(), + birthday: z + .string() + .regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format") + .optional(), }); /** @@ -194,7 +200,10 @@ export const freebitEsimActivationRequestSchema = z.object({ simkind: z.enum(["esim", "psim"]).default("esim"), planCode: z.string().optional(), contractLine: z.enum(["4G", "5G"]).optional(), - shipDate: z.string().regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format").optional(), + shipDate: z + .string() + .regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format") + .optional(), mnp: freebitEsimMnpSchema.optional(), // Identity fields (flattened for API) firstnameKanji: z.string().optional(), @@ -202,7 +211,10 @@ export const freebitEsimActivationRequestSchema = z.object({ firstnameZenKana: z.string().optional(), lastnameZenKana: z.string().optional(), gender: z.enum(["M", "F"]).optional(), - birthday: z.string().regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format").optional(), + birthday: z + .string() + .regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format") + .optional(), // Additional fields for reissue/exchange masterAccount: z.string().optional(), masterPassword: z.string().optional(), @@ -224,10 +236,12 @@ export const freebitEsimActivationResponseSchema = z.object({ resultCode: z.string(), resultMessage: z.string().optional(), data: z.unknown().optional(), - status: z.object({ - statusCode: z.union([z.string(), z.number()]), - message: z.string(), - }).optional(), + status: z + .object({ + statusCode: z.union([z.string(), z.number()]), + message: z.string(), + }) + .optional(), message: z.string().optional(), }); @@ -241,7 +255,10 @@ export const freebitEsimActivationParamsSchema = z.object({ planCode: z.string().optional(), contractLine: z.enum(["4G", "5G"]).optional(), aladinOperated: z.enum(["10", "20"]).default("10"), - shipDate: z.string().regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format").optional(), + shipDate: z + .string() + .regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format") + .optional(), mnp: freebitEsimMnpSchema.optional(), identity: freebitEsimIdentitySchema.optional(), }); @@ -271,4 +288,3 @@ export type FreebitQuotaHistoryResponse = z.infer; export type FreebitAuthRequest = z.infer; export type FreebitCancelAccountRequest = z.infer; - diff --git a/packages/domain/sim/providers/freebit/utils.ts b/packages/domain/sim/providers/freebit/utils.ts index aa767a83..41229ee0 100644 --- a/packages/domain/sim/providers/freebit/utils.ts +++ b/packages/domain/sim/providers/freebit/utils.ts @@ -1,6 +1,6 @@ /** * Freebit Provider Utilities - * + * * Provider-specific utilities for Freebit SIM API integration */ @@ -9,7 +9,7 @@ * Removes all non-digit characters from account string */ export function normalizeAccount(account: string): string { - return account.replace(/[^0-9]/g, ''); + return account.replace(/[^0-9]/g, ""); } /** @@ -25,8 +25,8 @@ export function validateAccount(account: string): boolean { */ export function formatDateForApi(date: Date): string { const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); return `${year}${month}${day}`; } @@ -36,11 +36,10 @@ export function formatDateForApi(date: Date): string { */ export function parseDateFromApi(dateString: string): Date | null { if (!/^\d{8}$/.test(dateString)) return null; - + const year = parseInt(dateString.substring(0, 4), 10); const month = parseInt(dateString.substring(4, 6), 10) - 1; // Month is 0-indexed const day = parseInt(dateString.substring(6, 8), 10); - + return new Date(year, month, day); } - diff --git a/packages/domain/sim/schema.ts b/packages/domain/sim/schema.ts index 8a5fef3d..164d42fc 100644 --- a/packages/domain/sim/schema.ts +++ b/packages/domain/sim/schema.ts @@ -138,12 +138,8 @@ export const simCancelRequestSchema = z.object({ }); export const simTopUpHistoryRequestSchema = z.object({ - fromDate: z - .string() - .regex(/^\d{8}$/, "From date must be in YYYYMMDD format"), - toDate: z - .string() - .regex(/^\d{8}$/, "To date must be in YYYYMMDD format"), + fromDate: z.string().regex(/^\d{8}$/, "From date must be in YYYYMMDD format"), + toDate: z.string().regex(/^\d{8}$/, "To date must be in YYYYMMDD format"), }); export const simFeaturesUpdateRequestSchema = z.object({ @@ -162,16 +158,19 @@ export const simReissueRequestSchema = z.object({ }); // Enhanced cancellation request with more details -export const simCancelFullRequestSchema = z.object({ - cancellationMonth: z.string().regex(/^\d{4}-\d{2}$/, "Cancellation month must be in YYYY-MM format"), - confirmRead: z.boolean(), - confirmCancel: z.boolean(), - alternativeEmail: z.string().email().optional().or(z.literal("")), - comments: z.string().max(1000).optional(), -}).refine( - (data) => data.confirmRead === true && data.confirmCancel === true, - { message: "You must confirm both checkboxes to proceed" } -); +export const simCancelFullRequestSchema = z + .object({ + cancellationMonth: z + .string() + .regex(/^\d{4}-\d{2}$/, "Cancellation month must be in YYYY-MM format"), + confirmRead: z.boolean(), + confirmCancel: z.boolean(), + alternativeEmail: z.string().email().optional().or(z.literal("")), + comments: z.string().max(1000).optional(), + }) + .refine(data => data.confirmRead === true && data.confirmCancel === true, { + message: "You must confirm both checkboxes to proceed", + }); // Top-up request with enhanced details for email export const simTopUpFullRequestSchema = z.object({ @@ -292,7 +291,10 @@ export const simOrderActivationMnpSchema = z.object({ firstnameZenKana: z.string().optional(), lastnameZenKana: z.string().optional(), gender: z.string().optional(), - birthday: z.string().regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format").optional(), + birthday: z + .string() + .regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format") + .optional(), }); export const simOrderActivationAddonsSchema = z.object({ @@ -300,42 +302,48 @@ export const simOrderActivationAddonsSchema = z.object({ callWaiting: z.boolean().optional(), }); -export const simOrderActivationRequestSchema = z.object({ - planSku: z.string().min(1, "Plan SKU is required"), - simType: simCardTypeSchema, - eid: z.string().min(15, "EID must be at least 15 characters").optional(), - activationType: simActivationTypeSchema, - scheduledAt: z.string().regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format").optional(), - addons: simOrderActivationAddonsSchema.optional(), - mnp: simOrderActivationMnpSchema.optional(), - msisdn: z.string().min(1, "Phone number (msisdn) is required"), - oneTimeAmountJpy: z.number().nonnegative("One-time amount must be non-negative"), - monthlyAmountJpy: z.number().nonnegative("Monthly amount must be non-negative"), -}).refine( - (data) => { - // If simType is eSIM, eid is required - if (data.simType === "eSIM" && (!data.eid || data.eid.length < 15)) { - return false; +export const simOrderActivationRequestSchema = z + .object({ + planSku: z.string().min(1, "Plan SKU is required"), + simType: simCardTypeSchema, + eid: z.string().min(15, "EID must be at least 15 characters").optional(), + activationType: simActivationTypeSchema, + scheduledAt: z + .string() + .regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format") + .optional(), + addons: simOrderActivationAddonsSchema.optional(), + mnp: simOrderActivationMnpSchema.optional(), + msisdn: z.string().min(1, "Phone number (msisdn) is required"), + oneTimeAmountJpy: z.number().nonnegative("One-time amount must be non-negative"), + monthlyAmountJpy: z.number().nonnegative("Monthly amount must be non-negative"), + }) + .refine( + data => { + // If simType is eSIM, eid is required + if (data.simType === "eSIM" && (!data.eid || data.eid.length < 15)) { + return false; + } + return true; + }, + { + message: "EID is required for eSIM and must be at least 15 characters", + path: ["eid"], } - return true; - }, - { - message: "EID is required for eSIM and must be at least 15 characters", - path: ["eid"], - } -).refine( - (data) => { - // If activationType is Scheduled, scheduledAt is required - if (data.activationType === "Scheduled" && !data.scheduledAt) { - return false; + ) + .refine( + data => { + // If activationType is Scheduled, scheduledAt is required + if (data.activationType === "Scheduled" && !data.scheduledAt) { + return false; + } + return true; + }, + { + message: "Scheduled date is required for Scheduled activation", + path: ["scheduledAt"], } - return true; - }, - { - message: "Scheduled date is required for Scheduled activation", - path: ["scheduledAt"], - } -); + ); export type SimOrderActivationRequest = z.infer; export type SimOrderActivationMnp = z.infer; diff --git a/packages/domain/subscriptions/contract.ts b/packages/domain/subscriptions/contract.ts index f3f5d2fd..0f8edad0 100644 --- a/packages/domain/subscriptions/contract.ts +++ b/packages/domain/subscriptions/contract.ts @@ -1,6 +1,6 @@ /** * Subscriptions Domain - Contract - * + * * Constants and types for the subscriptions domain. * All validated types are derived from schemas (see schema.ts). */ @@ -45,4 +45,4 @@ export type { SubscriptionList, SubscriptionQueryParams, SubscriptionQuery, -} from './schema.js'; +} from "./schema.js"; diff --git a/packages/domain/subscriptions/providers/whmcs/raw.types.ts b/packages/domain/subscriptions/providers/whmcs/raw.types.ts index 508edde2..b9e220cc 100644 --- a/packages/domain/subscriptions/providers/whmcs/raw.types.ts +++ b/packages/domain/subscriptions/providers/whmcs/raw.types.ts @@ -1,6 +1,6 @@ /** * WHMCS Subscriptions Provider - Raw Types - * + * * Type definitions for the WHMCS subscriptions API contract: * - Request parameter types (API inputs) * - Response types (API outputs) @@ -8,30 +8,24 @@ import { z } from "zod"; -const normalizeRequiredNumber = z.preprocess( - value => { - if (typeof value === "number") return value; - if (typeof value === "string" && value.trim().length > 0) { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : value; - } - return value; - }, - z.number() -); +const normalizeRequiredNumber = z.preprocess(value => { + if (typeof value === "number") return value; + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : value; + } + return value; +}, z.number()); -const normalizeOptionalNumber = z.preprocess( - value => { - if (value === undefined || value === null || value === "") return undefined; - if (typeof value === "number") return value; - if (typeof value === "string") { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : undefined; - } - return undefined; - }, - z.number().optional() -); +const normalizeOptionalNumber = z.preprocess(value => { + if (value === undefined || value === null || value === "") return undefined; + if (typeof value === "number") return value; + if (typeof value === "string") { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; + } + return undefined; +}, z.number().optional()); const optionalStringField = () => z @@ -117,7 +111,7 @@ export const whmcsProductRawSchema = z.object({ ns1: optionalStringField(), ns2: optionalStringField(), assignedips: optionalStringField(), - + // Pricing firstpaymentamount: z.union([z.string(), z.number()]).optional(), amount: z.union([z.string(), z.number()]).optional(), @@ -125,16 +119,16 @@ export const whmcsProductRawSchema = z.object({ billingcycle: z.string().optional(), paymentmethod: z.string().optional(), paymentmethodname: z.string().optional(), - + // Dates nextduedate: z.string().optional(), nextinvoicedate: z.string().optional(), - + // Status status: z.string(), username: z.string().optional(), password: z.string().optional(), - + // Notes notes: z.string().optional(), diskusage: normalizeOptionalNumber, @@ -142,18 +136,20 @@ export const whmcsProductRawSchema = z.object({ bwusage: normalizeOptionalNumber, bwlimit: normalizeOptionalNumber, lastupdate: z.string().optional(), - + // Custom fields customfields: whmcsCustomFieldsContainerSchema.optional(), configoptions: whmcsConfigOptionsContainerSchema.optional(), - + // Pricing details - pricing: z.object({ - amount: z.union([z.string(), z.number()]).optional(), - currency: z.string().optional(), - currencyprefix: z.string().optional(), - currencysuffix: z.string().optional(), - }).optional(), + pricing: z + .object({ + amount: z.union([z.string(), z.number()]).optional(), + currency: z.string().optional(), + currencyprefix: z.string().optional(), + currencysuffix: z.string().optional(), + }) + .optional(), }); export type WhmcsProductRaw = z.infer; @@ -167,13 +163,15 @@ export type WhmcsCustomField = z.infer; * WHMCS GetClientsProducts API response schema */ const whmcsProductContainerSchema = z.object({ - product: z - .preprocess(value => { + product: z.preprocess( + value => { if (value === null || value === undefined || value === "") { return undefined; } return value; - }, z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional()), + }, + z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional() + ), }); export const whmcsProductListResponseSchema = z.object({ diff --git a/packages/domain/support/providers/index.ts b/packages/domain/support/providers/index.ts index b27585bd..99fcf690 100644 --- a/packages/domain/support/providers/index.ts +++ b/packages/domain/support/providers/index.ts @@ -15,4 +15,3 @@ export const Salesforce = { export { SalesforceMapper, SalesforceRaw }; export * from "./salesforce/mapper.js"; export * from "./salesforce/raw.types.js"; - diff --git a/packages/domain/support/providers/salesforce/index.ts b/packages/domain/support/providers/salesforce/index.ts index 0f204088..44fc2a75 100644 --- a/packages/domain/support/providers/salesforce/index.ts +++ b/packages/domain/support/providers/salesforce/index.ts @@ -4,4 +4,3 @@ export * from "./raw.types.js"; export * from "./mapper.js"; - diff --git a/packages/domain/support/providers/salesforce/mapper.ts b/packages/domain/support/providers/salesforce/mapper.ts index 5930fe76..e87de79d 100644 --- a/packages/domain/support/providers/salesforce/mapper.ts +++ b/packages/domain/support/providers/salesforce/mapper.ts @@ -46,9 +46,7 @@ function nowIsoString(): string { * @param record - Raw Salesforce Case record from SOQL query * @returns Validated SupportCase domain object */ -export function transformSalesforceCaseToSupportCase( - record: SalesforceCaseRecord -): SupportCase { +export function transformSalesforceCaseToSupportCase(record: SalesforceCaseRecord): SupportCase { // Get raw values const rawStatus = ensureString(record.Status) ?? SALESFORCE_CASE_STATUS.NEW; const rawPriority = ensureString(record.Priority) ?? SALESFORCE_CASE_PRIORITY.MEDIUM; @@ -167,4 +165,3 @@ export function buildCaseByIdQuery( LIMIT 1 `.trim(); } - diff --git a/packages/domain/support/providers/salesforce/raw.types.ts b/packages/domain/support/providers/salesforce/raw.types.ts index e81c9e08..df7c1456 100644 --- a/packages/domain/support/providers/salesforce/raw.types.ts +++ b/packages/domain/support/providers/salesforce/raw.types.ts @@ -319,4 +319,3 @@ const PRIORITY_TO_SALESFORCE: Record = { export function toSalesforcePriority(displayPriority: string): string { return PRIORITY_TO_SALESFORCE[displayPriority] ?? SALESFORCE_CASE_PRIORITY.MEDIUM; } - diff --git a/packages/domain/support/schema.ts b/packages/domain/support/schema.ts index 86312b8f..51d4f9a4 100644 --- a/packages/domain/support/schema.ts +++ b/packages/domain/support/schema.ts @@ -1,9 +1,5 @@ import { z } from "zod"; -import { - SUPPORT_CASE_STATUS, - SUPPORT_CASE_PRIORITY, - SUPPORT_CASE_CATEGORY, -} from "./contract.js"; +import { SUPPORT_CASE_STATUS, SUPPORT_CASE_PRIORITY, SUPPORT_CASE_CATEGORY } from "./contract.js"; /** * Portal status values (mapped from Salesforce Japanese API names) diff --git a/packages/domain/toolkit/README.md b/packages/domain/toolkit/README.md index 0e2da0a5..54f97118 100644 --- a/packages/domain/toolkit/README.md +++ b/packages/domain/toolkit/README.md @@ -5,6 +5,7 @@ Utility functions and helpers for the domain layer. This package contains **util ## Purpose The toolkit provides pure utility functions for common operations like: + - String manipulation - Date/time formatting - Currency formatting @@ -43,10 +44,10 @@ Validation determines if input is valid or invalid: ```typescript // These are VALIDATION functions → Use common/validation.ts -isValidEmail(email) // Returns true/false -isValidUuid(id) // Returns true/false -isValidUrl(url) // Returns true/false -validateUrlOrThrow(url) // Throws if invalid +isValidEmail(email); // Returns true/false +isValidUuid(id); // Returns true/false +isValidUrl(url); // Returns true/false +validateUrlOrThrow(url); // Throws if invalid ``` **Location**: `packages/domain/common/validation.ts` @@ -57,10 +58,10 @@ Utilities transform, extract, or manipulate data: ```typescript // These are UTILITY functions → Use toolkit -getEmailDomain(email) // Extracts domain from email -normalizeEmail(email) // Lowercases and trims -ensureProtocol(url) // Adds https:// if missing -getHostname(url) // Extracts hostname +getEmailDomain(email); // Extracts domain from email +normalizeEmail(email); // Lowercases and trims +ensureProtocol(url); // Adds https:// if missing +getHostname(url); // Extracts hostname ``` **Location**: `packages/domain/toolkit/` @@ -72,8 +73,8 @@ getHostname(url) // Extracts hostname ```typescript import { formatCurrency, formatDate } from "@customer-portal/domain/toolkit/formatting"; -const price = formatCurrency(1000, "JPY"); // "¥1,000" -const date = formatDate(new Date()); // "2025-10-08" +const price = formatCurrency(1000, "JPY"); // "¥1,000" +const date = formatDate(new Date()); // "2025-10-08" ``` ### Type Guards @@ -92,8 +93,8 @@ if (isString(value)) { ```typescript import { ensureProtocol, getHostname } from "@customer-portal/domain/toolkit/validation/url"; -const fullUrl = ensureProtocol("example.com"); // "https://example.com" -const host = getHostname("https://example.com/path"); // "example.com" +const fullUrl = ensureProtocol("example.com"); // "https://example.com" +const host = getHostname("https://example.com/path"); // "example.com" ``` ### Email Utilities @@ -101,55 +102,58 @@ const host = getHostname("https://example.com/path"); // "example.com" ```typescript import { getEmailDomain, normalizeEmail } from "@customer-portal/domain/toolkit/validation/email"; -const domain = getEmailDomain("user@example.com"); // "example.com" -const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com" +const domain = getEmailDomain("user@example.com"); // "example.com" +const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com" ``` ## When to Use What -| Task | Use | Example | -|------|-----|---------| -| Validate email format | `common/validation.ts` | `isValidEmail(email)` | -| Extract email domain | `toolkit/validation/email.ts` | `getEmailDomain(email)` | -| Validate URL format | `common/validation.ts` | `isValidUrl(url)` | -| Add protocol to URL | `toolkit/validation/url.ts` | `ensureProtocol(url)` | -| Format currency | `toolkit/formatting/currency.ts` | `formatCurrency(amount, "JPY")` | -| Format date | `toolkit/formatting/date.ts` | `formatDate(date)` | +| Task | Use | Example | +| --------------------- | -------------------------------- | ------------------------------- | +| Validate email format | `common/validation.ts` | `isValidEmail(email)` | +| Extract email domain | `toolkit/validation/email.ts` | `getEmailDomain(email)` | +| Validate URL format | `common/validation.ts` | `isValidUrl(url)` | +| Add protocol to URL | `toolkit/validation/url.ts` | `ensureProtocol(url)` | +| Format currency | `toolkit/formatting/currency.ts` | `formatCurrency(amount, "JPY")` | +| Format date | `toolkit/formatting/date.ts` | `formatDate(date)` | ## Best Practices 1. **Use validation functions for validation** + ```typescript // ✅ Good import { isValidEmail } from "@customer-portal/domain/common/validation"; if (!isValidEmail(email)) throw new Error("Invalid email"); - + // ❌ Bad - don't write custom validation if (!email.includes("@")) throw new Error("Invalid email"); ``` 2. **Use utility functions for transformations** + ```typescript // ✅ Good import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email"; const clean = normalizeEmail(email); - + // ❌ Bad - don't duplicate utility logic const clean = email.trim().toLowerCase(); ``` 3. **Don't mix validation and utilities** + ```typescript // ❌ Bad - mixing concerns function processEmail(email: string) { - if (!email.includes("@")) return null; // Validation - return email.toLowerCase(); // Utility + if (!email.includes("@")) return null; // Validation + return email.toLowerCase(); // Utility } - + // ✅ Good - separate concerns import { isValidEmail } from "@customer-portal/domain/common/validation"; import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email"; - + function processEmail(email: string) { if (!isValidEmail(email)) return null; return normalizeEmail(email); @@ -181,4 +185,3 @@ If you're unsure whether something belongs in toolkit or common/validation: - **Ask**: "Does this transform or extract data?" - YES → It's a utility → Use `toolkit/` - NO → Might be validation → Use `common/validation.ts` - diff --git a/packages/domain/toolkit/formatting/date.ts b/packages/domain/toolkit/formatting/date.ts index b6f3e50b..e016afef 100644 --- a/packages/domain/toolkit/formatting/date.ts +++ b/packages/domain/toolkit/formatting/date.ts @@ -1,6 +1,6 @@ /** * Toolkit - Date Formatting - * + * * Utilities for formatting dates and times. */ @@ -17,10 +17,7 @@ export interface DateFormatOptions { /** * Format an ISO date string for display */ -export function formatDate( - isoString: string, - options: DateFormatOptions = {} -): string { +export function formatDate(isoString: string, options: DateFormatOptions = {}): string { const { locale = "en-US", dateStyle = "medium", @@ -31,7 +28,7 @@ export function formatDate( try { const date = new Date(isoString); - + if (isNaN(date.getTime())) { return isoString; // Return original if invalid } @@ -51,10 +48,7 @@ export function formatDate( /** * Format a date relative to now (e.g., "2 days ago", "in 3 hours") */ -export function formatRelativeDate( - isoString: string, - options: { locale?: string } = {} -): string { +export function formatRelativeDate(isoString: string, options: { locale?: string } = {}): string { const { locale = "en-US" } = options; try { @@ -90,4 +84,3 @@ export function isValidDate(dateString: string): boolean { const date = new Date(dateString); return !isNaN(date.getTime()); } - diff --git a/packages/domain/toolkit/formatting/index.ts b/packages/domain/toolkit/formatting/index.ts index 54584657..baff258e 100644 --- a/packages/domain/toolkit/formatting/index.ts +++ b/packages/domain/toolkit/formatting/index.ts @@ -1,6 +1,6 @@ /** * Toolkit - Formatting - * + * * Formatting utilities for currency, dates, phone numbers, etc. */ @@ -8,4 +8,3 @@ export * from "./currency.js"; export * from "./date.js"; export * from "./phone.js"; export * from "./text.js"; - diff --git a/packages/domain/toolkit/formatting/phone.ts b/packages/domain/toolkit/formatting/phone.ts index 9d9f8b1f..d5da9c66 100644 --- a/packages/domain/toolkit/formatting/phone.ts +++ b/packages/domain/toolkit/formatting/phone.ts @@ -1,6 +1,6 @@ /** * Toolkit - Phone Number Formatting - * + * * Utilities for formatting phone numbers. */ @@ -37,12 +37,12 @@ export function formatPhoneNumber(phone: string): string { */ export function normalizePhoneNumber(phone: string, defaultCountryCode = "1"): string { const digits = phone.replace(/\D/g, ""); - + // If already has country code, return with + if (digits.length >= 10 && !digits.startsWith(defaultCountryCode)) { return `+${digits}`; } - + // Add default country code return `+${defaultCountryCode}${digits}`; } @@ -56,7 +56,7 @@ export function formatPhoneForWhmcs(phone: string): string { // Remove all non-digit characters except leading + const hasPlus = phone.startsWith("+"); const digits = phone.replace(/\D/g, ""); - + if (digits.length === 0) { return phone; } @@ -65,7 +65,7 @@ export function formatPhoneForWhmcs(phone: string): string { if (digits.startsWith("81") && digits.length >= 11) { return `+81.${digits.slice(2)}`; } - + // For US/Canada numbers (10 digits or 11 starting with 1) if (digits.length === 10) { return `+1.${digits}`; @@ -88,4 +88,3 @@ export function formatPhoneForWhmcs(phone: string): string { // Return with + prefix if it had one, otherwise as-is return hasPlus ? `+${digits}` : digits; } - diff --git a/packages/domain/toolkit/formatting/text.ts b/packages/domain/toolkit/formatting/text.ts index d8b70638..5fe0c14b 100644 --- a/packages/domain/toolkit/formatting/text.ts +++ b/packages/domain/toolkit/formatting/text.ts @@ -1,6 +1,6 @@ /** * Toolkit - Text Formatting - * + * * Utilities for text manipulation and formatting. */ @@ -56,12 +56,11 @@ export function maskString(str: string, visibleStart = 3, visibleEnd = 3, maskCh if (str.length <= visibleStart + visibleEnd) { return str; } - + const start = str.slice(0, visibleStart); const end = str.slice(-visibleEnd); const maskedLength = str.length - visibleStart - visibleEnd; const masked = maskChar.repeat(maskedLength); - + return `${start}${masked}${end}`; } - diff --git a/packages/domain/toolkit/index.ts b/packages/domain/toolkit/index.ts index 3dde8d79..f9c804bc 100644 --- a/packages/domain/toolkit/index.ts +++ b/packages/domain/toolkit/index.ts @@ -1,6 +1,6 @@ /** * Domain Toolkit - * + * * Utility functions and helpers used across all domain packages. */ @@ -24,4 +24,3 @@ export { isSuccess, isError, } from "./typing/helpers.js"; - diff --git a/packages/domain/toolkit/typing/assertions.ts b/packages/domain/toolkit/typing/assertions.ts index 548df512..1adfc0bd 100644 --- a/packages/domain/toolkit/typing/assertions.ts +++ b/packages/domain/toolkit/typing/assertions.ts @@ -1,6 +1,6 @@ /** * Toolkit - Type Assertions - * + * * Runtime assertion utilities for type safety. */ @@ -62,4 +62,3 @@ export function assertNumber( export function assertNever(value: never, message = "Unexpected value"): never { throw new AssertionError(`${message}: ${JSON.stringify(value)}`); } - diff --git a/packages/domain/toolkit/typing/guards.ts b/packages/domain/toolkit/typing/guards.ts index 6c388c87..276d1ae7 100644 --- a/packages/domain/toolkit/typing/guards.ts +++ b/packages/domain/toolkit/typing/guards.ts @@ -1,6 +1,6 @@ /** * Toolkit - Type Guards - * + * * Type guard utilities for runtime type checking. */ @@ -59,4 +59,3 @@ export function isDefined(value: T | null | undefined): value is T { export function filterDefined(arr: (T | null | undefined)[]): T[] { return arr.filter(isDefined); } - diff --git a/packages/domain/toolkit/typing/index.ts b/packages/domain/toolkit/typing/index.ts index 4bb111cf..20d258d0 100644 --- a/packages/domain/toolkit/typing/index.ts +++ b/packages/domain/toolkit/typing/index.ts @@ -1,10 +1,9 @@ /** * Toolkit - Typing - * + * * TypeScript type utilities and runtime type checking. */ export * from "./guards.js"; export * from "./assertions.js"; export * from "./helpers.js"; - diff --git a/packages/domain/toolkit/validation/email.ts b/packages/domain/toolkit/validation/email.ts index c8e9dbc9..a8646c61 100644 --- a/packages/domain/toolkit/validation/email.ts +++ b/packages/domain/toolkit/validation/email.ts @@ -1,6 +1,6 @@ /** * Toolkit - Email Utilities - * + * * Email utility functions (extraction, normalization). * For email validation, use functions from common/validation.ts */ @@ -19,4 +19,3 @@ export function getEmailDomain(email: string): string | null { export function normalizeEmail(email: string): string { return email.trim().toLowerCase(); } - diff --git a/packages/domain/toolkit/validation/index.ts b/packages/domain/toolkit/validation/index.ts index 8b5d8332..378167d6 100644 --- a/packages/domain/toolkit/validation/index.ts +++ b/packages/domain/toolkit/validation/index.ts @@ -1,6 +1,6 @@ /** * Toolkit - Validation - * + * * Validation utilities for common data types. */ @@ -8,5 +8,3 @@ export * from "./email.js"; export * from "./url.js"; export * from "./string.js"; export * from "./helpers.js"; - - diff --git a/packages/domain/toolkit/validation/string.ts b/packages/domain/toolkit/validation/string.ts index 85551697..b922aa02 100644 --- a/packages/domain/toolkit/validation/string.ts +++ b/packages/domain/toolkit/validation/string.ts @@ -1,6 +1,6 @@ /** * Toolkit - String Validation - * + * * String validation utilities. */ @@ -45,4 +45,3 @@ export function isAlpha(str: string): boolean { export function isNumeric(str: string): boolean { return /^\d+$/.test(str); } - diff --git a/packages/domain/toolkit/validation/url.ts b/packages/domain/toolkit/validation/url.ts index ff36c54e..d8585cd5 100644 --- a/packages/domain/toolkit/validation/url.ts +++ b/packages/domain/toolkit/validation/url.ts @@ -1,6 +1,6 @@ /** * Toolkit - URL Utilities - * + * * URL parsing and manipulation utilities. * For URL validation, use functions from common/validation.ts */ @@ -26,4 +26,3 @@ export function getHostname(url: string): string | null { return null; } } - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b8dbf96..a7c7a047 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,6 +241,9 @@ importers: tailwindcss: specifier: ^4.1.17 version: 4.1.17 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@4.1.17) typescript: specifier: "catalog:" version: 5.9.3 @@ -6874,6 +6877,14 @@ packages: integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==, } + tailwindcss-animate@1.0.7: + resolution: + { + integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==, + } + peerDependencies: + tailwindcss: ">=3.0.0 || insiders" + tailwindcss@4.1.17: resolution: { @@ -11504,6 +11515,10 @@ snapshots: tailwind-merge@3.4.0: {} + tailwindcss-animate@1.0.7(tailwindcss@4.1.17): + dependencies: + tailwindcss: 4.1.17 + tailwindcss@4.1.17: {} tapable@2.3.0: {}