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.
This commit is contained in:
T. Narantuya 2025-08-30 16:45:22 +09:00
parent 9a7c1a06bb
commit cde7cb3e3c
13 changed files with 202 additions and 132 deletions

View File

@ -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 {}

View File

@ -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,
};

View File

@ -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 },
],
},
];

View File

@ -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<number>('RATE_LIMIT_TTL', 60000), // 1 minute
limit: configService.get<number>('RATE_LIMIT_LIMIT', 100), // 100 requests
},
// Stricter rate limiting for authentication endpoints
{
name: 'auth',
ttl: configService.get<number>('AUTH_RATE_LIMIT_TTL', 900000), // 15 minutes
limit: configService.get<number>('AUTH_RATE_LIMIT_LIMIT', 3), // 3 attempts
},
];

View File

@ -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"]

View File

@ -11,6 +11,7 @@
"outDir": "./dist",
"baseUrl": "./",
"removeComments": true,
"target": "ES2024",
// Path mappings
"paths": {

View File

@ -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: {

View File

@ -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"

View File

@ -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<InternetPlan[]>([]);
const [installations, setInstallations] = useState<InternetInstallation[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [eligibility, setEligibility] = useState<string>("");
@ -31,11 +32,15 @@ export default function InternetPlansPage() {
setLoading(true);
setError(null);
const plans = await authenticatedApi.get<InternetPlan[]>("/catalog/internet/plans");
const [plans, installations] = await Promise.all([
authenticatedApi.get<InternetPlan[]>("/catalog/internet/plans"),
authenticatedApi.get<InternetInstallation[]>("/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() {
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{plans.map(plan => (
<InternetPlanCard key={plan.id} plan={plan} />
<InternetPlanCard key={plan.id} plan={plan} installations={installations} />
))}
</div>
@ -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 }) {
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
Monthly: ¥{plan.monthlyPrice?.toLocaleString()} | One-time: ¥{plan.setupFee?.toLocaleString() || '22,800'}
Monthly: ¥{plan.monthlyPrice?.toLocaleString()}
{installations.length > 0 && (
<span className="text-gray-600 text-sm ml-2">
(+ installation from ¥{Math.min(...installations.map(i => i.price || 0)).toLocaleString()})
</span>
)}
</li>
</>
)}

View File

@ -135,19 +135,7 @@ export default function VpnPlansPage() {
</div>
</div>
{plan.features && plan.features.length > 0 && (
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Features:</h4>
<ul className="text-sm text-gray-700 space-y-1">
{plan.features.map((feature, index) => (
<li key={index} className="flex items-start">
<CheckIcon className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
{feature}
</li>
))}
</ul>
</div>
)}
{/* VPN plans don't have features defined in the type structure */}
<AnimatedButton
href={`/catalog/vpn/configure?plan=${plan.sku}`}

View File

@ -12,7 +12,7 @@
"predev": "pnpm --filter @customer-portal/shared build",
"dev": "./scripts/dev/manage.sh apps",
"dev:all": "pnpm --parallel --filter @customer-portal/shared --filter @customer-portal/portal --filter @customer-portal/bff run dev",
"build": "pnpm --recursive -w --if-present run build",
"build": "pnpm --recursive --reporter=default run build",
"start": "pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run start",
"test": "pnpm --recursive run test",
"lint": "pnpm --recursive run lint",
@ -50,7 +50,6 @@
"dev:watch": "pnpm --parallel --filter @customer-portal/shared --filter @customer-portal/portal --filter @customer-portal/bff run dev"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.34.0",
"@types/node": "^24.3.0",

32
pnpm-lock.yaml generated
View File

@ -205,9 +205,6 @@ importers:
ts-jest:
specifier: ^29.4.1
version: 29.4.1(@babel/core@7.28.3)(@jest/transform@30.0.5)(@jest/types@30.0.5)(babel-jest@30.0.5(@babel/core@7.28.3))(jest-util@30.0.5)(jest@30.0.5(@types/node@24.3.0)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)))(typescript@5.9.2)
ts-loader:
specifier: ^9.5.2
version: 9.5.2(typescript@5.9.2)(webpack@5.100.2)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@24.3.0)(typescript@5.9.2)
@ -262,6 +259,9 @@ importers:
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
tw-animate-css:
specifier: ^1.3.7
version: 1.3.7
world-countries:
specifier: ^5.1.0
version: 5.1.0
@ -287,9 +287,6 @@ importers:
tailwindcss:
specifier: ^4.1.12
version: 4.1.12
tw-animate-css:
specifier: ^1.3.7
version: 1.3.7
typescript:
specifier: ^5.9.2
version: 5.9.2
@ -4400,10 +4397,6 @@ packages:
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
engines: {node: '>= 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

View File

@ -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