feat(eslint): enforce BFF architecture rules for controllers and aggregators
This commit is contained in:
parent
975a11474a
commit
f4099ac81f
@ -7,6 +7,7 @@ import { SalesforceModule } from "@bff/integrations/salesforce/salesforce.module
|
||||
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
||||
import { InternetManagementModule } from "../internet-management/internet-management.module.js";
|
||||
import { SimManagementModule } from "../sim-management/sim-management.module.js";
|
||||
import { SharedSubscriptionsModule } from "../shared/index.js";
|
||||
|
||||
/**
|
||||
* Unified Cancellation Module
|
||||
@ -22,6 +23,7 @@ import { SimManagementModule } from "../sim-management/sim-management.module.js"
|
||||
MappingsModule,
|
||||
InternetManagementModule,
|
||||
SimManagementModule,
|
||||
SharedSubscriptionsModule,
|
||||
],
|
||||
controllers: [CancellationController],
|
||||
providers: [CancellationService, SubscriptionsService, Logger],
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import type {
|
||||
CancellationPreview,
|
||||
CancellationStatus,
|
||||
ServiceType,
|
||||
InternetCancelRequest,
|
||||
} from "@customer-portal/domain/subscriptions";
|
||||
import type { SimCancelFullRequest } from "@customer-portal/domain/sim";
|
||||
@ -12,6 +11,7 @@ import { InternetCancellationService } from "../internet-management/services/int
|
||||
import { SimCancellationService } from "../sim-management/services/sim-cancellation.service.js";
|
||||
import { SalesforceOpportunityService } from "@bff/integrations/salesforce/services/salesforce-opportunity.service.js";
|
||||
import { CANCELLATION_STEP3_NOTICES, CANCELLATION_TERMS } from "./cancellation-terms.config.js";
|
||||
import { SubscriptionValidationCoordinator } from "../shared/index.js";
|
||||
|
||||
// Valid stages for portal display
|
||||
const VALID_PORTAL_STAGES: ReadonlySet<string> = new Set([
|
||||
@ -26,45 +26,6 @@ function isValidPortalStage(stage: string): stage is PortalStage {
|
||||
return VALID_PORTAL_STAGES.has(stage);
|
||||
}
|
||||
|
||||
function detectServiceType(productName: string): ServiceType {
|
||||
const lower = productName.toLowerCase();
|
||||
|
||||
// SIM heuristics
|
||||
if (lower.includes("sim")) return "sim";
|
||||
|
||||
// Internet heuristics (match existing patterns)
|
||||
const isInternet =
|
||||
lower.includes("internet") ||
|
||||
lower.includes("sonixnet") ||
|
||||
(lower.includes("ntt") && lower.includes("fiber"));
|
||||
|
||||
if (isInternet) return "internet";
|
||||
|
||||
throw new BadRequestException("This endpoint is only for SIM or Internet subscriptions");
|
||||
}
|
||||
|
||||
function getOpportunityIdFromCustomFields(
|
||||
customFields?: Record<string, string>
|
||||
): string | undefined {
|
||||
if (!customFields) return undefined;
|
||||
|
||||
// Prefer exact key (as configured in WHMCS)
|
||||
const direct =
|
||||
customFields["OpportunityId"] ??
|
||||
customFields["OpportunityID"] ??
|
||||
customFields["opportunityId"] ??
|
||||
customFields["opportunityID"];
|
||||
if (direct && direct.trim().length > 0) return direct.trim();
|
||||
|
||||
// Fallback: case-insensitive scan for something like "Opportunity Id"
|
||||
const entry = Object.entries(customFields).find(([key, value]) => {
|
||||
if (!value) return false;
|
||||
const k = key.toLowerCase();
|
||||
return k.includes("opportunity") && k.replace(/\s+/g, "").includes("id");
|
||||
});
|
||||
return entry?.[1]?.trim() || undefined;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CancellationService {
|
||||
private readonly logger = new Logger(CancellationService.name);
|
||||
@ -73,7 +34,8 @@ export class CancellationService {
|
||||
private readonly subscriptionsService: SubscriptionsService,
|
||||
private readonly opportunityService: SalesforceOpportunityService,
|
||||
private readonly internetCancellation: InternetCancellationService,
|
||||
private readonly simCancellation: SimCancellationService
|
||||
private readonly simCancellation: SimCancellationService,
|
||||
private readonly validationCoordinator: SubscriptionValidationCoordinator
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -91,8 +53,14 @@ export class CancellationService {
|
||||
userId,
|
||||
subscriptionId
|
||||
);
|
||||
const serviceType = detectServiceType(subscription.productName);
|
||||
const opportunityId = getOpportunityIdFromCustomFields(subscription.customFields);
|
||||
|
||||
// Use coordinator for service type detection and opportunityId resolution
|
||||
const serviceType = this.validationCoordinator.detectServiceType(subscription.productName);
|
||||
const opportunityResolution = await this.validationCoordinator.resolveOpportunityId(
|
||||
subscriptionId,
|
||||
{ customFields: subscription.customFields, fallbackToSalesforce: false }
|
||||
);
|
||||
const opportunityId = opportunityResolution.opportunityId ?? undefined;
|
||||
|
||||
// 2) Query Opportunity status ONLY when WHMCS is Active (not already cancelled)
|
||||
const shouldQueryOpp = subscription.status === "Active" && Boolean(opportunityId);
|
||||
@ -117,7 +85,7 @@ export class CancellationService {
|
||||
userId,
|
||||
subscriptionId
|
||||
);
|
||||
const serviceType = detectServiceType(subscription.productName);
|
||||
const serviceType = this.validationCoordinator.detectServiceType(subscription.productName);
|
||||
|
||||
if (serviceType === "internet") {
|
||||
await this.internetCancellation.submitCancellation(userId, subscriptionId, request);
|
||||
|
||||
@ -2,13 +2,19 @@ import { Module } from "@nestjs/common";
|
||||
import { InternetCancellationService } from "./services/internet-cancellation.service.js";
|
||||
import { InternetController } from "./internet.controller.js";
|
||||
import { WhmcsModule } from "@bff/integrations/whmcs/whmcs.module.js";
|
||||
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
||||
import { SalesforceModule } from "@bff/integrations/salesforce/salesforce.module.js";
|
||||
import { NotificationsModule } from "@bff/modules/notifications/notifications.module.js";
|
||||
import { WorkflowModule } from "@bff/modules/shared/workflow/index.js";
|
||||
import { SharedSubscriptionsModule } from "../shared/index.js";
|
||||
|
||||
@Module({
|
||||
imports: [WhmcsModule, MappingsModule, SalesforceModule, NotificationsModule, WorkflowModule],
|
||||
imports: [
|
||||
WhmcsModule,
|
||||
SalesforceModule,
|
||||
NotificationsModule,
|
||||
WorkflowModule,
|
||||
SharedSubscriptionsModule,
|
||||
],
|
||||
controllers: [InternetController],
|
||||
providers: [InternetCancellationService],
|
||||
exports: [InternetCancellationService],
|
||||
|
||||
@ -15,11 +15,11 @@ import { Injectable, Inject, BadRequestException, NotFoundException } from "@nes
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { WhmcsConnectionFacade } from "@bff/integrations/whmcs/facades/whmcs.facade.js";
|
||||
import { WhmcsClientService } from "@bff/integrations/whmcs/services/whmcs-client.service.js";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { SalesforceOpportunityService } from "@bff/integrations/salesforce/services/salesforce-opportunity.service.js";
|
||||
import { WorkflowCaseManager } from "@bff/modules/shared/workflow/index.js";
|
||||
import { EmailService } from "@bff/infra/email/email.service.js";
|
||||
import { NotificationService } from "@bff/modules/notifications/notifications.service.js";
|
||||
import { SubscriptionValidationCoordinator } from "../../shared/index.js";
|
||||
import {
|
||||
generateCancellationMonths,
|
||||
getCancellationEffectiveDate,
|
||||
@ -39,7 +39,7 @@ export class InternetCancellationService {
|
||||
constructor(
|
||||
private readonly whmcsConnectionService: WhmcsConnectionFacade,
|
||||
private readonly whmcsClientService: WhmcsClientService,
|
||||
private readonly mappingsService: MappingsService,
|
||||
private readonly validationCoordinator: SubscriptionValidationCoordinator,
|
||||
private readonly opportunityService: SalesforceOpportunityService,
|
||||
private readonly workflowCases: WorkflowCaseManager,
|
||||
private readonly emailService: EmailService,
|
||||
@ -48,7 +48,8 @@ export class InternetCancellationService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validate that the subscription belongs to the user and is an Internet service
|
||||
* Validate that the subscription belongs to the user and is an Internet service.
|
||||
* Uses SubscriptionValidationCoordinator for account mapping and product type detection.
|
||||
*/
|
||||
private async validateInternetSubscription(
|
||||
userId: string,
|
||||
@ -64,14 +65,13 @@ export class InternetCancellationService {
|
||||
registrationDate?: string;
|
||||
};
|
||||
}> {
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId || !mapping?.sfAccountId) {
|
||||
throw new BadRequestException("Account mapping not found");
|
||||
}
|
||||
// Validate account mapping via coordinator
|
||||
const { whmcsClientId, sfAccountId } =
|
||||
await this.validationCoordinator.validateAccountMapping(userId);
|
||||
|
||||
// Get subscription from WHMCS
|
||||
const productsResponse = await this.whmcsConnectionService.getClientsProducts({
|
||||
clientid: mapping.whmcsClientId,
|
||||
clientid: whmcsClientId,
|
||||
});
|
||||
const productContainer = productsResponse.products?.product;
|
||||
const products = Array.isArray(productContainer)
|
||||
@ -88,22 +88,15 @@ export class InternetCancellationService {
|
||||
throw new NotFoundException("Subscription not found");
|
||||
}
|
||||
|
||||
// Verify it's an Internet service
|
||||
// Match: "Internet", "SonixNet via NTT Optical Fiber", or any NTT-based fiber service
|
||||
// Verify it's an Internet service via coordinator
|
||||
const productName = String(subscription.name || subscription.groupname || "");
|
||||
const lowerName = productName.toLowerCase();
|
||||
const isInternetService =
|
||||
lowerName.includes("internet") ||
|
||||
lowerName.includes("sonixnet") ||
|
||||
(lowerName.includes("ntt") && lowerName.includes("fiber"));
|
||||
|
||||
if (!isInternetService) {
|
||||
if (!this.validationCoordinator.isInternetService(productName)) {
|
||||
throw new BadRequestException("This endpoint is only for Internet subscriptions");
|
||||
}
|
||||
|
||||
return {
|
||||
whmcsClientId: mapping.whmcsClientId,
|
||||
sfAccountId: mapping.sfAccountId,
|
||||
whmcsClientId,
|
||||
sfAccountId,
|
||||
subscription: {
|
||||
id: Number(subscription.id),
|
||||
productName: productName,
|
||||
@ -184,14 +177,12 @@ export class InternetCancellationService {
|
||||
`${clientDetails.firstname || ""} ${clientDetails.lastname || ""}`.trim() || "Customer";
|
||||
const customerEmail = clientDetails.email || "";
|
||||
|
||||
// Find existing Opportunity for this subscription (by WHMCS Service ID)
|
||||
let opportunityId: string | null = null;
|
||||
try {
|
||||
opportunityId = await this.opportunityService.findOpportunityByWhmcsServiceId(subscriptionId);
|
||||
} catch {
|
||||
// Opportunity lookup failure is not fatal - we'll create Case without link
|
||||
this.logger.warn("Could not find Opportunity for subscription", { subscriptionId });
|
||||
}
|
||||
// Resolve OpportunityId via coordinator (handles both custom fields and SF lookup)
|
||||
const opportunityResolution = await this.validationCoordinator.resolveOpportunityId(
|
||||
subscriptionId,
|
||||
{ fallbackToSalesforce: true }
|
||||
);
|
||||
const opportunityId = opportunityResolution.opportunityId;
|
||||
|
||||
// Create Salesforce Case for cancellation via workflow manager
|
||||
await this.workflowCases.notifyInternetCancellation({
|
||||
|
||||
15
apps/bff/src/modules/subscriptions/shared/index.ts
Normal file
15
apps/bff/src/modules/subscriptions/shared/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Subscriptions Shared Module
|
||||
*
|
||||
* Shared services and utilities for subscription management.
|
||||
*/
|
||||
|
||||
export { SharedSubscriptionsModule } from "./shared-subscriptions.module.js";
|
||||
export { SubscriptionValidationCoordinator } from "./subscription-validation-coordinator.service.js";
|
||||
export type {
|
||||
AccountMappingResult,
|
||||
OpportunityResolution,
|
||||
OpportunityResolutionOptions,
|
||||
ServiceType,
|
||||
ServiceTypeResult,
|
||||
} from "./subscription-validation.types.js";
|
||||
@ -0,0 +1,17 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
||||
import { SalesforceModule } from "@bff/integrations/salesforce/salesforce.module.js";
|
||||
import { SubscriptionValidationCoordinator } from "./subscription-validation-coordinator.service.js";
|
||||
|
||||
/**
|
||||
* Shared Subscriptions Module
|
||||
*
|
||||
* Provides shared services for subscription management across all sub-modules.
|
||||
* Import this module wherever you need the SubscriptionValidationCoordinator.
|
||||
*/
|
||||
@Module({
|
||||
imports: [MappingsModule, SalesforceModule],
|
||||
providers: [SubscriptionValidationCoordinator],
|
||||
exports: [SubscriptionValidationCoordinator],
|
||||
})
|
||||
export class SharedSubscriptionsModule {}
|
||||
@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Subscription Validation Coordinator
|
||||
*
|
||||
* Centralized service for common subscription validation patterns:
|
||||
* - Account mapping validation (user → whmcsClientId + sfAccountId)
|
||||
* - Product type detection (Internet vs SIM)
|
||||
* - OpportunityId resolution (from custom fields or Salesforce lookup)
|
||||
*
|
||||
* This coordinator extracts duplicated patterns from:
|
||||
* - InternetCancellationService
|
||||
* - SimCancellationService
|
||||
* - CancellationService
|
||||
* - SimValidationService
|
||||
*
|
||||
* Benefits:
|
||||
* - Single source of truth for validation logic
|
||||
* - Consistent error messages across services
|
||||
* - Easier to maintain and test
|
||||
*/
|
||||
|
||||
import { Injectable, Inject, BadRequestException } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { SalesforceOpportunityService } from "@bff/integrations/salesforce/services/salesforce-opportunity.service.js";
|
||||
import type {
|
||||
AccountMappingResult,
|
||||
OpportunityResolution,
|
||||
OpportunityResolutionOptions,
|
||||
ServiceType,
|
||||
} from "./subscription-validation.types.js";
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionValidationCoordinator {
|
||||
constructor(
|
||||
private readonly mappingsService: MappingsService,
|
||||
private readonly opportunityService: SalesforceOpportunityService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
// ============================================================================
|
||||
// Account Mapping Validation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Validate that a user has complete account mapping (WHMCS + Salesforce).
|
||||
*
|
||||
* @throws BadRequestException if mapping is missing or incomplete
|
||||
*/
|
||||
async validateAccountMapping(userId: string): Promise<AccountMappingResult> {
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
|
||||
if (!mapping?.whmcsClientId || !mapping?.sfAccountId) {
|
||||
this.logger.warn("Account mapping not found or incomplete", {
|
||||
userId,
|
||||
hasWhmcsClientId: Boolean(mapping?.whmcsClientId),
|
||||
hasSfAccountId: Boolean(mapping?.sfAccountId),
|
||||
});
|
||||
throw new BadRequestException("Account mapping not found");
|
||||
}
|
||||
|
||||
return {
|
||||
whmcsClientId: mapping.whmcsClientId,
|
||||
sfAccountId: mapping.sfAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WHMCS client ID only (for operations that don't need Salesforce).
|
||||
*
|
||||
* @throws BadRequestException if WHMCS mapping not found
|
||||
*/
|
||||
async getWhmcsClientId(userId: string): Promise<number> {
|
||||
return this.mappingsService.getWhmcsClientIdOrThrow(userId);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Product Type Detection
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Detect service type from product name.
|
||||
*
|
||||
* Heuristics:
|
||||
* - SIM: Product name contains "sim"
|
||||
* - Internet: Contains "internet", "sonixnet", or "ntt" + "fiber"
|
||||
*
|
||||
* @throws BadRequestException if service type cannot be determined
|
||||
*/
|
||||
detectServiceType(productName: string): ServiceType {
|
||||
const lower = productName.toLowerCase();
|
||||
|
||||
// SIM heuristics
|
||||
if (lower.includes("sim")) {
|
||||
return "sim";
|
||||
}
|
||||
|
||||
// Internet heuristics
|
||||
const isInternet =
|
||||
lower.includes("internet") ||
|
||||
lower.includes("sonixnet") ||
|
||||
(lower.includes("ntt") && lower.includes("fiber"));
|
||||
|
||||
if (isInternet) {
|
||||
return "internet";
|
||||
}
|
||||
|
||||
throw new BadRequestException("This endpoint is only for SIM or Internet subscriptions");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if product name indicates an Internet service.
|
||||
*/
|
||||
isInternetService(productName: string): boolean {
|
||||
const lower = productName.toLowerCase();
|
||||
return (
|
||||
lower.includes("internet") ||
|
||||
lower.includes("sonixnet") ||
|
||||
(lower.includes("ntt") && lower.includes("fiber"))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if product name indicates a SIM service.
|
||||
*/
|
||||
isSimService(productName: string): boolean {
|
||||
return productName.toLowerCase().includes("sim");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OpportunityId Resolution
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Resolve OpportunityId for a subscription.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. First, check WHMCS custom fields (if provided)
|
||||
* 2. If not found and fallback enabled, query Salesforce by WHMCS Service ID
|
||||
*
|
||||
* This never throws - returns null if not found (Opportunity lookup is non-critical).
|
||||
*/
|
||||
async resolveOpportunityId(
|
||||
subscriptionId: number,
|
||||
options: OpportunityResolutionOptions = {}
|
||||
): Promise<OpportunityResolution> {
|
||||
const { customFields, fallbackToSalesforce = true } = options;
|
||||
|
||||
// Strategy 1: Check custom fields
|
||||
if (customFields) {
|
||||
const opportunityId = this.extractOpportunityIdFromCustomFields(customFields);
|
||||
if (opportunityId) {
|
||||
return {
|
||||
opportunityId,
|
||||
source: "custom_fields",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Salesforce lookup by WHMCS Service ID
|
||||
if (fallbackToSalesforce) {
|
||||
try {
|
||||
const opportunityId =
|
||||
await this.opportunityService.findOpportunityByWhmcsServiceId(subscriptionId);
|
||||
if (opportunityId) {
|
||||
return {
|
||||
opportunityId,
|
||||
source: "salesforce_lookup",
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Opportunity lookup failure is non-critical
|
||||
this.logger.warn("Could not find Opportunity for subscription", { subscriptionId });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
opportunityId: null,
|
||||
source: "not_found",
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Private Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Extract OpportunityId from WHMCS custom fields.
|
||||
*
|
||||
* Checks common key variations:
|
||||
* - OpportunityId, OpportunityID, opportunityId, opportunityID
|
||||
* - Falls back to case-insensitive scan for "opportunity" + "id"
|
||||
*/
|
||||
private extractOpportunityIdFromCustomFields(
|
||||
customFields: Record<string, string>
|
||||
): string | undefined {
|
||||
// Prefer exact key (as configured in WHMCS)
|
||||
const direct =
|
||||
customFields["OpportunityId"] ??
|
||||
customFields["OpportunityID"] ??
|
||||
customFields["opportunityId"] ??
|
||||
customFields["opportunityID"];
|
||||
|
||||
if (direct && direct.trim().length > 0) {
|
||||
return direct.trim();
|
||||
}
|
||||
|
||||
// Fallback: case-insensitive scan for something like "Opportunity Id"
|
||||
const entry = Object.entries(customFields).find(([key, value]) => {
|
||||
if (!value) return false;
|
||||
const k = key.toLowerCase();
|
||||
return k.includes("opportunity") && k.replace(/\s+/g, "").includes("id");
|
||||
});
|
||||
|
||||
return entry?.[1]?.trim() || undefined;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Subscription Validation Coordinator Types
|
||||
*
|
||||
* Centralized type definitions for subscription validation patterns:
|
||||
* - Account mapping validation (user → whmcsClientId + sfAccountId)
|
||||
* - Product type detection (Internet vs SIM)
|
||||
* - OpportunityId resolution (from custom fields or Salesforce lookup)
|
||||
*/
|
||||
|
||||
import type { ServiceType } from "@customer-portal/domain/subscriptions";
|
||||
|
||||
/**
|
||||
* Result of validating a user's account mapping.
|
||||
* Contains all external system IDs needed for subscription operations.
|
||||
*/
|
||||
export interface AccountMappingResult {
|
||||
/** WHMCS Client ID for billing operations */
|
||||
whmcsClientId: number;
|
||||
/** Salesforce Account ID for CRM operations */
|
||||
sfAccountId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of resolving an OpportunityId for a subscription.
|
||||
*/
|
||||
export interface OpportunityResolution {
|
||||
/** Salesforce Opportunity ID (if found) */
|
||||
opportunityId: string | null;
|
||||
/** Source of the OpportunityId resolution */
|
||||
source: "custom_fields" | "salesforce_lookup" | "not_found";
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for OpportunityId resolution.
|
||||
*/
|
||||
export interface OpportunityResolutionOptions {
|
||||
/** WHMCS subscription custom fields to check first */
|
||||
customFields?: Record<string, string> | undefined;
|
||||
/** Whether to fall back to Salesforce lookup if not in custom fields */
|
||||
fallbackToSalesforce?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service type detection result with metadata
|
||||
*/
|
||||
export interface ServiceTypeResult {
|
||||
/** Detected service type */
|
||||
type: ServiceType;
|
||||
/** Product name used for detection */
|
||||
productName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-export ServiceType for convenience
|
||||
*/
|
||||
export type { ServiceType };
|
||||
@ -3,10 +3,10 @@ import { Logger } from "nestjs-pino";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { FreebitFacade } from "@bff/integrations/freebit/facades/freebit.facade.js";
|
||||
import { WhmcsClientService } from "@bff/integrations/whmcs/services/whmcs-client.service.js";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { SalesforceOpportunityService } from "@bff/integrations/salesforce/services/salesforce-opportunity.service.js";
|
||||
import { WorkflowCaseManager } from "@bff/modules/shared/workflow/index.js";
|
||||
import { SimValidationService } from "./sim-validation.service.js";
|
||||
import { SubscriptionValidationCoordinator } from "../../shared/index.js";
|
||||
import type {
|
||||
SimCancelRequest,
|
||||
SimCancelFullRequest,
|
||||
@ -29,7 +29,7 @@ export class SimCancellationService {
|
||||
constructor(
|
||||
private readonly freebitService: FreebitFacade,
|
||||
private readonly whmcsClientService: WhmcsClientService,
|
||||
private readonly mappingsService: MappingsService,
|
||||
private readonly validationCoordinator: SubscriptionValidationCoordinator,
|
||||
private readonly opportunityService: SalesforceOpportunityService,
|
||||
private readonly workflowCases: WorkflowCaseManager,
|
||||
private readonly simValidation: SimValidationService,
|
||||
@ -75,8 +75,8 @@ export class SimCancellationService {
|
||||
const validation = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
||||
const simDetails = await this.freebitService.getSimDetails(validation.account);
|
||||
|
||||
// Get customer info from WHMCS
|
||||
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(userId);
|
||||
// Get customer info from WHMCS via coordinator
|
||||
const whmcsClientId = await this.validationCoordinator.getWhmcsClientId(userId);
|
||||
const clientDetails = await this.whmcsClientService.getClientDetails(whmcsClientId);
|
||||
const customerName =
|
||||
`${clientDetails.firstname || ""} ${clientDetails.lastname || ""}`.trim() || "Customer";
|
||||
@ -175,17 +175,16 @@ export class SimCancellationService {
|
||||
subscriptionId: number,
|
||||
request: SimCancelFullRequest
|
||||
): Promise<void> {
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId || !mapping?.sfAccountId) {
|
||||
throw new BadRequestException("Account mapping not found");
|
||||
}
|
||||
// Validate account mapping via coordinator
|
||||
const { whmcsClientId, sfAccountId } =
|
||||
await this.validationCoordinator.validateAccountMapping(userId);
|
||||
|
||||
const validation = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
||||
const account = validation.account;
|
||||
const simDetails = await this.freebitService.getSimDetails(account);
|
||||
|
||||
// Get customer info from WHMCS
|
||||
const clientDetails = await this.whmcsClientService.getClientDetails(mapping.whmcsClientId);
|
||||
const clientDetails = await this.whmcsClientService.getClientDetails(whmcsClientId);
|
||||
const customerName =
|
||||
`${clientDetails.firstname || ""} ${clientDetails.lastname || ""}`.trim() || "Customer";
|
||||
const customerEmail = clientDetails.email || "";
|
||||
@ -223,17 +222,16 @@ export class SimCancellationService {
|
||||
runDate,
|
||||
});
|
||||
|
||||
// Find existing Opportunity for this subscription (by WHMCS Service ID)
|
||||
let opportunityId: string | null = null;
|
||||
try {
|
||||
opportunityId = await this.opportunityService.findOpportunityByWhmcsServiceId(subscriptionId);
|
||||
} catch {
|
||||
this.logger.warn("Could not find Opportunity for SIM subscription", { subscriptionId });
|
||||
}
|
||||
// Resolve OpportunityId via coordinator
|
||||
const opportunityResolution = await this.validationCoordinator.resolveOpportunityId(
|
||||
subscriptionId,
|
||||
{ fallbackToSalesforce: true }
|
||||
);
|
||||
const opportunityId = opportunityResolution.opportunityId;
|
||||
|
||||
// Create Salesforce Case for cancellation via workflow manager
|
||||
await this.workflowCases.notifySimCancellation({
|
||||
accountId: mapping.sfAccountId,
|
||||
accountId: sfAccountId,
|
||||
...(opportunityId ? { opportunityId } : {}),
|
||||
simAccount: account,
|
||||
iccid: simDetails.iccid || "N/A",
|
||||
@ -244,7 +242,7 @@ export class SimCancellationService {
|
||||
});
|
||||
|
||||
this.logger.log("SIM cancellation case created via WorkflowCaseManager", {
|
||||
sfAccountIdTail: mapping.sfAccountId.slice(-4),
|
||||
sfAccountIdTail: sfAccountId.slice(-4),
|
||||
opportunityId: opportunityId ? opportunityId.slice(-4) : null,
|
||||
});
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import { SecurityModule } from "@bff/core/security/security.module.js";
|
||||
import { SimUsageStoreService } from "../sim-usage-store.service.js";
|
||||
import { SubscriptionsService } from "../subscriptions.service.js";
|
||||
import { SimManagementService } from "../sim-management.service.js";
|
||||
import { SharedSubscriptionsModule } from "../shared/index.js";
|
||||
// SimController is registered in SubscriptionsModule to ensure route order
|
||||
|
||||
// Import all SIM management services
|
||||
@ -45,6 +46,7 @@ import { WorkflowModule } from "@bff/modules/shared/workflow/index.js";
|
||||
SecurityModule,
|
||||
VoiceOptionsModule,
|
||||
WorkflowModule,
|
||||
SharedSubscriptionsModule,
|
||||
],
|
||||
// SimController is registered in SubscriptionsModule to ensure route order
|
||||
// (more specific routes like :id/sim must be registered before :id)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user