From cde7cb3e3c3a5776582171fdb194ac041d01fcbb Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Sat, 30 Aug 2025 16:45:22 +0900 Subject: [PATCH] Update package configurations and enhance BFF module structure - Modified build command in package.json for improved reporting. - Updated pnpm-lock.yaml to remove obsolete dependencies and add new ones. - Enhanced TypeScript configuration in BFF for better compatibility with ES2024. - Refactored app.module.ts for clearer module organization and added comments for better understanding. - Adjusted next.config.mjs to conditionally enable standalone output based on environment. - Improved InternetPlansPage to fetch and display installation options alongside internet plans. - Cleaned up unnecessary comments and code in VPN plans page for better readability. - Updated manage.sh script to build shared packages and BFF before starting development applications. --- apps/bff/src/app.module.ts | 141 ++++++++---------- apps/bff/src/common/config/app.config.ts | 47 ++++++ apps/bff/src/common/config/router.config.ts | 32 ++++ .../bff/src/common/config/throttler.config.ts | 22 +++ apps/bff/tsconfig.build.json | 6 +- apps/bff/tsconfig.json | 1 + apps/portal/next.config.mjs | 4 +- apps/portal/package.json | 1 + apps/portal/src/app/catalog/internet/page.tsx | 20 ++- apps/portal/src/app/catalog/vpn/page.tsx | 14 +- package.json | 3 +- pnpm-lock.yaml | 32 +--- scripts/dev/manage.sh | 11 +- 13 files changed, 202 insertions(+), 132 deletions(-) create mode 100644 apps/bff/src/common/config/app.config.ts create mode 100644 apps/bff/src/common/config/router.config.ts create mode 100644 apps/bff/src/common/config/throttler.config.ts diff --git a/apps/bff/src/app.module.ts b/apps/bff/src/app.module.ts index 2dc70243..4b60a43f 100644 --- a/apps/bff/src/app.module.ts +++ b/apps/bff/src/app.module.ts @@ -1,85 +1,76 @@ -import { Module } from "@nestjs/common"; -import { RouterModule } from "@nestjs/core"; -import { ConfigModule, ConfigService } from "@nestjs/config"; -import { ThrottlerModule } from "@nestjs/throttler"; -import { AuthModule } from "./auth/auth.module"; -import { UsersModule } from "./users/users.module"; -import { MappingsModule } from "./mappings/mappings.module"; -import { CatalogModule } from "./catalog/catalog.module"; -import { OrdersModule } from "./orders/orders.module"; -import { InvoicesModule } from "./invoices/invoices.module"; -import { SubscriptionsModule } from "./subscriptions/subscriptions.module"; -import { CasesModule } from "./cases/cases.module"; -import { WebhooksModule } from "./webhooks/webhooks.module"; -import { VendorsModule } from "./vendors/vendors.module"; -import { JobsModule } from "./jobs/jobs.module"; -import { HealthModule } from "./health/health.module"; -import { EmailModule } from "./common/email/email.module"; -import { PrismaModule } from "./common/prisma/prisma.module"; -import { AuditModule } from "./common/audit/audit.module"; -import { RedisModule } from "./common/redis/redis.module"; -import { LoggingModule } from "./common/logging/logging.module"; -import { CacheModule } from "./common/cache/cache.module"; -import * as path from "path"; -import { validateEnv } from "./common/config/env.validation"; +import { Module } from '@nestjs/common'; +import { RouterModule } from '@nestjs/core'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ThrottlerModule } from '@nestjs/throttler'; -// Support multiple .env files across environments and run contexts -const repoRoot = path.resolve(__dirname, "../../../.."); -const nodeEnv = process.env.NODE_ENV || "development"; -const envFilePath = [ - // Prefer repo root env files - path.resolve(repoRoot, `.env.${nodeEnv}.local`), - path.resolve(repoRoot, `.env.${nodeEnv}`), - path.resolve(repoRoot, ".env.local"), - path.resolve(repoRoot, ".env"), - // Fallback to local working directory - `.env.${nodeEnv}.local`, - `.env.${nodeEnv}`, - ".env.local", - ".env", -]; +// Configuration +import { appConfig } from './common/config/app.config'; +import { createThrottlerConfig } from './common/config/throttler.config'; +import { apiRoutes } from './common/config/router.config'; +// Core Infrastructure Modules +import { LoggingModule } from './common/logging/logging.module'; +import { PrismaModule } from './common/prisma/prisma.module'; +import { RedisModule } from './common/redis/redis.module'; +import { CacheModule } from './common/cache/cache.module'; +import { AuditModule } from './common/audit/audit.module'; +import { EmailModule } from './common/email/email.module'; + +// External Integration Modules +import { VendorsModule } from './vendors/vendors.module'; +import { JobsModule } from './jobs/jobs.module'; + +// Feature Modules +import { AuthModule } from './auth/auth.module'; +import { UsersModule } from './users/users.module'; +import { MappingsModule } from './mappings/mappings.module'; +import { CatalogModule } from './catalog/catalog.module'; +import { OrdersModule } from './orders/orders.module'; +import { InvoicesModule } from './invoices/invoices.module'; +import { SubscriptionsModule } from './subscriptions/subscriptions.module'; +import { CasesModule } from './cases/cases.module'; +import { WebhooksModule } from './webhooks/webhooks.module'; + +// System Modules +import { HealthModule } from './health/health.module'; + +/** + * Main application module + * + * Architecture: + * - Core infrastructure modules provide foundational services + * - External integration modules handle third-party services + * - Feature modules implement business logic + * - System modules provide monitoring and health checks + * + * All feature modules are grouped under "/api" prefix via RouterModule + * Health endpoints remain at root level for monitoring tools + */ @Module({ imports: [ - // Configuration - ConfigModule.forRoot({ - isGlobal: true, - envFilePath, - validate: validateEnv, - }), + // === CONFIGURATION === + ConfigModule.forRoot(appConfig), - // Logging + // === INFRASTRUCTURE === LoggingModule, - - // Rate limiting with environment-based configuration ThrottlerModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], - useFactory: (configService: ConfigService) => [ - { - ttl: configService.get("RATE_LIMIT_TTL", 60000), - limit: configService.get("RATE_LIMIT_LIMIT", 100), - }, - // Stricter rate limiting for auth endpoints - { - ttl: configService.get("AUTH_RATE_LIMIT_TTL", 900000), - limit: configService.get("AUTH_RATE_LIMIT_LIMIT", 3), - name: "auth", - }, - ], + useFactory: createThrottlerConfig, }), - // Core modules + // === CORE SERVICES === PrismaModule, RedisModule, CacheModule, AuditModule, - VendorsModule, - JobsModule, - HealthModule, EmailModule, - // Feature modules + // === EXTERNAL INTEGRATIONS === + VendorsModule, + JobsModule, + + // === FEATURE MODULES === AuthModule, UsersModule, MappingsModule, @@ -89,18 +80,12 @@ const envFilePath = [ SubscriptionsModule, CasesModule, WebhooksModule, - // Route grouping: apply "/api" prefix to all feature modules except health - RouterModule.register([ - { path: "api", module: AuthModule }, - { path: "api", module: UsersModule }, - { path: "api", module: MappingsModule }, - { path: "api", module: CatalogModule }, - { path: "api", module: OrdersModule }, - { path: "api", module: InvoicesModule }, - { path: "api", module: SubscriptionsModule }, - { path: "api", module: CasesModule }, - { path: "api", module: WebhooksModule }, - ]), + + // === SYSTEM MODULES === + HealthModule, + + // === ROUTING === + RouterModule.register(apiRoutes), ], }) -export class AppModule {} +export class AppModule {} \ No newline at end of file diff --git a/apps/bff/src/common/config/app.config.ts b/apps/bff/src/common/config/app.config.ts new file mode 100644 index 00000000..e2d2fb73 --- /dev/null +++ b/apps/bff/src/common/config/app.config.ts @@ -0,0 +1,47 @@ +import { ConfigModuleOptions } from '@nestjs/config'; +import { validateEnv } from './env.validation'; +import * as path from 'path'; + +/** + * Resolves the project root directory + * Works both in development (src/) and production (dist/) environments + */ +function getProjectRoot(): string { + // In development: process.cwd() should be the project root + // In production: we need to navigate up from the compiled location + const cwd = process.cwd(); + + // If we're running from apps/bff directory, go up to project root + if (cwd.endsWith('/apps/bff')) { + return path.resolve(cwd, '../..'); + } + + // If we're running from project root, use it directly + return cwd; +} + +const projectRoot = getProjectRoot(); +const nodeEnv = process.env.NODE_ENV || 'development'; + +/** + * Application configuration for NestJS ConfigModule + * Handles environment file loading with proper fallbacks + */ +export const appConfig: ConfigModuleOptions = { + isGlobal: true, + envFilePath: [ + // Project root environment files (highest priority) + path.resolve(projectRoot, `.env.${nodeEnv}.local`), + path.resolve(projectRoot, `.env.${nodeEnv}`), + path.resolve(projectRoot, '.env.local'), + path.resolve(projectRoot, '.env'), + // Fallback to relative paths + `.env.${nodeEnv}.local`, + `.env.${nodeEnv}`, + '.env.local', + '.env', + ], + validate: validateEnv, + // Enable expansion of variables (e.g., ${VAR_NAME}) + expandVariables: true, +}; diff --git a/apps/bff/src/common/config/router.config.ts b/apps/bff/src/common/config/router.config.ts new file mode 100644 index 00000000..573543ba --- /dev/null +++ b/apps/bff/src/common/config/router.config.ts @@ -0,0 +1,32 @@ +import { Routes } from '@nestjs/core'; +import { AuthModule } from '../../auth/auth.module'; +import { UsersModule } from '../../users/users.module'; +import { MappingsModule } from '../../mappings/mappings.module'; +import { CatalogModule } from '../../catalog/catalog.module'; +import { OrdersModule } from '../../orders/orders.module'; +import { InvoicesModule } from '../../invoices/invoices.module'; +import { SubscriptionsModule } from '../../subscriptions/subscriptions.module'; +import { CasesModule } from '../../cases/cases.module'; +import { WebhooksModule } from '../../webhooks/webhooks.module'; + +/** + * API routing configuration + * Groups feature modules under the "/api" prefix + * Health endpoints remain at root level for monitoring tools + */ +export const apiRoutes: Routes = [ + { + path: 'api', + children: [ + { path: '', module: AuthModule }, + { path: '', module: UsersModule }, + { path: '', module: MappingsModule }, + { path: '', module: CatalogModule }, + { path: '', module: OrdersModule }, + { path: '', module: InvoicesModule }, + { path: '', module: SubscriptionsModule }, + { path: '', module: CasesModule }, + { path: '', module: WebhooksModule }, + ], + }, +]; diff --git a/apps/bff/src/common/config/throttler.config.ts b/apps/bff/src/common/config/throttler.config.ts new file mode 100644 index 00000000..5ff87d38 --- /dev/null +++ b/apps/bff/src/common/config/throttler.config.ts @@ -0,0 +1,22 @@ +import { ConfigService } from '@nestjs/config'; +import { ThrottlerModuleOptions } from '@nestjs/throttler'; + +/** + * Throttler configuration factory + * Provides rate limiting configuration based on environment variables + */ +export const createThrottlerConfig = ( + configService: ConfigService, +): ThrottlerModuleOptions => [ + // General rate limiting + { + ttl: configService.get('RATE_LIMIT_TTL', 60000), // 1 minute + limit: configService.get('RATE_LIMIT_LIMIT', 100), // 100 requests + }, + // Stricter rate limiting for authentication endpoints + { + name: 'auth', + ttl: configService.get('AUTH_RATE_LIMIT_TTL', 900000), // 15 minutes + limit: configService.get('AUTH_RATE_LIMIT_LIMIT', 3), // 3 attempts + }, +]; diff --git a/apps/bff/tsconfig.build.json b/apps/bff/tsconfig.build.json index 8166da9d..08dca41f 100644 --- a/apps/bff/tsconfig.build.json +++ b/apps/bff/tsconfig.build.json @@ -2,11 +2,15 @@ "extends": "./tsconfig.json", "compilerOptions": { "noEmit": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, "incremental": true, "tsBuildInfoFile": "./tsconfig.build.tsbuildinfo", "outDir": "./dist", "sourceMap": true, - "declaration": false + "declaration": false, + "module": "CommonJS", + "target": "ES2024" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "test", "**/*.spec.ts"] diff --git a/apps/bff/tsconfig.json b/apps/bff/tsconfig.json index 5e9864a5..a48a1223 100644 --- a/apps/bff/tsconfig.json +++ b/apps/bff/tsconfig.json @@ -11,6 +11,7 @@ "outDir": "./dist", "baseUrl": "./", "removeComments": true, + "target": "ES2024", // Path mappings "paths": { diff --git a/apps/portal/next.config.mjs b/apps/portal/next.config.mjs index ab54ff9c..90c76ec5 100644 --- a/apps/portal/next.config.mjs +++ b/apps/portal/next.config.mjs @@ -1,8 +1,8 @@ /* eslint-env node */ /** @type {import('next').NextConfig} */ const nextConfig = { - // Enable standalone output for production deployment - output: "standalone", + // Enable standalone output only for production deployment + output: process.env.NODE_ENV === "production" ? "standalone" : undefined, // Turbopack configuration (Next.js 15.5+) turbopack: { diff --git a/apps/portal/package.json b/apps/portal/package.json index a6d4b07e..d6e14695 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -27,6 +27,7 @@ "react-dom": "19.1.1", "react-hook-form": "^7.62.0", "tailwind-merge": "^3.3.1", + "tw-animate-css": "^1.3.7", "world-countries": "^5.1.0", "zod": "^4.0.17", "zustand": "^5.0.8" diff --git a/apps/portal/src/app/catalog/internet/page.tsx b/apps/portal/src/app/catalog/internet/page.tsx index cbaca8d0..85ee5f1d 100644 --- a/apps/portal/src/app/catalog/internet/page.tsx +++ b/apps/portal/src/app/catalog/internet/page.tsx @@ -13,13 +13,14 @@ import { } from "@heroicons/react/24/outline"; import { authenticatedApi } from "@/lib/api"; -import { InternetPlan } from "@/shared/types/catalog.types"; +import { InternetPlan, InternetInstallation } from "@/shared/types/catalog.types"; import { LoadingSpinner } from "@/components/catalog/loading-spinner"; import { AnimatedCard } from "@/components/catalog/animated-card"; import { AnimatedButton } from "@/components/catalog/animated-button"; export default function InternetPlansPage() { const [plans, setPlans] = useState([]); + const [installations, setInstallations] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [eligibility, setEligibility] = useState(""); @@ -31,11 +32,15 @@ export default function InternetPlansPage() { setLoading(true); setError(null); - const plans = await authenticatedApi.get("/catalog/internet/plans"); + const [plans, installations] = await Promise.all([ + authenticatedApi.get("/catalog/internet/plans"), + authenticatedApi.get("/catalog/internet/installations") + ]); if (mounted) { // Plans are now ordered by Salesforce Display_Order__c field setPlans(plans); + setInstallations(installations); if (plans.length > 0) { setEligibility(plans[0].offeringType || "Home 1G"); } @@ -144,7 +149,7 @@ export default function InternetPlansPage() { <>
{plans.map(plan => ( - + ))}
@@ -189,7 +194,7 @@ export default function InternetPlansPage() { ); } -function InternetPlanCard({ plan }: { plan: InternetPlan }) { +function InternetPlanCard({ plan, installations }: { plan: InternetPlan; installations: InternetInstallation[] }) { const isGold = plan.tier === "Gold"; const isPlatinum = plan.tier === "Platinum"; const isSilver = plan.tier === "Silver"; @@ -274,7 +279,12 @@ function InternetPlanCard({ plan }: { plan: InternetPlan }) {
  • - Monthly: ¥{plan.monthlyPrice?.toLocaleString()} | One-time: ¥{plan.setupFee?.toLocaleString() || '22,800'} + Monthly: ¥{plan.monthlyPrice?.toLocaleString()} + {installations.length > 0 && ( + + (+ installation from ¥{Math.min(...installations.map(i => i.price || 0)).toLocaleString()}) + + )}
  • )} diff --git a/apps/portal/src/app/catalog/vpn/page.tsx b/apps/portal/src/app/catalog/vpn/page.tsx index 88043287..989e5552 100644 --- a/apps/portal/src/app/catalog/vpn/page.tsx +++ b/apps/portal/src/app/catalog/vpn/page.tsx @@ -135,19 +135,7 @@ export default function VpnPlansPage() { - {plan.features && plan.features.length > 0 && ( -
    -

    Features:

    -
      - {plan.features.map((feature, index) => ( -
    • - - {feature} -
    • - ))} -
    -
    - )} + {/* VPN plans don't have features defined in the type structure */} = 8'} - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - speakeasy@2.0.0: resolution: {integrity: sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==} engines: {node: '>= 0.10.0'} @@ -4673,13 +4666,6 @@ packages: jest-util: optional: true - ts-loader@9.5.2: - resolution: {integrity: sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==} - engines: {node: '>=12.0.0'} - peerDependencies: - typescript: '*' - webpack: ^5.0.0 - ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -9690,8 +9676,6 @@ snapshots: source-map@0.7.4: {} - source-map@0.7.6: {} - speakeasy@2.0.0: dependencies: base32.js: 0.0.1 @@ -9960,16 +9944,6 @@ snapshots: babel-jest: 30.0.5(@babel/core@7.28.3) jest-util: 30.0.5 - ts-loader@9.5.2(typescript@5.9.2)(webpack@5.100.2): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 - semver: 7.7.2 - source-map: 0.7.6 - typescript: 5.9.2 - webpack: 5.100.2 - ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2): dependencies: '@cspotcode/source-map-support': 0.8.1 diff --git a/scripts/dev/manage.sh b/scripts/dev/manage.sh index 43f6a8e2..b12843a2 100755 --- a/scripts/dev/manage.sh +++ b/scripts/dev/manage.sh @@ -102,6 +102,14 @@ start_apps() { source "$ENV_FILE" 2>/dev/null || true set +a + # Build shared package first (required by both apps) + log "🔨 Building shared package..." + pnpm --filter @customer-portal/shared build + + # Build BFF first to ensure dist directory exists for watch mode + log "🔨 Building BFF for initial setup..." + pnpm --filter @customer-portal/bff build + # Show startup information log "🎯 Starting development applications..." log "🔗 BFF API: http://localhost:${BFF_PORT:-4000}/api" @@ -111,8 +119,7 @@ start_apps() { log "📚 API Docs: http://localhost:${BFF_PORT:-4000}/api/docs" log "Starting apps with hot-reload..." - # Start Prisma Studio (opens browser) - (cd "$PROJECT_ROOT/apps/bff" && pnpm db:studio &) + # Prisma Studio can be started manually with: pnpm db:studio # Start apps (portal + bff) with hot reload in parallel pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run dev