import { Controller, Get, Request, UseGuards, Header } from "@nestjs/common"; import { RateLimitGuard, RateLimit } from "@bff/core/rate-limiting/index.js"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import { Public } from "@bff/modules/auth/decorators/public.decorator.js"; import { parseInternetCatalog, parseSimCatalog, parseVpnCatalog, type InternetAddonCatalogItem, type InternetInstallationCatalogItem, type InternetPlanCatalogItem, type SimActivationFeeCatalogItem, type SimCatalogCollection, type SimCatalogProduct, type VpnCatalogProduct, type VpnCatalogCollection, } from "@customer-portal/domain/services"; import { InternetServicesService } from "./application/internet-services.service.js"; import { SimServicesService } from "./application/sim-services.service.js"; import { VpnServicesService } from "./application/vpn-services.service.js"; import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard.js"; const HEADER_CACHE_CONTROL = "Cache-Control"; const CACHE_CONTROL_PRIVATE = "private, no-store"; const CACHE_CONTROL_PUBLIC = "public, max-age=300, s-maxage=300"; @Controller("services") @Public() // Allow public access - services can be browsed without authentication @UseGuards(SalesforceReadThrottleGuard, RateLimitGuard) export class ServicesController { constructor( private internetCatalog: InternetServicesService, private simCatalog: SimServicesService, private vpnCatalog: VpnServicesService ) {} @Get("internet/plans") @RateLimit({ limit: 20, ttl: 60 }) // 20 requests per minute @Header(HEADER_CACHE_CONTROL, CACHE_CONTROL_PRIVATE) // Personalised responses: avoid browser caching (realtime invalidation relies on refetch) async getInternetPlans(@Request() req: RequestWithUser): Promise<{ plans: InternetPlanCatalogItem[]; installations: InternetInstallationCatalogItem[]; addons: InternetAddonCatalogItem[]; }> { const userId = req.user?.id; if (!userId) { const catalog = await this.internetCatalog.getCatalogData(); return parseInternetCatalog(catalog); } const [plans, installations, addons] = await Promise.all([ this.internetCatalog.getPlansForUser(userId), this.internetCatalog.getInstallations(), this.internetCatalog.getAddons(), ]); return parseInternetCatalog({ plans, installations, addons }); } @Get("internet/addons") @Header(HEADER_CACHE_CONTROL, CACHE_CONTROL_PUBLIC) // 5 minutes async getInternetAddons(): Promise { return this.internetCatalog.getAddons(); } @Get("internet/installations") @Header(HEADER_CACHE_CONTROL, CACHE_CONTROL_PUBLIC) // 5 minutes async getInternetInstallations(): Promise { return this.internetCatalog.getInstallations(); } @Get("sim/plans") @RateLimit({ limit: 20, ttl: 60 }) // 20 requests per minute @Header(HEADER_CACHE_CONTROL, CACHE_CONTROL_PRIVATE) // Personalised responses: avoid browser caching (realtime invalidation relies on refetch) async getSimCatalogData(@Request() req: RequestWithUser): Promise { const userId = req.user?.id; if (!userId) { const catalog = await this.simCatalog.getCatalogData(); return parseSimCatalog({ ...catalog, plans: catalog.plans.filter(plan => !plan.simHasFamilyDiscount), }); } const [plans, activationFees, addons] = await Promise.all([ this.simCatalog.getPlansForUser(userId), this.simCatalog.getActivationFees(), this.simCatalog.getAddons(), ]); return parseSimCatalog({ plans, activationFees, addons }); } @Get("sim/activation-fees") @Header(HEADER_CACHE_CONTROL, CACHE_CONTROL_PUBLIC) // 5 minutes async getSimActivationFees(): Promise { return this.simCatalog.getActivationFees(); } @Get("sim/addons") @Header(HEADER_CACHE_CONTROL, CACHE_CONTROL_PUBLIC) // 5 minutes async getSimAddons(): Promise { return this.simCatalog.getAddons(); } @Get("vpn/plans") @RateLimit({ limit: 20, ttl: 60 }) // 20 requests per minute @Header(HEADER_CACHE_CONTROL, CACHE_CONTROL_PUBLIC) // 5 minutes async getVpnPlans(): Promise { // Backwards-compatible: return the full VPN catalog (plans + activation fees) const catalog = await this.vpnCatalog.getCatalogData(); return parseVpnCatalog(catalog); } @Get("vpn/activation-fees") @Header(HEADER_CACHE_CONTROL, CACHE_CONTROL_PUBLIC) // 5 minutes async getVpnActivationFees(): Promise { return this.vpnCatalog.getActivationFees(); } }