From ece89de49adbb6f49e11d26d3256b0c6605d926b Mon Sep 17 00:00:00 2001 From: barsa Date: Fri, 12 Dec 2025 14:35:19 +0900 Subject: [PATCH] Update TypeScript and ESLint configurations for improved type safety and compatibility - Modified ESLint configuration to support file patterns for TypeScript files. - Updated TypeScript configurations across multiple applications to use ES2024 and enable composite builds. - Refactored type inference in domain modules to utilize Zod's infer type for better type safety. - Enhanced utility functions to handle various data types more robustly, improving overall code quality. --- apps/bff/tsconfig.json | 2 + apps/portal/tsconfig.json | 4 +- eslint.config.mjs | 15 +++--- packages/domain/billing/constants.ts | 4 +- packages/domain/dashboard/contract.ts | 22 ++++---- packages/domain/orders/validation.ts | 54 ++++++++----------- packages/domain/providers/whmcs/utils.ts | 34 +++++++----- .../domain/sim/providers/freebit/mapper.ts | 15 +++--- packages/domain/sim/validation.ts | 45 ++++++++-------- .../subscriptions/providers/whmcs/mapper.ts | 27 ++++++---- packages/domain/toolkit/typing/helpers.ts | 42 ++++++--------- packages/domain/toolkit/validation/helpers.ts | 34 +++--------- tsconfig.base.json | 2 +- 13 files changed, 143 insertions(+), 157 deletions(-) diff --git a/apps/bff/tsconfig.json b/apps/bff/tsconfig.json index 7eff1e4e..67088c6b 100644 --- a/apps/bff/tsconfig.json +++ b/apps/bff/tsconfig.json @@ -5,6 +5,8 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "strictPropertyInitialization": false, + "composite": true, + "tsBuildInfoFile": ".typecheck/tsconfig.tsbuildinfo", "baseUrl": ".", "paths": { "@/*": ["src/*"], diff --git a/apps/portal/tsconfig.json b/apps/portal/tsconfig.json index 98726133..3a57a1ba 100644 --- a/apps/portal/tsconfig.json +++ b/apps/portal/tsconfig.json @@ -2,12 +2,14 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "target": "ES2022", - "lib": ["ESNext", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "jsx": "preserve", "noEmit": true, "plugins": [{ "name": "next" }], + "composite": true, + "tsBuildInfoFile": ".typecheck/tsconfig.tsbuildinfo", "baseUrl": ".", "paths": { "@/*": ["./src/*"] diff --git a/eslint.config.mjs b/eslint.config.mjs index 78433008..e307cfff 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -38,7 +38,7 @@ export default [ // TypeScript recommended rules (fast, no type info) ...tseslint.configs.recommended.map((config) => ({ ...config, - files: ["**/*.ts", "**/*.tsx"], + files: ["**/*.{ts,tsx}"], languageOptions: { ...(config.languageOptions || {}), // Keep config simple: allow both environments; app-specific blocks can tighten later @@ -64,12 +64,6 @@ export default [ // Backend & domain packages { files: ["apps/bff/**/*.ts", "packages/domain/**/*.ts"], - languageOptions: { - parserOptions: { - projectService: true, - tsconfigRootDir: process.cwd(), - }, - }, rules: { "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], @@ -178,7 +172,12 @@ export default [ // Node globals for config files { - files: ["*.config.mjs", "apps/portal/next.config.mjs"], + files: [ + "*.config.*", + "apps/portal/next.config.mjs", + "config/**/*.{js,cjs,mjs}", + "scripts/**/*.{js,cjs,mjs}", + ], languageOptions: { globals: { ...globals.node }, }, diff --git a/packages/domain/billing/constants.ts b/packages/domain/billing/constants.ts index 0bf8cb42..cba0b650 100644 --- a/packages/domain/billing/constants.ts +++ b/packages/domain/billing/constants.ts @@ -68,8 +68,8 @@ export const VALID_INVOICE_LIST_STATUSES = [ /** * Check if a status string is valid for invoices */ -export function isValidInvoiceStatus(status: string): boolean { - return VALID_INVOICE_STATUSES.includes(status as any); +export function isValidInvoiceStatus(status: string): status is ValidInvoiceStatus { + return (VALID_INVOICE_STATUSES as readonly string[]).includes(status); } /** diff --git a/packages/domain/dashboard/contract.ts b/packages/domain/dashboard/contract.ts index ef5d0ea8..99e247d8 100644 --- a/packages/domain/dashboard/contract.ts +++ b/packages/domain/dashboard/contract.ts @@ -1,5 +1,5 @@ -import { z } from "zod"; -import { +import type { infer as ZodInfer } from "zod"; +import type { activityTypeSchema, activitySchema, dashboardStatsSchema, @@ -11,12 +11,12 @@ import { dashboardSummaryResponseSchema, } from "./schema.js"; -export type ActivityType = z.infer; -export type Activity = z.infer; -export type DashboardStats = z.infer; -export type NextInvoice = z.infer; -export type DashboardSummary = z.infer; -export type DashboardError = z.infer; -export type ActivityFilter = z.infer; -export type ActivityFilterConfig = z.infer; -export type DashboardSummaryResponse = z.infer; +export type ActivityType = ZodInfer; +export type Activity = ZodInfer; +export type DashboardStats = ZodInfer; +export type NextInvoice = ZodInfer; +export type DashboardSummary = ZodInfer; +export type DashboardError = ZodInfer; +export type ActivityFilter = ZodInfer; +export type ActivityFilterConfig = ZodInfer; +export type DashboardSummaryResponse = ZodInfer; diff --git a/packages/domain/orders/validation.ts b/packages/domain/orders/validation.ts index 52816849..562bca31 100644 --- a/packages/domain/orders/validation.ts +++ b/packages/domain/orders/validation.ts @@ -1,11 +1,11 @@ /** * Orders Domain - Validation - * + * * Extended business validation rules for orders. * These rules represent domain logic and should be reusable across frontend/backend. */ -import { z } from "zod"; +import type { infer as ZodInfer } from "zod"; import { orderBusinessValidationSchema } from "./schema.js"; // ============================================================================ @@ -18,7 +18,7 @@ import { orderBusinessValidationSchema } from "./schema.js"; */ export function hasSimServicePlan(skus: string[]): boolean { return skus.some( - (sku) => + sku => sku.toUpperCase().includes("SIM") && !sku.toUpperCase().includes("ACTIVATION") && !sku.toUpperCase().includes("ADDON") @@ -38,9 +38,7 @@ export function hasSimActivationFee(_skus: string[]): boolean { */ export function hasVpnActivationFee(skus: string[]): boolean { return skus.some( - (sku) => - sku.toUpperCase().includes("VPN") && - sku.toUpperCase().includes("ACTIVATION") + sku => sku.toUpperCase().includes("VPN") && sku.toUpperCase().includes("ACTIVATION") ); } @@ -50,7 +48,7 @@ export function hasVpnActivationFee(skus: string[]): boolean { */ export function hasInternetServicePlan(skus: string[]): boolean { return skus.some( - (sku) => + sku => sku.toUpperCase().includes("INTERNET") && !sku.toUpperCase().includes("INSTALL") && !sku.toUpperCase().includes("ADDON") @@ -62,7 +60,7 @@ export function hasInternetServicePlan(skus: string[]): boolean { * (filters out installation, addons, activation fees) */ export function getMainServiceSkus(skus: string[]): string[] { - return skus.filter((sku) => { + return skus.filter(sku => { const upperSku = sku.toUpperCase(); return ( !upperSku.includes("INSTALL") && @@ -79,10 +77,10 @@ export function getMainServiceSkus(skus: string[]): string[] { /** * Complete order validation including all SKU business rules - * + * * This schema delegates to the helper functions above for DRY validation. * Helper functions are reusable in both schema refinements and imperative validation. - * + * * Validates: * - Basic order structure (from orderBusinessValidationSchema) * - SIM orders have service plan + activation fee @@ -90,29 +88,20 @@ export function getMainServiceSkus(skus: string[]): string[] { * - Internet orders have service plan */ export const orderWithSkuValidationSchema = orderBusinessValidationSchema - .refine( - (data) => data.orderType !== "SIM" || hasSimServicePlan(data.skus), - { - message: "SIM orders must include a SIM service plan", - path: ["skus"], - } - ) - .refine( - (data) => data.orderType !== "VPN" || hasVpnActivationFee(data.skus), - { - message: "VPN orders require an activation fee", - path: ["skus"], - } - ) - .refine( - (data) => data.orderType !== "Internet" || hasInternetServicePlan(data.skus), - { - message: "Internet orders require a service plan", - path: ["skus"], - } - ); + .refine(data => data.orderType !== "SIM" || hasSimServicePlan(data.skus), { + message: "SIM orders must include a SIM service plan", + path: ["skus"], + }) + .refine(data => data.orderType !== "VPN" || hasVpnActivationFee(data.skus), { + message: "VPN orders require an activation fee", + path: ["skus"], + }) + .refine(data => data.orderType !== "Internet" || hasInternetServicePlan(data.skus), { + message: "Internet orders require a service plan", + path: ["skus"], + }); -export type OrderWithSkuValidation = z.infer; +export type OrderWithSkuValidation = ZodInfer; // ============================================================================ // Validation Error Messages @@ -143,4 +132,3 @@ export function getOrderTypeValidationError(orderType: string, skus: string[]): return null; } - diff --git a/packages/domain/providers/whmcs/utils.ts b/packages/domain/providers/whmcs/utils.ts index 2c4c8999..80563255 100644 --- a/packages/domain/providers/whmcs/utils.ts +++ b/packages/domain/providers/whmcs/utils.ts @@ -1,7 +1,7 @@ /** * Shared WHMCS Provider Utilities * Single source of truth for WHMCS data parsing - * + * * Raw API types are source of truth - no fallbacks or variations expected. */ @@ -12,7 +12,7 @@ export function parseAmount(amount: string | number | undefined): number { if (typeof amount === "number") return amount; if (!amount) return 0; - + const cleaned = String(amount).replace(/[^\d.-]/g, ""); const parsed = Number.parseFloat(cleaned); return Number.isNaN(parsed) ? 0 : parsed; @@ -24,10 +24,10 @@ export function parseAmount(amount: string | number | undefined): number { */ export function formatDate(input?: string | null): string | undefined { if (!input) return undefined; - + const date = new Date(input); if (Number.isNaN(date.getTime())) return undefined; - + return date.toISOString(); } @@ -55,7 +55,10 @@ export function normalizeCycle( defaultCycle: T ): T { if (!cycle) return defaultCycle; - const normalized = cycle.trim().toLowerCase().replace(/[_\s-]+/g, " "); + const normalized = cycle + .trim() + .toLowerCase() + .replace(/[_\s-]+/g, " "); return cycleMap[normalized] ?? defaultCycle; } @@ -92,17 +95,22 @@ export function getCustomFieldsMap(customFields: unknown): Record = {}; for (const entry of normalizeCustomFieldEntries(customFields)) { + const idRaw = "id" in entry ? entry.id : undefined; const id = - "id" in entry && entry.id !== undefined && entry.id !== null - ? String(entry.id).trim() - : undefined; - const name = - "name" in entry && typeof entry.name === "string" - ? entry.name.trim() - : undefined; + typeof idRaw === "string" + ? idRaw.trim() + : typeof idRaw === "number" + ? String(idRaw) + : undefined; + const name = "name" in entry && typeof entry.name === "string" ? entry.name.trim() : undefined; const rawValue = "value" in entry ? entry.value : undefined; if (rawValue === undefined || rawValue === null) continue; - const value = typeof rawValue === "string" ? rawValue : String(rawValue); + const value = + typeof rawValue === "string" + ? rawValue + : typeof rawValue === "number" || typeof rawValue === "boolean" + ? String(rawValue) + : undefined; if (!value) continue; if (id) map[id] = value; diff --git a/packages/domain/sim/providers/freebit/mapper.ts b/packages/domain/sim/providers/freebit/mapper.ts index fecf4383..8da233c3 100644 --- a/packages/domain/sim/providers/freebit/mapper.ts +++ b/packages/domain/sim/providers/freebit/mapper.ts @@ -64,12 +64,12 @@ function mapSimStatus(status: string | undefined): SimStatus { function deriveSimType(sizeValue: unknown, eid?: string | number | null): SimType { const simSizeStr = typeof sizeValue === "number" ? String(sizeValue) : sizeValue; const raw = typeof simSizeStr === "string" ? simSizeStr.toLowerCase() : undefined; - + const eidStr = typeof eid === "number" ? String(eid) : eid; if (eidStr && eidStr.length > 0) { return "esim"; } - + switch (raw) { case "nano": return "nano"; @@ -90,11 +90,12 @@ export function transformFreebitAccountDetails(raw: unknown): SimDetails { } const sanitizedAccount = asString(account.account); - const simSizeValue = account.simSize ?? (account as any).size; + const legacySize = "size" in account ? (account as { size?: unknown }).size : undefined; + const simSizeValue = account.simSize ?? legacySize; const eidValue = account.eid; const simType = deriveSimType( - typeof simSizeValue === 'number' ? String(simSizeValue) : simSizeValue, - typeof eidValue === 'number' ? String(eidValue) : eidValue + typeof simSizeValue === "number" ? String(simSizeValue) : simSizeValue, + typeof eidValue === "number" ? String(eidValue) : eidValue ); const voiceMailEnabled = parseBooleanFlag(account.voicemail ?? account.voiceMail); const callWaitingEnabled = parseBooleanFlag(account.callwaiting ?? account.callWaiting); @@ -202,7 +203,9 @@ export function transformFreebitEsimAddAccountResponse(raw: unknown) { return freebitEsimAddAccountRawSchema.parse(raw); } -export type FreebitEsimAddAccountResponse = ReturnType; +export type FreebitEsimAddAccountResponse = ReturnType< + typeof transformFreebitEsimAddAccountResponse +>; export function transformFreebitEsimActivationResponse(raw: unknown) { return freebitEsimAddAccountRawSchema.parse(raw); diff --git a/packages/domain/sim/validation.ts b/packages/domain/sim/validation.ts index 173b851c..a762a1c4 100644 --- a/packages/domain/sim/validation.ts +++ b/packages/domain/sim/validation.ts @@ -1,6 +1,6 @@ /** * SIM Domain - Validation - * + * * Business validation functions for SIM subscriptions. * These functions contain no infrastructure dependencies. */ @@ -9,33 +9,31 @@ import type { Subscription } from "../subscriptions/schema.js"; /** * Check if a subscription is a SIM service - * + * * Business rule: A subscription is considered a SIM service if its product name * or group name contains "SIM" (case-insensitive). - * + * * @param subscription - The subscription to check * @returns true if this is a SIM subscription */ export function isSimSubscription(subscription: Subscription): boolean { const productName = subscription.productName?.toLowerCase() || ""; const groupName = subscription.groupName?.toLowerCase() || ""; - + return productName.includes("sim") || groupName.includes("sim"); } /** * Extract SIM account identifier from subscription data - * + * * The SIM account identifier (typically a phone number/MSISDN) can be stored * in various places depending on the subscription source and configuration. * This function tries multiple locations in priority order. - * + * * @param subscription - The subscription to extract the account from * @returns The SIM account identifier (phone number), or null if not found */ -export function extractSimAccountFromSubscription( - subscription: Subscription -): string | null { +export function extractSimAccountFromSubscription(subscription: Subscription): string | null { // 1. Try domain field first (most common location) if (subscription.domain && subscription.domain.trim()) { return subscription.domain.trim(); @@ -60,16 +58,14 @@ export function extractSimAccountFromSubscription( /** * Extract account from custom fields using known field names - * + * * WHMCS and other billing systems may store the SIM phone number in various * custom field names. This function checks all known variations. - * + * * @param customFields - The custom fields record to search * @returns The SIM account if found, null otherwise */ -function extractFromCustomFields( - customFields: Record -): string | null { +function extractFromCustomFields(customFields: Record): string | null { // Known field names for SIM phone numbers across different WHMCS configurations const phoneFields = [ // Standard field names @@ -79,7 +75,7 @@ function extractFromCustomFields( "phone_number", "mobile", "sim_phone", - + // Title case variations "Phone Number", "MSISDN", @@ -87,7 +83,7 @@ function extractFromCustomFields( "Mobile", "SIM Phone", "PhoneNumber", - + // Additional variations "mobile_number", "sim_number", @@ -97,7 +93,7 @@ function extractFromCustomFields( "Phone Number (SIM)", "Mobile Number", "SIM Number", - + // Underscore variations "SIM_Number", "SIM_Phone_Number", @@ -109,7 +105,15 @@ function extractFromCustomFields( for (const fieldName of phoneFields) { const value = customFields[fieldName]; if (value !== undefined && value !== null && value !== "") { - return String(value); + if (typeof value === "string") { + const trimmed = value.trim(); + if (trimmed) return trimmed; + continue; + } + + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } } } @@ -118,14 +122,13 @@ function extractFromCustomFields( /** * Clean SIM account format - * + * * Removes common formatting characters (hyphens, spaces, parentheses) * from phone numbers to get a clean numeric string. - * + * * @param account - The account string to clean * @returns Cleaned account string with only digits */ export function cleanSimAccount(account: string): string { return account.replace(/[-\s()]/g, ""); } - diff --git a/packages/domain/subscriptions/providers/whmcs/mapper.ts b/packages/domain/subscriptions/providers/whmcs/mapper.ts index 825ef2f4..e1e5f34a 100644 --- a/packages/domain/subscriptions/providers/whmcs/mapper.ts +++ b/packages/domain/subscriptions/providers/whmcs/mapper.ts @@ -1,11 +1,20 @@ /** * WHMCS Subscriptions Provider - Mapper - * + * * Transforms raw WHMCS product/service data into normalized subscription types. */ -import type { Subscription, SubscriptionStatus, SubscriptionCycle, SubscriptionList } from "../../contract.js"; -import { subscriptionSchema, subscriptionListSchema, subscriptionStatusSchema } from "../../schema.js"; +import type { + Subscription, + SubscriptionStatus, + SubscriptionCycle, + SubscriptionList, +} from "../../contract.js"; +import { + subscriptionSchema, + subscriptionListSchema, + subscriptionStatusSchema, +} from "../../schema.js"; import { type WhmcsProductRaw, whmcsProductRawSchema, @@ -25,7 +34,7 @@ export interface TransformSubscriptionOptions { } export interface TransformSubscriptionListResponseOptions extends TransformSubscriptionOptions { - status?: SubscriptionStatus | string; + status?: string; onItemError?: (error: unknown, product: WhmcsProductRaw) => void; } @@ -108,10 +117,10 @@ export function transformWhmcsSubscription( // Determine amount const amount = parseAmount( product.amount || - product.recurringamount || - product.pricing?.amount || - product.firstpaymentamount || - 0 + product.recurringamount || + product.pricing?.amount || + product.firstpaymentamount || + 0 ); // Transform to domain model @@ -203,7 +212,7 @@ export function transformWhmcsSubscriptionListResponse( export function filterSubscriptionsByStatus( list: SubscriptionList, - status: SubscriptionStatus | string + status: string ): SubscriptionList { const normalizedStatus = subscriptionStatusSchema.parse(status); const filtered = list.subscriptions.filter(sub => sub.status === normalizedStatus); diff --git a/packages/domain/toolkit/typing/helpers.ts b/packages/domain/toolkit/typing/helpers.ts index d1146ff7..efe30cc5 100644 --- a/packages/domain/toolkit/typing/helpers.ts +++ b/packages/domain/toolkit/typing/helpers.ts @@ -1,6 +1,6 @@ /** * Toolkit - Type Helpers - * + * * TypeScript utility types and helper functions. */ @@ -17,9 +17,7 @@ export type RequiredBy = Omit & Required> /** * Deep partial (makes all nested properties optional) */ -export type DeepPartial = T extends object - ? { [P in keyof T]?: DeepPartial } - : T; +export type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T; /** * Extract keys of a certain type @@ -51,22 +49,25 @@ export type PickByValue = Pick< /** * Safely access nested property */ -export function getNestedProperty( - obj: unknown, - path: string, - defaultValue?: T -): T | undefined { +export function getNestedProperty(obj: unknown, path: string, defaultValue?: T): T | undefined { const keys = path.split("."); - let current: any = obj; + let current: unknown = obj; + + const isIndexableObject = (value: unknown): value is Record => + typeof value === "object" && value !== null; for (const key of keys) { - if (current === null || current === undefined || typeof current !== "object") { + if (!isIndexableObject(current)) { return defaultValue; } current = current[key]; } - return current ?? defaultValue; + if (current === undefined || current === null) { + return defaultValue; + } + + return current as T; } // ============================================================================ @@ -113,36 +114,27 @@ export function createErrorState(error: E): AsyncState { /** * Type guard: check if state is idle */ -export function isIdle( - state: AsyncState -): state is { status: "idle" } { +export function isIdle(state: AsyncState): state is { status: "idle" } { return state.status === "idle"; } /** * Type guard: check if state is loading */ -export function isLoading( - state: AsyncState -): state is { status: "loading" } { +export function isLoading(state: AsyncState): state is { status: "loading" } { return state.status === "loading"; } /** * Type guard: check if state is success */ -export function isSuccess( - state: AsyncState -): state is { status: "success"; data: T } { +export function isSuccess(state: AsyncState): state is { status: "success"; data: T } { return state.status === "success"; } /** * Type guard: check if state is error */ -export function isError( - state: AsyncState -): state is { status: "error"; error: E } { +export function isError(state: AsyncState): state is { status: "error"; error: E } { return state.status === "error"; } - diff --git a/packages/domain/toolkit/validation/helpers.ts b/packages/domain/toolkit/validation/helpers.ts index 2741132b..34bede74 100644 --- a/packages/domain/toolkit/validation/helpers.ts +++ b/packages/domain/toolkit/validation/helpers.ts @@ -1,6 +1,6 @@ /** * Domain Toolkit - Validation Helpers - * + * * Common validation utilities that can be reused across all domains. */ @@ -41,12 +41,7 @@ export function sanitizePagination(options: { page: number; limit: number; } { - const { - page = 1, - limit = options.defaultLimit ?? 10, - minLimit = 1, - maxLimit = 100, - } = options; + const { page = 1, limit = options.defaultLimit ?? 10, minLimit = 1, maxLimit = 100 } = options; return { page: Math.max(1, Math.floor(page)), @@ -79,7 +74,7 @@ export function isValidEnumValue>( value: unknown, enumObj: T ): value is T[keyof T] { - return Object.values(enumObj).includes(value as any); + return Object.values(enumObj).some(enumValue => enumValue === value); } // ============================================================================ @@ -107,11 +102,7 @@ export function hasUniqueItems(items: T[]): boolean { /** * Check if number is within range (inclusive) */ -export function isInRange( - value: number, - min: number, - max: number -): boolean { +export function isInRange(value: number, min: number, max: number): boolean { return value >= min && value <= max; } @@ -172,13 +163,7 @@ export function createPaginationSchema(options?: { return z.object({ page: z.coerce.number().int().positive().optional().default(1), - limit: z.coerce - .number() - .int() - .min(minLimit) - .max(maxLimit) - .optional() - .default(defaultLimit), + limit: z.coerce.number().int().min(minLimit).max(maxLimit).optional().default(defaultLimit), offset: z.coerce.number().int().nonnegative().optional(), }); } @@ -206,11 +191,6 @@ export function isRecord(value: unknown): value is Record { * Type guard for checking if error is a Zod error */ export function isZodError(error: unknown): error is z.ZodError { - return ( - typeof error === "object" && - error !== null && - "issues" in error && - Array.isArray((error as any).issues) - ); + if (!isRecord(error)) return false; + return Array.isArray(error.issues); } - diff --git a/tsconfig.base.json b/tsconfig.base.json index 31394a02..9c8b7044 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2024", - "lib": ["ESNext"], + "lib": ["ES2024"], "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true,