Add Service and Component Structure for Internet and SIM Offerings

- Introduced new controllers for internet eligibility and service health checks to enhance backend functionality.
- Created service modules for internet, SIM, and VPN offerings, improving organization and maintainability.
- Developed various components for internet and SIM configuration, including forms and plan cards, to streamline user interactions.
- Implemented hooks for managing service configurations and eligibility checks, enhancing frontend data handling.
- Updated utility functions for pricing and catalog operations to support new service structures and improve performance.
This commit is contained in:
barsa 2025-12-25 13:20:45 +09:00
parent 6bc7695b22
commit 38bb40b88b
183 changed files with 362 additions and 346 deletions

View File

@ -292,7 +292,7 @@ When running `pnpm dev:tools`, you get access to:
### Catalog & Orders
- `GET /api/catalog` - WHMCS GetProducts (cached 5-15m)
- `GET /api/services/*` - Services catalog endpoints (internet/sim/vpn)
- `POST /api/orders` - WHMCS AddOrder with idempotency
### Invoices

View File

@ -30,7 +30,7 @@ import { AuthModule } from "@bff/modules/auth/auth.module.js";
import { UsersModule } from "@bff/modules/users/users.module.js";
import { MeStatusModule } from "@bff/modules/me-status/me-status.module.js";
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
import { CatalogModule } from "@bff/modules/catalog/catalog.module.js";
import { ServicesModule } from "@bff/modules/services/services.module.js";
import { OrdersModule } from "@bff/modules/orders/orders.module.js";
import { InvoicesModule } from "@bff/modules/invoices/invoices.module.js";
import { SubscriptionsModule } from "@bff/modules/subscriptions/subscriptions.module.js";
@ -84,7 +84,7 @@ import { HealthModule } from "@bff/modules/health/health.module.js";
UsersModule,
MeStatusModule,
MappingsModule,
CatalogModule,
ServicesModule,
OrdersModule,
InvoicesModule,
SubscriptionsModule,

View File

@ -3,7 +3,7 @@ import { AuthModule } from "@bff/modules/auth/auth.module.js";
import { UsersModule } from "@bff/modules/users/users.module.js";
import { MeStatusModule } from "@bff/modules/me-status/me-status.module.js";
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
import { CatalogModule } from "@bff/modules/catalog/catalog.module.js";
import { ServicesModule } from "@bff/modules/services/services.module.js";
import { OrdersModule } from "@bff/modules/orders/orders.module.js";
import { InvoicesModule } from "@bff/modules/invoices/invoices.module.js";
import { SubscriptionsModule } from "@bff/modules/subscriptions/subscriptions.module.js";
@ -22,7 +22,7 @@ export const apiRoutes: Routes = [
{ path: "", module: UsersModule },
{ path: "", module: MeStatusModule },
{ path: "", module: MappingsModule },
{ path: "", module: CatalogModule },
{ path: "", module: ServicesModule },
{ path: "", module: OrdersModule },
{ path: "", module: InvoicesModule },
{ path: "", module: SubscriptionsModule },

View File

@ -64,11 +64,13 @@ Redis-backed caching system with CDC (Change Data Capture) event-driven invalida
**No TTL** - Cache persists indefinitely until CDC event triggers invalidation.
**Pros:**
- Real-time invalidation when data changes
- Zero stale data for customer-visible fields
- Optimal for frequently read, infrequently changed data
**Example:**
```typescript
@Injectable()
export class OrdersCacheService {
@ -88,11 +90,13 @@ export class OrdersCacheService {
**Fixed TTL** - Cache expires after a set duration.
**Pros:**
- Simple, predictable behavior
- Good for external systems without CDC
- Automatic cleanup of stale data
**Example:**
```typescript
@Injectable()
export class WhmcsCacheService {
@ -160,7 +164,8 @@ All cache services track performance metrics:
```
Access via health endpoints:
- `GET /health/catalog/cache`
- `GET /api/health/services/cache`
- `GET /health`
## Creating a New Cache Service
@ -194,7 +199,7 @@ export class MyDomainCacheService {
```typescript
async getMyData(id: string, fetcher: () => Promise<MyData>): Promise<MyData> {
const key = `mydomain:${id}`;
// Check cache
const cached = await this.cache.get<MyData>(key);
if (cached) {
@ -255,6 +260,7 @@ domain:type:identifier[:subkey]
```
Examples:
- `orders:account:001xx000003EgI1AAK`
- `orders:detail:80122000000D4UGAA0`
- `catalog:internet:acc_001:jp`
@ -288,7 +294,7 @@ Provides global `REDIS_CLIENT` using ioredis.
GET /health
# Catalog cache metrics
GET /health/catalog/cache
GET /api/health/services/cache
```
### Response Format
@ -357,4 +363,3 @@ console.log(`${count} keys using ${usage} bytes`);
- [Salesforce CDC Events](../../integrations/salesforce/events/README.md)
- [Order Fulfillment Flow](../../modules/orders/docs/FULFILLMENT.md)
- [Redis Configuration](../redis/README.md)

View File

@ -4,7 +4,7 @@ import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import PubSubApiClientPkg from "salesforce-pubsub-api-client";
import { SalesforceConnection } from "../services/salesforce-connection.service.js";
import { CatalogCacheService } from "@bff/modules/catalog/services/catalog-cache.service.js";
import { CatalogCacheService } from "@bff/modules/services/services/catalog-cache.service.js";
import { RealtimeService } from "@bff/infra/realtime/realtime.service.js";
import { AccountNotificationHandler } from "@bff/modules/notifications/account-cdc-listener.service.js";
@ -195,8 +195,8 @@ export class CatalogCdcSubscriber implements OnModuleInit, OnModuleDestroy {
}
);
await this.invalidateAllCatalogs();
// Full invalidation already implies all clients should refetch catalog
this.realtime.publish("global:catalog", "catalog.changed", {
// Full invalidation already implies all clients should refetch services
this.realtime.publish("global:services", "services.changed", {
reason: "product.cdc.fallback_full_invalidation",
timestamp: new Date().toISOString(),
});
@ -204,7 +204,7 @@ export class CatalogCdcSubscriber implements OnModuleInit, OnModuleDestroy {
}
// Product changes can affect catalog results for all users
this.realtime.publish("global:catalog", "catalog.changed", {
this.realtime.publish("global:services", "services.changed", {
reason: "product.cdc",
timestamp: new Date().toISOString(),
});
@ -249,14 +249,14 @@ export class CatalogCdcSubscriber implements OnModuleInit, OnModuleDestroy {
}
);
await this.invalidateAllCatalogs();
this.realtime.publish("global:catalog", "catalog.changed", {
this.realtime.publish("global:services", "services.changed", {
reason: "pricebook.cdc.fallback_full_invalidation",
timestamp: new Date().toISOString(),
});
return;
}
this.realtime.publish("global:catalog", "catalog.changed", {
this.realtime.publish("global:services", "services.changed", {
reason: "pricebook.cdc",
timestamp: new Date().toISOString(),
});
@ -316,7 +316,7 @@ export class CatalogCdcSubscriber implements OnModuleInit, OnModuleDestroy {
}
// Notify connected portals immediately (multi-instance safe via Redis pub/sub)
this.realtime.publish(`account:sf:${accountId}`, "catalog.eligibility.changed", {
this.realtime.publish(`account:sf:${accountId}`, "services.eligibility.changed", {
timestamp: new Date().toISOString(),
});

View File

@ -2,7 +2,7 @@ import { Module, forwardRef } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { IntegrationsModule } from "@bff/integrations/integrations.module.js";
import { OrdersModule } from "@bff/modules/orders/orders.module.js";
import { CatalogModule } from "@bff/modules/catalog/catalog.module.js";
import { ServicesModule } from "@bff/modules/services/services.module.js";
import { NotificationsModule } from "@bff/modules/notifications/notifications.module.js";
import { CatalogCdcSubscriber } from "./catalog-cdc.subscriber.js";
import { OrderCdcSubscriber } from "./order-cdc.subscriber.js";
@ -12,7 +12,7 @@ import { OrderCdcSubscriber } from "./order-cdc.subscriber.js";
ConfigModule,
forwardRef(() => IntegrationsModule),
forwardRef(() => OrdersModule),
forwardRef(() => CatalogModule),
forwardRef(() => ServicesModule),
forwardRef(() => NotificationsModule),
],
providers: [

View File

@ -33,7 +33,7 @@ import type {
} from "@customer-portal/domain/payments";
import type { WhmcsGetClientsProductsParams } from "@customer-portal/domain/subscriptions";
import type { WhmcsProductListResponse } from "@customer-portal/domain/subscriptions";
import type { WhmcsCatalogProductListResponse } from "@customer-portal/domain/catalog";
import type { WhmcsCatalogProductListResponse } from "@customer-portal/domain/services";
import type { WhmcsErrorResponse } from "@customer-portal/domain/common";
import type { WhmcsRequestOptions, WhmcsConnectionStats } from "../types/connection.types.js";

View File

@ -11,7 +11,7 @@ import type {
import {
Providers as CatalogProviders,
type WhmcsCatalogProductNormalized,
} from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
import type { WhmcsCreateSsoTokenParams } from "@customer-portal/domain/customer";

View File

@ -20,7 +20,7 @@ import { WhmcsOrderService } from "./services/whmcs-order.service.js";
import type { WhmcsAddClientParams, WhmcsClientResponse } from "@customer-portal/domain/customer";
import type { WhmcsGetClientsProductsParams } from "@customer-portal/domain/subscriptions";
import type { WhmcsProductListResponse } from "@customer-portal/domain/subscriptions";
import type { WhmcsCatalogProductNormalized } from "@customer-portal/domain/catalog";
import type { WhmcsCatalogProductNormalized } from "@customer-portal/domain/services";
import { Logger } from "nestjs-pino";
@Injectable()

View File

@ -3,7 +3,7 @@ import { MeStatusController } from "./me-status.controller.js";
import { MeStatusService } from "./me-status.service.js";
import { UsersModule } from "@bff/modules/users/users.module.js";
import { OrdersModule } from "@bff/modules/orders/orders.module.js";
import { CatalogModule } from "@bff/modules/catalog/catalog.module.js";
import { ServicesModule } from "@bff/modules/services/services.module.js";
import { VerificationModule } from "@bff/modules/verification/verification.module.js";
import { WhmcsModule } from "@bff/integrations/whmcs/whmcs.module.js";
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
@ -14,7 +14,7 @@ import { SalesforceModule } from "@bff/integrations/salesforce/salesforce.module
imports: [
UsersModule,
OrdersModule,
CatalogModule,
ServicesModule,
VerificationModule,
WhmcsModule,
MappingsModule,

View File

@ -2,7 +2,7 @@ import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { UsersFacade } from "@bff/modules/users/application/users.facade.js";
import { OrderOrchestrator } from "@bff/modules/orders/services/order-orchestrator.service.js";
import { InternetCatalogService } from "@bff/modules/catalog/services/internet-catalog.service.js";
import { InternetCatalogService } from "@bff/modules/services/services/internet-catalog.service.js";
import { ResidenceCardService } from "@bff/modules/verification/residence-card.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { WhmcsPaymentService } from "@bff/integrations/whmcs/services/whmcs-payment.service.js";
@ -15,7 +15,7 @@ import {
type PaymentMethodsStatus,
} from "@customer-portal/domain/dashboard";
import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications";
import type { InternetEligibilityDetails } from "@customer-portal/domain/catalog";
import type { InternetEligibilityDetails } from "@customer-portal/domain/services";
import type { ResidenceCardVerification } from "@customer-portal/domain/customer";
import type { OrderSummary } from "@customer-portal/domain/orders";

View File

@ -6,7 +6,7 @@ import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
import { UsersModule } from "@bff/modules/users/users.module.js";
import { CoreConfigModule } from "@bff/core/config/config.module.js";
import { DatabaseModule } from "@bff/core/database/database.module.js";
import { CatalogModule } from "@bff/modules/catalog/catalog.module.js";
import { ServicesModule } from "@bff/modules/services/services.module.js";
import { CacheModule } from "@bff/infra/cache/cache.module.js";
import { VerificationModule } from "@bff/modules/verification/verification.module.js";
import { NotificationsModule } from "@bff/modules/notifications/notifications.module.js";
@ -39,7 +39,7 @@ import { OrderFieldConfigModule } from "./config/order-field-config.module.js";
UsersModule,
CoreConfigModule,
DatabaseModule,
CatalogModule,
ServicesModule,
CacheModule,
VerificationModule,
NotificationsModule,

View File

@ -20,10 +20,10 @@ import type {
SimCatalogProduct,
SimActivationFeeCatalogItem,
VpnCatalogProduct,
} from "@customer-portal/domain/catalog";
import { InternetCatalogService } from "@bff/modules/catalog/services/internet-catalog.service.js";
import { SimCatalogService } from "@bff/modules/catalog/services/sim-catalog.service.js";
import { VpnCatalogService } from "@bff/modules/catalog/services/vpn-catalog.service.js";
} from "@customer-portal/domain/services";
import { InternetCatalogService } from "@bff/modules/services/services/internet-catalog.service.js";
import { SimCatalogService } from "@bff/modules/services/services/sim-catalog.service.js";
import { VpnCatalogService } from "@bff/modules/services/services/vpn-catalog.service.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
@Injectable()

View File

@ -5,7 +5,7 @@ import { SalesforceConnection } from "@bff/integrations/salesforce/services/sale
import type {
SalesforceProduct2Record,
SalesforcePricebookEntryRecord,
} from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import type { SalesforceResponse } from "@customer-portal/domain/common";
import {
assertSalesforceId,

View File

@ -13,8 +13,8 @@ import {
import type { Providers } from "@customer-portal/domain/subscriptions";
type WhmcsProduct = Providers.WhmcsRaw.WhmcsProductRaw;
import { SimCatalogService } from "@bff/modules/catalog/services/sim-catalog.service.js";
import { InternetCatalogService } from "@bff/modules/catalog/services/internet-catalog.service.js";
import { SimCatalogService } from "@bff/modules/services/services/sim-catalog.service.js";
import { InternetCatalogService } from "@bff/modules/services/services/internet-catalog.service.js";
import { OrderPricebookService, type PricebookProductMeta } from "./order-pricebook.service.js";
import { PaymentValidatorService } from "./payment-validator.service.js";
import { ResidenceCardService } from "@bff/modules/verification/residence-card.service.js";

View File

@ -67,14 +67,14 @@ export class RealtimeController {
}
);
const globalCatalogStream = this.realtime.subscribe("global:catalog", {
const globalServicesStream = this.realtime.subscribe("global:services", {
// Avoid duplicate ready/heartbeat noise on the combined stream.
readyEvent: null,
heartbeatEvent: null,
heartbeatMs: 0,
});
return merge(accountStream, globalCatalogStream).pipe(
return merge(accountStream, globalServicesStream).pipe(
finalize(() => {
this.limiter.release(req.user.id);
this.logger.debug("Account realtime stream disconnected", {

View File

@ -5,7 +5,7 @@ import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js";
import { InternetCatalogService } from "./services/internet-catalog.service.js";
import { addressSchema } from "@customer-portal/domain/customer";
import type { InternetEligibilityDetails } from "@customer-portal/domain/catalog";
import type { InternetEligibilityDetails } from "@customer-portal/domain/services";
const eligibilityRequestSchema = z.object({
notes: z.string().trim().max(2000).optional(),
@ -21,10 +21,10 @@ type EligibilityRequest = z.infer<typeof eligibilityRequestSchema>;
* - fetching current Salesforce eligibility value
* - requesting a (manual) eligibility/availability check
*
* Note: CatalogController is @Public, so we keep these endpoints in a separate controller
* Note: ServicesController is @Public, so we keep these endpoints in a separate controller
* to ensure GlobalAuthGuard enforces authentication.
*/
@Controller("catalog/internet")
@Controller("services/internet")
@UseGuards(RateLimitGuard)
export class InternetEligibilityController {
constructor(private readonly internetCatalog: InternetCatalogService) {}

View File

@ -3,7 +3,7 @@ import { CatalogCacheService } from "./services/catalog-cache.service.js";
import type { CatalogCacheSnapshot } from "./services/catalog-cache.service.js";
import { Public } from "@bff/modules/auth/decorators/public.decorator.js";
interface CatalogCacheHealthResponse {
interface ServicesCacheHealthResponse {
timestamp: string;
metrics: CatalogCacheSnapshot;
ttl: {
@ -14,13 +14,13 @@ interface CatalogCacheHealthResponse {
};
}
@Controller("health/catalog")
@Controller("health/services")
@Public()
export class CatalogHealthController {
export class ServicesHealthController {
constructor(private readonly catalogCache: CatalogCacheService) {}
@Get("cache")
getCacheMetrics(): CatalogCacheHealthResponse {
getCacheMetrics(): ServicesCacheHealthResponse {
const ttl = this.catalogCache.getTtlConfiguration();
return {
timestamp: new Date().toISOString(),

View File

@ -12,16 +12,16 @@ import {
type SimCatalogCollection,
type SimCatalogProduct,
type VpnCatalogProduct,
} from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import { InternetCatalogService } from "./services/internet-catalog.service.js";
import { SimCatalogService } from "./services/sim-catalog.service.js";
import { VpnCatalogService } from "./services/vpn-catalog.service.js";
import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard.js";
@Controller("catalog")
@Public() // Allow public access - catalog can be browsed without authentication
@Controller("services")
@Public() // Allow public access - services can be browsed without authentication
@UseGuards(SalesforceReadThrottleGuard, RateLimitGuard)
export class CatalogController {
export class ServicesController {
constructor(
private internetCatalog: InternetCatalogService,
private simCatalog: SimCatalogService,

View File

@ -1,6 +1,6 @@
import { Module, forwardRef } from "@nestjs/common";
import { CatalogController } from "./catalog.controller.js";
import { CatalogHealthController } from "./catalog-health.controller.js";
import { ServicesController } from "./services.controller.js";
import { ServicesHealthController } from "./services-health.controller.js";
import { InternetEligibilityController } from "./internet-eligibility.controller.js";
import { IntegrationsModule } from "@bff/integrations/integrations.module.js";
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
@ -22,7 +22,7 @@ import { CatalogCacheService } from "./services/catalog-cache.service.js";
CacheModule,
QueueModule,
],
controllers: [CatalogController, CatalogHealthController, InternetEligibilityController],
controllers: [ServicesController, ServicesHealthController, InternetEligibilityController],
providers: [
BaseCatalogService,
InternetCatalogService,
@ -32,4 +32,4 @@ import { CatalogCacheService } from "./services/catalog-cache.service.js";
],
exports: [InternetCatalogService, SimCatalogService, VpnCatalogService, CatalogCacheService],
})
export class CatalogModule {}
export class ServicesModule {}

View File

@ -14,8 +14,8 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js";
import type {
SalesforceProduct2WithPricebookEntries,
SalesforcePricebookEntryRecord,
} from "@customer-portal/domain/catalog";
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import { Providers as CatalogProviders } from "@customer-portal/domain/services";
import type { SalesforceResponse } from "@customer-portal/domain/common";
@Injectable()

View File

@ -9,14 +9,14 @@ import type {
InternetAddonCatalogItem,
InternetEligibilityDetails,
InternetEligibilityStatus,
} from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import {
Providers as CatalogProviders,
enrichInternetPlanMetadata,
inferAddonTypeFromSku,
inferInstallationTermFromSku,
internetEligibilityDetailsSchema,
} from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
import { OpportunityResolutionService } from "@bff/integrations/salesforce/services/opportunity-resolution.service.js";

View File

@ -6,8 +6,8 @@ import type {
SalesforceProduct2WithPricebookEntries,
SimCatalogProduct,
SimActivationFeeCatalogItem,
} from "@customer-portal/domain/catalog";
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import { Providers as CatalogProviders } from "@customer-portal/domain/services";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
import { Logger } from "nestjs-pino";

View File

@ -6,8 +6,8 @@ import { BaseCatalogService } from "./base-catalog.service.js";
import type {
SalesforceProduct2WithPricebookEntries,
VpnCatalogProduct,
} from "@customer-portal/domain/catalog";
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import { Providers as CatalogProviders } from "@customer-portal/domain/services";
@Injectable()
export class VpnCatalogService extends BaseCatalogService {

View File

@ -12,8 +12,8 @@ import { SimScheduleService } from "./sim-schedule.service.js";
import { SimActionRunnerService } from "./sim-action-runner.service.js";
import { SimManagementQueueService } from "../queue/sim-management.queue.js";
import { SimApiNotificationService } from "./sim-api-notification.service.js";
import { SimCatalogService } from "@bff/modules/catalog/services/sim-catalog.service.js";
import type { SimCatalogProduct } from "@customer-portal/domain/catalog";
import { SimCatalogService } from "@bff/modules/services/services/sim-catalog.service.js";
import type { SimCatalogProduct } from "@customer-portal/domain/services";
// Mapping from Salesforce SKU to Freebit plan code
const SKU_TO_FREEBIT_PLAN_CODE: Record<string, string> = {

View File

@ -27,7 +27,7 @@ import { SimManagementQueueService } from "./queue/sim-management.queue.js";
import { SimManagementProcessor } from "./queue/sim-management.processor.js";
import { SimVoiceOptionsService } from "./services/sim-voice-options.service.js";
import { SimCallHistoryService } from "./services/sim-call-history.service.js";
import { CatalogModule } from "@bff/modules/catalog/catalog.module.js";
import { ServicesModule } from "@bff/modules/services/services.module.js";
import { NotificationsModule } from "@bff/modules/notifications/notifications.module.js";
@Module({
@ -37,7 +37,7 @@ import { NotificationsModule } from "@bff/modules/notifications/notifications.mo
SalesforceModule,
MappingsModule,
EmailModule,
CatalogModule,
ServicesModule,
SftpModule,
NotificationsModule,
],

View File

@ -4,8 +4,8 @@
* Configure internet plan for unauthenticated users.
*/
import { PublicInternetConfigureView } from "@/features/catalog/views/PublicInternetConfigure";
import { RedirectAuthenticatedToAccountServices } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountServices";
import { PublicInternetConfigureView } from "@/features/services/views/PublicInternetConfigure";
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
export default function PublicInternetConfigurePage() {
return (

View File

@ -4,8 +4,8 @@
* Displays internet plans for unauthenticated users.
*/
import { PublicInternetPlansView } from "@/features/catalog/views/PublicInternetPlans";
import { RedirectAuthenticatedToAccountServices } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountServices";
import { PublicInternetPlansView } from "@/features/services/views/PublicInternetPlans";
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
export default function PublicInternetPlansPage() {
return (

View File

@ -1,4 +1,4 @@
import { ServicesGrid } from "@/features/catalog/components/common/ServicesGrid";
import { ServicesGrid } from "@/features/services/components/common/ServicesGrid";
interface ServicesPageProps {
basePath?: string;

View File

@ -4,8 +4,8 @@
* Configure SIM plan for unauthenticated users.
*/
import { PublicSimConfigureView } from "@/features/catalog/views/PublicSimConfigure";
import { RedirectAuthenticatedToAccountServices } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountServices";
import { PublicSimConfigureView } from "@/features/services/views/PublicSimConfigure";
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
export default function PublicSimConfigurePage() {
return (

View File

@ -4,8 +4,8 @@
* Displays SIM plans for unauthenticated users.
*/
import { PublicSimPlansView } from "@/features/catalog/views/PublicSimPlans";
import { RedirectAuthenticatedToAccountServices } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountServices";
import { PublicSimPlansView } from "@/features/services/views/PublicSimPlans";
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
export default function PublicSimPlansPage() {
return (

View File

@ -4,8 +4,8 @@
* Displays VPN plans for unauthenticated users.
*/
import { PublicVpnPlansView } from "@/features/catalog/views/PublicVpnPlans";
import { RedirectAuthenticatedToAccountServices } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountServices";
import { PublicVpnPlansView } from "@/features/services/views/PublicVpnPlans";
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
export default function PublicVpnPlansPage() {
return (

View File

@ -1,4 +1,4 @@
import { InternetConfigureContainer } from "@/features/catalog/views/InternetConfigure";
import { InternetConfigureContainer } from "@/features/services/views/InternetConfigure";
export default function AccountInternetConfigurePage() {
return <InternetConfigureContainer />;

View File

@ -1,4 +1,4 @@
import { InternetPlansContainer } from "@/features/catalog/views/InternetPlans";
import { InternetPlansContainer } from "@/features/services/views/InternetPlans";
export default function AccountInternetPlansPage() {
return <InternetPlansContainer />;

View File

@ -1,5 +1,5 @@
import type { ReactNode } from "react";
export default function AccountShopLayout({ children }: { children: ReactNode }) {
export default function AccountServicesLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}

View File

@ -1,6 +1,6 @@
import { ServicesGrid } from "@/features/catalog/components/common/ServicesGrid";
import { ServicesGrid } from "@/features/services/components/common/ServicesGrid";
export default function AccountShopPage() {
export default function AccountServicesPage() {
return (
<div className="py-8">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-4 sm:px-6 md:px-8">

View File

@ -1,4 +1,4 @@
import { SimConfigureContainer } from "@/features/catalog/views/SimConfigure";
import { SimConfigureContainer } from "@/features/services/views/SimConfigure";
export default function AccountSimConfigurePage() {
return <SimConfigureContainer />;

View File

@ -1,4 +1,4 @@
import { SimPlansContainer } from "@/features/catalog/views/SimPlans";
import { SimPlansContainer } from "@/features/services/views/SimPlans";
export default function AccountSimPlansPage() {
return <SimPlansContainer />;

View File

@ -1,4 +1,4 @@
import { VpnPlansView } from "@/features/catalog/views/VpnPlans";
import { VpnPlansView } from "@/features/services/views/VpnPlans";
export default function AccountVpnPlansPage() {
return <VpnPlansView />;

View File

@ -1,5 +1,5 @@
import SubscriptionsListContainer from "@/features/subscriptions/views/SubscriptionsList";
export default function AccountServicesPage() {
export default function AccountSubscriptionsPage() {
return <SubscriptionsListContainer />;
}

View File

@ -98,7 +98,7 @@ export function AppShell({ children }: AppShellProps) {
useEffect(() => {
setExpandedItems(prev => {
const next = new Set(prev);
if (pathname.startsWith("/account/services")) next.add("My Services");
if (pathname.startsWith("/account/subscriptions")) next.add("Subscriptions");
if (pathname.startsWith("/account/billing")) next.add("Billing");
if (pathname.startsWith("/account/support")) next.add("Support");
if (pathname.startsWith("/account/settings")) next.add("Settings");

View File

@ -37,9 +37,9 @@ export const baseNavigation: NavigationItem[] = [
],
},
{
name: "My Services",
name: "Subscriptions",
icon: ServerIcon,
children: [{ name: "All Services", href: "/account/my-services" }],
children: [{ name: "All Subscriptions", href: "/account/subscriptions" }],
},
{ name: "Services", href: "/account/services", icon: Squares2X2Icon },
{
@ -64,17 +64,17 @@ export function computeNavigation(activeSubscriptions?: Subscription[]): Navigat
children: item.children ? [...item.children] : undefined,
}));
const subIdx = nav.findIndex(n => n.name === "My Services");
const subIdx = nav.findIndex(n => n.name === "Subscriptions");
if (subIdx >= 0) {
const dynamicChildren = (activeSubscriptions || []).map(sub => ({
name: truncate(sub.productName || `Subscription ${sub.id}`, 28),
href: `/account/my-services/${sub.id}`,
href: `/account/subscriptions/${sub.id}`,
tooltip: sub.productName || `Subscription ${sub.id}`,
}));
nav[subIdx] = {
...nav[subIdx],
children: [{ name: "All Services", href: "/account/my-services" }, ...dynamicChildren],
children: [{ name: "All Subscriptions", href: "/account/subscriptions" }, ...dynamicChildren],
};
}

View File

@ -2,7 +2,7 @@
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { MapPinIcon, PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { AddressForm, type AddressFormProps } from "@/features/catalog/components";
import { AddressForm, type AddressFormProps } from "@/features/services/components";
import type { Address } from "@customer-portal/domain/customer";
import { getCountryName } from "@/lib/constants/countries";

View File

@ -19,7 +19,7 @@ export function useAddressEdit(initial: AddressFormData) {
const requestData = addressFormToRequest(formData);
await accountService.updateAddress(requestData);
// Address changes can affect server-personalized catalog results (eligibility).
await queryClient.invalidateQueries({ queryKey: queryKeys.catalog.all() });
await queryClient.invalidateQueries({ queryKey: queryKeys.services.all() });
},
[queryClient]
);

View File

@ -113,7 +113,7 @@ export function useProfileData() {
phoneCountryCode: next.phoneCountryCode,
});
// Address changes can affect server-personalized catalog results (eligibility).
await queryClient.invalidateQueries({ queryKey: queryKeys.catalog.all() });
await queryClient.invalidateQueries({ queryKey: queryKeys.services.all() });
setBillingInfo({ address: next });
setAddress(next);
return true;

View File

@ -14,7 +14,7 @@ import {
import { useAuthStore } from "@/features/auth/services/auth.store";
import { accountService } from "@/features/account/services/account.service";
import { useProfileEdit } from "@/features/account/hooks/useProfileEdit";
import { AddressForm } from "@/features/catalog/components/base/AddressForm";
import { AddressForm } from "@/features/services/components/base/AddressForm";
import { Button } from "@/components/atoms/button";
import { StatusPill } from "@/components/atoms/status-pill";
import { useAddressEdit } from "@/features/account/hooks/useAddressEdit";

View File

@ -72,7 +72,7 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
clipRule="evenodd"
/>
</svg>
Service #{item.serviceId}
Subscription #{item.serviceId}
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground">
@ -104,7 +104,7 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
if (isLinked) {
return (
<Link key={item.id} href={`/account/services/${item.serviceId}`} className="block">
<Link key={item.id} href={`/account/subscriptions/${item.serviceId}`} className="block">
{itemContent}
</Link>
);

View File

@ -25,7 +25,7 @@ export function InvoiceItemRow({
? "border-blue-200 bg-blue-50 hover:bg-blue-100 cursor-pointer hover:shadow-sm"
: "border-gray-200 bg-gray-50"
}`}
onClick={serviceId ? () => router.push(`/account/services/${serviceId}`) : undefined}
onClick={serviceId ? () => router.push(`/account/subscriptions/${serviceId}`) : undefined}
>
<div className="flex-1 min-w-0">
<div
@ -39,7 +39,7 @@ export function InvoiceItemRow({
)}
{serviceId && (
<div className="text-xs text-blue-700 mt-1 font-medium">
Service #{serviceId} Click to view
Subscription #{serviceId} Click to view
</div>
)}
</div>

View File

@ -1 +0,0 @@
export { catalogService } from "./catalog.service";

View File

@ -1,2 +0,0 @@
export * from "./catalog.utils";
export * from "./pricing";

View File

@ -10,15 +10,15 @@ import { Button } from "@/components/atoms/button";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { InlineToast } from "@/components/atoms/inline-toast";
import { StatusPill } from "@/components/atoms/status-pill";
import { AddressConfirmation } from "@/features/catalog/components/base/AddressConfirmation";
import { AddressConfirmation } from "@/features/services/components/base/AddressConfirmation";
import { useCheckoutStore } from "@/features/checkout/stores/checkout.store";
import { ordersService } from "@/features/orders/services/orders.service";
import { usePaymentMethods } from "@/features/billing/hooks/useBilling";
import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh";
import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscriptions";
import { ACTIVE_INTERNET_SUBSCRIPTION_WARNING } from "@/features/checkout/constants";
import { useInternetEligibility } from "@/features/catalog/hooks/useInternetEligibility";
import { useRequestInternetEligibilityCheck } from "@/features/catalog/hooks/useInternetEligibility";
import { useInternetEligibility } from "@/features/services/hooks/useInternetEligibility";
import { useRequestInternetEligibilityCheck } from "@/features/services/hooks/useInternetEligibility";
import {
useResidenceCardVerification,
useSubmitResidenceCard,

View File

@ -4,7 +4,7 @@ import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { ShoppingCartIcon } from "@heroicons/react/24/outline";
import { Button } from "@/components/atoms/button";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
/**
* EmptyCartRedirect - Shown when checkout is accessed without a cart

View File

@ -40,7 +40,7 @@ export function getActivityNavigationPath(activity: Activity): string | null {
case "invoice_paid":
return `/account/billing/invoices/${activity.relatedId}`;
case "service_activated":
return `/account/services/${activity.relatedId}`;
return `/account/subscriptions/${activity.relatedId}`;
case "case_created":
case "case_closed":
return `/account/support/${activity.relatedId}`;

View File

@ -10,7 +10,7 @@ import { ErrorState } from "@/components/atoms/error-state";
import { PageLayout } from "@/components/templates";
import { cn } from "@/lib/utils";
import { InlineToast } from "@/components/atoms/inline-toast";
import { useInternetEligibility } from "@/features/catalog/hooks";
import { useInternetEligibility } from "@/features/services/hooks";
export function DashboardView() {
const { user, isAuthenticated, loading: authLoading, clearLoading } = useAuthStore();

View File

@ -42,15 +42,15 @@ export function AccountEventsListener() {
const parsed = JSON.parse(event.data) as RealtimeEventEnvelope;
if (!parsed || typeof parsed !== "object") return;
if (parsed.event === "catalog.eligibility.changed") {
logger.info("Received catalog.eligibility.changed; invalidating catalog queries");
void queryClient.invalidateQueries({ queryKey: queryKeys.catalog.all() });
if (parsed.event === "services.eligibility.changed") {
logger.info("Received services.eligibility.changed; invalidating services queries");
void queryClient.invalidateQueries({ queryKey: queryKeys.services.all() });
return;
}
if (parsed.event === "catalog.changed") {
logger.info("Received catalog.changed; invalidating catalog queries");
void queryClient.invalidateQueries({ queryKey: queryKeys.catalog.all() });
if (parsed.event === "services.changed") {
logger.info("Received services.changed; invalidating services queries");
void queryClient.invalidateQueries({ queryKey: queryKeys.services.all() });
return;
}

View File

@ -1,7 +1,7 @@
"use client";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import type { CatalogProductBase } from "@customer-portal/domain/catalog";
import type { CatalogProductBase } from "@customer-portal/domain/services";
interface AddonGroupProps {
addons: Array<CatalogProductBase & { bundledAddonId?: string; isBundledAddon?: boolean }>;
selectedAddonSkus: string[];

View File

@ -156,7 +156,7 @@ export function AddressConfirmation({
const updatedAddress = await accountService.updateAddress(sanitizedAddress);
// Address changes can affect server-personalized catalog results (eligibility).
await queryClient.invalidateQueries({ queryKey: queryKeys.catalog.all() });
await queryClient.invalidateQueries({ queryKey: queryKeys.services.all() });
// Rebuild BillingInfo from updated address
const updatedInfo: BillingInfo = {

View File

@ -10,7 +10,7 @@ import { Button } from "@/components/atoms/button";
import { useRouter } from "next/navigation";
// Align with shared catalog contracts
import type { CatalogProductBase } from "@customer-portal/domain/catalog";
import type { CatalogProductBase } from "@customer-portal/domain/services";
import type { CheckoutTotals } from "@customer-portal/domain/orders";
// Enhanced order item representation for UI summary

View File

@ -1,5 +1,5 @@
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
import type { CatalogProductBase } from "@customer-portal/domain/catalog";
import type { CatalogProductBase } from "@customer-portal/domain/services";
import { useRouter } from "next/navigation";
import { Button } from "@/components/atoms/button";

View File

@ -3,7 +3,7 @@
import { ReactNode } from "react";
import { CurrencyYenIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
import { Formatting } from "@customer-portal/domain/toolkit";
import type { PricingTier } from "@customer-portal/domain/catalog";
import type { PricingTier } from "@customer-portal/domain/services";
const { formatCurrency } = Formatting;

View File

@ -1,7 +1,7 @@
"use client";
import type { InternetInstallationCatalogItem } from "@customer-portal/domain/catalog";
import { CardPricing } from "@/features/catalog/components/base/CardPricing";
import type { InternetInstallationCatalogItem } from "@customer-portal/domain/services";
import { CardPricing } from "@/features/services/components/base/CardPricing";
interface InstallationOptionsProps {
installations: InternetInstallationCatalogItem[];

View File

@ -1,7 +1,7 @@
"use client";
import { InternetConfigureContainer } from "./configure";
import type { UseInternetConfigureResult } from "@/features/catalog/hooks/useInternetConfigure";
import type { UseInternetConfigureResult } from "@/features/services/hooks/useInternetConfigure";
interface Props extends UseInternetConfigureResult {
onConfirm: () => void;

View File

@ -2,7 +2,7 @@
import { Home, Building2, Zap } from "lucide-react";
import { Button } from "@/components/atoms/button";
import { CardBadge } from "@/features/catalog/components/base/CardBadge";
import { CardBadge } from "@/features/services/components/base/CardBadge";
import { cn } from "@/lib/utils";
interface TierInfo {

View File

@ -6,15 +6,15 @@ import { ArrowRightIcon, CheckIcon } from "@heroicons/react/24/outline";
import type {
InternetPlanCatalogItem,
InternetInstallationCatalogItem,
} from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import { useRouter } from "next/navigation";
import { CardPricing } from "@/features/catalog/components/base/CardPricing";
import { CardBadge } from "@/features/catalog/components/base/CardBadge";
import type { BadgeVariant } from "@/features/catalog/components/base/CardBadge";
import { useCatalogStore } from "@/features/catalog/services/catalog.store";
import { CardPricing } from "@/features/services/components/base/CardPricing";
import { CardBadge } from "@/features/services/components/base/CardBadge";
import type { BadgeVariant } from "@/features/services/components/base/CardBadge";
import { useCatalogStore } from "@/features/services/services/services.store";
import { IS_DEVELOPMENT } from "@/config/environment";
import { parsePlanName } from "@/features/catalog/components/internet/utils/planName";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { parsePlanName } from "@/features/services/components/internet/utils/planName";
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
interface InternetPlanCardProps {
plan: InternetPlanCatalogItem;

View File

@ -2,10 +2,10 @@
import { BoltIcon } from "@heroicons/react/24/outline";
import { Button } from "@/components/atoms/button";
import { CardBadge } from "@/features/catalog/components/base/CardBadge";
import { CardBadge } from "@/features/services/components/base/CardBadge";
import { cn } from "@/lib/utils";
import type { TierInfo } from "@/features/catalog/components/internet/InternetOfferingCard";
import { InternetModalShell } from "@/features/catalog/components/internet/InternetModalShell";
import type { TierInfo } from "@/features/services/components/internet/InternetOfferingCard";
import { InternetModalShell } from "@/features/services/components/internet/InternetModalShell";
interface InternetTierPricingModalProps {
isOpen: boolean;

View File

@ -2,10 +2,10 @@
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { Button } from "@/components/atoms/button";
import { CardBadge } from "@/features/catalog/components/base/CardBadge";
import type { BadgeVariant } from "@/features/catalog/components/base/CardBadge";
import { parsePlanName } from "@/features/catalog/components/internet/utils/planName";
import type { InternetPlanCatalogItem } from "@customer-portal/domain/catalog";
import { CardBadge } from "@/features/services/components/base/CardBadge";
import type { BadgeVariant } from "@/features/services/components/base/CardBadge";
import { parsePlanName } from "@/features/services/components/internet/utils/planName";
import type { InternetPlanCatalogItem } from "@customer-portal/domain/services";
interface PlanHeaderProps {
plan: InternetPlanCatalogItem;

View File

@ -3,7 +3,7 @@
import { useState } from "react";
import { ChevronDown, ChevronUp, Home, Building2, Zap, Info, X } from "lucide-react";
import { Button } from "@/components/atoms/button";
import { CardBadge } from "@/features/catalog/components/base/CardBadge";
import { CardBadge } from "@/features/services/components/base/CardBadge";
import { cn } from "@/lib/utils";
interface TierInfo {

View File

@ -8,7 +8,7 @@ import type {
InternetPlanCatalogItem,
InternetInstallationCatalogItem,
InternetAddonCatalogItem,
} from "@customer-portal/domain/catalog";
} from "@customer-portal/domain/services";
import type { AccessModeValue } from "@customer-portal/domain/orders";
import { ConfigureLoadingSkeleton } from "./components/ConfigureLoadingSkeleton";
import { ServiceConfigurationStep } from "./steps/ServiceConfigurationStep";
@ -16,8 +16,8 @@ import { InstallationStep } from "./steps/InstallationStep";
import { AddonsStep } from "./steps/AddonsStep";
import { ReviewOrderStep } from "./steps/ReviewOrderStep";
import { useConfigureState } from "./hooks/useConfigureState";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { PlanHeader } from "@/features/catalog/components/internet/PlanHeader";
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
import { PlanHeader } from "@/features/services/components/internet/PlanHeader";
interface Props {
plan: InternetPlanCatalogItem | null;

Some files were not shown because too many files have changed in this diff Show More