Refactor WHMCS integration to enhance invoice management capabilities. Introduce methods for creating, updating, and capturing payments for invoices, while removing legacy transformer services. Update type definitions for invoice-related API interactions and improve error handling across invoice operations. Streamline service dependencies and ensure consistent logging for better maintainability.
This commit is contained in:
parent
065e2f9acf
commit
9fafd227b9
@ -24,7 +24,7 @@ export class FreebitClientService {
|
|||||||
*/
|
*/
|
||||||
async makeAuthenticatedRequest<
|
async makeAuthenticatedRequest<
|
||||||
TResponse extends FreebitResponseBase,
|
TResponse extends FreebitResponseBase,
|
||||||
TPayload extends Record<string, unknown>,
|
TPayload extends object,
|
||||||
>(endpoint: string, payload: TPayload): Promise<TResponse> {
|
>(endpoint: string, payload: TPayload): Promise<TResponse> {
|
||||||
const authKey = await this.authService.getAuthKey();
|
const authKey = await this.authService.getAuthKey();
|
||||||
const config = this.authService.getConfig();
|
const config = this.authService.getConfig();
|
||||||
|
|||||||
@ -10,11 +10,17 @@ import {
|
|||||||
statusKey as sfStatusKey,
|
statusKey as sfStatusKey,
|
||||||
latestSeenKey as sfLatestSeenKey,
|
latestSeenKey as sfLatestSeenKey,
|
||||||
} from "./event-keys.util";
|
} from "./event-keys.util";
|
||||||
|
import type {
|
||||||
|
SalesforcePubSubEvent,
|
||||||
|
SalesforcePubSubError,
|
||||||
|
SalesforcePubSubSubscription,
|
||||||
|
SalesforcePubSubCallbackType,
|
||||||
|
} from "../types/pubsub-events.types";
|
||||||
|
|
||||||
type SubscribeCallback = (
|
type SubscribeCallback = (
|
||||||
subscription: { topicName: string },
|
subscription: SalesforcePubSubSubscription,
|
||||||
callbackType: string,
|
callbackType: SalesforcePubSubCallbackType,
|
||||||
data: unknown
|
data: SalesforcePubSubEvent | SalesforcePubSubError | unknown
|
||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
|
|
||||||
interface PubSubClient {
|
interface PubSubClient {
|
||||||
@ -110,27 +116,17 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
|
|||||||
const argTypes = [typeof subscription, typeof callbackType, typeof data];
|
const argTypes = [typeof subscription, typeof callbackType, typeof data];
|
||||||
const type = callbackType;
|
const type = callbackType;
|
||||||
const typeNorm = String(type || "").toLowerCase();
|
const typeNorm = String(type || "").toLowerCase();
|
||||||
const topic = (subscription as { topicName?: string })?.topicName || this.channel;
|
const topic = subscription.topicName || this.channel;
|
||||||
|
|
||||||
if (typeNorm === "data" || typeNorm === "event") {
|
if (typeNorm === "data" || typeNorm === "event") {
|
||||||
const event = data as Record<string, unknown>;
|
const event = data as SalesforcePubSubEvent;
|
||||||
// Basic breadcrumb to confirm we are handling data callbacks
|
// Basic breadcrumb to confirm we are handling data callbacks
|
||||||
this.logger.debug("SF Pub/Sub data callback received", {
|
this.logger.debug("SF Pub/Sub data callback received", {
|
||||||
topic,
|
topic,
|
||||||
argTypes,
|
argTypes,
|
||||||
hasPayload: ((): boolean => {
|
hasPayload: Boolean(event?.payload),
|
||||||
if (!event || typeof event !== "object") return false;
|
|
||||||
const maybePayload = event["payload"];
|
|
||||||
return typeof maybePayload === "object" && maybePayload !== null;
|
|
||||||
})(),
|
|
||||||
});
|
});
|
||||||
const payload = ((): Record<string, unknown> | undefined => {
|
const payload = event?.payload;
|
||||||
const p = event["payload"];
|
|
||||||
if (typeof p === "object" && p !== null) {
|
|
||||||
return p as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Only check parsed payload
|
// Only check parsed payload
|
||||||
const orderIdVal = payload?.["OrderId__c"] ?? payload?.["OrderId"];
|
const orderIdVal = payload?.["OrderId__c"] ?? payload?.["OrderId"];
|
||||||
@ -189,14 +185,12 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
|
|||||||
this.logger.warn("SF Pub/Sub stream error", { topic, data });
|
this.logger.warn("SF Pub/Sub stream error", { topic, data });
|
||||||
try {
|
try {
|
||||||
// Detect replay id corruption and auto-recover once by clearing the cursor and resubscribing
|
// Detect replay id corruption and auto-recover once by clearing the cursor and resubscribing
|
||||||
const maybeObj = (data || {}) as Record<string, unknown>;
|
const errorData = data as SalesforcePubSubError;
|
||||||
const details = typeof maybeObj["details"] === "string" ? maybeObj["details"] : "";
|
const details = errorData.details || "";
|
||||||
const metadata = (maybeObj["metadata"] || {}) as Record<string, unknown>;
|
const metadata = errorData.metadata || {};
|
||||||
const errorCodes = Array.isArray((metadata as { [k: string]: unknown })["error-code"])
|
const errorCodes = Array.isArray(metadata["error-code"]) ? metadata["error-code"] : [];
|
||||||
? ((metadata as { [k: string]: unknown })["error-code"] as unknown[])
|
const hasCorruptionCode = errorCodes.some(code =>
|
||||||
: [];
|
String(code).includes("replayid.corrupted")
|
||||||
const hasCorruptionCode = errorCodes.some(v =>
|
|
||||||
String(v).includes("replayid.corrupted")
|
|
||||||
);
|
);
|
||||||
const mentionsReplayValidation = /Replay ID validation failed/i.test(details);
|
const mentionsReplayValidation = /Replay ID validation failed/i.test(details);
|
||||||
|
|
||||||
@ -228,12 +222,8 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unknown callback type: log once with minimal context
|
// Unknown callback type: log once with minimal context
|
||||||
const maybeEvent = data as Record<string, unknown> | undefined;
|
const maybeEvent = data as SalesforcePubSubEvent | undefined;
|
||||||
const hasPayload = ((): boolean => {
|
const hasPayload = Boolean(maybeEvent?.payload);
|
||||||
if (!maybeEvent || typeof maybeEvent !== "object") return false;
|
|
||||||
const p = maybeEvent["payload"];
|
|
||||||
return typeof p === "object" && p !== null;
|
|
||||||
})();
|
|
||||||
this.logger.debug("SF Pub/Sub callback ignored (unknown type)", {
|
this.logger.debug("SF Pub/Sub callback ignored (unknown type)", {
|
||||||
type,
|
type,
|
||||||
topic,
|
topic,
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Salesforce Pub/Sub Event Types
|
||||||
|
* Based on Salesforce Platform Event structure
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SalesforcePubSubSubscription {
|
||||||
|
topicName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SalesforcePubSubEventPayload {
|
||||||
|
OrderId__c?: string;
|
||||||
|
OrderId?: string;
|
||||||
|
// Add other known fields as needed
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SalesforcePubSubEvent {
|
||||||
|
payload: SalesforcePubSubEventPayload;
|
||||||
|
replayId?: number;
|
||||||
|
// Add other known event fields
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SalesforcePubSubErrorMetadata {
|
||||||
|
"error-code"?: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SalesforcePubSubError {
|
||||||
|
details?: string;
|
||||||
|
metadata?: SalesforcePubSubErrorMetadata;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SalesforcePubSubCallbackType = "data" | "event" | "grpcstatus" | "end" | "error";
|
||||||
|
|
||||||
|
export interface SalesforcePubSubCallback {
|
||||||
|
subscription: SalesforcePubSubSubscription;
|
||||||
|
callbackType: SalesforcePubSubCallbackType;
|
||||||
|
data: SalesforcePubSubEvent | SalesforcePubSubError | unknown;
|
||||||
|
}
|
||||||
@ -10,12 +10,18 @@ import type {
|
|||||||
WhmcsCatalogProductsResponse,
|
WhmcsCatalogProductsResponse,
|
||||||
WhmcsPayMethodsResponse,
|
WhmcsPayMethodsResponse,
|
||||||
WhmcsPaymentGatewaysResponse,
|
WhmcsPaymentGatewaysResponse,
|
||||||
|
WhmcsCreateInvoiceResponse,
|
||||||
|
WhmcsUpdateInvoiceResponse,
|
||||||
|
WhmcsCapturePaymentResponse,
|
||||||
WhmcsGetInvoicesParams,
|
WhmcsGetInvoicesParams,
|
||||||
WhmcsGetClientsProductsParams,
|
WhmcsGetClientsProductsParams,
|
||||||
WhmcsCreateSsoTokenParams,
|
WhmcsCreateSsoTokenParams,
|
||||||
WhmcsValidateLoginParams,
|
WhmcsValidateLoginParams,
|
||||||
WhmcsAddClientParams,
|
WhmcsAddClientParams,
|
||||||
WhmcsGetPayMethodsParams,
|
WhmcsGetPayMethodsParams,
|
||||||
|
WhmcsCreateInvoiceParams,
|
||||||
|
WhmcsUpdateInvoiceParams,
|
||||||
|
WhmcsCapturePaymentParams,
|
||||||
} from "../../types/whmcs-api.types";
|
} from "../../types/whmcs-api.types";
|
||||||
import { WhmcsHttpClientService } from "./whmcs-http-client.service";
|
import { WhmcsHttpClientService } from "./whmcs-http-client.service";
|
||||||
import { WhmcsConfigService } from "../config/whmcs-config.service";
|
import { WhmcsConfigService } from "../config/whmcs-config.service";
|
||||||
@ -136,6 +142,62 @@ export class WhmcsApiMethodsService {
|
|||||||
return this.makeRequest("GetPaymentMethods", {});
|
return this.makeRequest("GetPaymentMethods", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createInvoice(params: WhmcsCreateInvoiceParams): Promise<WhmcsCreateInvoiceResponse> {
|
||||||
|
return this.makeRequest("CreateInvoice", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateInvoice(params: WhmcsUpdateInvoiceParams): Promise<WhmcsUpdateInvoiceResponse> {
|
||||||
|
return this.makeRequest("UpdateInvoice", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async capturePayment(params: WhmcsCapturePaymentParams): Promise<WhmcsCapturePaymentResponse> {
|
||||||
|
return this.makeRequest("CapturePayment", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ORDER API METHODS (Used by order services)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
async addOrder(params: Record<string, unknown>) {
|
||||||
|
return this.makeRequest("AddOrder", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrders(params: Record<string, unknown> = {}) {
|
||||||
|
return this.makeRequest("GetOrders", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ADMIN API METHODS (require admin auth)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
async acceptOrder(orderId: number): Promise<{ result: string }> {
|
||||||
|
if (!this.configService.hasAdminAuth()) {
|
||||||
|
throw new Error("Admin authentication required for AcceptOrder");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.makeRequest<{ result: string }>(
|
||||||
|
"AcceptOrder",
|
||||||
|
{
|
||||||
|
orderid: orderId.toString(),
|
||||||
|
autosetup: true,
|
||||||
|
sendemail: false
|
||||||
|
},
|
||||||
|
{ useAdminAuth: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelOrder(orderId: number): Promise<{ result: string }> {
|
||||||
|
if (!this.configService.hasAdminAuth()) {
|
||||||
|
throw new Error("Admin authentication required for CancelOrder");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.makeRequest<{ result: string }>(
|
||||||
|
"CancelOrder",
|
||||||
|
{ orderid: orderId },
|
||||||
|
{ useAdminAuth: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// SSO API METHODS
|
// SSO API METHODS
|
||||||
|
|||||||
@ -14,6 +14,9 @@ import type {
|
|||||||
WhmcsGetClientsProductsParams,
|
WhmcsGetClientsProductsParams,
|
||||||
WhmcsGetPayMethodsParams,
|
WhmcsGetPayMethodsParams,
|
||||||
WhmcsCreateSsoTokenParams,
|
WhmcsCreateSsoTokenParams,
|
||||||
|
WhmcsCreateInvoiceParams,
|
||||||
|
WhmcsUpdateInvoiceParams,
|
||||||
|
WhmcsCapturePaymentParams,
|
||||||
} from "../../types/whmcs-api.types";
|
} from "../../types/whmcs-api.types";
|
||||||
import type {
|
import type {
|
||||||
WhmcsRequestOptions,
|
WhmcsRequestOptions,
|
||||||
@ -138,6 +141,45 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
|||||||
return this.apiMethods.getInvoice(invoiceId);
|
return this.apiMethods.getInvoice(invoiceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createInvoice(params: WhmcsCreateInvoiceParams) {
|
||||||
|
return this.apiMethods.createInvoice(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateInvoice(params: WhmcsUpdateInvoiceParams) {
|
||||||
|
return this.apiMethods.updateInvoice(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async capturePayment(params: WhmcsCapturePaymentParams) {
|
||||||
|
return this.apiMethods.capturePayment(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ORDER OPERATIONS (Used by order services)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
async addOrder(params: Record<string, unknown>) {
|
||||||
|
return this.apiMethods.addOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrders(params: Record<string, unknown> = {}) {
|
||||||
|
return this.apiMethods.getOrders(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ADMIN OPERATIONS (Used by order services)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept an order (requires admin authentication)
|
||||||
|
*/
|
||||||
|
async acceptOrder(orderId: number) {
|
||||||
|
return this.apiMethods.acceptOrder(orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelOrder(orderId: number) {
|
||||||
|
return this.apiMethods.cancelOrder(orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// PRODUCT/SUBSCRIPTION API METHODS
|
// PRODUCT/SUBSCRIPTION API METHODS
|
||||||
|
|||||||
@ -10,25 +10,19 @@ import type {
|
|||||||
WhmcsAddClientResponse,
|
WhmcsAddClientResponse,
|
||||||
WhmcsCatalogProductsResponse,
|
WhmcsCatalogProductsResponse,
|
||||||
WhmcsPayMethodsResponse,
|
WhmcsPayMethodsResponse,
|
||||||
WhmcsAddPayMethodResponse,
|
|
||||||
WhmcsPaymentGatewaysResponse,
|
WhmcsPaymentGatewaysResponse,
|
||||||
WhmcsCreateInvoiceResponse,
|
WhmcsCreateInvoiceResponse,
|
||||||
WhmcsUpdateInvoiceResponse,
|
WhmcsUpdateInvoiceResponse,
|
||||||
WhmcsCapturePaymentResponse,
|
WhmcsCapturePaymentResponse,
|
||||||
WhmcsAddCreditResponse,
|
|
||||||
WhmcsAddInvoicePaymentResponse,
|
|
||||||
WhmcsGetInvoicesParams,
|
WhmcsGetInvoicesParams,
|
||||||
WhmcsGetClientsProductsParams,
|
WhmcsGetClientsProductsParams,
|
||||||
WhmcsCreateSsoTokenParams,
|
WhmcsCreateSsoTokenParams,
|
||||||
WhmcsValidateLoginParams,
|
WhmcsValidateLoginParams,
|
||||||
WhmcsAddClientParams,
|
WhmcsAddClientParams,
|
||||||
WhmcsGetPayMethodsParams,
|
WhmcsGetPayMethodsParams,
|
||||||
WhmcsAddPayMethodParams,
|
|
||||||
WhmcsCreateInvoiceParams,
|
WhmcsCreateInvoiceParams,
|
||||||
WhmcsUpdateInvoiceParams,
|
WhmcsUpdateInvoiceParams,
|
||||||
WhmcsCapturePaymentParams,
|
WhmcsCapturePaymentParams,
|
||||||
WhmcsAddCreditParams,
|
|
||||||
WhmcsAddInvoicePaymentParams,
|
|
||||||
} from "../types/whmcs-api.types";
|
} from "../types/whmcs-api.types";
|
||||||
|
|
||||||
// Re-export the config interface for backward compatibility
|
// Re-export the config interface for backward compatibility
|
||||||
@ -116,6 +110,11 @@ export class WhmcsConnectionService {
|
|||||||
return this.orchestrator.updateInvoice(params);
|
return this.orchestrator.updateInvoice(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async capturePayment(params: WhmcsCapturePaymentParams): Promise<WhmcsCapturePaymentResponse> {
|
||||||
|
return this.orchestrator.capturePayment(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// PRODUCT/SUBSCRIPTION API METHODS
|
// PRODUCT/SUBSCRIPTION API METHODS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -136,26 +135,37 @@ export class WhmcsConnectionService {
|
|||||||
return this.orchestrator.getPaymentMethods(params);
|
return this.orchestrator.getPaymentMethods(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPaymentMethod(params: WhmcsAddPayMethodParams): Promise<WhmcsAddPayMethodResponse> {
|
|
||||||
return this.orchestrator.addPaymentMethod(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPaymentGateways(): Promise<WhmcsPaymentGatewaysResponse> {
|
async getPaymentGateways(): Promise<WhmcsPaymentGatewaysResponse> {
|
||||||
return this.orchestrator.getPaymentGateways();
|
return this.orchestrator.getPaymentGateways();
|
||||||
}
|
}
|
||||||
|
|
||||||
async capturePayment(params: WhmcsCapturePaymentParams): Promise<WhmcsCapturePaymentResponse> {
|
// Legacy method name for backward compatibility
|
||||||
return this.orchestrator.capturePayment(params);
|
async getPayMethods(params: WhmcsGetPayMethodsParams): Promise<WhmcsPayMethodsResponse> {
|
||||||
|
return this.getPaymentMethods(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addCredit(params: WhmcsAddCreditParams): Promise<WhmcsAddCreditResponse> {
|
async getProducts(): Promise<WhmcsCatalogProductsResponse> {
|
||||||
return this.orchestrator.addCredit(params);
|
return this.orchestrator.getProducts() as Promise<WhmcsCatalogProductsResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addInvoicePayment(
|
async addOrder(params: Record<string, unknown>) {
|
||||||
params: WhmcsAddInvoicePaymentParams
|
return this.orchestrator.addOrder(params);
|
||||||
): Promise<WhmcsAddInvoicePaymentResponse> {
|
}
|
||||||
return this.orchestrator.addInvoicePayment(params);
|
|
||||||
|
async getOrders(params: Record<string, unknown> = {}) {
|
||||||
|
return this.orchestrator.getOrders(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptOrder(orderId: number): Promise<{ result: string }> {
|
||||||
|
return this.orchestrator.acceptOrder(orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelOrder(orderId: number): Promise<{ result: string }> {
|
||||||
|
return this.orchestrator.cancelOrder(orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseUrl(): string {
|
||||||
|
return this.orchestrator.getBaseUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -166,17 +176,6 @@ export class WhmcsConnectionService {
|
|||||||
return this.orchestrator.createSsoToken(params);
|
return this.orchestrator.createSsoToken(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// ADMIN API METHODS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
async acceptOrder(orderId: number): Promise<{ result: string }> {
|
|
||||||
return this.orchestrator.acceptOrder(orderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(orderId: number): Promise<{ result: string }> {
|
|
||||||
return this.orchestrator.cancelOrder(orderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// UTILITY METHODS
|
// UTILITY METHODS
|
||||||
|
|||||||
@ -3,11 +3,17 @@ import { Logger } from "nestjs-pino";
|
|||||||
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
||||||
import { Invoice, InvoiceList, invoiceListSchema, invoiceSchema } from "@customer-portal/domain";
|
import { Invoice, InvoiceList, invoiceListSchema, invoiceSchema } from "@customer-portal/domain";
|
||||||
import { WhmcsConnectionService } from "./whmcs-connection.service";
|
import { WhmcsConnectionService } from "./whmcs-connection.service";
|
||||||
import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer";
|
import { InvoiceTransformerService } from "../transformers/services/invoice-transformer.service";
|
||||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
||||||
import {
|
import {
|
||||||
WhmcsGetInvoicesParams,
|
WhmcsGetInvoicesParams,
|
||||||
WhmcsInvoicesResponse,
|
WhmcsInvoicesResponse,
|
||||||
|
WhmcsCreateInvoiceParams,
|
||||||
|
WhmcsUpdateInvoiceParams,
|
||||||
|
WhmcsCapturePaymentParams,
|
||||||
|
WhmcsCreateInvoiceResponse,
|
||||||
|
WhmcsUpdateInvoiceResponse,
|
||||||
|
WhmcsCapturePaymentResponse,
|
||||||
} from "../types/whmcs-api.types";
|
} from "../types/whmcs-api.types";
|
||||||
|
|
||||||
export interface InvoiceFilters {
|
export interface InvoiceFilters {
|
||||||
@ -21,7 +27,7 @@ export class WhmcsInvoiceService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(Logger) private readonly logger: Logger,
|
@Inject(Logger) private readonly logger: Logger,
|
||||||
private readonly connectionService: WhmcsConnectionService,
|
private readonly connectionService: WhmcsConnectionService,
|
||||||
private readonly dataTransformer: WhmcsDataTransformer,
|
private readonly invoiceTransformer: InvoiceTransformerService,
|
||||||
private readonly cacheService: WhmcsCacheService
|
private readonly cacheService: WhmcsCacheService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -172,7 +178,7 @@ export class WhmcsInvoiceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transform invoice
|
// Transform invoice
|
||||||
const invoice = this.dataTransformer.transformInvoice(response);
|
const invoice = this.invoiceTransformer.transformInvoice(response);
|
||||||
|
|
||||||
const parseResult = invoiceSchema.safeParse(invoice);
|
const parseResult = invoiceSchema.safeParse(invoice);
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
@ -222,7 +228,7 @@ export class WhmcsInvoiceService {
|
|||||||
const invoices = response.invoices.invoice
|
const invoices = response.invoices.invoice
|
||||||
.map(whmcsInvoice => {
|
.map(whmcsInvoice => {
|
||||||
try {
|
try {
|
||||||
return this.dataTransformer.transformInvoice(whmcsInvoice);
|
return this.invoiceTransformer.transformInvoice(whmcsInvoice);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, {
|
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@ -259,4 +265,223 @@ export class WhmcsInvoiceService {
|
|||||||
},
|
},
|
||||||
} satisfies InvoiceList;
|
} satisfies InvoiceList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Invoice Creation and Payment Methods (Used by SIM/Order services)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new invoice for a client
|
||||||
|
*/
|
||||||
|
async createInvoice(params: {
|
||||||
|
clientId: number;
|
||||||
|
description: string;
|
||||||
|
amount: number;
|
||||||
|
currency?: string;
|
||||||
|
dueDate?: Date;
|
||||||
|
notes?: string;
|
||||||
|
}): Promise<{ id: number; number: string; total: number; status: string }> {
|
||||||
|
try {
|
||||||
|
const dueDateStr = params.dueDate
|
||||||
|
? params.dueDate.toISOString().split("T")[0]
|
||||||
|
: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]; // 7 days from now
|
||||||
|
|
||||||
|
const whmcsParams: WhmcsCreateInvoiceParams = {
|
||||||
|
userid: params.clientId,
|
||||||
|
status: "Unpaid",
|
||||||
|
sendnotification: false, // Don't send email notification automatically
|
||||||
|
duedate: dueDateStr,
|
||||||
|
notes: params.notes,
|
||||||
|
itemdescription1: params.description,
|
||||||
|
itemamount1: params.amount,
|
||||||
|
itemtaxed1: false, // No tax for data top-ups for now
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.connectionService.createInvoice(whmcsParams);
|
||||||
|
|
||||||
|
if (response.result !== "success") {
|
||||||
|
throw new Error(`WHMCS invoice creation failed: ${response.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Created WHMCS invoice ${response.invoiceid} for client ${params.clientId}`, {
|
||||||
|
invoiceId: response.invoiceid,
|
||||||
|
amount: params.amount,
|
||||||
|
description: params.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: response.invoiceid,
|
||||||
|
number: `INV-${response.invoiceid}`,
|
||||||
|
total: params.amount,
|
||||||
|
status: response.status,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to create invoice for client ${params.clientId}`, {
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing invoice
|
||||||
|
*/
|
||||||
|
async updateInvoice(params: {
|
||||||
|
invoiceId: number;
|
||||||
|
status?:
|
||||||
|
| "Draft"
|
||||||
|
| "Unpaid"
|
||||||
|
| "Paid"
|
||||||
|
| "Cancelled"
|
||||||
|
| "Refunded"
|
||||||
|
| "Collections"
|
||||||
|
| "Payment Pending";
|
||||||
|
dueDate?: Date;
|
||||||
|
notes?: string;
|
||||||
|
}): Promise<{ success: boolean; message?: string }> {
|
||||||
|
try {
|
||||||
|
const whmcsParams: WhmcsUpdateInvoiceParams = {
|
||||||
|
invoiceid: params.invoiceId,
|
||||||
|
status: params.status,
|
||||||
|
duedate: params.dueDate ? params.dueDate.toISOString().split("T")[0] : undefined,
|
||||||
|
notes: params.notes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.connectionService.updateInvoice(whmcsParams);
|
||||||
|
|
||||||
|
if (response.result !== "success") {
|
||||||
|
throw new Error(`WHMCS invoice update failed: ${response.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Updated WHMCS invoice ${params.invoiceId}`, {
|
||||||
|
invoiceId: params.invoiceId,
|
||||||
|
status: params.status,
|
||||||
|
notes: params.notes,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: response.message,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to update invoice ${params.invoiceId}`, {
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture payment for an invoice using the client's default payment method
|
||||||
|
*/
|
||||||
|
async capturePayment(params: {
|
||||||
|
invoiceId: number;
|
||||||
|
amount: number;
|
||||||
|
currency?: string;
|
||||||
|
}): Promise<{ success: boolean; transactionId?: string; error?: string }> {
|
||||||
|
try {
|
||||||
|
const whmcsParams: WhmcsCapturePaymentParams = {
|
||||||
|
invoiceid: params.invoiceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.connectionService.capturePayment(whmcsParams);
|
||||||
|
|
||||||
|
if (response.result === "success") {
|
||||||
|
this.logger.log(`Successfully captured payment for invoice ${params.invoiceId}`, {
|
||||||
|
invoiceId: params.invoiceId,
|
||||||
|
transactionId: response.transactionid,
|
||||||
|
amount: response.amount,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate invoice cache since status changed
|
||||||
|
await this.cacheService.invalidateInvoice(`invoice-${params.invoiceId}`, params.invoiceId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
transactionId: response.transactionid,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Payment capture failed for invoice ${params.invoiceId}`, {
|
||||||
|
invoiceId: params.invoiceId,
|
||||||
|
error: response.message || response.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return user-friendly error message instead of technical API error
|
||||||
|
const userFriendlyError = this.getUserFriendlyPaymentError(
|
||||||
|
response.message || response.error || "Unknown payment error"
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: userFriendlyError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to capture payment for invoice ${params.invoiceId}`, {
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return user-friendly error message for exceptions
|
||||||
|
const userFriendlyError = this.getUserFriendlyPaymentError(getErrorMessage(error));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: userFriendlyError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert technical payment errors to user-friendly messages
|
||||||
|
*/
|
||||||
|
private getUserFriendlyPaymentError(technicalError: string): string {
|
||||||
|
if (!technicalError) {
|
||||||
|
return "Unable to process payment. Please try again or contact support.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorLower = technicalError.toLowerCase();
|
||||||
|
|
||||||
|
// WHMCS API permission errors
|
||||||
|
if (errorLower.includes("invalid permissions") || errorLower.includes("not allowed")) {
|
||||||
|
return "Payment processing is temporarily unavailable. Please contact support for assistance.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication/authorization errors
|
||||||
|
if (
|
||||||
|
errorLower.includes("unauthorized") ||
|
||||||
|
errorLower.includes("forbidden") ||
|
||||||
|
errorLower.includes("403")
|
||||||
|
) {
|
||||||
|
return "Payment processing is temporarily unavailable. Please contact support for assistance.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network/timeout errors
|
||||||
|
if (
|
||||||
|
errorLower.includes("timeout") ||
|
||||||
|
errorLower.includes("network") ||
|
||||||
|
errorLower.includes("connection")
|
||||||
|
) {
|
||||||
|
return "Payment processing timed out. Please try again.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment method errors
|
||||||
|
if (
|
||||||
|
errorLower.includes("payment method") ||
|
||||||
|
errorLower.includes("card") ||
|
||||||
|
errorLower.includes("insufficient funds")
|
||||||
|
) {
|
||||||
|
return "Unable to process payment with your current payment method. Please check your payment details or try a different method.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic API errors
|
||||||
|
if (errorLower.includes("api") || errorLower.includes("http") || errorLower.includes("error")) {
|
||||||
|
return "Payment processing failed. Please try again or contact support if the issue persists.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
return "Unable to process payment. Please try again or contact support.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,13 +95,7 @@ export class WhmcsOrderService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Call WHMCS AcceptOrder API
|
// Call WHMCS AcceptOrder API
|
||||||
const response = (await this.connection.acceptOrder({
|
const response = (await this.connection.acceptOrder(orderId)) as Record<string, unknown>;
|
||||||
orderid: orderId.toString(),
|
|
||||||
// Ensure module provisioning is executed even if product config is different
|
|
||||||
autosetup: true,
|
|
||||||
// Suppress customer emails to remain consistent with earlier noemail flag
|
|
||||||
sendemail: false,
|
|
||||||
})) as Record<string, unknown>;
|
|
||||||
|
|
||||||
if (response.result !== "success") {
|
if (response.result !== "success") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
PaymentMethod,
|
PaymentMethod,
|
||||||
} from "@customer-portal/domain";
|
} from "@customer-portal/domain";
|
||||||
import { WhmcsConnectionService } from "./whmcs-connection.service";
|
import { WhmcsConnectionService } from "./whmcs-connection.service";
|
||||||
import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer";
|
import { PaymentTransformerService } from "../transformers/services/payment-transformer.service";
|
||||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
||||||
import type {
|
import type {
|
||||||
WhmcsCreateSsoTokenParams,
|
WhmcsCreateSsoTokenParams,
|
||||||
@ -21,7 +21,7 @@ export class WhmcsPaymentService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(Logger) private readonly logger: Logger,
|
@Inject(Logger) private readonly logger: Logger,
|
||||||
private readonly connectionService: WhmcsConnectionService,
|
private readonly connectionService: WhmcsConnectionService,
|
||||||
private readonly dataTransformer: WhmcsDataTransformer,
|
private readonly paymentTransformer: PaymentTransformerService,
|
||||||
private readonly cacheService: WhmcsCacheService
|
private readonly cacheService: WhmcsCacheService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ export class WhmcsPaymentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch pay methods (use the documented WHMCS structure)
|
// Fetch pay methods (use the documented WHMCS structure)
|
||||||
const response: WhmcsPayMethodsResponse = await this.connectionService.getPayMethods({
|
const response: WhmcsPayMethodsResponse = await this.connectionService.getPaymentMethods({
|
||||||
clientid: clientId,
|
clientid: clientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ export class WhmcsPaymentService {
|
|||||||
let methods = paymentMethodsArray
|
let methods = paymentMethodsArray
|
||||||
.map((pm: WhmcsPaymentMethod) => {
|
.map((pm: WhmcsPaymentMethod) => {
|
||||||
try {
|
try {
|
||||||
return this.dataTransformer.transformPaymentMethod(pm);
|
return this.paymentTransformer.transformPaymentMethod(pm);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to transform payment method`, {
|
this.logger.error(`Failed to transform payment method`, {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@ -130,7 +130,7 @@ export class WhmcsPaymentService {
|
|||||||
const gateways = response.gateways.gateway
|
const gateways = response.gateways.gateway
|
||||||
.map(whmcsGateway => {
|
.map(whmcsGateway => {
|
||||||
try {
|
try {
|
||||||
return this.dataTransformer.transformPaymentGateway(whmcsGateway);
|
return this.paymentTransformer.transformPaymentGateway(whmcsGateway);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to transform payment gateway ${whmcsGateway.name}`, {
|
this.logger.error(`Failed to transform payment gateway ${whmcsGateway.name}`, {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@ -218,7 +218,7 @@ export class WhmcsPaymentService {
|
|||||||
*/
|
*/
|
||||||
async getProducts(): Promise<unknown> {
|
async getProducts(): Promise<unknown> {
|
||||||
try {
|
try {
|
||||||
const response = await this.connectionService.getProducts();
|
const response = await this.connectionService.getCatalogProducts();
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error("Failed to get products", {
|
this.logger.error("Failed to get products", {
|
||||||
@ -243,12 +243,6 @@ export class WhmcsPaymentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform product data (delegate to transformer)
|
|
||||||
*/
|
|
||||||
transformProduct(whmcsProduct: Record<string, unknown>): unknown {
|
|
||||||
return this.dataTransformer.transformProduct(whmcsProduct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize WHMCS SSO redirect URLs to absolute using configured base URL.
|
* Normalize WHMCS SSO redirect URLs to absolute using configured base URL.
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Logger } from "nestjs-pino";
|
|||||||
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
||||||
import { Subscription, SubscriptionList } from "@customer-portal/domain";
|
import { Subscription, SubscriptionList } from "@customer-portal/domain";
|
||||||
import { WhmcsConnectionService } from "./whmcs-connection.service";
|
import { WhmcsConnectionService } from "./whmcs-connection.service";
|
||||||
import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer";
|
import { SubscriptionTransformerService } from "../transformers/services/subscription-transformer.service";
|
||||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
||||||
import { WhmcsGetClientsProductsParams } from "../types/whmcs-api.types";
|
import { WhmcsGetClientsProductsParams } from "../types/whmcs-api.types";
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ export class WhmcsSubscriptionService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(Logger) private readonly logger: Logger,
|
@Inject(Logger) private readonly logger: Logger,
|
||||||
private readonly connectionService: WhmcsConnectionService,
|
private readonly connectionService: WhmcsConnectionService,
|
||||||
private readonly dataTransformer: WhmcsDataTransformer,
|
private readonly subscriptionTransformer: SubscriptionTransformerService,
|
||||||
private readonly cacheService: WhmcsCacheService
|
private readonly cacheService: WhmcsCacheService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ export class WhmcsSubscriptionService {
|
|||||||
const subscriptions = response.products.product
|
const subscriptions = response.products.product
|
||||||
.map(whmcsProduct => {
|
.map(whmcsProduct => {
|
||||||
try {
|
try {
|
||||||
return this.dataTransformer.transformSubscription(whmcsProduct);
|
return this.subscriptionTransformer.transformSubscription(whmcsProduct);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to transform subscription ${whmcsProduct.id}`, {
|
this.logger.error(`Failed to transform subscription ${whmcsProduct.id}`, {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
|
|||||||
@ -75,7 +75,8 @@ export class InvoiceTransformerService {
|
|||||||
const message = DataUtils.toErrorMessage(error);
|
const message = DataUtils.toErrorMessage(error);
|
||||||
this.logger.error(`Failed to transform invoice ${invoiceId}`, {
|
this.logger.error(`Failed to transform invoice ${invoiceId}`, {
|
||||||
error: message,
|
error: message,
|
||||||
whmcsData: DataUtils.sanitizeForLog(whmcsInvoice as unknown as Record<string, unknown>),
|
invoiceId: whmcsInvoice.invoiceid || whmcsInvoice.id,
|
||||||
|
status: whmcsInvoice.status,
|
||||||
});
|
});
|
||||||
throw new Error(`Failed to transform invoice: ${message}`);
|
throw new Error(`Failed to transform invoice: ${message}`);
|
||||||
}
|
}
|
||||||
@ -85,53 +86,35 @@ export class InvoiceTransformerService {
|
|||||||
* Transform WHMCS invoice items to our standard format
|
* Transform WHMCS invoice items to our standard format
|
||||||
*/
|
*/
|
||||||
private transformInvoiceItems(items: WhmcsInvoiceItems | undefined): InvoiceItem[] {
|
private transformInvoiceItems(items: WhmcsInvoiceItems | undefined): InvoiceItem[] {
|
||||||
if (!items) return [];
|
if (!items || !items.item) return [];
|
||||||
|
|
||||||
try {
|
// WHMCS API returns either an array or single item
|
||||||
const itemsArray = Array.isArray(items.item) ? items.item : [items.item];
|
const itemsArray = Array.isArray(items.item) ? items.item : [items.item];
|
||||||
|
|
||||||
return itemsArray
|
return itemsArray.map(item => this.transformSingleInvoiceItem(item));
|
||||||
.filter(item => item && typeof item === "object")
|
|
||||||
.map(item => this.transformSingleInvoiceItem(item))
|
|
||||||
.filter(Boolean) as InvoiceItem[];
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn("Failed to transform invoice items", {
|
|
||||||
error: DataUtils.toErrorMessage(error),
|
|
||||||
itemsData: DataUtils.sanitizeForLog(items as unknown as Record<string, unknown>),
|
|
||||||
});
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform a single invoice item
|
* Transform a single invoice item using exact WHMCS API structure
|
||||||
*/
|
*/
|
||||||
private transformSingleInvoiceItem(item: Record<string, unknown>): InvoiceItem | null {
|
private transformSingleInvoiceItem(item: WhmcsInvoiceItems['item'][0]): InvoiceItem {
|
||||||
try {
|
const transformedItem: InvoiceItem = {
|
||||||
const transformedItem: InvoiceItem = {
|
id: item.id,
|
||||||
id: DataUtils.safeNumber(item.id, 0),
|
description: item.description,
|
||||||
description: DataUtils.safeString(item.description, "Unknown Item"),
|
amount: DataUtils.parseAmount(item.amount),
|
||||||
amount: DataUtils.parseAmount(item.amount),
|
quantity: 1, // WHMCS invoice items don't have quantity field, always 1
|
||||||
quantity: DataUtils.safeNumber(item.qty, 1),
|
type: item.type,
|
||||||
type: DataUtils.safeString(item.type, "Unknown"),
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// Add service ID if available
|
// Add service ID from relid field
|
||||||
if (item.relid) {
|
if (item.relid) {
|
||||||
transformedItem.serviceId = DataUtils.safeNumber(item.relid);
|
transformedItem.serviceId = item.relid;
|
||||||
}
|
|
||||||
|
|
||||||
// Note: taxable property is not part of the InvoiceItem schema
|
|
||||||
// Tax information is handled at the invoice level
|
|
||||||
|
|
||||||
return transformedItem;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn("Failed to transform single invoice item", {
|
|
||||||
error: DataUtils.toErrorMessage(error),
|
|
||||||
itemData: DataUtils.sanitizeForLog(item),
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: taxed field exists in WHMCS but not in our domain schema
|
||||||
|
// Tax information is handled at the invoice level
|
||||||
|
|
||||||
|
return transformedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -21,16 +21,13 @@ export class PaymentTransformerService {
|
|||||||
transformPaymentGateway(whmcsGateway: WhmcsPaymentGateway): PaymentGateway {
|
transformPaymentGateway(whmcsGateway: WhmcsPaymentGateway): PaymentGateway {
|
||||||
try {
|
try {
|
||||||
const gateway: PaymentGateway = {
|
const gateway: PaymentGateway = {
|
||||||
name: DataUtils.safeString(whmcsGateway.name),
|
name: whmcsGateway.name,
|
||||||
displayName: DataUtils.safeString(
|
displayName: whmcsGateway.display_name,
|
||||||
whmcsGateway.display_name || whmcsGateway.name,
|
type: whmcsGateway.type,
|
||||||
whmcsGateway.name
|
isActive: whmcsGateway.active,
|
||||||
),
|
acceptsCreditCards: whmcsGateway.accepts_credit_cards || false,
|
||||||
type: this.normalizeGatewayType(DataUtils.safeString(whmcsGateway.type, "manual")),
|
acceptsBankAccount: whmcsGateway.accepts_bank_account || false,
|
||||||
isActive: DataUtils.safeBoolean(whmcsGateway.active),
|
supportsTokenization: whmcsGateway.supports_tokenization || false,
|
||||||
acceptsCreditCards: DataUtils.safeBoolean(whmcsGateway.accepts_credit_cards),
|
|
||||||
acceptsBankAccount: DataUtils.safeBoolean(whmcsGateway.accepts_bank_account),
|
|
||||||
supportsTokenization: DataUtils.safeBoolean(whmcsGateway.supports_tokenization),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.validator.validatePaymentGateway(gateway)) {
|
if (!this.validator.validatePaymentGateway(gateway)) {
|
||||||
@ -51,119 +48,44 @@ export class PaymentTransformerService {
|
|||||||
* Transform WHMCS payment method to shared PaymentMethod interface
|
* Transform WHMCS payment method to shared PaymentMethod interface
|
||||||
*/
|
*/
|
||||||
transformPaymentMethod(whmcsPayMethod: WhmcsPaymentMethod): PaymentMethod {
|
transformPaymentMethod(whmcsPayMethod: WhmcsPaymentMethod): PaymentMethod {
|
||||||
try {
|
const transformed: PaymentMethod = {
|
||||||
// Handle field name variations between different WHMCS API responses
|
id: whmcsPayMethod.id,
|
||||||
const payMethodId = whmcsPayMethod.id || whmcsPayMethod.paymethodid;
|
type: whmcsPayMethod.type,
|
||||||
const gatewayName = whmcsPayMethod.gateway_name || whmcsPayMethod.gateway || whmcsPayMethod.type;
|
description: whmcsPayMethod.description,
|
||||||
const lastFour = whmcsPayMethod.last_four || whmcsPayMethod.lastfour || whmcsPayMethod.last4;
|
gatewayName: whmcsPayMethod.gateway_name || "",
|
||||||
const cardType = whmcsPayMethod.card_type || whmcsPayMethod.cardtype || whmcsPayMethod.brand;
|
isDefault: false, // Default value, can be set by calling service
|
||||||
const expiryDate = whmcsPayMethod.expiry_date || whmcsPayMethod.expdate || whmcsPayMethod.expiry;
|
};
|
||||||
|
|
||||||
if (!payMethodId) {
|
// Add credit card specific fields
|
||||||
throw new Error("Payment method ID is required");
|
if (whmcsPayMethod.last_four) {
|
||||||
}
|
transformed.lastFour = whmcsPayMethod.last_four;
|
||||||
|
|
||||||
const transformed: PaymentMethod = {
|
|
||||||
id: DataUtils.safeNumber(payMethodId, 0),
|
|
||||||
type: this.normalizePaymentType(gatewayName),
|
|
||||||
description: this.buildPaymentDescription(whmcsPayMethod),
|
|
||||||
gatewayName: DataUtils.safeString(gatewayName),
|
|
||||||
isDefault: DataUtils.safeBoolean(whmcsPayMethod.is_default || whmcsPayMethod.default),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add credit card specific fields
|
|
||||||
if (lastFour) {
|
|
||||||
transformed.lastFour = DataUtils.safeString(lastFour);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cardType) {
|
|
||||||
transformed.ccType = DataUtils.safeString(cardType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expiryDate) {
|
|
||||||
transformed.expiryDate = this.normalizeExpiryDate(expiryDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add bank account specific fields
|
|
||||||
if (whmcsPayMethod.account_type) {
|
|
||||||
transformed.accountType = DataUtils.safeString(whmcsPayMethod.account_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: routingNumber is not part of the PaymentMethod interface
|
|
||||||
// This would need to be added to the interface if needed
|
|
||||||
|
|
||||||
if (!this.validator.validatePaymentMethod(transformed)) {
|
|
||||||
throw new Error("Transformed payment method failed validation");
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformed;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error("Failed to transform payment method", {
|
|
||||||
error: DataUtils.toErrorMessage(error),
|
|
||||||
whmcsData: DataUtils.sanitizeForLog(whmcsPayMethod as unknown as Record<string, unknown>),
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (whmcsPayMethod.cc_type) {
|
||||||
|
transformed.ccType = whmcsPayMethod.cc_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (whmcsPayMethod.expiry_date) {
|
||||||
|
transformed.expiryDate = this.normalizeExpiryDate(whmcsPayMethod.expiry_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add bank account specific fields
|
||||||
|
if (whmcsPayMethod.account_type) {
|
||||||
|
transformed.accountType = whmcsPayMethod.account_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (whmcsPayMethod.bank_name) {
|
||||||
|
transformed.bankName = whmcsPayMethod.bank_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validator.validatePaymentMethod(transformed)) {
|
||||||
|
throw new Error("Transformed payment method failed validation");
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a human-readable description for the payment method
|
|
||||||
*/
|
|
||||||
private buildPaymentDescription(whmcsPayMethod: WhmcsPaymentMethod): string {
|
|
||||||
const gatewayName = whmcsPayMethod.gateway_name || whmcsPayMethod.gateway || whmcsPayMethod.type;
|
|
||||||
const lastFour = whmcsPayMethod.last_four || whmcsPayMethod.lastfour || whmcsPayMethod.last4;
|
|
||||||
const cardType = whmcsPayMethod.card_type || whmcsPayMethod.cardtype || whmcsPayMethod.brand;
|
|
||||||
|
|
||||||
// For credit cards
|
|
||||||
if (lastFour && cardType) {
|
|
||||||
return `${cardType} ending in ${lastFour}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastFour) {
|
|
||||||
return `Card ending in ${lastFour}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For bank accounts
|
|
||||||
if (whmcsPayMethod.account_type && whmcsPayMethod.routing_number) {
|
|
||||||
return `${whmcsPayMethod.account_type} account`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to gateway name
|
|
||||||
if (gatewayName) {
|
|
||||||
return `${gatewayName} payment method`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Payment method";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize payment type from gateway name
|
|
||||||
*/
|
|
||||||
private normalizePaymentType(gatewayName: string): string {
|
|
||||||
if (!gatewayName) return "unknown";
|
|
||||||
|
|
||||||
const gateway = gatewayName.toLowerCase();
|
|
||||||
|
|
||||||
// Credit card gateways
|
|
||||||
if (gateway.includes("stripe") || gateway.includes("paypal") ||
|
|
||||||
gateway.includes("square") || gateway.includes("authorize")) {
|
|
||||||
return "credit_card";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bank transfer gateways
|
|
||||||
if (gateway.includes("bank") || gateway.includes("ach") ||
|
|
||||||
gateway.includes("wire") || gateway.includes("transfer")) {
|
|
||||||
return "bank_account";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Digital wallets
|
|
||||||
if (gateway.includes("paypal") || gateway.includes("apple") ||
|
|
||||||
gateway.includes("google") || gateway.includes("amazon")) {
|
|
||||||
return "digital_wallet";
|
|
||||||
}
|
|
||||||
|
|
||||||
return gatewayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize expiry date to MM/YY format
|
* Normalize expiry date to MM/YY format
|
||||||
@ -205,7 +127,7 @@ export class PaymentTransformerService {
|
|||||||
const transformed = this.transformPaymentMethod(whmcsPayMethod);
|
const transformed = this.transformPaymentMethod(whmcsPayMethod);
|
||||||
results.push(transformed);
|
results.push(transformed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const payMethodId = whmcsPayMethod?.id || whmcsPayMethod?.paymethodid || "unknown";
|
const payMethodId = whmcsPayMethod?.id || "unknown";
|
||||||
const message = DataUtils.toErrorMessage(error);
|
const message = DataUtils.toErrorMessage(error);
|
||||||
errors.push(`Payment method ${payMethodId}: ${message}`);
|
errors.push(`Payment method ${payMethodId}: ${message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export class SubscriptionTransformerService {
|
|||||||
|
|
||||||
// Safety override: If we have no recurring amount but have first payment, treat as one-time
|
// Safety override: If we have no recurring amount but have first payment, treat as one-time
|
||||||
if (recurringAmount === 0 && firstPaymentAmount > 0) {
|
if (recurringAmount === 0 && firstPaymentAmount > 0) {
|
||||||
normalizedCycle = "One Time";
|
normalizedCycle = "Monthly"; // Default to Monthly for one-time payments
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription: Subscription = {
|
const subscription: Subscription = {
|
||||||
@ -73,7 +73,9 @@ export class SubscriptionTransformerService {
|
|||||||
const message = DataUtils.toErrorMessage(error);
|
const message = DataUtils.toErrorMessage(error);
|
||||||
this.logger.error(`Failed to transform subscription ${whmcsProduct.id}`, {
|
this.logger.error(`Failed to transform subscription ${whmcsProduct.id}`, {
|
||||||
error: message,
|
error: message,
|
||||||
whmcsData: DataUtils.sanitizeForLog(whmcsProduct as unknown as Record<string, unknown>),
|
productId: whmcsProduct.id,
|
||||||
|
status: whmcsProduct.status,
|
||||||
|
productName: whmcsProduct.name || whmcsProduct.productname,
|
||||||
});
|
});
|
||||||
throw new Error(`Failed to transform subscription: ${message}`);
|
throw new Error(`Failed to transform subscription: ${message}`);
|
||||||
}
|
}
|
||||||
@ -113,7 +115,7 @@ export class SubscriptionTransformerService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn("Failed to extract custom fields", {
|
this.logger.warn("Failed to extract custom fields", {
|
||||||
error: DataUtils.toErrorMessage(error),
|
error: DataUtils.toErrorMessage(error),
|
||||||
customFieldsData: DataUtils.sanitizeForLog(customFields as unknown as Record<string, unknown>),
|
customFieldsCount: customFields?.length || 0,
|
||||||
});
|
});
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
PaymentMethod,
|
PaymentMethod,
|
||||||
PaymentGateway,
|
PaymentGateway,
|
||||||
} from "@customer-portal/domain";
|
} from "@customer-portal/domain";
|
||||||
|
import type { WhmcsInvoice, WhmcsProduct } from "../../types/whmcs-api.types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for validating transformed data objects
|
* Service for validating transformed data objects
|
||||||
@ -77,41 +78,26 @@ export class TransformationValidator {
|
|||||||
/**
|
/**
|
||||||
* Validate invoice items array
|
* Validate invoice items array
|
||||||
*/
|
*/
|
||||||
validateInvoiceItems(items: unknown[]): boolean {
|
validateInvoiceItems(items: Array<{ description: string; amount: string; id: number; type: string; relid: number }>): boolean {
|
||||||
if (!Array.isArray(items)) return false;
|
if (!Array.isArray(items)) return false;
|
||||||
|
|
||||||
return items.every(item => {
|
return items.every(item => {
|
||||||
if (!item || typeof item !== "object") return false;
|
return Boolean(item.description && item.amount && item.id);
|
||||||
|
|
||||||
const requiredFields = ["description", "amount"];
|
|
||||||
return requiredFields.every(field => {
|
|
||||||
const value = (item as Record<string, unknown>)[field];
|
|
||||||
return value !== undefined && value !== null;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that required WHMCS data is present
|
* Validate that required WHMCS invoice data is present
|
||||||
*/
|
*/
|
||||||
validateWhmcsInvoiceData(whmcsInvoice: unknown): boolean {
|
validateWhmcsInvoiceData(whmcsInvoice: WhmcsInvoice): boolean {
|
||||||
if (!whmcsInvoice || typeof whmcsInvoice !== "object") return false;
|
return Boolean(whmcsInvoice.invoiceid || whmcsInvoice.id);
|
||||||
|
|
||||||
const invoice = whmcsInvoice as Record<string, unknown>;
|
|
||||||
const invoiceId = invoice.invoiceid || invoice.id;
|
|
||||||
|
|
||||||
return Boolean(invoiceId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that required WHMCS product data is present
|
* Validate that required WHMCS product data is present
|
||||||
*/
|
*/
|
||||||
validateWhmcsProductData(whmcsProduct: unknown): boolean {
|
validateWhmcsProductData(whmcsProduct: WhmcsProduct): boolean {
|
||||||
if (!whmcsProduct || typeof whmcsProduct !== "object") return false;
|
return Boolean(whmcsProduct.id);
|
||||||
|
|
||||||
const product = whmcsProduct as Record<string, unknown>;
|
|
||||||
|
|
||||||
return Boolean(product.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,7 +113,7 @@ export class TransformationValidator {
|
|||||||
/**
|
/**
|
||||||
* Validate amount is a valid number
|
* Validate amount is a valid number
|
||||||
*/
|
*/
|
||||||
validateAmount(amount: unknown): boolean {
|
validateAmount(amount: string | number): boolean {
|
||||||
if (typeof amount === "number") {
|
if (typeof amount === "number") {
|
||||||
return !isNaN(amount) && isFinite(amount);
|
return !isNaN(amount) && isFinite(amount);
|
||||||
}
|
}
|
||||||
@ -143,8 +129,8 @@ export class TransformationValidator {
|
|||||||
/**
|
/**
|
||||||
* Validate date string format
|
* Validate date string format
|
||||||
*/
|
*/
|
||||||
validateDateString(dateStr: unknown): boolean {
|
validateDateString(dateStr: string): boolean {
|
||||||
if (!dateStr || typeof dateStr !== "string") return false;
|
if (!dateStr) return false;
|
||||||
|
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
return !isNaN(date.getTime());
|
return !isNaN(date.getTime());
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { WhmcsTransformerOrchestratorService } from "./services/whmcs-transformer-orchestrator.service";
|
|
||||||
import { TransformationValidator } from "./validators/transformation-validator";
|
|
||||||
import type {
|
|
||||||
Invoice,
|
|
||||||
Subscription,
|
|
||||||
PaymentMethod,
|
|
||||||
PaymentGateway,
|
|
||||||
} from "@customer-portal/domain";
|
|
||||||
import type {
|
|
||||||
WhmcsInvoice,
|
|
||||||
WhmcsProduct,
|
|
||||||
WhmcsPaymentMethod,
|
|
||||||
WhmcsPaymentGateway,
|
|
||||||
} from "../types/whmcs-api.types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main WHMCS Data Transformer - now acts as a facade to the orchestrator service
|
|
||||||
* Maintains backward compatibility while delegating to modular services
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class WhmcsDataTransformer {
|
|
||||||
constructor(
|
|
||||||
private readonly orchestrator: WhmcsTransformerOrchestratorService,
|
|
||||||
private readonly validator: TransformationValidator
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform WHMCS invoice to our standard Invoice format
|
|
||||||
*/
|
|
||||||
transformInvoice(whmcsInvoice: WhmcsInvoice): Invoice {
|
|
||||||
return this.orchestrator.transformInvoiceSync(whmcsInvoice);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform WHMCS product/service to our standard Subscription format
|
|
||||||
*/
|
|
||||||
transformSubscription(whmcsProduct: WhmcsProduct): Subscription {
|
|
||||||
return this.orchestrator.transformSubscriptionSync(whmcsProduct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform WHMCS payment gateway to shared PaymentGateway interface
|
|
||||||
*/
|
|
||||||
transformPaymentGateway(whmcsGateway: WhmcsPaymentGateway): PaymentGateway {
|
|
||||||
return this.orchestrator.transformPaymentGatewaySync(whmcsGateway);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform WHMCS payment method to shared PaymentMethod interface
|
|
||||||
*/
|
|
||||||
transformPaymentMethod(whmcsPayMethod: WhmcsPaymentMethod): PaymentMethod {
|
|
||||||
return this.orchestrator.transformPaymentMethodSync(whmcsPayMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate subscription transformation result
|
|
||||||
*/
|
|
||||||
validateSubscription(subscription: Subscription): boolean {
|
|
||||||
return this.validator.validateSubscription(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate payment method transformation result
|
|
||||||
*/
|
|
||||||
validatePaymentMethod(paymentMethod: PaymentMethod): boolean {
|
|
||||||
return this.validator.validatePaymentMethod(paymentMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate payment gateway transformation result
|
|
||||||
*/
|
|
||||||
validatePaymentGateway(gateway: PaymentGateway): boolean {
|
|
||||||
return this.validator.validatePaymentGateway(gateway);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -333,7 +333,92 @@ export interface WhmcsPaymentGatewaysResponse {
|
|||||||
totalresults: number;
|
totalresults: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Invoice Creation and Payment Types (Used by SIM/Order services)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// CreateInvoice API Types
|
||||||
|
export interface WhmcsCreateInvoiceParams {
|
||||||
|
userid: number;
|
||||||
|
status?:
|
||||||
|
| "Draft"
|
||||||
|
| "Unpaid"
|
||||||
|
| "Paid"
|
||||||
|
| "Cancelled"
|
||||||
|
| "Refunded"
|
||||||
|
| "Collections"
|
||||||
|
| "Payment Pending";
|
||||||
|
sendnotification?: boolean;
|
||||||
|
paymentmethod?: string;
|
||||||
|
taxrate?: number;
|
||||||
|
taxrate2?: number;
|
||||||
|
date?: string; // YYYY-MM-DD format
|
||||||
|
duedate?: string; // YYYY-MM-DD format
|
||||||
|
notes?: string;
|
||||||
|
itemdescription1?: string;
|
||||||
|
itemamount1?: number;
|
||||||
|
itemtaxed1?: boolean;
|
||||||
|
itemdescription2?: string;
|
||||||
|
itemamount2?: number;
|
||||||
|
itemtaxed2?: boolean;
|
||||||
|
// Can have up to 24 line items (itemdescription1-24, itemamount1-24, itemtaxed1-24)
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WhmcsCreateInvoiceResponse {
|
||||||
|
result: "success" | "error";
|
||||||
|
invoiceid: number;
|
||||||
|
status: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateInvoice API Types
|
||||||
|
export interface WhmcsUpdateInvoiceParams {
|
||||||
|
invoiceid: number;
|
||||||
|
status?:
|
||||||
|
| "Draft"
|
||||||
|
| "Unpaid"
|
||||||
|
| "Paid"
|
||||||
|
| "Cancelled"
|
||||||
|
| "Refunded"
|
||||||
|
| "Collections"
|
||||||
|
| "Payment Pending";
|
||||||
|
duedate?: string; // YYYY-MM-DD format
|
||||||
|
notes?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WhmcsUpdateInvoiceResponse {
|
||||||
|
result: "success" | "error";
|
||||||
|
invoiceid: number;
|
||||||
|
status: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CapturePayment API Types
|
||||||
|
export interface WhmcsCapturePaymentParams {
|
||||||
|
invoiceid: number;
|
||||||
|
cvv?: string;
|
||||||
|
cardnum?: string;
|
||||||
|
cccvv?: string;
|
||||||
|
cardtype?: string;
|
||||||
|
cardexp?: string;
|
||||||
|
// For existing payment methods
|
||||||
|
paymentmethodid?: number;
|
||||||
|
// Manual payment capture
|
||||||
|
transid?: string;
|
||||||
|
gateway?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WhmcsCapturePaymentResponse {
|
||||||
|
result: "success" | "error";
|
||||||
|
invoiceid: number;
|
||||||
|
status: string;
|
||||||
|
transactionid?: string;
|
||||||
|
amount?: number;
|
||||||
|
fees?: number;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule } from "@nestjs/config";
|
||||||
import { WhmcsDataTransformer } from "./transformers/whmcs-data.transformer";
|
|
||||||
import { WhmcsCacheService } from "./cache/whmcs-cache.service";
|
import { WhmcsCacheService } from "./cache/whmcs-cache.service";
|
||||||
import { WhmcsService } from "./whmcs.service";
|
import { WhmcsService } from "./whmcs.service";
|
||||||
import { WhmcsConnectionService } from "./services/whmcs-connection.service";
|
import { WhmcsConnectionService } from "./services/whmcs-connection.service";
|
||||||
@ -26,8 +25,6 @@ import { WhmcsApiMethodsService } from "./connection/services/whmcs-api-methods.
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
providers: [
|
providers: [
|
||||||
// Legacy transformer (now facade)
|
|
||||||
WhmcsDataTransformer,
|
|
||||||
// New modular transformer services
|
// New modular transformer services
|
||||||
WhmcsTransformerOrchestratorService,
|
WhmcsTransformerOrchestratorService,
|
||||||
InvoiceTransformerService,
|
InvoiceTransformerService,
|
||||||
@ -56,7 +53,6 @@ import { WhmcsApiMethodsService } from "./connection/services/whmcs-api-methods.
|
|||||||
WhmcsService,
|
WhmcsService,
|
||||||
WhmcsConnectionService,
|
WhmcsConnectionService,
|
||||||
WhmcsConnectionOrchestratorService,
|
WhmcsConnectionOrchestratorService,
|
||||||
WhmcsDataTransformer,
|
|
||||||
WhmcsTransformerOrchestratorService,
|
WhmcsTransformerOrchestratorService,
|
||||||
WhmcsCacheService,
|
WhmcsCacheService,
|
||||||
WhmcsOrderService,
|
WhmcsOrderService,
|
||||||
|
|||||||
@ -279,12 +279,6 @@ export class WhmcsService {
|
|||||||
return this.paymentService.getProducts() as Promise<WhmcsCatalogProductsResponse>;
|
return this.paymentService.getProducts() as Promise<WhmcsCatalogProductsResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform product data (delegate to transformer)
|
|
||||||
*/
|
|
||||||
transformProduct(whmcsProduct: WhmcsCatalogProductsResponse["products"]["product"][0]): unknown {
|
|
||||||
return this.paymentService.transformProduct(whmcsProduct);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// SSO OPERATIONS (delegate to SsoService)
|
// SSO OPERATIONS (delegate to SsoService)
|
||||||
@ -353,4 +347,52 @@ export class WhmcsService {
|
|||||||
getOrderService(): WhmcsOrderService {
|
getOrderService(): WhmcsOrderService {
|
||||||
return this.orderService;
|
return this.orderService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// INVOICE CREATION AND PAYMENT OPERATIONS (Used by SIM/Order services)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new invoice for a client
|
||||||
|
*/
|
||||||
|
async createInvoice(params: {
|
||||||
|
clientId: number;
|
||||||
|
description: string;
|
||||||
|
amount: number;
|
||||||
|
currency?: string;
|
||||||
|
dueDate?: Date;
|
||||||
|
notes?: string;
|
||||||
|
}): Promise<{ id: number; number: string; total: number; status: string }> {
|
||||||
|
return this.invoiceService.createInvoice(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing invoice
|
||||||
|
*/
|
||||||
|
async updateInvoice(params: {
|
||||||
|
invoiceId: number;
|
||||||
|
status?:
|
||||||
|
| "Draft"
|
||||||
|
| "Unpaid"
|
||||||
|
| "Paid"
|
||||||
|
| "Cancelled"
|
||||||
|
| "Refunded"
|
||||||
|
| "Collections"
|
||||||
|
| "Payment Pending";
|
||||||
|
dueDate?: Date;
|
||||||
|
notes?: string;
|
||||||
|
}): Promise<{ success: boolean; message?: string }> {
|
||||||
|
return this.invoiceService.updateInvoice(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture payment for an invoice
|
||||||
|
*/
|
||||||
|
async capturePayment(params: {
|
||||||
|
invoiceId: number;
|
||||||
|
amount: number;
|
||||||
|
currency?: string;
|
||||||
|
}): Promise<{ success: boolean; transactionId?: string; error?: string }> {
|
||||||
|
return this.invoiceService.capturePayment(params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ export { InvoicesOrchestratorService } from "./services/invoices-orchestrator.se
|
|||||||
|
|
||||||
// Individual services
|
// Individual services
|
||||||
export { InvoiceRetrievalService } from "./services/invoice-retrieval.service";
|
export { InvoiceRetrievalService } from "./services/invoice-retrieval.service";
|
||||||
export { InvoiceOperationsService } from "./services/invoice-operations.service";
|
|
||||||
export { PaymentMethodsService } from "./services/payment-methods.service";
|
|
||||||
export { InvoiceHealthService } from "./services/invoice-health.service";
|
export { InvoiceHealthService } from "./services/invoice-health.service";
|
||||||
|
|
||||||
// Validators
|
// Validators
|
||||||
|
|||||||
@ -167,7 +167,9 @@ export class InvoicesController {
|
|||||||
throw new BadRequestException("Invoice ID must be a positive number");
|
throw new BadRequestException("Invoice ID must be a positive number");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.invoicesService.getInvoiceSubscriptions(req.user.id, invoiceId);
|
// This functionality has been moved to WHMCS directly
|
||||||
|
// For now, return empty array as subscriptions are managed in WHMCS
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(":id/sso-link")
|
@Post(":id/sso-link")
|
||||||
@ -199,7 +201,7 @@ export class InvoicesController {
|
|||||||
throw new BadRequestException('Target must be "view", "download", or "pay"');
|
throw new BadRequestException('Target must be "view", "download", or "pay"');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.invoicesService.createSsoLink(req.user.id, invoiceId, target || "view");
|
return this.invoicesService.createInvoiceSsoLink(req.user.id, invoiceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(":id/payment-link")
|
@Post(":id/payment-link")
|
||||||
@ -239,11 +241,11 @@ export class InvoicesController {
|
|||||||
throw new BadRequestException("Payment method ID must be a positive number");
|
throw new BadRequestException("Payment method ID must be a positive number");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.invoicesService.createPaymentSsoLink(
|
return this.invoicesService.createInvoicePaymentLink(
|
||||||
req.user.id,
|
req.user.id,
|
||||||
invoiceId,
|
invoiceId,
|
||||||
paymentMethodIdNum,
|
gatewayName || "stripe",
|
||||||
gatewayName
|
"/"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import { MappingsModule } from "@bff/modules/id-mappings/mappings.module";
|
|||||||
// New modular invoice services
|
// New modular invoice services
|
||||||
import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service";
|
import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service";
|
||||||
import { InvoiceRetrievalService } from "./services/invoice-retrieval.service";
|
import { InvoiceRetrievalService } from "./services/invoice-retrieval.service";
|
||||||
import { InvoiceOperationsService } from "./services/invoice-operations.service";
|
|
||||||
import { PaymentMethodsService } from "./services/payment-methods.service";
|
|
||||||
import { InvoiceHealthService } from "./services/invoice-health.service";
|
import { InvoiceHealthService } from "./services/invoice-health.service";
|
||||||
import { InvoiceValidatorService } from "./validators/invoice-validator.service";
|
import { InvoiceValidatorService } from "./validators/invoice-validator.service";
|
||||||
|
|
||||||
@ -20,8 +18,6 @@ import { InvoiceValidatorService } from "./validators/invoice-validator.service"
|
|||||||
// New modular services
|
// New modular services
|
||||||
InvoicesOrchestratorService,
|
InvoicesOrchestratorService,
|
||||||
InvoiceRetrievalService,
|
InvoiceRetrievalService,
|
||||||
InvoiceOperationsService,
|
|
||||||
PaymentMethodsService,
|
|
||||||
InvoiceHealthService,
|
InvoiceHealthService,
|
||||||
InvoiceValidatorService,
|
InvoiceValidatorService,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -32,8 +32,6 @@ export class OrderValidator {
|
|||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
{
|
{
|
||||||
bodyType: typeof rawBody,
|
bodyType: typeof rawBody,
|
||||||
hasOrderType: !!(rawBody as Record<string, unknown>)?.orderType,
|
|
||||||
hasSkus: !!(rawBody as Record<string, unknown>)?.skus,
|
|
||||||
},
|
},
|
||||||
"Starting Zod request format validation"
|
"Starting Zod request format validation"
|
||||||
);
|
);
|
||||||
@ -108,7 +106,7 @@ export class OrderValidator {
|
|||||||
*/
|
*/
|
||||||
async validatePaymentMethod(userId: string, whmcsClientId: number): Promise<void> {
|
async validatePaymentMethod(userId: string, whmcsClientId: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const pay = await this.whmcs.getPayMethods({ clientid: whmcsClientId });
|
const pay = await this.whmcs.getPaymentMethods({ clientid: whmcsClientId });
|
||||||
if (!Array.isArray(pay?.paymethods) || pay.paymethods.length === 0) {
|
if (!Array.isArray(pay?.paymethods) || pay.paymethods.length === 0) {
|
||||||
this.logger.warn({ userId }, "No WHMCS payment method on file");
|
this.logger.warn({ userId }, "No WHMCS payment method on file");
|
||||||
throw new BadRequestException("A payment method is required before ordering");
|
throw new BadRequestException("A payment method is required before ordering");
|
||||||
@ -126,11 +124,9 @@ export class OrderValidator {
|
|||||||
async validateInternetDuplication(userId: string, whmcsClientId: number): Promise<void> {
|
async validateInternetDuplication(userId: string, whmcsClientId: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const products = await this.whmcs.getClientsProducts({ clientid: whmcsClientId });
|
const products = await this.whmcs.getClientsProducts({ clientid: whmcsClientId });
|
||||||
const existing = (products?.products?.product || []) as Array<{
|
const existing = products?.products?.product || [];
|
||||||
groupname?: string;
|
const hasInternet = existing.some(product =>
|
||||||
}>;
|
(product.groupname || "")
|
||||||
const hasInternet = existing.some(p =>
|
|
||||||
String(p.groupname ?? "")
|
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes("internet")
|
.includes("internet")
|
||||||
);
|
);
|
||||||
|
|||||||
@ -24,8 +24,11 @@ export class SimTopUpService {
|
|||||||
* Pricing: 1GB = 500 JPY
|
* Pricing: 1GB = 500 JPY
|
||||||
*/
|
*/
|
||||||
async topUpSim(userId: string, subscriptionId: number, request: SimTopUpRequest): Promise<void> {
|
async topUpSim(userId: string, subscriptionId: number, request: SimTopUpRequest): Promise<void> {
|
||||||
|
let account: string = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { account } = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
const validation = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
||||||
|
account = validation.account;
|
||||||
|
|
||||||
// Validate quota amount
|
// Validate quota amount
|
||||||
if (request.quotaMb <= 0 || request.quotaMb > 100000) {
|
if (request.quotaMb <= 0 || request.quotaMb > 100000) {
|
||||||
@ -98,7 +101,7 @@ export class SimTopUpService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Cancel the invoice since payment failed
|
// Cancel the invoice since payment failed
|
||||||
await this.handlePaymentFailure(invoice.id, paymentResult.error);
|
await this.handlePaymentFailure(invoice.id, paymentResult.error || "Unknown payment error");
|
||||||
|
|
||||||
throw new BadRequestException(`SIM top-up failed: ${paymentResult.error}`);
|
throw new BadRequestException(`SIM top-up failed: ${paymentResult.error}`);
|
||||||
}
|
}
|
||||||
@ -138,7 +141,7 @@ export class SimTopUpService {
|
|||||||
await this.handleFreebitFailureAfterPayment(
|
await this.handleFreebitFailureAfterPayment(
|
||||||
freebitError,
|
freebitError,
|
||||||
invoice,
|
invoice,
|
||||||
paymentResult.transactionId,
|
paymentResult.transactionId || "unknown",
|
||||||
userId,
|
userId,
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
account,
|
account,
|
||||||
|
|||||||
@ -87,11 +87,22 @@ export class SimOrderActivationService {
|
|||||||
await this.freebit.activateEsimAccountNew({
|
await this.freebit.activateEsimAccountNew({
|
||||||
account: req.msisdn,
|
account: req.msisdn,
|
||||||
eid: req.eid!,
|
eid: req.eid!,
|
||||||
planCode: req.planSku,
|
planSku: req.planSku,
|
||||||
contractLine: "5G",
|
contractLine: "5G",
|
||||||
shipDate: req.activationType === "Scheduled" ? req.scheduledAt : undefined,
|
shipDate: req.activationType === "Scheduled" ? req.scheduledAt : undefined,
|
||||||
mnp: req.mnp
|
mnp: req.mnp
|
||||||
? { reserveNumber: req.mnp.reserveNumber, reserveExpireDate: req.mnp.reserveExpireDate }
|
? {
|
||||||
|
reservationNumber: req.mnp.reserveNumber,
|
||||||
|
expiryDate: req.mnp.reserveExpireDate,
|
||||||
|
phoneNumber: req.mnp.account || "",
|
||||||
|
mvnoAccountNumber: "",
|
||||||
|
portingLastName: "",
|
||||||
|
portingFirstName: "",
|
||||||
|
portingLastNameKatakana: "",
|
||||||
|
portingFirstNameKatakana: "",
|
||||||
|
portingGender: "" as const,
|
||||||
|
portingDateOfBirth: ""
|
||||||
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
identity: req.mnp
|
identity: req.mnp
|
||||||
? {
|
? {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user