Enhance ESLint Rules and Refactor Domain Imports

- Updated ESLint configuration to enforce stricter import rules for the @customer-portal/domain package, promoting better import hygiene and preventing deep imports.
- Refactored various files across the BFF and portal applications to comply with the new import rules, ensuring that only the appropriate modules are imported from the domain.
- Cleaned up unused imports and optimized code structure for improved maintainability and clarity.
- Updated documentation to reflect changes in import practices and domain structure.
This commit is contained in:
barsa 2025-12-26 14:53:03 +09:00
parent fcc9bc247e
commit a3dbd07183
70 changed files with 597 additions and 389 deletions

View File

@ -6,4 +6,5 @@ alwaysApply: true
1. Have types and validation in the shared domain layer.
2. Keep business logic out of the frontend; use services and APIs instead.
3. Reuse existing types and functions; extend them when additional behavior is needed.
4. Follow the established folder structures documented in docs/STRUCTURE.md.
4. Read and understand the structures and workflows documented in docs. Start from docs/README.md
5. Follow structures and our codebase rules inside docs/development directory

View File

@ -9,7 +9,7 @@
import type { User as PrismaUser } from "@prisma/client";
import type { UserAuth } from "@customer-portal/domain/customer";
import { Providers as CustomerProviders } from "@customer-portal/domain/customer";
import * as CustomerProviders from "@customer-portal/domain/customer/providers";
type PrismaUserRaw = Parameters<typeof CustomerProviders.Portal.mapPrismaUserToUserAuth>[0];

View File

@ -9,7 +9,7 @@ import {
type SalesforceAccountPortalUpdate,
} from "./services/salesforce-account.service.js";
import { SalesforceOperationException } from "@bff/core/exceptions/domain-exceptions.js";
import type { SalesforceOrderRecord } from "@customer-portal/domain/orders";
import type { SalesforceOrderRecord } from "@customer-portal/domain/orders/providers";
/**
* Salesforce Service - Facade for Salesforce operations

View File

@ -3,8 +3,8 @@ import { Logger } from "nestjs-pino";
import { ConfigService } from "@nestjs/config";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { SalesforceConnection } from "./salesforce-connection.service.js";
import type { SalesforceAccountRecord } from "@customer-portal/domain/customer";
import type { SalesforceResponse } from "@customer-portal/domain/common";
import type { SalesforceAccountRecord } from "@customer-portal/domain/customer/providers";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import { customerNumberSchema, salesforceIdSchema } from "@customer-portal/domain/common";
/**

View File

@ -14,16 +14,15 @@ import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "./salesforce-connection.service.js";
import { assertSalesforceId } from "../utils/soql.util.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import type { SalesforceResponse } from "@customer-portal/domain/common";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import type { SupportCase, CreateCaseRequest } from "@customer-portal/domain/support";
import type { SalesforceCaseRecord } from "@customer-portal/domain/support/providers";
import {
type SupportCase,
type SalesforceCaseRecord,
type CreateCaseRequest,
SALESFORCE_CASE_ORIGIN,
SALESFORCE_CASE_STATUS,
SALESFORCE_CASE_PRIORITY,
Providers,
} from "@customer-portal/domain/support";
} from "@customer-portal/domain/support/providers";
import * as Providers from "@customer-portal/domain/support/providers";
// Access the mapper directly to avoid unbound method issues
const salesforceMapper = Providers.Salesforce;

View File

@ -21,7 +21,7 @@ import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "./salesforce-connection.service.js";
import { assertSalesforceId } from "../utils/soql.util.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import type { SalesforceResponse } from "@customer-portal/domain/common";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import {
type OpportunityStageValue,
type OpportunityProductTypeValue,

View File

@ -16,14 +16,13 @@ import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "./salesforce-connection.service.js";
import { assertSalesforceId, buildInClause } from "../utils/soql.util.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import {
Providers as OrderProviders,
type OrderDetails,
type OrderSummary,
type SalesforceOrderRecord,
type SalesforceOrderItemRecord,
} from "@customer-portal/domain/orders";
import type { SalesforceResponse } from "@customer-portal/domain/common";
import type { OrderDetails, OrderSummary } from "@customer-portal/domain/orders";
import type {
SalesforceOrderItemRecord,
SalesforceOrderRecord,
} from "@customer-portal/domain/orders/providers";
import * as OrderProviders from "@customer-portal/domain/orders/providers";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import { OrderFieldMapService } from "@bff/modules/orders/config/order-field-map.service.js";
/**

View File

@ -14,7 +14,7 @@ import type {
WhmcsAddClientResponse,
WhmcsValidateLoginResponse,
WhmcsSsoResponse,
} from "@customer-portal/domain/customer";
} from "@customer-portal/domain/customer/providers";
import type {
WhmcsGetInvoicesParams,
WhmcsCreateInvoiceParams,
@ -25,16 +25,18 @@ import type {
WhmcsCreateInvoiceResponse,
WhmcsUpdateInvoiceResponse,
WhmcsCapturePaymentResponse,
} from "@customer-portal/domain/billing";
import type { WhmcsGetPayMethodsParams } from "@customer-portal/domain/payments";
} from "@customer-portal/domain/billing/providers";
import type { WhmcsGetPayMethodsParams } from "@customer-portal/domain/payments/providers";
import type {
WhmcsPaymentMethodListResponse,
WhmcsPaymentGatewayListResponse,
} 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/services";
import type { WhmcsErrorResponse } from "@customer-portal/domain/common";
} from "@customer-portal/domain/payments/providers";
import type {
WhmcsGetClientsProductsParams,
WhmcsProductListResponse,
} from "@customer-portal/domain/subscriptions/providers";
import type { WhmcsCatalogProductListResponse } from "@customer-portal/domain/services/providers";
import type { WhmcsErrorResponse } from "@customer-portal/domain/common/providers";
import type { WhmcsRequestOptions, WhmcsConnectionStats } from "../types/connection.types.js";
/**

View File

@ -1,9 +1,6 @@
import { Injectable, HttpStatus } from "@nestjs/common";
import {
ErrorCode,
type ErrorCodeType,
type WhmcsErrorResponse,
} from "@customer-portal/domain/common";
import { ErrorCode, type ErrorCodeType } from "@customer-portal/domain/common";
import type { WhmcsErrorResponse } from "@customer-portal/domain/common/providers";
import { DomainHttpException } from "@bff/core/http/domain-http.exception.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";

View File

@ -1,7 +1,7 @@
import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import type { WhmcsResponse } from "@customer-portal/domain/common";
import type { WhmcsResponse } from "@customer-portal/domain/common/providers";
import type {
WhmcsApiConfig,
WhmcsRequestOptions,

View File

@ -3,7 +3,7 @@ import { Logger } from "nestjs-pino";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
import { Providers as CustomerProviders } from "@customer-portal/domain/customer";
import * as CustomerProviders from "@customer-portal/domain/customer/providers";
import type { WhmcsClient } from "@customer-portal/domain/customer";
/**

View File

@ -9,8 +9,8 @@ import type {
WhmcsClientResponse,
WhmcsAddClientResponse,
WhmcsValidateLoginResponse,
} from "@customer-portal/domain/customer";
import { Providers as CustomerProviders } from "@customer-portal/domain/customer";
} from "@customer-portal/domain/customer/providers";
import * as CustomerProviders from "@customer-portal/domain/customer/providers";
import type { WhmcsClient } from "@customer-portal/domain/customer";
@Injectable()

View File

@ -4,16 +4,14 @@ import { Logger } from "nestjs-pino";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import {
FALLBACK_CURRENCY,
type WhmcsCurrenciesResponse,
type WhmcsCurrency,
} from "@customer-portal/domain/billing";
import { FALLBACK_CURRENCY } from "@customer-portal/domain/billing";
import type { Currency } from "@customer-portal/domain/billing";
import type { WhmcsCurrenciesResponse } from "@customer-portal/domain/billing/providers";
@Injectable()
export class WhmcsCurrencyService implements OnModuleInit, OnModuleDestroy {
private defaultCurrency: WhmcsCurrency | null = null;
private currencies: WhmcsCurrency[] = [];
private defaultCurrency: Currency | null = null;
private currencies: Currency[] = [];
private readonly REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
private refreshTimer: NodeJS.Timeout | null = null;
@ -70,21 +68,21 @@ export class WhmcsCurrencyService implements OnModuleInit, OnModuleDestroy {
/**
* Get the default currency (first currency from WHMCS or JPY fallback)
*/
getDefaultCurrency(): WhmcsCurrency {
getDefaultCurrency(): Currency {
return this.defaultCurrency ? { ...this.defaultCurrency } : { ...FALLBACK_CURRENCY };
}
/**
* Get all available currencies
*/
getAllCurrencies(): WhmcsCurrency[] {
getAllCurrencies(): Currency[] {
return this.currencies;
}
/**
* Find currency by code
*/
getCurrencyByCode(code: string): WhmcsCurrency | null {
getCurrencyByCode(code: string): Currency | null {
return this.currencies.find(c => c.code.toUpperCase() === code.toUpperCase()) || null;
}
@ -140,8 +138,8 @@ export class WhmcsCurrencyService implements OnModuleInit, OnModuleDestroy {
* Parse WHMCS response format into currency objects
* Handles both flat format (currencies[currency][0][id]) and nested format (currencies.currency[])
*/
private parseWhmcsCurrenciesResponse(response: WhmcsCurrenciesResponse): WhmcsCurrency[] {
const currencies: WhmcsCurrency[] = [];
private parseWhmcsCurrenciesResponse(response: WhmcsCurrenciesResponse): Currency[] {
const currencies: Currency[] = [];
// Check if response has nested currency structure
if (
@ -154,7 +152,7 @@ export class WhmcsCurrencyService implements OnModuleInit, OnModuleDestroy {
: [response.currencies.currency];
for (const currencyData of currencyArray) {
const currency: WhmcsCurrency = {
const currency: Currency = {
id: parseInt(String(currencyData.id)) || 0,
code: String(currencyData.code || ""),
prefix: String(currencyData.prefix || ""),
@ -184,7 +182,7 @@ export class WhmcsCurrencyService implements OnModuleInit, OnModuleDestroy {
// Build currency objects from the flat response
for (const index of currencyIndices) {
const currency: WhmcsCurrency = {
const currency: Currency = {
id: parseInt(String(response[`currencies[currency][${index}][id]`])) || 0,
code: String(response[`currencies[currency][${index}][code]`] || ""),
prefix: String(response[`currencies[currency][${index}][prefix]`] || ""),

View File

@ -2,8 +2,9 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino";
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js";
import { invoiceListSchema, invoiceSchema, Providers } from "@customer-portal/domain/billing";
import { invoiceListSchema, invoiceSchema } from "@customer-portal/domain/billing";
import type { Invoice, InvoiceList } from "@customer-portal/domain/billing";
import * as Providers from "@customer-portal/domain/billing/providers";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsCurrencyService } from "./whmcs-currency.service.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
@ -12,14 +13,14 @@ import type {
WhmcsCreateInvoiceParams,
WhmcsUpdateInvoiceParams,
WhmcsCapturePaymentParams,
} from "@customer-portal/domain/billing";
} from "@customer-portal/domain/billing/providers";
import type {
WhmcsInvoiceListResponse,
WhmcsInvoiceResponse,
WhmcsCreateInvoiceResponse,
WhmcsUpdateInvoiceResponse,
WhmcsCapturePaymentResponse,
} from "@customer-portal/domain/billing";
} from "@customer-portal/domain/billing/providers";
export type InvoiceFilters = Partial<{
status: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";

View File

@ -9,12 +9,12 @@ import type {
WhmcsAddOrderParams,
WhmcsAddOrderResponse,
WhmcsOrderResult,
} from "@customer-portal/domain/orders";
} from "@customer-portal/domain/orders/providers";
import * as Providers from "@customer-portal/domain/orders/providers";
import {
Providers,
whmcsAddOrderResponseSchema,
whmcsAcceptOrderResponseSchema,
} from "@customer-portal/domain/orders";
} from "@customer-portal/domain/orders/providers";
export type { WhmcsOrderItem, WhmcsAddOrderParams, WhmcsOrderResult };

View File

@ -1,27 +1,25 @@
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino";
import { Injectable, Inject } from "@nestjs/common";
import { Providers } from "@customer-portal/domain/payments";
import * as Providers from "@customer-portal/domain/payments/providers";
import type {
PaymentMethodList,
PaymentGateway,
PaymentGatewayList,
PaymentMethod,
} from "@customer-portal/domain/payments";
import {
Providers as CatalogProviders,
type WhmcsCatalogProductNormalized,
} from "@customer-portal/domain/services";
import type { WhmcsCatalogProductNormalized } from "@customer-portal/domain/services/providers";
import * as CatalogProviders from "@customer-portal/domain/services/providers";
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";
import type { WhmcsGetPayMethodsParams } from "@customer-portal/domain/payments";
import type { WhmcsCreateSsoTokenParams } from "@customer-portal/domain/customer/providers";
import type { WhmcsGetPayMethodsParams } from "@customer-portal/domain/payments/providers";
import type {
WhmcsPaymentMethod,
WhmcsPaymentMethodListResponse,
WhmcsPaymentGateway,
WhmcsPaymentGatewayListResponse,
} from "@customer-portal/domain/payments";
} from "@customer-portal/domain/payments/providers";
@Injectable()
export class WhmcsPaymentService {

View File

@ -2,8 +2,10 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino";
import { Injectable, Inject } from "@nestjs/common";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import type { WhmcsCreateSsoTokenParams } from "@customer-portal/domain/customer";
import type { WhmcsSsoResponse } from "@customer-portal/domain/customer";
import type {
WhmcsCreateSsoTokenParams,
WhmcsSsoResponse,
} from "@customer-portal/domain/customer/providers";
@Injectable()
export class WhmcsSsoService {

View File

@ -2,12 +2,12 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino";
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js";
import { Providers } from "@customer-portal/domain/subscriptions";
import * as Providers from "@customer-portal/domain/subscriptions/providers";
import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsCurrencyService } from "./whmcs-currency.service.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
import type { WhmcsGetClientsProductsParams } from "@customer-portal/domain/subscriptions";
import type { WhmcsGetClientsProductsParams } from "@customer-portal/domain/subscriptions/providers";
export interface SubscriptionFilters {
status?: string;

View File

@ -2,12 +2,8 @@ import { Injectable, Inject } from "@nestjs/common";
import type { Invoice, InvoiceList } from "@customer-portal/domain/billing";
import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions";
import type { PaymentMethodList, PaymentGatewayList } from "@customer-portal/domain/payments";
import {
Providers as CustomerProviders,
addressSchema,
type Address,
type WhmcsClient,
} from "@customer-portal/domain/customer";
import { addressSchema, type Address, type WhmcsClient } from "@customer-portal/domain/customer";
import * as CustomerProviders from "@customer-portal/domain/customer/providers";
import { WhmcsConnectionOrchestratorService } from "./connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsInvoiceService } from "./services/whmcs-invoice.service.js";
import type { InvoiceFilters } from "./services/whmcs-invoice.service.js";
@ -17,10 +13,15 @@ import { WhmcsClientService } from "./services/whmcs-client.service.js";
import { WhmcsPaymentService } from "./services/whmcs-payment.service.js";
import { WhmcsSsoService } from "./services/whmcs-sso.service.js";
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/services";
import type {
WhmcsAddClientParams,
WhmcsClientResponse,
} from "@customer-portal/domain/customer/providers";
import type {
WhmcsGetClientsProductsParams,
WhmcsProductListResponse,
} from "@customer-portal/domain/subscriptions/providers";
import type { WhmcsCatalogProductNormalized } from "@customer-portal/domain/services/providers";
import { Logger } from "nestjs-pino";
@Injectable()

View File

@ -27,7 +27,7 @@ import {
type ValidateSignupRequest,
} from "@customer-portal/domain/auth";
import { ErrorCode } from "@customer-portal/domain/common";
import { Providers as CustomerProviders } from "@customer-portal/domain/customer";
import * as CustomerProviders from "@customer-portal/domain/customer/providers";
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
import type { User as PrismaUser } from "@prisma/client";
import { CacheService } from "@bff/infra/cache/cache.service.js";

View File

@ -14,7 +14,7 @@ import { WhmcsAccountDiscoveryService } from "@bff/integrations/whmcs/services/w
import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
import { Providers as CustomerProviders } from "@customer-portal/domain/customer";
import * as CustomerProviders from "@customer-portal/domain/customer/providers";
import type { User } from "@customer-portal/domain/customer";
import {
PORTAL_SOURCE_MIGRATED,

View File

@ -1,7 +1,7 @@
import { Controller, Get } from "@nestjs/common";
import { Public } from "@bff/modules/auth/decorators/public.decorator.js";
import { WhmcsCurrencyService } from "../../integrations/whmcs/services/whmcs-currency.service.js";
import type { WhmcsCurrency } from "@customer-portal/domain/billing";
import type { Currency } from "@customer-portal/domain/billing";
@Controller("currency")
export class CurrencyController {
@ -9,13 +9,13 @@ export class CurrencyController {
@Public()
@Get("default")
getDefaultCurrency(): WhmcsCurrency {
getDefaultCurrency(): Currency {
return this.currencyService.getDefaultCurrency();
}
@Public()
@Get("all")
getAllCurrencies(): WhmcsCurrency[] {
getAllCurrencies(): Currency[] {
return this.currencyService.getAllCurrencies();
}
}

View File

@ -5,7 +5,7 @@ import {
defaultSalesforceOrderFieldMap,
type PartialSalesforceOrderFieldMap,
type SalesforceOrderFieldMap,
} from "@customer-portal/domain/orders";
} from "@customer-portal/domain/orders/providers";
const unique = <T>(values: T[]): T[] => Array.from(new Set(values));

View File

@ -14,11 +14,9 @@ import { OrderEventsService } from "./order-events.service.js";
import { OrdersCacheService } from "./orders-cache.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { NotificationService } from "@bff/modules/notifications/notifications.service.js";
import {
type OrderDetails,
type OrderFulfillmentValidationResult,
Providers as OrderProviders,
} from "@customer-portal/domain/orders";
import type { OrderDetails } from "@customer-portal/domain/orders";
import type { OrderFulfillmentValidationResult } from "@customer-portal/domain/orders/providers";
import * as OrderProviders from "@customer-portal/domain/orders/providers";
import { OPPORTUNITY_STAGE } from "@customer-portal/domain/opportunity";
import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications";
import { salesforceAccountIdSchema } from "@customer-portal/domain/common";

View File

@ -4,9 +4,9 @@ import { SalesforceService } from "@bff/integrations/salesforce/salesforce.servi
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { sfOrderIdParamSchema } from "@customer-portal/domain/orders";
import type { OrderFulfillmentValidationResult } from "@customer-portal/domain/orders";
import type { OrderFulfillmentValidationResult } from "@customer-portal/domain/orders/providers";
import { salesforceAccountIdSchema } from "@customer-portal/domain/common";
import type { SalesforceOrderRecord } from "@customer-portal/domain/orders/providers/salesforce/raw.types";
import type { SalesforceOrderRecord } from "@customer-portal/domain/orders/providers";
import { PaymentValidatorService } from "./payment-validator.service.js";
/**

View File

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

View File

@ -9,7 +9,7 @@ import {
type CreateOrderRequest,
type OrderBusinessValidation,
} from "@customer-portal/domain/orders";
import type { Providers } from "@customer-portal/domain/subscriptions";
import type * as Providers from "@customer-portal/domain/subscriptions/providers";
type WhmcsProduct = Providers.WhmcsRaw.WhmcsProductRaw;
import { SimServicesService } from "@bff/modules/services/services/sim-services.service.js";

View File

@ -12,11 +12,11 @@ import {
} from "@bff/integrations/salesforce/utils/services-query-builder.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import type {
SalesforceProduct2WithPricebookEntries,
SalesforcePricebookEntryRecord,
} from "@customer-portal/domain/services";
import { Providers as CatalogProviders } from "@customer-portal/domain/services";
import type { SalesforceResponse } from "@customer-portal/domain/common";
SalesforceProduct2WithPricebookEntries,
} from "@customer-portal/domain/services/providers";
import * as CatalogProviders from "@customer-portal/domain/services/providers";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
@Injectable()
export class BaseServicesService {

View File

@ -3,20 +3,20 @@ import { ConfigService } from "@nestjs/config";
import { BaseServicesService } from "./base-services.service.js";
import { ServicesCacheService } from "./services-cache.service.js";
import type {
SalesforceProduct2WithPricebookEntries,
InternetPlanCatalogItem,
InternetInstallationCatalogItem,
InternetAddonCatalogItem,
InternetEligibilityDetails,
InternetEligibilityStatus,
} from "@customer-portal/domain/services";
import type { SalesforceProduct2WithPricebookEntries } from "@customer-portal/domain/services/providers";
import {
Providers as CatalogProviders,
enrichInternetPlanMetadata,
inferAddonTypeFromSku,
inferInstallationTermFromSku,
internetEligibilityDetailsSchema,
} from "@customer-portal/domain/services";
import * as CatalogProviders from "@customer-portal/domain/services/providers";
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";
@ -27,7 +27,7 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { assertSalesforceId } from "@bff/integrations/salesforce/utils/soql.util.js";
import { assertSoqlFieldName } from "@bff/integrations/salesforce/utils/soql.util.js";
import type { InternetEligibilityCheckRequest } from "./internet-eligibility.types.js";
import type { SalesforceResponse } from "@customer-portal/domain/common";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
// (removed unused opportunity constants import)
@Injectable()

View File

@ -3,11 +3,11 @@ import { ConfigService } from "@nestjs/config";
import { BaseServicesService } from "./base-services.service.js";
import { ServicesCacheService } from "./services-cache.service.js";
import type {
SalesforceProduct2WithPricebookEntries,
SimCatalogProduct,
SimActivationFeeCatalogItem,
} from "@customer-portal/domain/services";
import { Providers as CatalogProviders } from "@customer-portal/domain/services";
import type { SalesforceProduct2WithPricebookEntries } from "@customer-portal/domain/services/providers";
import * as CatalogProviders from "@customer-portal/domain/services/providers";
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

@ -4,11 +4,9 @@ import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
import { BaseServicesService } from "./base-services.service.js";
import { ServicesCacheService } from "./services-cache.service.js";
import type {
SalesforceProduct2WithPricebookEntries,
VpnCatalogProduct,
} from "@customer-portal/domain/services";
import { Providers as CatalogProviders } from "@customer-portal/domain/services";
import type { VpnCatalogProduct } from "@customer-portal/domain/services";
import type { SalesforceProduct2WithPricebookEntries } from "@customer-portal/domain/services/providers";
import * as CatalogProviders from "@customer-portal/domain/services/providers";
@Injectable()
export class VpnServicesService extends BaseServicesService {

View File

@ -3,7 +3,7 @@ import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
import type { SimTopUpPricing, SimTopUpPricingPreviewResponse } from "@customer-portal/domain/sim";
import type { SalesforceResponse } from "@customer-portal/domain/common";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import { sanitizeSoqlLiteral } from "@bff/integrations/salesforce/utils/soql.util.js";
@Injectable()

View File

@ -33,7 +33,7 @@ import type {
SimPlanChangeResult,
} from "@customer-portal/domain/subscriptions";
import type { InvoiceList } from "@customer-portal/domain/billing";
import { createPaginationSchema } from "@customer-portal/domain/toolkit/validation/helpers";
import { Validation } from "@customer-portal/domain/toolkit";
import {
simTopupRequestSchema,
simChangePlanRequestSchema,
@ -82,7 +82,7 @@ import {
} from "@customer-portal/domain/subscriptions";
import { invoiceListSchema } from "@customer-portal/domain/billing";
const subscriptionInvoiceQuerySchema = createPaginationSchema({
const subscriptionInvoiceQuerySchema = Validation.createPaginationSchema({
defaultLimit: 10,
maxLimit: 100,
minLimit: 1,

View File

@ -15,7 +15,7 @@ import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service.js";
import { WhmcsCacheService } from "@bff/integrations/whmcs/cache/whmcs-cache.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { Logger } from "nestjs-pino";
import type { Providers } from "@customer-portal/domain/subscriptions";
import type * as Providers from "@customer-portal/domain/subscriptions/providers";
type WhmcsProduct = Providers.WhmcsRaw.WhmcsProductRaw;

View File

@ -10,12 +10,12 @@ import { Logger } from "nestjs-pino";
import type { User as PrismaUser } from "@prisma/client";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import {
Providers as CustomerProviders,
addressSchema,
combineToUser,
type Address,
type User,
} from "@customer-portal/domain/customer";
import * as CustomerProviders from "@customer-portal/domain/customer/providers";
import {
updateCustomerProfileRequestSchema,
type UpdateCustomerProfileRequest,

View File

@ -7,7 +7,7 @@ import {
assertSalesforceId,
assertSoqlFieldName,
} from "@bff/integrations/salesforce/utils/soql.util.js";
import type { SalesforceResponse } from "@customer-portal/domain/common";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import {
residenceCardVerificationSchema,
type ResidenceCardVerification,

View File

@ -206,17 +206,17 @@ The application uses Zod for type-safe form validation. Use the `useZodForm` hoo
```tsx
import { useZodForm } from "@/core/forms";
import { loginFormSchema, type LoginFormData } from "@customer-portal/domain";
import { loginRequestSchema, type LoginRequest } from "@customer-portal/domain/auth";
function MyForm() {
const { values, errors, touched, isSubmitting, setValue, setTouchedField, handleSubmit } =
useZodForm({
schema: loginFormSchema,
schema: loginRequestSchema,
initialValues: {
email: "",
password: "",
},
onSubmit: async data => {
onSubmit: async (data: LoginRequest) => {
// Handle form submission
await submitLogin(data);
},
@ -241,7 +241,7 @@ function MyForm() {
**Available Zod schemas in `@customer-portal/domain`:**
- `loginFormSchema` - Login form validation
- `loginRequestSchema` - Login request validation (shared domain schema)
- `signupFormSchema` - User registration
- `profileEditFormSchema` - Profile updates
- `addressFormSchema` - Address validation

View File

@ -15,7 +15,7 @@ import {
type QuickActionTask,
type DashboardTaskSummary,
} from "@customer-portal/domain/dashboard";
import { formatCurrency as formatCurrencyUtil } from "@customer-portal/domain/toolkit";
import { Formatting } from "@customer-portal/domain/toolkit";
// Re-export domain business logic for backward compatibility
export {
@ -27,6 +27,8 @@ export {
type DashboardTaskSummary,
};
const formatCurrencyUtil = Formatting.formatCurrency;
/**
* Get navigation path for an activity
*/

View File

@ -3,10 +3,10 @@
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "@/lib/api";
import { currencyService } from "@/lib/services/currency.service";
import { FALLBACK_CURRENCY, type WhmcsCurrency } from "@customer-portal/domain/billing";
import { FALLBACK_CURRENCY, type Currency } from "@customer-portal/domain/billing";
export function useCurrency() {
const { data, isLoading, isError, error } = useQuery<WhmcsCurrency>({
const { data, isLoading, isError, error } = useQuery<Currency>({
queryKey: queryKeys.currency.default(),
queryFn: () => currencyService.getDefaultCurrency(),
retry: 2,

View File

@ -2,7 +2,7 @@
import { useCurrency } from "@/lib/hooks/useCurrency";
import { FALLBACK_CURRENCY } from "@customer-portal/domain/billing";
import { formatCurrency as baseFormatCurrency } from "@customer-portal/domain/toolkit";
import { Formatting } from "@customer-portal/domain/toolkit";
export type FormatCurrencyOptions = {
currency?: string;
@ -44,7 +44,7 @@ export function useFormatCurrency() {
? currencyOrOptions.showSymbol
: undefined) ?? options?.showSymbol;
return baseFormatCurrency(amount, overrideCurrency ?? fallbackCurrency, {
return Formatting.formatCurrency(amount, overrideCurrency ?? fallbackCurrency, {
currencySymbol: overrideSymbol ?? fallbackSymbol,
locale,
showSymbol,

View File

@ -1,16 +1,16 @@
import { apiClient, getDataOrThrow } from "@/lib/api";
import { FALLBACK_CURRENCY, type WhmcsCurrency } from "@customer-portal/domain/billing";
import { FALLBACK_CURRENCY, type Currency } from "@customer-portal/domain/billing";
export { FALLBACK_CURRENCY };
export const currencyService = {
async getDefaultCurrency(): Promise<WhmcsCurrency> {
const response = await apiClient.GET<WhmcsCurrency>("/api/currency/default");
async getDefaultCurrency(): Promise<Currency> {
const response = await apiClient.GET<Currency>("/api/currency/default");
return getDataOrThrow(response, "Failed to get default currency");
},
async getAllCurrencies(): Promise<WhmcsCurrency[]> {
const response = await apiClient.GET<WhmcsCurrency[]>("/api/currency/all");
async getAllCurrencies(): Promise<Currency[]> {
const response = await apiClient.GET<Currency[]>("/api/currency/all");
return getDataOrThrow(response, "Failed to get currencies");
},
};

View File

@ -102,9 +102,8 @@ Feature guides explaining how the portal functions:
### BFF (Backend for Frontend)
| Document | Description |
| ----------------------------------------------------------------- | ---------------------------- |
| ----------------------------------------------------------------- | --------------------------- |
| [Integration Patterns](./development/bff/integration-patterns.md) | Clean architecture patterns |
| [Validation Standard](./development/bff/validation.md) | DTO validation + global pipe |
| [DB Mappers](./development/bff/db-mappers.md) | Database mapping |
| [Order Status Updates](./development/bff/order-status-updates.md) | Status update strategy |

View File

@ -140,11 +140,12 @@ packages/domain/
#### **Usage**
Import via `@customer-portal/domain`:
Import via module entrypoints:
```typescript
import { Invoice, SIM_LIFECYCLE_STAGE, OrderStatus } from "@customer-portal/domain";
import { invoiceSchema, orderSchema } from "@customer-portal/domain/validation";
import { invoiceSchema, type Invoice } from "@customer-portal/domain/billing";
import { orderSummarySchema, type OrderSummary } from "@customer-portal/domain/orders";
import { SIM_STATUS } from "@customer-portal/domain/sim";
```
#### **Integration with BFF**

View File

@ -0,0 +1,53 @@
## Import Hygiene Guide (Domain)
### Principles
- **No deep imports**: internal file layout is not part of the contract.
- **Barrels define the public API**: if its not exported from the entrypoint, its not public.
- **Providers are integration-only**: Portal must never import provider adapters/types.
### Allowed import levels
- **Default (Portal + BFF)**:
- `@customer-portal/domain/<module>`
- `@customer-portal/domain/toolkit`
- **BFF-only (integration/infrastructure)**:
- `@customer-portal/domain/<module>/providers`
### Never
- `@customer-portal/domain/<module>/**` (anything deeper than the module entrypoint)
- `@customer-portal/domain/<module>/providers/**` (anything deeper than `/providers`)
- `apps/portal/**` importing any `@customer-portal/domain/*/providers`
### Rule of thumb
Import from the **highest stable entrypoint** that contains what you need.
- If it exists in `@customer-portal/domain/<module>`, dont import a deeper file.
- If its provider-specific, use `@customer-portal/domain/<module>/providers` (BFF-only).
- If its cross-domain utility, use `@customer-portal/domain/toolkit`.
### When to create a new explicit entrypoint (instead of deep-importing)
Create/adjust exports when:
- The symbol is used in 2+ apps (Portal + BFF), or many call sites.
- The symbol is part of a workflow that should remain stable (pagination, formatting, shared validation helpers).
Where to export it:
- **Module root** (`@customer-portal/domain/<module>`): normalized domain types/models, schemas, provider-agnostic helpers.
- **Providers entrypoint** (`.../<module>/providers`): provider adapters, mapper/query builder logic, raw provider shapes (if truly needed).
- **Toolkit** (`@customer-portal/domain/toolkit`): shared utilities (`Formatting`, `Validation`, `Typing` namespaces).
### Naming conventions
- **Module root**: `Invoice`, `invoiceSchema`, `validateOrderBusinessRules`
- **Providers**: `Whmcs`, `Salesforce`, `Freebit` namespaces; raw shapes should be obviously integration-only.
### PR checklist
- No `@customer-portal/domain/*/*` imports (except exact `.../<module>/providers` in BFF).
- Portal has **zero** `.../providers` imports.
- No wildcard subpath exports added to `packages/domain/package.json#exports`.

View File

@ -20,6 +20,8 @@
- Adding new providers = adding new folders (no refactoring)
- Single package (`@customer-portal/domain`) for all types
**Import rules**: See [`docs/development/domain/import-hygiene.md`](import-hygiene.md).
---
## 📦 Package Structure
@ -130,9 +132,9 @@ const validated = invoiceSchema.parse(rawData);
import { Invoice, invoiceSchema } from "@customer-portal/domain/billing";
import {
transformWhmcsInvoice,
whmcsInvoiceRawSchema,
type WhmcsInvoiceRaw,
} from "@customer-portal/domain/billing/providers/whmcs/mapper";
import { whmcsInvoiceRawSchema } from "@customer-portal/domain/billing/providers/whmcs/raw.types";
} from "@customer-portal/domain/billing/providers";
// Transform raw API data
const whmcsData: WhmcsInvoiceRaw = await whmcsApi.getInvoice(id);
@ -256,7 +258,7 @@ Raw types and mappers stay in `providers/`:
```typescript
// ✅ GOOD - Isolated
import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers/whmcs/mapper";
import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers";
// ❌ BAD - Would leak WHMCS details into app code
import { WhmcsInvoiceRaw } from "@somewhere/global";
@ -335,7 +337,7 @@ export function transformStripeInvoice(raw: unknown): Invoice {
```typescript
// No changes to domain contract needed!
import { transformStripeInvoice } from "@customer-portal/domain/billing/providers/stripe/mapper";
import { transformStripeInvoice } from "@customer-portal/domain/billing/providers";
const invoice = transformStripeInvoice(stripeData);
```

View File

@ -155,17 +155,79 @@ export default [
"no-restricted-imports": [
"error",
{
paths: [
{
name: "@customer-portal/domain",
message:
"Do not import @customer-portal/domain (root). Use @customer-portal/domain/<module> instead.",
},
],
patterns: [
{
group: ["@customer-portal/domain/**/src/**"],
message: "Import from @customer-portal/domain/<module> instead of internals.",
},
{
group: ["@customer-portal/domain/*/*", "!@customer-portal/domain/*/providers"],
message:
"No deep @customer-portal/domain imports. Use @customer-portal/domain/<module> (or BFF-only: .../<module>/providers).",
},
{
group: ["@customer-portal/domain/*/providers/*"],
message:
"Do not deep-import provider internals. Import from @customer-portal/domain/<module>/providers only.",
},
],
},
],
},
},
// =============================================================================
// Portal: hard boundary — must not import provider adapters/types
// =============================================================================
{
files: ["apps/portal/src/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "@customer-portal/domain",
message:
"Do not import @customer-portal/domain (root). Use @customer-portal/domain/<module> instead.",
},
],
patterns: [
{
group: ["@customer-portal/domain/**/src/**"],
message: "Import from @customer-portal/domain/<module> instead of internals.",
},
{
group: ["@customer-portal/domain/*/providers"],
message:
"Portal must not import provider adapters/types. Import normalized domain models from @customer-portal/domain/<module> instead.",
},
{
group: ["@customer-portal/domain/*/*", "!@customer-portal/domain/*/providers"],
message: "No deep @customer-portal/domain imports. Use @customer-portal/domain/<module> only.",
},
{
group: ["@customer-portal/domain/*/providers/*"],
message:
"Do not deep-import provider internals. Import from @customer-portal/domain/<module>/providers only (BFF-only).",
},
],
},
],
},
},
{
files: ["apps/portal/src/app/(authenticated)/layout.tsx"],
rules: { "no-restricted-imports": "off" },
},
// =============================================================================
// BFF: stricter type safety (type-aware)
// =============================================================================

View File

@ -4,7 +4,7 @@
* Domain constants for billing validation and business rules.
*/
import type { WhmcsCurrency } from "./providers/whmcs/raw.types.js";
import type { Currency } from "./schema.js";
// ============================================================================
// Currency Defaults
@ -15,7 +15,7 @@ import type { WhmcsCurrency } from "./providers/whmcs/raw.types.js";
* is unavailable. This ensures a single source of truth for default currency
* formatting behaviour.
*/
export const FALLBACK_CURRENCY: WhmcsCurrency = {
export const FALLBACK_CURRENCY: Currency = {
id: 1,
code: "JPY",
prefix: "¥",

View File

@ -15,6 +15,7 @@ export * from "./schema.js";
// Re-export types for convenience
export type {
Currency,
InvoiceStatus,
InvoiceItem,
Invoice,
@ -29,23 +30,3 @@ export type {
InvoiceSsoQuery,
InvoicePaymentLinkQuery,
} from "./schema.js";
// Provider adapters
export * as Providers from "./providers/index.js";
// Re-export provider raw types (request and response)
export type {
// Request params
WhmcsGetInvoicesParams,
WhmcsCreateInvoiceParams,
WhmcsUpdateInvoiceParams,
WhmcsCapturePaymentParams,
// Response types
WhmcsInvoiceListResponse,
WhmcsInvoiceResponse,
WhmcsCreateInvoiceResponse,
WhmcsUpdateInvoiceResponse,
WhmcsCapturePaymentResponse,
WhmcsCurrency,
WhmcsCurrenciesResponse,
} from "./providers/whmcs/raw.types.js";

View File

@ -7,6 +7,25 @@
import { z } from "zod";
// ============================================================================
// Currency (Domain Model)
// ============================================================================
/**
* Normalized currency model used across the Portal and BFF.
* This is intentionally provider-agnostic (even if sourced from WHMCS).
*/
export const currencySchema = z.object({
id: z.number().int().positive(),
code: z.string().min(1),
prefix: z.string(),
suffix: z.string().optional(),
format: z.string().optional(),
rate: z.string().optional(),
});
export type Currency = z.infer<typeof currencySchema>;
// Invoice Status Schema
export const invoiceStatusSchema = z.enum([
"Draft",

View File

@ -8,10 +8,3 @@ export * from "./types.js";
export * from "./schema.js";
export * from "./validation.js";
export * from "./errors.js";
// Common provider types (generic wrappers used across domains)
export * as CommonProviders from "./providers/index.js";
// Re-export provider types for convenience
export type { WhmcsResponse, WhmcsErrorResponse } from "./providers/whmcs.js";
export type { SalesforceResponse } from "./providers/salesforce.js";

View File

@ -4,5 +4,11 @@
* Generic provider-specific response structures used across multiple domains.
*/
export * as Whmcs from "./whmcs.js";
export type { WhmcsResponse, WhmcsErrorResponse } from "./whmcs.js";
export { whmcsResponseSchema, whmcsErrorResponseSchema } from "./whmcs.js";
export type { SalesforceResponse } from "./salesforce.js";
export { salesforceResponseSchema } from "./salesforce.js";
// Salesforce raw types (integration-only utilities)
export * as Salesforce from "./salesforce/index.js";

View File

@ -68,34 +68,6 @@ export {
* - Providers.Whmcs.transformWhmcsClientResponse()
* - Providers.Portal.mapPrismaUserToUserAuth()
*/
export * as Providers from "./providers/index.js";
// ============================================================================
// Provider Raw Response Types (Selective Exports)
// ============================================================================
/**
* WHMCS API raw types (request and response)
* Only exported for BFF integration convenience
*/
export type {
// Request params
WhmcsAddClientParams,
WhmcsValidateLoginParams,
WhmcsCreateSsoTokenParams,
// Response types
WhmcsClientResponse,
WhmcsAddClientResponse,
WhmcsValidateLoginResponse,
WhmcsSsoResponse,
} from "./providers/whmcs/raw.types.js";
// ============================================================================
// Provider-Specific Types (For Integrations)
// ============================================================================
/**
* Salesforce integration types
* Provider-specific, not validated at runtime
*/
export type { SalesforceAccountFieldMap, SalesforceAccountRecord } from "./contract.js";
// NOTE: Provider adapters and provider-specific types are intentionally not exported
// from the module root. Import BFF-only provider APIs from:
// @customer-portal/domain/customer/providers

View File

@ -8,3 +8,15 @@
export * as Portal from "./portal/index.js";
export * as Whmcs from "./whmcs/index.js";
// Provider-specific integration types (BFF-only)
export type { SalesforceAccountFieldMap, SalesforceAccountRecord } from "../contract.js";
export type {
WhmcsAddClientParams,
WhmcsValidateLoginParams,
WhmcsCreateSsoTokenParams,
WhmcsClientResponse,
WhmcsAddClientResponse,
WhmcsValidateLoginResponse,
WhmcsSsoResponse,
} from "./whmcs/raw.types.js";

View File

@ -6,7 +6,6 @@
*/
import type { UserIdMapping } from "../mappings/contract.js";
import type { SalesforceOrderRecord } from "./providers/salesforce/raw.types.js";
import type { OrderConfigurations } from "./schema.js";
// ============================================================================
@ -178,12 +177,9 @@ export interface OrderCreateResponse {
/**
* Order fulfillment validation result
*/
export interface OrderFulfillmentValidationResult {
sfOrder: SalesforceOrderRecord;
clientId: number;
isAlreadyProvisioned: boolean;
whmcsOrderId?: string;
}
// NOTE: Provider-specific fulfillment types are intentionally not part of the public orders contract.
// BFF integration code should import them from:
// @customer-portal/domain/orders/providers
// ============================================================================
// Re-export Types from Schema (Schema-First Approach)

View File

@ -18,7 +18,6 @@ export {
type CheckoutTotals,
type CheckoutCart,
type OrderCreateResponse,
type OrderFulfillmentValidationResult,
// Constants
ORDER_TYPE,
ORDER_STATUS,
@ -83,9 +82,6 @@ export type {
} from "./schema.js";
// Provider adapters
export * as Providers from "./providers/index.js";
// Re-export provider types for convenience
export * from "./providers/whmcs/raw.types.js";
export * from "./providers/salesforce/raw.types.js";
export * from "./providers/salesforce/field-map.js";
// NOTE: Provider adapters are intentionally not exported from the module root.
// Import BFF-only provider adapters from:
// @customer-portal/domain/orders/providers

View File

@ -7,6 +7,7 @@ import * as WhmcsRaw from "./whmcs/raw.types.js";
import * as SalesforceFieldMap from "./salesforce/field-map.js";
import * as SalesforceMapper from "./salesforce/mapper.js";
import * as SalesforceRaw from "./salesforce/raw.types.js";
export * from "./types.js";
export const Whmcs = {
...WhmcsMapper,

View File

@ -0,0 +1,14 @@
import type { SalesforceOrderRecord } from "./salesforce/raw.types.js";
/**
* Order fulfillment validation result (integration-only)
*
* This intentionally lives under the providers entrypoint to avoid leaking
* raw provider shapes into the public orders module API.
*/
export interface OrderFulfillmentValidationResult {
sfOrder: SalesforceOrderRecord;
clientId: number;
isAlreadyProvisioned: boolean;
whmcsOrderId?: string;
}

View File

@ -11,157 +11,113 @@
"dist"
],
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./auth": {
"import": "./dist/auth/index.js",
"types": "./dist/auth/index.d.ts"
},
"./auth/*": {
"import": "./dist/auth/*.js",
"types": "./dist/auth/*.d.ts"
},
"./billing": {
"import": "./dist/billing/index.js",
"types": "./dist/billing/index.d.ts"
},
"./billing/*": {
"import": "./dist/billing/*.js",
"types": "./dist/billing/*.d.ts"
"./billing/providers": {
"import": "./dist/billing/providers/index.js",
"types": "./dist/billing/providers/index.d.ts"
},
"./services": {
"import": "./dist/services/index.js",
"types": "./dist/services/index.d.ts"
},
"./services/*": {
"import": "./dist/services/*.js",
"types": "./dist/services/*.d.ts"
"./services/providers": {
"import": "./dist/services/providers/index.js",
"types": "./dist/services/providers/index.d.ts"
},
"./checkout": {
"import": "./dist/checkout/index.js",
"types": "./dist/checkout/index.d.ts"
},
"./checkout/*": {
"import": "./dist/checkout/*.js",
"types": "./dist/checkout/*.d.ts"
},
"./common": {
"import": "./dist/common/index.js",
"types": "./dist/common/index.d.ts"
},
"./common/*": {
"import": "./dist/common/*.js",
"types": "./dist/common/*.d.ts"
"./common/providers": {
"import": "./dist/common/providers/index.js",
"types": "./dist/common/providers/index.d.ts"
},
"./customer": {
"import": "./dist/customer/index.js",
"types": "./dist/customer/index.d.ts"
},
"./customer/*": {
"import": "./dist/customer/*.js",
"types": "./dist/customer/*.d.ts"
"./customer/providers": {
"import": "./dist/customer/providers/index.js",
"types": "./dist/customer/providers/index.d.ts"
},
"./dashboard": {
"import": "./dist/dashboard/index.js",
"types": "./dist/dashboard/index.d.ts"
},
"./dashboard/*": {
"import": "./dist/dashboard/*.js",
"types": "./dist/dashboard/*.d.ts"
},
"./mappings": {
"import": "./dist/mappings/index.js",
"types": "./dist/mappings/index.d.ts"
},
"./mappings/*": {
"import": "./dist/mappings/*.js",
"types": "./dist/mappings/*.d.ts"
},
"./opportunity": {
"import": "./dist/opportunity/index.js",
"types": "./dist/opportunity/index.d.ts"
},
"./opportunity/*": {
"import": "./dist/opportunity/*.js",
"types": "./dist/opportunity/*.d.ts"
},
"./orders": {
"import": "./dist/orders/index.js",
"types": "./dist/orders/index.d.ts"
},
"./orders/*": {
"import": "./dist/orders/*.js",
"types": "./dist/orders/*.d.ts"
"./orders/providers": {
"import": "./dist/orders/providers/index.js",
"types": "./dist/orders/providers/index.d.ts"
},
"./payments": {
"import": "./dist/payments/index.js",
"types": "./dist/payments/index.d.ts"
},
"./payments/*": {
"import": "./dist/payments/*.js",
"types": "./dist/payments/*.d.ts"
"./payments/providers": {
"import": "./dist/payments/providers/index.js",
"types": "./dist/payments/providers/index.d.ts"
},
"./realtime": {
"import": "./dist/realtime/index.js",
"types": "./dist/realtime/index.d.ts"
},
"./realtime/*": {
"import": "./dist/realtime/*.js",
"types": "./dist/realtime/*.d.ts"
},
"./sim": {
"import": "./dist/sim/index.js",
"types": "./dist/sim/index.d.ts"
},
"./sim/*": {
"import": "./dist/sim/*.js",
"types": "./dist/sim/*.d.ts"
},
"./sim/providers/freebit": {
"import": "./dist/sim/providers/freebit/index.js",
"types": "./dist/sim/providers/freebit/index.d.ts"
"./sim/providers": {
"import": "./dist/sim/providers/index.js",
"types": "./dist/sim/providers/index.d.ts"
},
"./subscriptions": {
"import": "./dist/subscriptions/index.js",
"types": "./dist/subscriptions/index.d.ts"
},
"./subscriptions/*": {
"import": "./dist/subscriptions/*.js",
"types": "./dist/subscriptions/*.d.ts"
"./subscriptions/providers": {
"import": "./dist/subscriptions/providers/index.js",
"types": "./dist/subscriptions/providers/index.d.ts"
},
"./support": {
"import": "./dist/support/index.js",
"types": "./dist/support/index.d.ts"
},
"./support/*": {
"import": "./dist/support/*.js",
"types": "./dist/support/*.d.ts"
"./support/providers": {
"import": "./dist/support/providers/index.js",
"types": "./dist/support/providers/index.d.ts"
},
"./toolkit": {
"import": "./dist/toolkit/index.js",
"types": "./dist/toolkit/index.d.ts"
},
"./toolkit/*": {
"import": "./dist/toolkit/*.js",
"types": "./dist/toolkit/*.d.ts"
},
"./notifications": {
"import": "./dist/notifications/index.js",
"types": "./dist/notifications/index.d.ts"
},
"./notifications/*": {
"import": "./dist/notifications/*.js",
"types": "./dist/notifications/*.d.ts"
},
"./salesforce": {
"import": "./dist/salesforce/index.js",
"types": "./dist/salesforce/index.d.ts"
},
"./salesforce/*": {
"import": "./dist/salesforce/*.js",
"types": "./dist/salesforce/*.d.ts"
}
},
"scripts": {

View File

@ -21,17 +21,3 @@ export type {
PaymentGateway,
PaymentGatewayList,
} from "./schema.js";
// Provider adapters
export * as Providers from "./providers/index.js";
// Re-export provider raw types (request and response)
export type {
// Request params
WhmcsGetPayMethodsParams,
// Response types
WhmcsPaymentMethod,
WhmcsPaymentMethodListResponse,
WhmcsPaymentGateway,
WhmcsPaymentGatewayListResponse,
} from "./providers/whmcs/raw.types.js";

View File

@ -6,13 +6,7 @@
* Types are derived from Zod schemas (Schema-First Approach)
*/
// Provider-specific types
export {
type SalesforceProductFieldMap,
type PricingTier,
type CatalogFilter,
type CatalogPriceInfo,
} from "./contract.js";
export { type PricingTier, type CatalogPriceInfo } from "./contract.js";
// Schemas (includes derived types)
export * from "./schema.js";
@ -40,21 +34,5 @@ export type {
CatalogProduct,
} from "./schema.js";
// Provider adapters
export * as Providers from "./providers/index.js";
// Re-export provider raw types for convenience
export * from "./providers/salesforce/raw.types.js";
// Re-export WHMCS provider types
export type {
WhmcsCatalogProduct,
WhmcsCatalogProductListResponse,
} from "./providers/whmcs/raw.types.js";
export type {
WhmcsCatalogProductNormalized,
WhmcsCatalogPricing,
} from "./providers/whmcs/mapper.js";
// Utilities
export * from "./utils.js";

View File

@ -84,6 +84,3 @@ export type {
} from "./schema.js";
export type { SimPlanCode } from "./contract.js";
export type { SimPlanOption, SimFeatureToggleSnapshot } from "./helpers.js";
// Provider adapters
export * as Providers from "./providers/index.js";

View File

@ -37,16 +37,3 @@ export {
internetCancellationPreviewSchema,
internetCancelRequestSchema,
} from "./schema.js";
// Provider adapters
export * as Providers from "./providers/index.js";
// Re-export provider raw types (request and response)
export type {
// Request params
WhmcsGetClientsProductsParams,
// Response types
WhmcsProductListResponse,
} from "./providers/whmcs/raw.types.js";
export { whmcsProductListResponseSchema } from "./providers/whmcs/raw.types.js";

View File

@ -16,10 +16,3 @@ export {
// Schemas (includes derived types)
export * from "./schema.js";
// Provider adapters
export * as Providers from "./providers/index.js";
// Re-export provider types for convenience
export * from "./providers/salesforce/raw.types.js";
export * from "./providers/salesforce/mapper.js";

View File

@ -71,18 +71,18 @@ getHostname(url); // Extracts hostname
### Formatting
```typescript
import { formatCurrency, formatDate } from "@customer-portal/domain/toolkit/formatting";
import { Formatting } from "@customer-portal/domain/toolkit";
const price = formatCurrency(1000, "JPY"); // "¥1,000"
const date = formatDate(new Date()); // "2025-10-08"
const price = Formatting.formatCurrency(1000, "JPY"); // "¥1,000"
const date = Formatting.formatDate(new Date()); // "2025-10-08"
```
### Type Guards
```typescript
import { isString, isNumber } from "@customer-portal/domain/toolkit/typing";
import { Typing } from "@customer-portal/domain/toolkit";
if (isString(value)) {
if (Typing.isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
@ -91,31 +91,31 @@ if (isString(value)) {
### URL Utilities
```typescript
import { ensureProtocol, getHostname } from "@customer-portal/domain/toolkit/validation/url";
import { Validation } from "@customer-portal/domain/toolkit";
const fullUrl = ensureProtocol("example.com"); // "https://example.com"
const host = getHostname("https://example.com/path"); // "example.com"
const fullUrl = Validation.ensureProtocol("example.com"); // "https://example.com"
const host = Validation.getHostname("https://example.com/path"); // "example.com"
```
### Email Utilities
```typescript
import { getEmailDomain, normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
import { Validation } from "@customer-portal/domain/toolkit";
const domain = getEmailDomain("user@example.com"); // "example.com"
const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com"
const domain = Validation.getEmailDomain("user@example.com"); // "example.com"
const normalized = Validation.normalizeEmail(" User@Example.COM "); // "user@example.com"
```
## When to Use What
| Task | Use | Example |
| --------------------- | -------------------------------- | ------------------------------- |
| --------------------- | ---------------------- | ------------------------------------------ |
| Validate email format | `common/validation.ts` | `isValidEmail(email)` |
| Extract email domain | `toolkit/validation/email.ts` | `getEmailDomain(email)` |
| Extract email domain | `toolkit` | `Validation.getEmailDomain(email)` |
| Validate URL format | `common/validation.ts` | `isValidUrl(url)` |
| Add protocol to URL | `toolkit/validation/url.ts` | `ensureProtocol(url)` |
| Format currency | `toolkit/formatting/currency.ts` | `formatCurrency(amount, "JPY")` |
| Format date | `toolkit/formatting/date.ts` | `formatDate(date)` |
| Add protocol to URL | `toolkit` | `Validation.ensureProtocol(url)` |
| Format currency | `toolkit` | `Formatting.formatCurrency(amount, "JPY")` |
| Format date | `toolkit` | `Formatting.formatDate(date)` |
## Best Practices
@ -134,8 +134,8 @@ const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com"
```typescript
// ✅ Good
import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
const clean = normalizeEmail(email);
import { Validation } from "@customer-portal/domain/toolkit";
const clean = Validation.normalizeEmail(email);
// ❌ Bad - don't duplicate utility logic
const clean = email.trim().toLowerCase();
@ -152,11 +152,11 @@ const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com"
// ✅ Good - separate concerns
import { isValidEmail } from "@customer-portal/domain/common/validation";
import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email";
import { Validation } from "@customer-portal/domain/toolkit";
function processEmail(email: string) {
if (!isValidEmail(email)) return null;
return normalizeEmail(email);
return Validation.normalizeEmail(email);
}
```

View File

@ -7,20 +7,3 @@
export * as Formatting from "./formatting/index.js";
export * as Validation from "./validation/index.js";
export * as Typing from "./typing/index.js";
// Re-export commonly used utilities for convenience
export { formatCurrency } from "./formatting/currency.js";
export type { SupportedCurrency } from "./formatting/currency.js";
// Re-export AsyncState types and helpers
export type { AsyncState } from "./typing/helpers.js";
export {
createIdleState,
createLoadingState,
createSuccessState,
createErrorState,
isIdle,
isLoading,
isSuccess,
isError,
} from "./typing/helpers.js";

View File

@ -10,6 +10,12 @@ cd "$ROOT_DIR"
echo "[domain] Building @customer-portal/domain…"
pnpm --filter @customer-portal/domain build >/dev/null
echo "[domain] Checking exports contract…"
node ./scripts/domain/check-exports.mjs
echo "[domain] Checking import contract…"
bash ./scripts/domain/check-import-contract.sh
if command -v git >/dev/null 2>&1; then
if git diff --quiet -- packages/domain/dist; then
echo "[domain] OK: packages/domain/dist is up to date."

View File

@ -0,0 +1,26 @@
import fs from "node:fs/promises";
import path from "node:path";
const ROOT = process.cwd();
const pkgPath = path.join(ROOT, "packages", "domain", "package.json");
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
const exportsField = pkg.exports;
if (!exportsField || typeof exportsField !== "object") {
console.error("[domain] ERROR: package.json exports field is missing or invalid.");
process.exit(1);
}
const keys = Object.keys(exportsField);
const wildcardKeys = keys.filter(k => k.includes("*"));
if (wildcardKeys.length > 0) {
console.error("[domain] ERROR: wildcard subpath exports are not allowed:");
for (const k of wildcardKeys) console.error(`- ${k}`);
process.exit(1);
}
console.log("[domain] OK: package.json exports contains no wildcard keys.");

View File

@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
# Guardrail: enforce the domain import contract.
#
# Allowed:
# - @customer-portal/domain/<module>
# - @customer-portal/domain/toolkit
# - BFF-only: @customer-portal/domain/<module>/providers
#
# Never:
# - @customer-portal/domain/<module>/<anything-else>
# - @customer-portal/domain/<module>/providers/<anything-else>
# - Portal importing any .../providers
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
fail() {
echo "[domain] ERROR: $1" >&2
exit 1
}
echo "[domain] Checking for illegal deep imports in apps/…"
# Root import is forbidden (hard error). Only match actual import statements.
if grep -RInE --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.next \
--include='*.ts' --include='*.tsx' \
"(from[[:space:]]+[\"']@customer-portal/domain[\"'])|(import[[:space:]]+[\"']@customer-portal/domain[\"'])" \
apps >/dev/null; then
echo "[domain] Found forbidden root imports (@customer-portal/domain):"
grep -RInE --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.next \
--include='*.ts' --include='*.tsx' \
"(from[[:space:]]+[\"']@customer-portal/domain[\"'])|(import[[:space:]]+[\"']@customer-portal/domain[\"'])" \
apps | head -200
fail "Root import is forbidden. Use @customer-portal/domain/<module>."
fi
# Any 3+ segment import like @customer-portal/domain/a/b/c is illegal anywhere.
if grep -RInE --include='*.ts' --include='*.tsx' \
"@customer-portal/domain/[^\"'[:space:]]+/[^\"'[:space:]]+/[^\"'[:space:]]+" \
apps >/dev/null; then
echo "[domain] Found illegal deep imports (3+ segments):"
grep -RInE --include='*.ts' --include='*.tsx' \
"@customer-portal/domain/[^\"'[:space:]]+/[^\"'[:space:]]+/[^\"'[:space:]]+" \
apps | head -200
fail "Deep imports detected. Use @customer-portal/domain/<module> or .../<module>/providers."
fi
echo "[domain] Checking Portal boundary (no providers imports)…"
if grep -RInE --include='*.ts' --include='*.tsx' \
"@customer-portal/domain/[^\"'[:space:]]+/providers" \
apps/portal/src >/dev/null; then
echo "[domain] Found provider imports in Portal:"
grep -RInE --include='*.ts' --include='*.tsx' \
"@customer-portal/domain/[^\"'[:space:]]+/providers" \
apps/portal/src | head -200
fail "Portal must not import provider adapters/types."
fi
echo "[domain] OK: import contract checks passed."

View File

@ -0,0 +1,129 @@
import fs from "node:fs/promises";
import path from "node:path";
const ROOT = process.cwd();
const TARGET_DIRS = [
path.join(ROOT, "apps", "bff", "src"),
path.join(ROOT, "apps", "portal", "src"),
];
const FILE_EXTS = new Set([".ts", ".tsx"]);
async function* walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const e of entries) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
if (e.name === "node_modules" || e.name === "dist" || e.name.startsWith(".")) continue;
yield* walk(p);
} else if (e.isFile()) {
if (FILE_EXTS.has(path.extname(e.name))) yield p;
}
}
}
function replaceProvidersDeepImports(code) {
// "@customer-portal/domain/<module>/providers/<anything>" -> ".../<module>/providers"
return code
.replaceAll(
/from\s+"@customer-portal\/domain\/([a-z-]+)\/providers\/[^"]+"/g,
'from "@customer-portal/domain/$1/providers"'
)
.replaceAll(
/from\s+'@customer-portal\/domain\/([a-z-]+)\/providers\/[^']+'/g,
"from '@customer-portal/domain/$1/providers'"
);
}
function replaceCommonProviderTypes(code) {
// Move provider response wrapper types out of common root -> common/providers
// Only touches *type-only* imports to avoid moving runtime exports accidentally.
return code
.replaceAll(
/import\s+type\s+\{([^}]*)\}\s+from\s+"@customer-portal\/domain\/common";/g,
(m, spec) => {
const s = String(spec);
const needsMove =
s.includes("WhmcsResponse") || s.includes("WhmcsErrorResponse") || s.includes("SalesforceResponse");
if (!needsMove) return m;
return `import type {${spec}} from "@customer-portal/domain/common/providers";`;
}
)
.replaceAll(
/import\s+type\s+\{([^}]*)\}\s+from\s+'@customer-portal\/domain\/common';/g,
(m, spec) => {
const s = String(spec);
const needsMove =
s.includes("WhmcsResponse") || s.includes("WhmcsErrorResponse") || s.includes("SalesforceResponse");
if (!needsMove) return m;
return `import type {${spec}} from '@customer-portal/domain/common/providers';`;
}
);
}
function replaceProvidersNamespaceImports(code) {
// import { Providers } from "@customer-portal/domain/<module>";
// import { Providers as Foo } from "@customer-portal/domain/<module>";
return code
.replaceAll(
/import\s+\{\s*Providers\s*\}\s+from\s+"@customer-portal\/domain\/([a-z-]+)";/g,
'import * as Providers from "@customer-portal/domain/$1/providers";'
)
.replaceAll(
/import\s+\{\s*Providers\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\}\s+from\s+"@customer-portal\/domain\/([a-z-]+)";/g,
'import * as $1 from "@customer-portal/domain/$2/providers";'
)
.replaceAll(
/import\s+\{\s*Providers\s*\}\s+from\s+'@customer-portal\/domain\/([a-z-]+)';/g,
"import * as Providers from '@customer-portal/domain/$1/providers';"
)
.replaceAll(
/import\s+\{\s*Providers\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\}\s+from\s+'@customer-portal\/domain\/([a-z-]+)';/g,
"import * as $1 from '@customer-portal/domain/$2/providers';"
);
}
function replaceToolkitPaginationHelper(code) {
if (!code.includes("@customer-portal/domain/toolkit/validation/helpers")) return code;
let next = code;
next = next
.replaceAll(
/import\s+\{\s*createPaginationSchema\s*\}\s+from\s+"@customer-portal\/domain\/toolkit\/validation\/helpers";/g,
'import { Validation } from "@customer-portal/domain/toolkit";'
)
.replaceAll(
/import\s+\{\s*createPaginationSchema\s*\}\s+from\s+'@customer-portal\/domain\/toolkit\/validation\/helpers';/g,
"import { Validation } from '@customer-portal/domain/toolkit';"
);
// Update call sites
next = next.replaceAll(/\bcreatePaginationSchema\b/g, "Validation.createPaginationSchema");
return next;
}
function transform(code) {
let next = code;
next = replaceProvidersDeepImports(next);
next = replaceCommonProviderTypes(next);
next = replaceProvidersNamespaceImports(next);
next = replaceToolkitPaginationHelper(next);
return next;
}
let changedFiles = 0;
for (const dir of TARGET_DIRS) {
for await (const file of walk(dir)) {
const before = await fs.readFile(file, "utf8");
const after = transform(before);
if (after !== before) {
await fs.writeFile(file, after, "utf8");
changedFiles += 1;
}
}
}
console.log(`codemod-domain-imports: updated ${changedFiles} file(s)`);

View File

@ -48,7 +48,7 @@ migrate_directory() {
-e 's|@customer-portal/contracts/sim|@customer-portal/domain/sim|g' \
-e 's|@customer-portal/schemas/business/sim|@customer-portal/domain/sim|g' \
-e 's|@customer-portal/integrations-freebit/mappers/sim|@customer-portal/domain/sim|g' \
-e 's|@customer-portal/schemas/integrations/freebit/requests|@customer-portal/domain/sim/providers/freebit|g' \
-e 's|@customer-portal/schemas/integrations/freebit/requests|@customer-portal/domain/sim/providers|g' \
{} +
# Orders domain