Refactor Salesforce Integration and Update Billing Logic

- Replaced the OrderFieldConfigModule with SalesforceOrderFieldConfigModule in the Salesforce integration module to streamline configuration management.
- Updated SalesforceOrderService to utilize SalesforceOrderFieldMapService, enhancing consistency in order field mapping.
- Refactored multiple controllers to remove inline query parsing, directly using query parameters for improved clarity and maintainability.
- Adjusted the ResidenceCardService to enhance error handling and response consistency when interacting with Salesforce.
- Cleaned up unused imports and optimized code structure for better maintainability across the BFF modules.
This commit is contained in:
barsa 2025-12-29 14:09:33 +09:00
parent ea188f098b
commit ed5c2ead63
22 changed files with 237 additions and 149 deletions

View File

@ -15,8 +15,19 @@ const SECTION_PREFIX: Record<keyof SalesforceOrderFieldMap, string> = {
product: "PRODUCT",
};
/**
* Salesforce Order Field Map Service
*
* Resolves Salesforce field names from environment configuration and provides
* query building utilities for Order-related SOQL queries.
*
* This service lives in the integrations layer because:
* - It is specific to Salesforce infrastructure
* - It handles SOQL query construction
* - It reads environment-specific field name overrides
*/
@Injectable()
export class OrderFieldMapService {
export class SalesforceOrderFieldMapService {
readonly fields: SalesforceOrderFieldMap;
constructor(private readonly config: ConfigService) {

View File

@ -0,0 +1,17 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { SalesforceOrderFieldMapService } from "./order-field-map.service.js";
/**
* Salesforce Order Field Configuration Module
*
* Provides the SalesforceOrderFieldMapService for Salesforce integration layer.
* This module is imported by SalesforceModule and can be imported by
* application modules that need access to Salesforce field mappings.
*/
@Module({
imports: [ConfigModule],
providers: [SalesforceOrderFieldMapService],
exports: [SalesforceOrderFieldMapService],
})
export class SalesforceOrderFieldConfigModule {}

View File

@ -8,12 +8,12 @@ import { SalesforceOrderService } from "./services/salesforce-order.service.js";
import { SalesforceCaseService } from "./services/salesforce-case.service.js";
import { SalesforceOpportunityService } from "./services/salesforce-opportunity.service.js";
import { OpportunityResolutionService } from "./services/opportunity-resolution.service.js";
import { OrderFieldConfigModule } from "@bff/modules/orders/config/order-field-config.module.js";
import { SalesforceOrderFieldConfigModule } from "./config/salesforce-order-field-config.module.js";
import { SalesforceReadThrottleGuard } from "./guards/salesforce-read-throttle.guard.js";
import { SalesforceWriteThrottleGuard } from "./guards/salesforce-write-throttle.guard.js";
@Module({
imports: [QueueModule, ConfigModule, OrderFieldConfigModule],
imports: [QueueModule, ConfigModule, SalesforceOrderFieldConfigModule],
providers: [
SalesforceConnection,
SalesforceAccountService,

View File

@ -26,7 +26,7 @@ import {
transformSalesforceOrderSummary,
} 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";
import { SalesforceOrderFieldMapService } from "../config/order-field-map.service.js";
/**
* Salesforce Order Service
@ -39,7 +39,7 @@ export class SalesforceOrderService {
constructor(
private readonly sf: SalesforceConnection,
@Inject(Logger) private readonly logger: Logger,
private readonly orderFieldMap: OrderFieldMapService
private readonly orderFieldMap: SalesforceOrderFieldMapService
) {}
private readonly compositeOrderReference = "order_ref";

View File

@ -103,12 +103,11 @@ export class BillingController {
@Query() query: InvoiceSsoQueryDto
): Promise<InvoiceSsoLink> {
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(req.user.id);
const parsedQuery = invoiceSsoQuerySchema.parse(query as unknown);
const ssoUrl = await this.whmcsService.whmcsSsoForInvoice(
whmcsClientId,
params.id,
parsedQuery.target
query.target
);
return {
@ -126,19 +125,18 @@ export class BillingController {
@Query() query: InvoicePaymentLinkQueryDto
): Promise<InvoicePaymentLink> {
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(req.user.id);
const parsedQuery = invoicePaymentLinkQuerySchema.parse(query as unknown);
const ssoResult = await this.whmcsService.createPaymentSsoToken(
whmcsClientId,
params.id,
parsedQuery.paymentMethodId,
parsedQuery.gatewayName
query.paymentMethodId,
query.gatewayName
);
return {
url: ssoResult.url,
expiresAt: ssoResult.expiresAt,
gatewayName: parsedQuery.gatewayName,
gatewayName: query.gatewayName,
};
}
}

View File

@ -0,0 +1,8 @@
## BFF-local domain contracts (id-mappings)
This folder contains **BFF-internal** contracts/schemas/validation for the `id-mappings` module.
- **Why its local**: these types are not part of the shared portal/BFF domain API; they describe internal mapping records and admin operations that are tightly coupled to the BFFs persistence model.
- **When to move it to `packages/domain/`**: if the portal (or another app) needs to consume these contracts directly, or we want shared runtime validation/error codes around mapping operations across apps.
If/when we migrate, follow the import hygiene rules in `docs/development/domain/import-hygiene.md` and expose only the required public surface from `@customer-portal/domain/<module>`.

View File

@ -140,7 +140,7 @@ export class MeStatusService {
const isValid = !Number.isNaN(dueDate.getTime());
const isOverdue = isValid ? dueDate.getTime() < Date.now() : false;
const formattedAmount = new Intl.NumberFormat("ja-JP", {
const formattedAmount = new Intl.NumberFormat("en-US", {
style: "currency",
currency: summary.nextInvoice.currency,
maximumFractionDigits: 0,

View File

@ -41,12 +41,10 @@ export class NotificationsController {
@Req() req: RequestWithUser,
@Query() query: NotificationQueryDto
): Promise<NotificationListResponse> {
const parsedQuery = notificationQuerySchema.parse(query as unknown);
return this.notificationService.getNotifications(req.user.id, {
limit: Math.min(parsedQuery.limit, 50), // Cap at 50
offset: parsedQuery.offset,
includeRead: parsedQuery.includeRead,
limit: Math.min(query.limit, 50), // Cap at 50
offset: query.offset,
includeRead: query.includeRead,
});
}

View File

@ -1,10 +0,0 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { OrderFieldMapService } from "./order-field-map.service.js";
@Module({
imports: [ConfigModule],
providers: [OrderFieldMapService],
exports: [OrderFieldMapService],
})
export class OrderFieldConfigModule {}

View File

@ -29,7 +29,7 @@ import { OrderFulfillmentErrorService } from "./services/order-fulfillment-error
import { SimFulfillmentService } from "./services/sim-fulfillment.service.js";
import { ProvisioningQueueService } from "./queue/provisioning.queue.js";
import { ProvisioningProcessor } from "./queue/provisioning.processor.js";
import { OrderFieldConfigModule } from "./config/order-field-config.module.js";
import { SalesforceOrderFieldConfigModule } from "@bff/integrations/salesforce/config/salesforce-order-field-config.module.js";
@Module({
imports: [
@ -41,7 +41,7 @@ import { OrderFieldConfigModule } from "./config/order-field-config.module.js";
CacheModule,
VerificationModule,
NotificationsModule,
OrderFieldConfigModule,
SalesforceOrderFieldConfigModule,
],
controllers: [OrdersController, CheckoutController],
providers: [

View File

@ -2,7 +2,7 @@ import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import type { OrderBusinessValidation, UserMapping } from "@customer-portal/domain/orders";
import { UsersFacade } from "@bff/modules/users/application/users.facade.js";
import { OrderFieldMapService } from "@bff/modules/orders/config/order-field-map.service.js";
import { SalesforceOrderFieldMapService } from "@bff/integrations/salesforce/config/order-field-map.service.js";
function assignIfString(target: Record<string, unknown>, key: string, value: unknown): void {
if (typeof value === "string" && value.trim().length > 0) {
@ -18,7 +18,7 @@ export class OrderBuilder {
constructor(
@Inject(Logger) private readonly logger: Logger,
private readonly usersFacade: UsersFacade,
private readonly orderFieldMap: OrderFieldMapService
private readonly orderFieldMap: SalesforceOrderFieldMapService
) {}
async buildOrderFields(
@ -61,7 +61,7 @@ export class OrderBuilder {
private addActivationFields(
orderFields: Record<string, unknown>,
body: OrderBusinessValidation,
fieldNames: OrderFieldMapService["fields"]["order"]
fieldNames: SalesforceOrderFieldMapService["fields"]["order"]
): void {
const config = body.configurations || {};
@ -73,7 +73,7 @@ export class OrderBuilder {
private addInternetFields(
orderFields: Record<string, unknown>,
body: OrderBusinessValidation,
fieldNames: OrderFieldMapService["fields"]["order"]
fieldNames: SalesforceOrderFieldMapService["fields"]["order"]
): void {
const config = body.configurations || {};
assignIfString(orderFields, fieldNames.accessMode, config.accessMode);
@ -82,7 +82,7 @@ export class OrderBuilder {
private addSimFields(
orderFields: Record<string, unknown>,
body: OrderBusinessValidation,
fieldNames: OrderFieldMapService["fields"]["order"]
fieldNames: SalesforceOrderFieldMapService["fields"]["order"]
): void {
const config = body.configurations || {};
assignIfString(orderFields, fieldNames.simType, config.simType);
@ -119,7 +119,7 @@ export class OrderBuilder {
orderFields: Record<string, unknown>,
userId: string,
body: OrderBusinessValidation,
fieldNames: OrderFieldMapService["fields"]["order"]
fieldNames: SalesforceOrderFieldMapService["fields"]["order"]
): Promise<void> {
try {
const profile = await this.usersFacade.getProfile(userId);

View File

@ -69,9 +69,8 @@ export class CallHistoryController {
@Get("sim/call-history/sftp-files")
@ZodResponse({ description: "List available SFTP files", type: SimSftpListResultResponseDto })
async listSftpFiles(@Query() query: SimSftpListQueryDto) {
const parsedQuery = simSftpListQuerySchema.parse(query as unknown);
const files = await this.simCallHistoryService.listSftpFiles(parsedQuery.path);
return { files, path: parsedQuery.path };
const files = await this.simCallHistoryService.listSftpFiles(query.path);
return { files, path: query.path };
}
/**
@ -84,8 +83,7 @@ export class CallHistoryController {
type: SimCallHistoryImportResultResponseDto,
})
async importCallHistory(@Query() query: SimCallHistoryImportQueryDto) {
const parsedQuery = simCallHistoryImportQuerySchema.parse(query as unknown);
const result = await this.simCallHistoryService.importCallHistory(parsedQuery.month);
const result = await this.simCallHistoryService.importCallHistory(query.month);
return result;
}
@ -105,13 +103,12 @@ export class CallHistoryController {
@Param() params: SubscriptionIdParamDto,
@Query() query: SimHistoryQueryDto
): Promise<SimDomesticCallHistoryResponse> {
const parsedQuery = simHistoryQuerySchema.parse(query as unknown);
const result = await this.simCallHistoryService.getDomesticCallHistory(
req.user.id,
params.id,
parsedQuery.month,
parsedQuery.page,
parsedQuery.limit
query.month,
query.page,
query.limit
);
return result;
}
@ -130,13 +127,12 @@ export class CallHistoryController {
@Param() params: SubscriptionIdParamDto,
@Query() query: SimHistoryQueryDto
): Promise<SimInternationalCallHistoryResponse> {
const parsedQuery = simHistoryQuerySchema.parse(query as unknown);
const result = await this.simCallHistoryService.getInternationalCallHistory(
req.user.id,
params.id,
parsedQuery.month,
parsedQuery.page,
parsedQuery.limit
query.month,
query.page,
query.limit
);
return result;
}
@ -152,13 +148,12 @@ export class CallHistoryController {
@Param() params: SubscriptionIdParamDto,
@Query() query: SimHistoryQueryDto
): Promise<SimSmsHistoryResponse> {
const parsedQuery = simHistoryQuerySchema.parse(query as unknown);
const result = await this.simCallHistoryService.getSmsHistory(
req.user.id,
params.id,
parsedQuery.month,
parsedQuery.page,
parsedQuery.limit
query.month,
query.page,
query.limit
);
return result;
}

View File

@ -193,8 +193,7 @@ export class SimController {
@Param() params: SubscriptionIdParamDto,
@Body() body: SimReissueEsimRequestDto
): Promise<SimActionResponse> {
const parsedBody = simReissueEsimRequestSchema.parse(body as unknown);
await this.simManagementService.reissueEsimProfile(req.user.id, params.id, parsedBody.newEid);
await this.simManagementService.reissueEsimProfile(req.user.id, params.id, body.newEid);
return { message: "eSIM profile reissue completed successfully" };
}

View File

@ -65,7 +65,7 @@ export class ResidenceCardController {
filename: file.originalname || "residence-card",
mimeType: file.mimetype,
sizeBytes: file.size,
content: file.buffer as unknown as Uint8Array<ArrayBuffer>,
content: file.buffer,
});
}
}

View File

@ -1,4 +1,4 @@
import { Injectable, Inject } from "@nestjs/common";
import { Injectable, Inject, HttpStatus } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
@ -9,12 +9,14 @@ import {
} from "@bff/integrations/salesforce/utils/soql.util.js";
import { normalizeSalesforceDateTimeToIsoUtc } from "@bff/integrations/salesforce/utils/datetime.util.js";
import type { SalesforceResponse } from "@customer-portal/domain/common/providers";
import { ErrorCode } from "@customer-portal/domain/common";
import {
residenceCardVerificationSchema,
type ResidenceCardVerification,
type ResidenceCardVerificationStatus,
} from "@customer-portal/domain/customer";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { DomainHttpException } from "@bff/core/http/domain-http.exception.js";
import { basename, extname } from "node:path";
function mapFileTypeToMime(fileType?: string | null): string | null {
@ -139,22 +141,21 @@ export class ResidenceCardService {
filename: string;
mimeType: string;
sizeBytes: number;
content: Uint8Array<ArrayBuffer>;
content: Buffer;
}): Promise<ResidenceCardVerification> {
const mapping = await this.mappings.findByUserId(params.userId);
if (!mapping?.sfAccountId) {
throw new Error("No Salesforce mapping found for current user");
throw new DomainHttpException(ErrorCode.ACCOUNT_MAPPING_MISSING, HttpStatus.BAD_REQUEST);
}
const sfAccountId = assertSalesforceId(mapping.sfAccountId, "sfAccountId");
const fileBuffer = Buffer.from(params.content as unknown as Uint8Array);
const versionData = fileBuffer.toString("base64");
const versionData = params.content.toString("base64");
const extension = extname(params.filename || "").replace(/^\./, "");
const title = basename(params.filename || "residence-card", extension ? `.${extension}` : "");
const create = this.sf.sobject("ContentVersion")?.create;
if (!create) {
throw new Error("Salesforce ContentVersion create method not available");
throw new DomainHttpException(ErrorCode.CONFIGURATION_ERROR, HttpStatus.SERVICE_UNAVAILABLE);
}
try {
@ -166,21 +167,30 @@ export class ResidenceCardService {
});
const id = (result as { id?: unknown })?.id;
if (typeof id !== "string" || id.trim().length === 0) {
throw new Error("Salesforce did not return a ContentVersion id");
throw new DomainHttpException(
ErrorCode.EXTERNAL_SERVICE_ERROR,
HttpStatus.SERVICE_UNAVAILABLE
);
}
} catch (error) {
if (error instanceof DomainHttpException) {
throw error;
}
this.logger.error("Failed to upload residence card to Salesforce Files", {
userId: params.userId,
sfAccountIdTail: sfAccountId.slice(-4),
error: getErrorMessage(error),
});
throw new Error("Failed to submit residence card. Please try again later.");
throw new DomainHttpException(
ErrorCode.EXTERNAL_SERVICE_ERROR,
HttpStatus.SERVICE_UNAVAILABLE
);
}
const fields = this.getAccountFieldNames();
const update = this.sf.sobject("Account")?.update;
if (!update) {
throw new Error("Salesforce Account update method not available");
throw new DomainHttpException(ErrorCode.CONFIGURATION_ERROR, HttpStatus.SERVICE_UNAVAILABLE);
}
await update({

View File

@ -9,44 +9,18 @@ import {
type UseQueryResult,
} from "@tanstack/react-query";
// ✅ Generic utilities from lib
import {
apiClient,
queryKeys,
getDataOrDefault,
getDataOrThrow,
type QueryParams,
} from "@/lib/api";
import { queryKeys } from "@/lib/api";
// ✅ Single consolidated import from domain
import {
// Types
type Invoice,
type InvoiceList,
type InvoiceSsoLink,
type InvoiceQueryParams,
// Schemas
invoiceSchema,
invoiceListSchema,
} from "@customer-portal/domain/billing";
import { type PaymentMethodList } from "@customer-portal/domain/payments";
import { useAuthSession } from "@/features/auth/services/auth.store";
// Constants
const EMPTY_INVOICE_LIST: InvoiceList = {
invoices: [],
pagination: {
page: 1,
totalItems: 0,
totalPages: 0,
},
};
const EMPTY_PAYMENT_METHODS: PaymentMethodList = {
paymentMethods: [],
totalCount: 0,
};
import { billingService } from "../services/billing.service";
// Type helpers for React Query
type InvoicesQueryKey = ReturnType<typeof queryKeys.billing.invoices>;
@ -74,44 +48,6 @@ type SsoLinkMutationOptions = UseMutationOptions<
{ invoiceId: number; target?: "view" | "download" | "pay" }
>;
// Helper functions
function toQueryParams(params: InvoiceQueryParams): QueryParams {
const query: QueryParams = {};
for (const [key, value] of Object.entries(params)) {
if (value === undefined) {
continue;
}
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
query[key] = value;
}
}
return query;
}
// API functions
async function fetchInvoices(params?: InvoiceQueryParams): Promise<InvoiceList> {
const query = params ? toQueryParams(params) : undefined;
const response = await apiClient.GET<InvoiceList>(
"/api/invoices",
query ? { params: { query } } : undefined
);
const data = getDataOrDefault(response, EMPTY_INVOICE_LIST);
return invoiceListSchema.parse(data);
}
async function fetchInvoice(id: string): Promise<Invoice> {
const response = await apiClient.GET<Invoice>("/api/invoices/{id}", {
params: { path: { id } },
});
const invoice = getDataOrThrow(response, "Invoice not found");
return invoiceSchema.parse(invoice);
}
async function fetchPaymentMethods(): Promise<PaymentMethodList> {
const response = await apiClient.GET<PaymentMethodList>("/api/invoices/payment-methods");
return getDataOrDefault(response, EMPTY_PAYMENT_METHODS);
}
// Exported hooks
export function useInvoices(
params?: InvoiceQueryParams,
@ -121,7 +57,7 @@ export function useInvoices(
const queryKeyParams = params ? { ...params } : undefined;
return useQuery({
queryKey: queryKeys.billing.invoices(queryKeyParams),
queryFn: () => fetchInvoices(params),
queryFn: () => billingService.getInvoices(params),
enabled: isAuthenticated,
...options,
});
@ -134,7 +70,7 @@ export function useInvoice(
const { isAuthenticated } = useAuthSession();
return useQuery({
queryKey: queryKeys.billing.invoice(id),
queryFn: () => fetchInvoice(id),
queryFn: () => billingService.getInvoice(id),
enabled: isAuthenticated && Boolean(id),
...options,
});
@ -146,7 +82,7 @@ export function usePaymentMethods(
const { isAuthenticated } = useAuthSession();
return useQuery({
queryKey: queryKeys.billing.paymentMethods(),
queryFn: fetchPaymentMethods,
queryFn: billingService.getPaymentMethods,
enabled: isAuthenticated,
...options,
});
@ -161,13 +97,7 @@ export function useCreateInvoiceSsoLink(
> {
return useMutation({
mutationFn: async ({ invoiceId, target }) => {
const response = await apiClient.POST<InvoiceSsoLink>("/api/invoices/{id}/sso-link", {
params: {
path: { id: invoiceId },
query: target ? { target } : undefined,
},
});
return getDataOrThrow(response, "Failed to create SSO link");
return billingService.createInvoiceSsoLink(invoiceId, target);
},
...options,
});
@ -177,12 +107,7 @@ export function useCreatePaymentMethodsSsoLink(
options?: UseMutationOptions<InvoiceSsoLink, Error, void>
): UseMutationResult<InvoiceSsoLink, Error, void> {
return useMutation({
mutationFn: async () => {
const response = await apiClient.POST<InvoiceSsoLink>("/api/auth/sso-link", {
body: { destination: "index.php?rp=/account/paymentmethods" },
});
return getDataOrThrow(response, "Failed to create payment methods SSO link");
},
mutationFn: billingService.createPaymentMethodsSsoLink,
...options,
});
}

View File

@ -1,2 +1,3 @@
export * from "./hooks";
export * from "./components";
export * from "./services";

View File

@ -0,0 +1,114 @@
/**
* Billing Service
*
* Handles all billing-related API calls.
* Hooks should use this service instead of calling the API directly.
*/
import { apiClient, getDataOrDefault, getDataOrThrow, type QueryParams } from "@/lib/api";
import {
type Invoice,
type InvoiceList,
type InvoiceSsoLink,
type InvoiceQueryParams,
invoiceSchema,
invoiceListSchema,
} from "@customer-portal/domain/billing";
import { type PaymentMethodList } from "@customer-portal/domain/payments";
// ============================================================================
// Constants
// ============================================================================
const EMPTY_INVOICE_LIST: InvoiceList = {
invoices: [],
pagination: {
page: 1,
totalItems: 0,
totalPages: 0,
},
};
const EMPTY_PAYMENT_METHODS: PaymentMethodList = {
paymentMethods: [],
totalCount: 0,
};
// ============================================================================
// Helpers
// ============================================================================
function toQueryParams(params: InvoiceQueryParams): QueryParams {
const query: QueryParams = {};
for (const [key, value] of Object.entries(params)) {
if (value === undefined) {
continue;
}
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
query[key] = value;
}
}
return query;
}
// ============================================================================
// API Functions
// ============================================================================
async function getInvoices(params?: InvoiceQueryParams): Promise<InvoiceList> {
const query = params ? toQueryParams(params) : undefined;
const response = await apiClient.GET<InvoiceList>(
"/api/invoices",
query ? { params: { query } } : undefined
);
const data = getDataOrDefault(response, EMPTY_INVOICE_LIST);
return invoiceListSchema.parse(data);
}
async function getInvoice(id: string): Promise<Invoice> {
const response = await apiClient.GET<Invoice>("/api/invoices/{id}", {
params: { path: { id } },
});
const invoice = getDataOrThrow(response, "Invoice not found");
return invoiceSchema.parse(invoice);
}
async function getPaymentMethods(): Promise<PaymentMethodList> {
const response = await apiClient.GET<PaymentMethodList>("/api/invoices/payment-methods");
return getDataOrDefault(response, EMPTY_PAYMENT_METHODS);
}
async function createInvoiceSsoLink(
invoiceId: number,
target?: "view" | "download" | "pay"
): Promise<InvoiceSsoLink> {
const response = await apiClient.POST<InvoiceSsoLink>("/api/invoices/{id}/sso-link", {
params: {
path: { id: invoiceId },
query: target ? { target } : undefined,
},
});
return getDataOrThrow(response, "Failed to create SSO link");
}
async function createPaymentMethodsSsoLink(): Promise<InvoiceSsoLink> {
const response = await apiClient.POST<InvoiceSsoLink>("/api/auth/sso-link", {
body: { destination: "index.php?rp=/account/paymentmethods" },
});
return getDataOrThrow(response, "Failed to create payment methods SSO link");
}
// ============================================================================
// Service Export
// ============================================================================
export const billingService = {
getInvoices,
getInvoice,
getPaymentMethods,
createInvoiceSsoLink,
createPaymentMethodsSsoLink,
} as const;
// Re-export constants for use in hooks
export { EMPTY_INVOICE_LIST, EMPTY_PAYMENT_METHODS };

View File

@ -0,0 +1 @@
export { billingService, EMPTY_INVOICE_LIST, EMPTY_PAYMENT_METHODS } from "./billing.service";

View File

@ -275,6 +275,16 @@ This allows gradual migration while new code uses unified types.
## Implementation Files
- `packages/domain/src/entities/product.ts` - Core unified types
- `packages/domain/src/entities/catalog.ts` - Re-exports with legacy aliases
- `packages/domain/src/entities/order.ts` - Updated order types
- **Note**: The domain package uses a provider-aware structure. The older `packages/domain/src/entities/*` paths referenced by earlier drafts are no longer accurate.
Current home for these concerns (examples):
- `packages/domain/services/contract.ts`: catalog/service product contracts
- `packages/domain/services/schema.ts`: Zod schemas for service catalog types
- `packages/domain/services/providers/salesforce/raw.types.ts`: Salesforce raw response shapes
- `packages/domain/services/providers/salesforce/mapper.ts`: Salesforce → domain transformations
For the overall structure and import rules, see:
- `docs/architecture/domain-layer.md`
- `docs/development/domain/structure.md`

View File

@ -57,6 +57,7 @@ export const ErrorCode = {
INSUFFICIENT_BALANCE: "BIZ_005",
SERVICE_UNAVAILABLE: "BIZ_006",
LEGACY_ACCOUNT_EXISTS: "BIZ_007",
ACCOUNT_MAPPING_MISSING: "BIZ_008",
// System Errors (SYS_*)
INTERNAL_ERROR: "SYS_001",
@ -116,6 +117,8 @@ export const ErrorMessages: Record<ErrorCodeType, string> = {
[ErrorCode.INSUFFICIENT_BALANCE]: "Insufficient account balance.",
[ErrorCode.SERVICE_UNAVAILABLE]:
"This service is temporarily unavailable. Please try again later.",
[ErrorCode.ACCOUNT_MAPPING_MISSING]:
"Your account isnt fully set up yet. Please contact support or try again later.",
// System
[ErrorCode.INTERNAL_ERROR]: "An unexpected error occurred. Please try again later.",
@ -299,6 +302,13 @@ export const ErrorMetadata: Record<ErrorCodeType, ErrorMetadata> = {
shouldRetry: true,
logLevel: "warn",
},
[ErrorCode.ACCOUNT_MAPPING_MISSING]: {
category: "business",
severity: "medium",
shouldLogout: false,
shouldRetry: false,
logLevel: "warn",
},
// System - high severity
[ErrorCode.INTERNAL_ERROR]: {

View File

@ -1,2 +1,3 @@
export * from "./raw.types.js";
export * from "./mapper.js";
export * from "./field-map.js";