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. 1. Have types and validation in the shared domain layer.
2. Keep business logic out of the frontend; use services and APIs instead. 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. 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 { User as PrismaUser } from "@prisma/client";
import type { UserAuth } from "@customer-portal/domain/customer"; 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]; type PrismaUserRaw = Parameters<typeof CustomerProviders.Portal.mapPrismaUserToUserAuth>[0];

View File

@ -9,7 +9,7 @@ import {
type SalesforceAccountPortalUpdate, type SalesforceAccountPortalUpdate,
} from "./services/salesforce-account.service.js"; } from "./services/salesforce-account.service.js";
import { SalesforceOperationException } from "@bff/core/exceptions/domain-exceptions.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 * Salesforce Service - Facade for Salesforce operations

View File

@ -3,8 +3,8 @@ import { Logger } from "nestjs-pino";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { SalesforceConnection } from "./salesforce-connection.service.js"; import { SalesforceConnection } from "./salesforce-connection.service.js";
import type { SalesforceAccountRecord } from "@customer-portal/domain/customer"; import type { SalesforceAccountRecord } from "@customer-portal/domain/customer/providers";
import type { SalesforceResponse } from "@customer-portal/domain/common"; import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import { customerNumberSchema, salesforceIdSchema } from "@customer-portal/domain/common"; 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 { SalesforceConnection } from "./salesforce-connection.service.js";
import { assertSalesforceId } from "../utils/soql.util.js"; import { assertSalesforceId } from "../utils/soql.util.js";
import { getErrorMessage } from "@bff/core/utils/error.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 { import {
type SupportCase,
type SalesforceCaseRecord,
type CreateCaseRequest,
SALESFORCE_CASE_ORIGIN, SALESFORCE_CASE_ORIGIN,
SALESFORCE_CASE_STATUS, SALESFORCE_CASE_STATUS,
SALESFORCE_CASE_PRIORITY, SALESFORCE_CASE_PRIORITY,
Providers, } from "@customer-portal/domain/support/providers";
} from "@customer-portal/domain/support"; import * as Providers from "@customer-portal/domain/support/providers";
// Access the mapper directly to avoid unbound method issues // Access the mapper directly to avoid unbound method issues
const salesforceMapper = Providers.Salesforce; const salesforceMapper = Providers.Salesforce;

View File

@ -21,7 +21,7 @@ import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "./salesforce-connection.service.js"; import { SalesforceConnection } from "./salesforce-connection.service.js";
import { assertSalesforceId } from "../utils/soql.util.js"; import { assertSalesforceId } from "../utils/soql.util.js";
import { getErrorMessage } from "@bff/core/utils/error.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 { import {
type OpportunityStageValue, type OpportunityStageValue,
type OpportunityProductTypeValue, type OpportunityProductTypeValue,

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { Injectable, Inject } from "@nestjs/common"; import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino"; import { Logger } from "nestjs-pino";
import { getErrorMessage } from "@bff/core/utils/error.util.js"; 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 { import type {
WhmcsApiConfig, WhmcsApiConfig,
WhmcsRequestOptions, WhmcsRequestOptions,

View File

@ -3,7 +3,7 @@ import { Logger } from "nestjs-pino";
import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.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"; import type { WhmcsClient } from "@customer-portal/domain/customer";
/** /**

View File

@ -9,8 +9,8 @@ import type {
WhmcsClientResponse, WhmcsClientResponse,
WhmcsAddClientResponse, WhmcsAddClientResponse,
WhmcsValidateLoginResponse, WhmcsValidateLoginResponse,
} from "@customer-portal/domain/customer"; } from "@customer-portal/domain/customer/providers";
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"; import type { WhmcsClient } from "@customer-portal/domain/customer";
@Injectable() @Injectable()

View File

@ -4,16 +4,14 @@ import { Logger } from "nestjs-pino";
import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js"; import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { import { FALLBACK_CURRENCY } from "@customer-portal/domain/billing";
FALLBACK_CURRENCY, import type { Currency } from "@customer-portal/domain/billing";
type WhmcsCurrenciesResponse, import type { WhmcsCurrenciesResponse } from "@customer-portal/domain/billing/providers";
type WhmcsCurrency,
} from "@customer-portal/domain/billing";
@Injectable() @Injectable()
export class WhmcsCurrencyService implements OnModuleInit, OnModuleDestroy { export class WhmcsCurrencyService implements OnModuleInit, OnModuleDestroy {
private defaultCurrency: WhmcsCurrency | null = null; private defaultCurrency: Currency | null = null;
private currencies: WhmcsCurrency[] = []; private currencies: Currency[] = [];
private readonly REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours private readonly REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
private refreshTimer: NodeJS.Timeout | null = null; 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) * Get the default currency (first currency from WHMCS or JPY fallback)
*/ */
getDefaultCurrency(): WhmcsCurrency { getDefaultCurrency(): Currency {
return this.defaultCurrency ? { ...this.defaultCurrency } : { ...FALLBACK_CURRENCY }; return this.defaultCurrency ? { ...this.defaultCurrency } : { ...FALLBACK_CURRENCY };
} }
/** /**
* Get all available currencies * Get all available currencies
*/ */
getAllCurrencies(): WhmcsCurrency[] { getAllCurrencies(): Currency[] {
return this.currencies; return this.currencies;
} }
/** /**
* Find currency by code * 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; 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 * Parse WHMCS response format into currency objects
* Handles both flat format (currencies[currency][0][id]) and nested format (currencies.currency[]) * Handles both flat format (currencies[currency][0][id]) and nested format (currencies.currency[])
*/ */
private parseWhmcsCurrenciesResponse(response: WhmcsCurrenciesResponse): WhmcsCurrency[] { private parseWhmcsCurrenciesResponse(response: WhmcsCurrenciesResponse): Currency[] {
const currencies: WhmcsCurrency[] = []; const currencies: Currency[] = [];
// Check if response has nested currency structure // Check if response has nested currency structure
if ( if (
@ -154,7 +152,7 @@ export class WhmcsCurrencyService implements OnModuleInit, OnModuleDestroy {
: [response.currencies.currency]; : [response.currencies.currency];
for (const currencyData of currencyArray) { for (const currencyData of currencyArray) {
const currency: WhmcsCurrency = { const currency: Currency = {
id: parseInt(String(currencyData.id)) || 0, id: parseInt(String(currencyData.id)) || 0,
code: String(currencyData.code || ""), code: String(currencyData.code || ""),
prefix: String(currencyData.prefix || ""), prefix: String(currencyData.prefix || ""),
@ -184,7 +182,7 @@ export class WhmcsCurrencyService implements OnModuleInit, OnModuleDestroy {
// Build currency objects from the flat response // Build currency objects from the flat response
for (const index of currencyIndices) { for (const index of currencyIndices) {
const currency: WhmcsCurrency = { const currency: Currency = {
id: parseInt(String(response[`currencies[currency][${index}][id]`])) || 0, id: parseInt(String(response[`currencies[currency][${index}][id]`])) || 0,
code: String(response[`currencies[currency][${index}][code]`] || ""), code: String(response[`currencies[currency][${index}][code]`] || ""),
prefix: String(response[`currencies[currency][${index}][prefix]`] || ""), 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 { Logger } from "nestjs-pino";
import { Injectable, NotFoundException, Inject } from "@nestjs/common"; import { Injectable, NotFoundException, Inject } from "@nestjs/common";
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js"; 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 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 { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsCurrencyService } from "./whmcs-currency.service.js"; import { WhmcsCurrencyService } from "./whmcs-currency.service.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js"; import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
@ -12,14 +13,14 @@ import type {
WhmcsCreateInvoiceParams, WhmcsCreateInvoiceParams,
WhmcsUpdateInvoiceParams, WhmcsUpdateInvoiceParams,
WhmcsCapturePaymentParams, WhmcsCapturePaymentParams,
} from "@customer-portal/domain/billing"; } from "@customer-portal/domain/billing/providers";
import type { import type {
WhmcsInvoiceListResponse, WhmcsInvoiceListResponse,
WhmcsInvoiceResponse, WhmcsInvoiceResponse,
WhmcsCreateInvoiceResponse, WhmcsCreateInvoiceResponse,
WhmcsUpdateInvoiceResponse, WhmcsUpdateInvoiceResponse,
WhmcsCapturePaymentResponse, WhmcsCapturePaymentResponse,
} from "@customer-portal/domain/billing"; } from "@customer-portal/domain/billing/providers";
export type InvoiceFilters = Partial<{ export type InvoiceFilters = Partial<{
status: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections"; status: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";

View File

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

View File

@ -1,27 +1,25 @@
import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino"; import { Logger } from "nestjs-pino";
import { Injectable, Inject } from "@nestjs/common"; import { Injectable, Inject } from "@nestjs/common";
import { Providers } from "@customer-portal/domain/payments"; import * as Providers from "@customer-portal/domain/payments/providers";
import type { import type {
PaymentMethodList, PaymentMethodList,
PaymentGateway, PaymentGateway,
PaymentGatewayList, PaymentGatewayList,
PaymentMethod, PaymentMethod,
} from "@customer-portal/domain/payments"; } from "@customer-portal/domain/payments";
import { import type { WhmcsCatalogProductNormalized } from "@customer-portal/domain/services/providers";
Providers as CatalogProviders, import * as CatalogProviders from "@customer-portal/domain/services/providers";
type WhmcsCatalogProductNormalized,
} from "@customer-portal/domain/services";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js"; import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
import type { WhmcsCreateSsoTokenParams } from "@customer-portal/domain/customer"; import type { WhmcsCreateSsoTokenParams } from "@customer-portal/domain/customer/providers";
import type { WhmcsGetPayMethodsParams } from "@customer-portal/domain/payments"; import type { WhmcsGetPayMethodsParams } from "@customer-portal/domain/payments/providers";
import type { import type {
WhmcsPaymentMethod, WhmcsPaymentMethod,
WhmcsPaymentMethodListResponse, WhmcsPaymentMethodListResponse,
WhmcsPaymentGateway, WhmcsPaymentGateway,
WhmcsPaymentGatewayListResponse, WhmcsPaymentGatewayListResponse,
} from "@customer-portal/domain/payments"; } from "@customer-portal/domain/payments/providers";
@Injectable() @Injectable()
export class WhmcsPaymentService { export class WhmcsPaymentService {

View File

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

View File

@ -2,12 +2,12 @@ import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino"; import { Logger } from "nestjs-pino";
import { Injectable, NotFoundException, Inject } from "@nestjs/common"; import { Injectable, NotFoundException, Inject } from "@nestjs/common";
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js"; 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 type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsCurrencyService } from "./whmcs-currency.service.js"; import { WhmcsCurrencyService } from "./whmcs-currency.service.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.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 { export interface SubscriptionFilters {
status?: string; 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 { Invoice, InvoiceList } from "@customer-portal/domain/billing";
import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions"; import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions";
import type { PaymentMethodList, PaymentGatewayList } from "@customer-portal/domain/payments"; import type { PaymentMethodList, PaymentGatewayList } from "@customer-portal/domain/payments";
import { import { addressSchema, type Address, type WhmcsClient } from "@customer-portal/domain/customer";
Providers as CustomerProviders, import * as CustomerProviders from "@customer-portal/domain/customer/providers";
addressSchema,
type Address,
type WhmcsClient,
} from "@customer-portal/domain/customer";
import { WhmcsConnectionOrchestratorService } from "./connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsConnectionOrchestratorService } from "./connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsInvoiceService } from "./services/whmcs-invoice.service.js"; import { WhmcsInvoiceService } from "./services/whmcs-invoice.service.js";
import type { InvoiceFilters } 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 { WhmcsPaymentService } from "./services/whmcs-payment.service.js";
import { WhmcsSsoService } from "./services/whmcs-sso.service.js"; import { WhmcsSsoService } from "./services/whmcs-sso.service.js";
import { WhmcsOrderService } from "./services/whmcs-order.service.js"; import { WhmcsOrderService } from "./services/whmcs-order.service.js";
import type { WhmcsAddClientParams, WhmcsClientResponse } from "@customer-portal/domain/customer"; import type {
import type { WhmcsGetClientsProductsParams } from "@customer-portal/domain/subscriptions"; WhmcsAddClientParams,
import type { WhmcsProductListResponse } from "@customer-portal/domain/subscriptions"; WhmcsClientResponse,
import type { WhmcsCatalogProductNormalized } from "@customer-portal/domain/services"; } 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"; import { Logger } from "nestjs-pino";
@Injectable() @Injectable()

View File

@ -27,7 +27,7 @@ import {
type ValidateSignupRequest, type ValidateSignupRequest,
} from "@customer-portal/domain/auth"; } from "@customer-portal/domain/auth";
import { ErrorCode } from "@customer-portal/domain/common"; 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 { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
import type { User as PrismaUser } from "@prisma/client"; import type { User as PrismaUser } from "@prisma/client";
import { CacheService } from "@bff/infra/cache/cache.service.js"; 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 { SalesforceService } from "@bff/integrations/salesforce/salesforce.service.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.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 type { User } from "@customer-portal/domain/customer";
import { import {
PORTAL_SOURCE_MIGRATED, PORTAL_SOURCE_MIGRATED,

View File

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

View File

@ -5,7 +5,7 @@ import {
defaultSalesforceOrderFieldMap, defaultSalesforceOrderFieldMap,
type PartialSalesforceOrderFieldMap, type PartialSalesforceOrderFieldMap,
type SalesforceOrderFieldMap, type SalesforceOrderFieldMap,
} from "@customer-portal/domain/orders"; } from "@customer-portal/domain/orders/providers";
const unique = <T>(values: T[]): T[] => Array.from(new Set(values)); 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 { OrdersCacheService } from "./orders-cache.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { NotificationService } from "@bff/modules/notifications/notifications.service.js"; import { NotificationService } from "@bff/modules/notifications/notifications.service.js";
import { import type { OrderDetails } from "@customer-portal/domain/orders";
type OrderDetails, import type { OrderFulfillmentValidationResult } from "@customer-portal/domain/orders/providers";
type OrderFulfillmentValidationResult, import * as OrderProviders from "@customer-portal/domain/orders/providers";
Providers as OrderProviders,
} from "@customer-portal/domain/orders";
import { OPPORTUNITY_STAGE } from "@customer-portal/domain/opportunity"; import { OPPORTUNITY_STAGE } from "@customer-portal/domain/opportunity";
import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications"; import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications";
import { salesforceAccountIdSchema } from "@customer-portal/domain/common"; 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 { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { sfOrderIdParamSchema } from "@customer-portal/domain/orders"; 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 { 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"; import { PaymentValidatorService } from "./payment-validator.service.js";
/** /**

View File

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

View File

@ -9,7 +9,7 @@ import {
type CreateOrderRequest, type CreateOrderRequest,
type OrderBusinessValidation, type OrderBusinessValidation,
} from "@customer-portal/domain/orders"; } 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; type WhmcsProduct = Providers.WhmcsRaw.WhmcsProductRaw;
import { SimServicesService } from "@bff/modules/services/services/sim-services.service.js"; 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"; } from "@bff/integrations/salesforce/utils/services-query-builder.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js";
import type { import type {
SalesforceProduct2WithPricebookEntries,
SalesforcePricebookEntryRecord, SalesforcePricebookEntryRecord,
} from "@customer-portal/domain/services"; SalesforceProduct2WithPricebookEntries,
import { Providers as CatalogProviders } from "@customer-portal/domain/services"; } from "@customer-portal/domain/services/providers";
import type { SalesforceResponse } from "@customer-portal/domain/common"; import * as CatalogProviders from "@customer-portal/domain/services/providers";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
@Injectable() @Injectable()
export class BaseServicesService { export class BaseServicesService {

View File

@ -3,20 +3,20 @@ import { ConfigService } from "@nestjs/config";
import { BaseServicesService } from "./base-services.service.js"; import { BaseServicesService } from "./base-services.service.js";
import { ServicesCacheService } from "./services-cache.service.js"; import { ServicesCacheService } from "./services-cache.service.js";
import type { import type {
SalesforceProduct2WithPricebookEntries,
InternetPlanCatalogItem, InternetPlanCatalogItem,
InternetInstallationCatalogItem, InternetInstallationCatalogItem,
InternetAddonCatalogItem, InternetAddonCatalogItem,
InternetEligibilityDetails, InternetEligibilityDetails,
InternetEligibilityStatus, InternetEligibilityStatus,
} from "@customer-portal/domain/services"; } from "@customer-portal/domain/services";
import type { SalesforceProduct2WithPricebookEntries } from "@customer-portal/domain/services/providers";
import { import {
Providers as CatalogProviders,
enrichInternetPlanMetadata, enrichInternetPlanMetadata,
inferAddonTypeFromSku, inferAddonTypeFromSku,
inferInstallationTermFromSku, inferInstallationTermFromSku,
internetEligibilityDetailsSchema, internetEligibilityDetailsSchema,
} from "@customer-portal/domain/services"; } 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 { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js"; import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
import { OpportunityResolutionService } from "@bff/integrations/salesforce/services/opportunity-resolution.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 { assertSalesforceId } from "@bff/integrations/salesforce/utils/soql.util.js";
import { assertSoqlFieldName } 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 { 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) // (removed unused opportunity constants import)
@Injectable() @Injectable()

View File

@ -3,11 +3,11 @@ import { ConfigService } from "@nestjs/config";
import { BaseServicesService } from "./base-services.service.js"; import { BaseServicesService } from "./base-services.service.js";
import { ServicesCacheService } from "./services-cache.service.js"; import { ServicesCacheService } from "./services-cache.service.js";
import type { import type {
SalesforceProduct2WithPricebookEntries,
SimCatalogProduct, SimCatalogProduct,
SimActivationFeeCatalogItem, SimActivationFeeCatalogItem,
} from "@customer-portal/domain/services"; } 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 { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js"; import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
import { Logger } from "nestjs-pino"; 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 { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
import { BaseServicesService } from "./base-services.service.js"; import { BaseServicesService } from "./base-services.service.js";
import { ServicesCacheService } from "./services-cache.service.js"; import { ServicesCacheService } from "./services-cache.service.js";
import type { import type { VpnCatalogProduct } from "@customer-portal/domain/services";
SalesforceProduct2WithPricebookEntries, import type { SalesforceProduct2WithPricebookEntries } from "@customer-portal/domain/services/providers";
VpnCatalogProduct, import * as CatalogProviders from "@customer-portal/domain/services/providers";
} from "@customer-portal/domain/services";
import { Providers as CatalogProviders } from "@customer-portal/domain/services";
@Injectable() @Injectable()
export class VpnServicesService extends BaseServicesService { export class VpnServicesService extends BaseServicesService {

View File

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

View File

@ -33,7 +33,7 @@ import type {
SimPlanChangeResult, SimPlanChangeResult,
} from "@customer-portal/domain/subscriptions"; } from "@customer-portal/domain/subscriptions";
import type { InvoiceList } from "@customer-portal/domain/billing"; 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 { import {
simTopupRequestSchema, simTopupRequestSchema,
simChangePlanRequestSchema, simChangePlanRequestSchema,
@ -82,7 +82,7 @@ import {
} from "@customer-portal/domain/subscriptions"; } from "@customer-portal/domain/subscriptions";
import { invoiceListSchema } from "@customer-portal/domain/billing"; import { invoiceListSchema } from "@customer-portal/domain/billing";
const subscriptionInvoiceQuerySchema = createPaginationSchema({ const subscriptionInvoiceQuerySchema = Validation.createPaginationSchema({
defaultLimit: 10, defaultLimit: 10,
maxLimit: 100, maxLimit: 100,
minLimit: 1, 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 { WhmcsCacheService } from "@bff/integrations/whmcs/cache/whmcs-cache.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { Logger } from "nestjs-pino"; 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; 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 type { User as PrismaUser } from "@prisma/client";
import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { import {
Providers as CustomerProviders,
addressSchema, addressSchema,
combineToUser, combineToUser,
type Address, type Address,
type User, type User,
} from "@customer-portal/domain/customer"; } from "@customer-portal/domain/customer";
import * as CustomerProviders from "@customer-portal/domain/customer/providers";
import { import {
updateCustomerProfileRequestSchema, updateCustomerProfileRequestSchema,
type UpdateCustomerProfileRequest, type UpdateCustomerProfileRequest,

View File

@ -7,7 +7,7 @@ import {
assertSalesforceId, assertSalesforceId,
assertSoqlFieldName, assertSoqlFieldName,
} from "@bff/integrations/salesforce/utils/soql.util.js"; } 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 { import {
residenceCardVerificationSchema, residenceCardVerificationSchema,
type ResidenceCardVerification, type ResidenceCardVerification,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -140,11 +140,12 @@ packages/domain/
#### **Usage** #### **Usage**
Import via `@customer-portal/domain`: Import via module entrypoints:
```typescript ```typescript
import { Invoice, SIM_LIFECYCLE_STAGE, OrderStatus } from "@customer-portal/domain"; import { invoiceSchema, type Invoice } from "@customer-portal/domain/billing";
import { invoiceSchema, orderSchema } from "@customer-portal/domain/validation"; import { orderSummarySchema, type OrderSummary } from "@customer-portal/domain/orders";
import { SIM_STATUS } from "@customer-portal/domain/sim";
``` ```
#### **Integration with BFF** #### **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) - Adding new providers = adding new folders (no refactoring)
- Single package (`@customer-portal/domain`) for all types - Single package (`@customer-portal/domain`) for all types
**Import rules**: See [`docs/development/domain/import-hygiene.md`](import-hygiene.md).
--- ---
## 📦 Package Structure ## 📦 Package Structure
@ -130,9 +132,9 @@ const validated = invoiceSchema.parse(rawData);
import { Invoice, invoiceSchema } from "@customer-portal/domain/billing"; import { Invoice, invoiceSchema } from "@customer-portal/domain/billing";
import { import {
transformWhmcsInvoice, transformWhmcsInvoice,
whmcsInvoiceRawSchema,
type WhmcsInvoiceRaw, type WhmcsInvoiceRaw,
} from "@customer-portal/domain/billing/providers/whmcs/mapper"; } from "@customer-portal/domain/billing/providers";
import { whmcsInvoiceRawSchema } from "@customer-portal/domain/billing/providers/whmcs/raw.types";
// Transform raw API data // Transform raw API data
const whmcsData: WhmcsInvoiceRaw = await whmcsApi.getInvoice(id); const whmcsData: WhmcsInvoiceRaw = await whmcsApi.getInvoice(id);
@ -256,7 +258,7 @@ Raw types and mappers stay in `providers/`:
```typescript ```typescript
// ✅ GOOD - Isolated // ✅ 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 // ❌ BAD - Would leak WHMCS details into app code
import { WhmcsInvoiceRaw } from "@somewhere/global"; import { WhmcsInvoiceRaw } from "@somewhere/global";
@ -335,7 +337,7 @@ export function transformStripeInvoice(raw: unknown): Invoice {
```typescript ```typescript
// No changes to domain contract needed! // 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); const invoice = transformStripeInvoice(stripeData);
``` ```

View File

@ -155,17 +155,79 @@ export default [
"no-restricted-imports": [ "no-restricted-imports": [
"error", "error",
{ {
paths: [
{
name: "@customer-portal/domain",
message:
"Do not import @customer-portal/domain (root). Use @customer-portal/domain/<module> instead.",
},
],
patterns: [ patterns: [
{ {
group: ["@customer-portal/domain/**/src/**"], group: ["@customer-portal/domain/**/src/**"],
message: "Import from @customer-portal/domain/<module> instead of internals.", 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) // BFF: stricter type safety (type-aware)
// ============================================================================= // =============================================================================

View File

@ -4,7 +4,7 @@
* Domain constants for billing validation and business rules. * 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 // 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 * is unavailable. This ensures a single source of truth for default currency
* formatting behaviour. * formatting behaviour.
*/ */
export const FALLBACK_CURRENCY: WhmcsCurrency = { export const FALLBACK_CURRENCY: Currency = {
id: 1, id: 1,
code: "JPY", code: "JPY",
prefix: "¥", prefix: "¥",

View File

@ -15,6 +15,7 @@ export * from "./schema.js";
// Re-export types for convenience // Re-export types for convenience
export type { export type {
Currency,
InvoiceStatus, InvoiceStatus,
InvoiceItem, InvoiceItem,
Invoice, Invoice,
@ -29,23 +30,3 @@ export type {
InvoiceSsoQuery, InvoiceSsoQuery,
InvoicePaymentLinkQuery, InvoicePaymentLinkQuery,
} from "./schema.js"; } 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"; 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 // Invoice Status Schema
export const invoiceStatusSchema = z.enum([ export const invoiceStatusSchema = z.enum([
"Draft", "Draft",

View File

@ -8,10 +8,3 @@ export * from "./types.js";
export * from "./schema.js"; export * from "./schema.js";
export * from "./validation.js"; export * from "./validation.js";
export * from "./errors.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. * 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"; export * as Salesforce from "./salesforce/index.js";

View File

@ -68,34 +68,6 @@ export {
* - Providers.Whmcs.transformWhmcsClientResponse() * - Providers.Whmcs.transformWhmcsClientResponse()
* - Providers.Portal.mapPrismaUserToUserAuth() * - Providers.Portal.mapPrismaUserToUserAuth()
*/ */
export * as Providers from "./providers/index.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
// 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";

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import * as WhmcsRaw from "./whmcs/raw.types.js";
import * as SalesforceFieldMap from "./salesforce/field-map.js"; import * as SalesforceFieldMap from "./salesforce/field-map.js";
import * as SalesforceMapper from "./salesforce/mapper.js"; import * as SalesforceMapper from "./salesforce/mapper.js";
import * as SalesforceRaw from "./salesforce/raw.types.js"; import * as SalesforceRaw from "./salesforce/raw.types.js";
export * from "./types.js";
export const Whmcs = { export const Whmcs = {
...WhmcsMapper, ...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" "dist"
], ],
"exports": { "exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./auth": { "./auth": {
"import": "./dist/auth/index.js", "import": "./dist/auth/index.js",
"types": "./dist/auth/index.d.ts" "types": "./dist/auth/index.d.ts"
}, },
"./auth/*": {
"import": "./dist/auth/*.js",
"types": "./dist/auth/*.d.ts"
},
"./billing": { "./billing": {
"import": "./dist/billing/index.js", "import": "./dist/billing/index.js",
"types": "./dist/billing/index.d.ts" "types": "./dist/billing/index.d.ts"
}, },
"./billing/*": { "./billing/providers": {
"import": "./dist/billing/*.js", "import": "./dist/billing/providers/index.js",
"types": "./dist/billing/*.d.ts" "types": "./dist/billing/providers/index.d.ts"
}, },
"./services": { "./services": {
"import": "./dist/services/index.js", "import": "./dist/services/index.js",
"types": "./dist/services/index.d.ts" "types": "./dist/services/index.d.ts"
}, },
"./services/*": { "./services/providers": {
"import": "./dist/services/*.js", "import": "./dist/services/providers/index.js",
"types": "./dist/services/*.d.ts" "types": "./dist/services/providers/index.d.ts"
}, },
"./checkout": { "./checkout": {
"import": "./dist/checkout/index.js", "import": "./dist/checkout/index.js",
"types": "./dist/checkout/index.d.ts" "types": "./dist/checkout/index.d.ts"
}, },
"./checkout/*": {
"import": "./dist/checkout/*.js",
"types": "./dist/checkout/*.d.ts"
},
"./common": { "./common": {
"import": "./dist/common/index.js", "import": "./dist/common/index.js",
"types": "./dist/common/index.d.ts" "types": "./dist/common/index.d.ts"
}, },
"./common/*": { "./common/providers": {
"import": "./dist/common/*.js", "import": "./dist/common/providers/index.js",
"types": "./dist/common/*.d.ts" "types": "./dist/common/providers/index.d.ts"
}, },
"./customer": { "./customer": {
"import": "./dist/customer/index.js", "import": "./dist/customer/index.js",
"types": "./dist/customer/index.d.ts" "types": "./dist/customer/index.d.ts"
}, },
"./customer/*": { "./customer/providers": {
"import": "./dist/customer/*.js", "import": "./dist/customer/providers/index.js",
"types": "./dist/customer/*.d.ts" "types": "./dist/customer/providers/index.d.ts"
}, },
"./dashboard": { "./dashboard": {
"import": "./dist/dashboard/index.js", "import": "./dist/dashboard/index.js",
"types": "./dist/dashboard/index.d.ts" "types": "./dist/dashboard/index.d.ts"
}, },
"./dashboard/*": {
"import": "./dist/dashboard/*.js",
"types": "./dist/dashboard/*.d.ts"
},
"./mappings": { "./mappings": {
"import": "./dist/mappings/index.js", "import": "./dist/mappings/index.js",
"types": "./dist/mappings/index.d.ts" "types": "./dist/mappings/index.d.ts"
}, },
"./mappings/*": {
"import": "./dist/mappings/*.js",
"types": "./dist/mappings/*.d.ts"
},
"./opportunity": { "./opportunity": {
"import": "./dist/opportunity/index.js", "import": "./dist/opportunity/index.js",
"types": "./dist/opportunity/index.d.ts" "types": "./dist/opportunity/index.d.ts"
}, },
"./opportunity/*": {
"import": "./dist/opportunity/*.js",
"types": "./dist/opportunity/*.d.ts"
},
"./orders": { "./orders": {
"import": "./dist/orders/index.js", "import": "./dist/orders/index.js",
"types": "./dist/orders/index.d.ts" "types": "./dist/orders/index.d.ts"
}, },
"./orders/*": { "./orders/providers": {
"import": "./dist/orders/*.js", "import": "./dist/orders/providers/index.js",
"types": "./dist/orders/*.d.ts" "types": "./dist/orders/providers/index.d.ts"
}, },
"./payments": { "./payments": {
"import": "./dist/payments/index.js", "import": "./dist/payments/index.js",
"types": "./dist/payments/index.d.ts" "types": "./dist/payments/index.d.ts"
}, },
"./payments/*": { "./payments/providers": {
"import": "./dist/payments/*.js", "import": "./dist/payments/providers/index.js",
"types": "./dist/payments/*.d.ts" "types": "./dist/payments/providers/index.d.ts"
}, },
"./realtime": { "./realtime": {
"import": "./dist/realtime/index.js", "import": "./dist/realtime/index.js",
"types": "./dist/realtime/index.d.ts" "types": "./dist/realtime/index.d.ts"
}, },
"./realtime/*": {
"import": "./dist/realtime/*.js",
"types": "./dist/realtime/*.d.ts"
},
"./sim": { "./sim": {
"import": "./dist/sim/index.js", "import": "./dist/sim/index.js",
"types": "./dist/sim/index.d.ts" "types": "./dist/sim/index.d.ts"
}, },
"./sim/*": { "./sim/providers": {
"import": "./dist/sim/*.js", "import": "./dist/sim/providers/index.js",
"types": "./dist/sim/*.d.ts" "types": "./dist/sim/providers/index.d.ts"
},
"./sim/providers/freebit": {
"import": "./dist/sim/providers/freebit/index.js",
"types": "./dist/sim/providers/freebit/index.d.ts"
}, },
"./subscriptions": { "./subscriptions": {
"import": "./dist/subscriptions/index.js", "import": "./dist/subscriptions/index.js",
"types": "./dist/subscriptions/index.d.ts" "types": "./dist/subscriptions/index.d.ts"
}, },
"./subscriptions/*": { "./subscriptions/providers": {
"import": "./dist/subscriptions/*.js", "import": "./dist/subscriptions/providers/index.js",
"types": "./dist/subscriptions/*.d.ts" "types": "./dist/subscriptions/providers/index.d.ts"
}, },
"./support": { "./support": {
"import": "./dist/support/index.js", "import": "./dist/support/index.js",
"types": "./dist/support/index.d.ts" "types": "./dist/support/index.d.ts"
}, },
"./support/*": { "./support/providers": {
"import": "./dist/support/*.js", "import": "./dist/support/providers/index.js",
"types": "./dist/support/*.d.ts" "types": "./dist/support/providers/index.d.ts"
}, },
"./toolkit": { "./toolkit": {
"import": "./dist/toolkit/index.js", "import": "./dist/toolkit/index.js",
"types": "./dist/toolkit/index.d.ts" "types": "./dist/toolkit/index.d.ts"
}, },
"./toolkit/*": {
"import": "./dist/toolkit/*.js",
"types": "./dist/toolkit/*.d.ts"
},
"./notifications": { "./notifications": {
"import": "./dist/notifications/index.js", "import": "./dist/notifications/index.js",
"types": "./dist/notifications/index.d.ts" "types": "./dist/notifications/index.d.ts"
}, },
"./notifications/*": {
"import": "./dist/notifications/*.js",
"types": "./dist/notifications/*.d.ts"
},
"./salesforce": { "./salesforce": {
"import": "./dist/salesforce/index.js", "import": "./dist/salesforce/index.js",
"types": "./dist/salesforce/index.d.ts" "types": "./dist/salesforce/index.d.ts"
},
"./salesforce/*": {
"import": "./dist/salesforce/*.js",
"types": "./dist/salesforce/*.d.ts"
} }
}, },
"scripts": { "scripts": {

View File

@ -21,17 +21,3 @@ export type {
PaymentGateway, PaymentGateway,
PaymentGatewayList, PaymentGatewayList,
} from "./schema.js"; } 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) * Types are derived from Zod schemas (Schema-First Approach)
*/ */
// Provider-specific types export { type PricingTier, type CatalogPriceInfo } from "./contract.js";
export {
type SalesforceProductFieldMap,
type PricingTier,
type CatalogFilter,
type CatalogPriceInfo,
} from "./contract.js";
// Schemas (includes derived types) // Schemas (includes derived types)
export * from "./schema.js"; export * from "./schema.js";
@ -40,21 +34,5 @@ export type {
CatalogProduct, CatalogProduct,
} from "./schema.js"; } 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 // Utilities
export * from "./utils.js"; export * from "./utils.js";

View File

@ -84,6 +84,3 @@ export type {
} from "./schema.js"; } from "./schema.js";
export type { SimPlanCode } from "./contract.js"; export type { SimPlanCode } from "./contract.js";
export type { SimPlanOption, SimFeatureToggleSnapshot } from "./helpers.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, internetCancellationPreviewSchema,
internetCancelRequestSchema, internetCancelRequestSchema,
} from "./schema.js"; } 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) // Schemas (includes derived types)
export * from "./schema.js"; 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 ### Formatting
```typescript ```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 price = Formatting.formatCurrency(1000, "JPY"); // "¥1,000"
const date = formatDate(new Date()); // "2025-10-08" const date = Formatting.formatDate(new Date()); // "2025-10-08"
``` ```
### Type Guards ### Type Guards
```typescript ```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 // TypeScript knows value is string here
console.log(value.toUpperCase()); console.log(value.toUpperCase());
} }
@ -91,31 +91,31 @@ if (isString(value)) {
### URL Utilities ### URL Utilities
```typescript ```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 fullUrl = Validation.ensureProtocol("example.com"); // "https://example.com"
const host = getHostname("https://example.com/path"); // "example.com" const host = Validation.getHostname("https://example.com/path"); // "example.com"
``` ```
### Email Utilities ### Email Utilities
```typescript ```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 domain = Validation.getEmailDomain("user@example.com"); // "example.com"
const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com" const normalized = Validation.normalizeEmail(" User@Example.COM "); // "user@example.com"
``` ```
## When to Use What ## When to Use What
| Task | Use | Example | | Task | Use | Example |
| --------------------- | -------------------------------- | ------------------------------- | | --------------------- | ---------------------- | ------------------------------------------ |
| Validate email format | `common/validation.ts` | `isValidEmail(email)` | | 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)` | | Validate URL format | `common/validation.ts` | `isValidUrl(url)` |
| Add protocol to URL | `toolkit/validation/url.ts` | `ensureProtocol(url)` | | Add protocol to URL | `toolkit` | `Validation.ensureProtocol(url)` |
| Format currency | `toolkit/formatting/currency.ts` | `formatCurrency(amount, "JPY")` | | Format currency | `toolkit` | `Formatting.formatCurrency(amount, "JPY")` |
| Format date | `toolkit/formatting/date.ts` | `formatDate(date)` | | Format date | `toolkit` | `Formatting.formatDate(date)` |
## Best Practices ## Best Practices
@ -134,8 +134,8 @@ const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com"
```typescript ```typescript
// ✅ Good // ✅ Good
import { normalizeEmail } from "@customer-portal/domain/toolkit/validation/email"; import { Validation } from "@customer-portal/domain/toolkit";
const clean = normalizeEmail(email); const clean = Validation.normalizeEmail(email);
// ❌ Bad - don't duplicate utility logic // ❌ Bad - don't duplicate utility logic
const clean = email.trim().toLowerCase(); const clean = email.trim().toLowerCase();
@ -152,11 +152,11 @@ const normalized = normalizeEmail(" User@Example.COM "); // "user@example.com"
// ✅ Good - separate concerns // ✅ Good - separate concerns
import { isValidEmail } from "@customer-portal/domain/common/validation"; 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) { function processEmail(email: string) {
if (!isValidEmail(email)) return null; 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 Formatting from "./formatting/index.js";
export * as Validation from "./validation/index.js"; export * as Validation from "./validation/index.js";
export * as Typing from "./typing/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…" echo "[domain] Building @customer-portal/domain…"
pnpm --filter @customer-portal/domain build >/dev/null 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 command -v git >/dev/null 2>&1; then
if git diff --quiet -- packages/domain/dist; then if git diff --quiet -- packages/domain/dist; then
echo "[domain] OK: packages/domain/dist is up to date." 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/contracts/sim|@customer-portal/domain/sim|g' \
-e 's|@customer-portal/schemas/business/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/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 # Orders domain