Refactor authentication module by removing the deprecated AuthAdminController and related token migration services. Update AuthModule to streamline dependencies and enhance structure. Adjust imports in various services and controllers for improved maintainability. Revise documentation to reflect the removal of admin endpoints and clarify the new authentication setup.
This commit is contained in:
parent
d04e343161
commit
cdec21e012
@ -119,7 +119,7 @@ export async function bootstrap(): Promise<INestApplication> {
|
||||
// Global exception filters
|
||||
app.useGlobalFilters(
|
||||
new AuthErrorFilter(app.get(Logger)), // Handle auth errors first
|
||||
new GlobalExceptionFilter(app.get(Logger), configService, app.get(SecureErrorMapperService)) // Handle all other errors
|
||||
new GlobalExceptionFilter(app.get(Logger), app.get(SecureErrorMapperService)) // Handle all other errors
|
||||
);
|
||||
|
||||
// Global authentication guard will be registered via APP_GUARD provider in AuthModule
|
||||
|
||||
@ -116,7 +116,7 @@ export class DistributedTransactionService {
|
||||
timeout,
|
||||
});
|
||||
|
||||
const stepResults: StepResultMap<TSteps> = {};
|
||||
const stepResults: StepResultMap<TSteps> = {} as StepResultMap<TSteps>;
|
||||
const executedSteps: string[] = [];
|
||||
const failedSteps: string[] = [];
|
||||
|
||||
@ -133,7 +133,8 @@ export class DistributedTransactionService {
|
||||
const result = await this.executeStepWithTimeout(step, timeout);
|
||||
const stepDuration = Date.now() - stepStartTime;
|
||||
|
||||
stepResults[step.id] = result;
|
||||
const key = step.id as keyof StepResultMap<TSteps>;
|
||||
Reflect.set(stepResults, key, result);
|
||||
executedSteps.push(step.id);
|
||||
|
||||
this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, {
|
||||
@ -197,12 +198,7 @@ export class DistributedTransactionService {
|
||||
});
|
||||
|
||||
// Execute rollbacks for completed steps
|
||||
const rollbacksExecuted = await this.executeRollbacks(
|
||||
steps,
|
||||
executedSteps,
|
||||
stepResults,
|
||||
transactionId
|
||||
);
|
||||
const rollbacksExecuted = await this.executeRollbacks(steps, executedSteps, transactionId);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@ -314,12 +310,8 @@ export class DistributedTransactionService {
|
||||
|
||||
if (!dbTransactionResult.success) {
|
||||
// Rollback external operations
|
||||
await this.executeRollbacks(
|
||||
externalSteps,
|
||||
Object.keys(externalResult.stepResults),
|
||||
externalResult.stepResults,
|
||||
transactionId
|
||||
);
|
||||
const executedExternalSteps = Object.keys(externalResult.stepResults) as string[];
|
||||
await this.executeRollbacks(externalSteps, executedExternalSteps, transactionId);
|
||||
throw new Error(dbTransactionResult.error || "Database transaction failed");
|
||||
}
|
||||
|
||||
@ -339,7 +331,7 @@ export class DistributedTransactionService {
|
||||
duration,
|
||||
stepsExecuted: externalResult?.stepsExecuted || 0,
|
||||
stepsRolledBack: 0,
|
||||
stepResults: (externalResult?.stepResults ?? {}) as TStepResults,
|
||||
stepResults: (externalResult?.stepResults ?? ({} as StepResultMap<TSteps>)) as TStepResults,
|
||||
failedSteps: externalResult?.failedSteps || [],
|
||||
};
|
||||
} catch (error) {
|
||||
@ -357,7 +349,7 @@ export class DistributedTransactionService {
|
||||
duration,
|
||||
stepsExecuted: 0,
|
||||
stepsRolledBack: 0,
|
||||
stepResults: {},
|
||||
stepResults: {} as TStepResults,
|
||||
failedSteps: [],
|
||||
};
|
||||
}
|
||||
@ -379,8 +371,7 @@ export class DistributedTransactionService {
|
||||
|
||||
private async executeRollbacks<TSteps extends readonly DistributedStep[]>(
|
||||
steps: TSteps,
|
||||
executedSteps: string[],
|
||||
_stepResults: Partial<StepResultMap<TSteps>>,
|
||||
executedSteps: readonly string[],
|
||||
transactionId: string
|
||||
): Promise<number> {
|
||||
this.logger.warn(`Executing rollbacks for ${executedSteps.length} steps [${transactionId}]`);
|
||||
|
||||
@ -255,7 +255,9 @@ export class TransactionService {
|
||||
);
|
||||
}
|
||||
|
||||
private enhanceContext(context: TransactionContext): TransactionContext {
|
||||
private enhanceContext(
|
||||
context: TransactionContext
|
||||
): TransactionContext & TransactionContextHelpers {
|
||||
const helpers: TransactionContextHelpers = {
|
||||
addOperation: (description: string) => {
|
||||
context.operations.push(`${new Date().toISOString()}: ${description}`);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Injectable, Inject, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import PQueue from "p-queue";
|
||||
export interface SalesforceQueueMetrics {
|
||||
totalRequests: number;
|
||||
completedRequests: number;
|
||||
@ -39,10 +38,29 @@ export interface SalesforceRequestOptions {
|
||||
* - Timeout: 10 minutes per request
|
||||
* - Rate limiting: Conservative 120 requests per minute (2 RPS)
|
||||
*/
|
||||
type PQueueCtor = new (options: {
|
||||
concurrency?: number;
|
||||
interval?: number;
|
||||
intervalCap?: number;
|
||||
timeout?: number;
|
||||
throwOnTimeout?: boolean;
|
||||
carryoverConcurrencyCount?: boolean;
|
||||
}) => PQueueInstance;
|
||||
|
||||
interface PQueueInstance {
|
||||
add<T>(fn: () => Promise<T>, options?: { priority?: number }): Promise<T>;
|
||||
clear(): void;
|
||||
onIdle(): Promise<void>;
|
||||
on(event: "add" | "next" | "idle" | "error", listener: (...args: unknown[]) => void): void;
|
||||
size: number;
|
||||
pending: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
private standardQueue: PQueue | null = null;
|
||||
private longRunningQueue: PQueue | null = null;
|
||||
private pQueueCtor: PQueueCtor | null = null;
|
||||
private standardQueue: PQueueInstance | null = null;
|
||||
private longRunningQueue: PQueueInstance | null = null;
|
||||
private readonly metrics: SalesforceQueueMetrics = {
|
||||
totalRequests: 0,
|
||||
completedRequests: 0,
|
||||
@ -66,8 +84,20 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
this.dailyUsageResetTime = this.getNextDayReset();
|
||||
}
|
||||
|
||||
private ensureQueuesInitialized(): { standardQueue: PQueue; longRunningQueue: PQueue } {
|
||||
private async loadPQueue(): Promise<PQueueCtor> {
|
||||
if (!this.pQueueCtor) {
|
||||
const module = await import("p-queue");
|
||||
this.pQueueCtor = module.default as PQueueCtor;
|
||||
}
|
||||
return this.pQueueCtor;
|
||||
}
|
||||
|
||||
private async ensureQueuesInitialized(): Promise<{
|
||||
standardQueue: PQueueInstance;
|
||||
longRunningQueue: PQueueInstance;
|
||||
}> {
|
||||
if (!this.standardQueue || !this.longRunningQueue) {
|
||||
const PQueue = await this.loadPQueue();
|
||||
const concurrency = this.configService.get<number>("SF_QUEUE_CONCURRENCY", 15);
|
||||
const longRunningConcurrency = this.configService.get<number>(
|
||||
"SF_QUEUE_LONG_RUNNING_CONCURRENCY",
|
||||
@ -109,14 +139,11 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
throw new Error("Failed to initialize Salesforce queues");
|
||||
}
|
||||
|
||||
return {
|
||||
standardQueue,
|
||||
longRunningQueue,
|
||||
};
|
||||
return { standardQueue, longRunningQueue };
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.ensureQueuesInitialized();
|
||||
async onModuleInit() {
|
||||
await this.ensureQueuesInitialized();
|
||||
const concurrency = this.configService.get<number>("SF_QUEUE_CONCURRENCY", 15);
|
||||
const longRunningConcurrency = this.configService.get<number>(
|
||||
"SF_QUEUE_LONG_RUNNING_CONCURRENCY",
|
||||
@ -171,7 +198,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
requestFn: () => Promise<T>,
|
||||
options: SalesforceRequestOptions = {}
|
||||
): Promise<T> {
|
||||
const { standardQueue, longRunningQueue } = this.ensureQueuesInitialized();
|
||||
const { standardQueue, longRunningQueue } = await this.ensureQueuesInitialized();
|
||||
// Check daily API usage
|
||||
this.checkDailyUsage();
|
||||
|
||||
@ -194,7 +221,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await queue.add(
|
||||
const result = (await queue.add(
|
||||
async () => {
|
||||
const waitTime = Date.now() - startTime;
|
||||
this.recordWaitTime(waitTime);
|
||||
@ -247,7 +274,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
{
|
||||
priority: options.priority ?? 0,
|
||||
}
|
||||
);
|
||||
)) as T;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
@ -341,7 +368,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
* Clear the queues (emergency use only)
|
||||
*/
|
||||
async clearQueues(): Promise<void> {
|
||||
const { standardQueue, longRunningQueue } = this.ensureQueuesInitialized();
|
||||
const { standardQueue, longRunningQueue } = await this.ensureQueuesInitialized();
|
||||
|
||||
this.logger.warn("Clearing Salesforce request queues", {
|
||||
standardQueueSize: standardQueue.size,
|
||||
@ -367,7 +394,8 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await requestFn();
|
||||
const result = await requestFn();
|
||||
return result;
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
@ -446,7 +474,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
this.logger.debug("Salesforce standard queue is idle");
|
||||
this.updateQueueMetrics();
|
||||
});
|
||||
this.standardQueue.on("error", (error: Error) => {
|
||||
this.standardQueue.on("error", (error: unknown) => {
|
||||
this.logger.error("Salesforce standard queue error", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
@ -458,7 +486,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
|
||||
this.logger.debug("Salesforce long-running queue is idle");
|
||||
this.updateQueueMetrics();
|
||||
});
|
||||
this.longRunningQueue.on("error", (error: Error) => {
|
||||
this.longRunningQueue.on("error", (error: unknown) => {
|
||||
this.logger.error("Salesforce long-running queue error", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
@ -1,7 +1,24 @@
|
||||
import { Injectable, Inject, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import PQueue from "p-queue";
|
||||
|
||||
type PQueueCtor = new (options: {
|
||||
concurrency?: number;
|
||||
interval?: number;
|
||||
intervalCap?: number;
|
||||
timeout?: number;
|
||||
throwOnTimeout?: boolean;
|
||||
carryoverConcurrencyCount?: boolean;
|
||||
}) => PQueueInstance;
|
||||
|
||||
interface PQueueInstance {
|
||||
add<T>(fn: () => Promise<T>, options?: { priority?: number }): Promise<T>;
|
||||
clear(): void;
|
||||
onIdle(): Promise<void>;
|
||||
on(event: "add" | "next" | "idle" | "error", listener: (...args: unknown[]) => void): void;
|
||||
size: number;
|
||||
pending: number;
|
||||
}
|
||||
export interface WhmcsQueueMetrics {
|
||||
totalRequests: number;
|
||||
completedRequests: number;
|
||||
@ -37,7 +54,8 @@ export interface WhmcsRequestOptions {
|
||||
*/
|
||||
@Injectable()
|
||||
export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
private queue: PQueue | null = null;
|
||||
private pQueueCtor: PQueueCtor | null = null;
|
||||
private queue: PQueueInstance | null = null;
|
||||
private readonly metrics: WhmcsQueueMetrics = {
|
||||
totalRequests: 0,
|
||||
completedRequests: 0,
|
||||
@ -57,8 +75,17 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly configService: ConfigService
|
||||
) {}
|
||||
|
||||
private ensureQueueInitialized(): PQueue {
|
||||
private async loadPQueue(): Promise<PQueueCtor> {
|
||||
if (!this.pQueueCtor) {
|
||||
const module = await import("p-queue");
|
||||
this.pQueueCtor = module.default as PQueueCtor;
|
||||
}
|
||||
return this.pQueueCtor;
|
||||
}
|
||||
|
||||
private async ensureQueueInitialized(): Promise<PQueueInstance> {
|
||||
if (!this.queue) {
|
||||
const PQueue = await this.loadPQueue();
|
||||
const concurrency = this.configService.get<number>("WHMCS_QUEUE_CONCURRENCY", 15);
|
||||
const intervalCap = this.configService.get<number>("WHMCS_QUEUE_INTERVAL_CAP", 300);
|
||||
const timeout = this.configService.get<number>("WHMCS_QUEUE_TIMEOUT_MS", 30000);
|
||||
@ -84,8 +111,8 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
return this.queue;
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.ensureQueueInitialized();
|
||||
async onModuleInit() {
|
||||
await this.ensureQueueInitialized();
|
||||
const concurrency = this.configService.get<number>("WHMCS_QUEUE_CONCURRENCY", 15);
|
||||
const intervalCap = this.configService.get<number>("WHMCS_QUEUE_INTERVAL_CAP", 300);
|
||||
const timeout = this.configService.get<number>("WHMCS_QUEUE_TIMEOUT_MS", 30000);
|
||||
@ -117,7 +144,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
* Execute a WHMCS API request through the queue
|
||||
*/
|
||||
async execute<T>(requestFn: () => Promise<T>, options: WhmcsRequestOptions = {}): Promise<T> {
|
||||
const queue = this.ensureQueueInitialized();
|
||||
const queue = await this.ensureQueueInitialized();
|
||||
const startTime = Date.now();
|
||||
const requestId = this.generateRequestId();
|
||||
|
||||
@ -132,7 +159,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await queue.add(
|
||||
const result = (await queue.add(
|
||||
async () => {
|
||||
const waitTime = Date.now() - startTime;
|
||||
this.recordWaitTime(waitTime);
|
||||
@ -174,7 +201,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
{
|
||||
priority: options.priority ?? 0,
|
||||
}
|
||||
);
|
||||
)) as T;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
@ -264,7 +291,8 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await requestFn();
|
||||
const result = await requestFn();
|
||||
return result;
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
@ -307,7 +335,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
|
||||
this.updateQueueMetrics();
|
||||
});
|
||||
|
||||
this.queue.on("error", (error: Error) => {
|
||||
this.queue.on("error", (error: unknown) => {
|
||||
this.logger.error("WHMCS queue error", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
@ -6,7 +6,6 @@ import { CsrfService } from "../services/csrf.service";
|
||||
type AuthenticatedRequest = Request & {
|
||||
user?: { id: string; sessionId?: string };
|
||||
sessionID?: string;
|
||||
cookies: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
@Controller("security/csrf")
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
||||
import { Invoice, InvoiceList, invoiceListSchema, invoiceSchema } from "@customer-portal/domain";
|
||||
import { Invoice, InvoiceList } from "@customer-portal/domain";
|
||||
import {
|
||||
invoiceListSchema,
|
||||
invoiceSchema,
|
||||
} from "@customer-portal/domain/validation/shared/entities";
|
||||
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service";
|
||||
import { InvoiceTransformerService } from "../transformers/services/invoice-transformer.service";
|
||||
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
||||
@ -62,7 +66,7 @@ export class WhmcsInvoiceService {
|
||||
const response = await this.connectionService.getInvoices(params);
|
||||
const transformed = this.transformInvoicesResponse(response, clientId, page, limit);
|
||||
|
||||
const result = invoiceListSchema.parse(transformed);
|
||||
const result = invoiceListSchema.parse(transformed as unknown);
|
||||
|
||||
// Cache the result
|
||||
await this.cacheService.setInvoicesList(userId, page, limit, status, result);
|
||||
@ -205,8 +209,8 @@ export class WhmcsInvoiceService {
|
||||
for (const whmcsInvoice of response.invoices.invoice) {
|
||||
try {
|
||||
const transformed = this.invoiceTransformer.transformInvoice(whmcsInvoice);
|
||||
const parsed = invoiceSchema.parse(transformed);
|
||||
invoices.push(parsed as any);
|
||||
const parsed = invoiceSchema.parse(transformed as unknown);
|
||||
invoices.push(parsed);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, {
|
||||
error: getErrorMessage(error),
|
||||
@ -312,14 +316,22 @@ export class WhmcsInvoiceService {
|
||||
| "Cancelled"
|
||||
| "Refunded"
|
||||
| "Collections"
|
||||
| "Payment Pending";
|
||||
| "Payment Pending"
|
||||
| "Overdue";
|
||||
dueDate?: Date;
|
||||
notes?: string;
|
||||
}): Promise<{ success: boolean; message?: string }> {
|
||||
try {
|
||||
let statusForUpdate: WhmcsUpdateInvoiceParams["status"];
|
||||
if (params.status === "Payment Pending") {
|
||||
statusForUpdate = "Unpaid";
|
||||
} else {
|
||||
statusForUpdate = params.status;
|
||||
}
|
||||
|
||||
const whmcsParams: WhmcsUpdateInvoiceParams = {
|
||||
invoiceid: params.invoiceId,
|
||||
status: params.status,
|
||||
status: statusForUpdate,
|
||||
duedate: params.dueDate ? params.dueDate.toISOString().split("T")[0] : undefined,
|
||||
notes: params.notes,
|
||||
};
|
||||
|
||||
@ -48,8 +48,16 @@ export class WhmcsPaymentService {
|
||||
clientid: clientId,
|
||||
});
|
||||
|
||||
// Use consolidated array shape
|
||||
const paymentMethodsArray: WhmcsPaymentMethod[] = Array.isArray(response?.paymethods)
|
||||
if (!response || response.result !== "success") {
|
||||
const message = response?.message ?? "GetPayMethods call failed";
|
||||
this.logger.error("WHMCS GetPayMethods returned error", {
|
||||
clientId,
|
||||
response,
|
||||
});
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const paymentMethodsArray: WhmcsPaymentMethod[] = Array.isArray(response.paymethods)
|
||||
? response.paymethods
|
||||
: [];
|
||||
|
||||
@ -69,9 +77,9 @@ export class WhmcsPaymentService {
|
||||
.filter((method): method is PaymentMethod => method !== null);
|
||||
|
||||
// Mark the first method as default (per product decision)
|
||||
methods = methods.map((m, i) =>
|
||||
i === 0 ? { ...m, isDefault: true } : { ...m, isDefault: false }
|
||||
);
|
||||
if (methods.length > 0) {
|
||||
methods = methods.map((m, index) => ({ ...m, isDefault: index === 0 }));
|
||||
}
|
||||
|
||||
const result: PaymentMethodList = { paymentMethods: methods, totalCount: methods.length };
|
||||
if (!options?.fresh) {
|
||||
|
||||
@ -168,14 +168,17 @@ export class InvoiceTransformerService {
|
||||
"Draft",
|
||||
"Unpaid",
|
||||
"Paid",
|
||||
"Pending",
|
||||
"Cancelled",
|
||||
"Refunded",
|
||||
"Collections",
|
||||
"Overdue",
|
||||
];
|
||||
|
||||
if (allowed.includes(status as Invoice["status"])) {
|
||||
return status as Invoice["status"];
|
||||
const normalizedStatus = status === "Payment Pending" ? "Pending" : status;
|
||||
|
||||
if (allowed.includes(normalizedStatus as Invoice["status"])) {
|
||||
return normalizedStatus as Invoice["status"];
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported WHMCS invoice status: ${status}`);
|
||||
|
||||
@ -49,18 +49,18 @@ export class PaymentTransformerService {
|
||||
id: whmcsPayMethod.id,
|
||||
type: whmcsPayMethod.type,
|
||||
description: whmcsPayMethod.description,
|
||||
gatewayName: whmcsPayMethod.gateway_name ?? undefined,
|
||||
gatewayName: whmcsPayMethod.gateway_name || undefined,
|
||||
contactType: whmcsPayMethod.contact_type || undefined,
|
||||
contactId: whmcsPayMethod.contact_id ?? undefined,
|
||||
cardLastFour: whmcsPayMethod.card_last_four || undefined,
|
||||
expiryDate: whmcsPayMethod.expiry_date || undefined,
|
||||
startDate: whmcsPayMethod.start_date || undefined,
|
||||
issueNumber: whmcsPayMethod.issue_number || undefined,
|
||||
cardType: whmcsPayMethod.card_type || undefined,
|
||||
remoteToken: whmcsPayMethod.remote_token || undefined,
|
||||
lastUpdated: whmcsPayMethod.last_updated || undefined,
|
||||
bankName: whmcsPayMethod.bank_name || undefined,
|
||||
isDefault: false,
|
||||
lastFour: whmcsPayMethod.card_last_four ?? undefined,
|
||||
expiryDate: whmcsPayMethod.expiry_date ?? undefined,
|
||||
bankName: whmcsPayMethod.bank_name ?? undefined,
|
||||
accountType: whmcsPayMethod.account_type ?? undefined,
|
||||
remoteToken: whmcsPayMethod.remote_token ?? undefined,
|
||||
ccType: whmcsPayMethod.card_type ?? undefined,
|
||||
cardBrand: whmcsPayMethod.card_type ?? undefined,
|
||||
billingContactId: whmcsPayMethod.billing_contact_id ?? undefined,
|
||||
createdAt: whmcsPayMethod.created_at ?? undefined,
|
||||
updatedAt: whmcsPayMethod.updated_at ?? undefined,
|
||||
};
|
||||
|
||||
if (!this.validator.validatePaymentMethod(transformed)) {
|
||||
|
||||
@ -126,7 +126,7 @@ export class WhmcsTransformerOrchestratorService {
|
||||
} catch (error) {
|
||||
this.logger.error("Payment method transformation failed in orchestrator", {
|
||||
error: DataUtils.toErrorMessage(error),
|
||||
payMethodId: whmcsPayMethod?.id || whmcsPayMethod?.paymethodid,
|
||||
payMethodId: whmcsPayMethod?.id,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@ -141,7 +141,7 @@ export class WhmcsTransformerOrchestratorService {
|
||||
} catch (error) {
|
||||
this.logger.error("Payment method transformation failed in orchestrator", {
|
||||
error: DataUtils.toErrorMessage(error),
|
||||
payMethodId: whmcsPayMethod?.id || whmcsPayMethod?.paymethodid,
|
||||
payMethodId: whmcsPayMethod?.id,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@ -297,31 +297,32 @@ export interface WhmcsCatalogProductsResponse {
|
||||
// Payment Method Types
|
||||
export interface WhmcsPaymentMethod {
|
||||
id: number;
|
||||
paymethodid?: number;
|
||||
type: "CreditCard" | "BankAccount" | "RemoteCreditCard" | "RemoteBankAccount";
|
||||
description: string;
|
||||
gateway_name?: string;
|
||||
contact_type?: string;
|
||||
contact_id?: number;
|
||||
card_last_four?: string;
|
||||
expiry_date?: string;
|
||||
bank_name?: string;
|
||||
account_type?: string;
|
||||
remote_token?: string;
|
||||
start_date?: string;
|
||||
issue_number?: string;
|
||||
card_type?: string;
|
||||
billing_contact_id?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
remote_token?: string;
|
||||
last_updated?: string;
|
||||
bank_name?: string;
|
||||
}
|
||||
|
||||
export interface WhmcsPayMethodsResponse {
|
||||
// Consolidated (preferred) response shape for GetPayMethods
|
||||
paymethods: WhmcsPaymentMethod[];
|
||||
totalresults?: number;
|
||||
result: "success" | "error";
|
||||
clientid?: number | string;
|
||||
paymethods?: WhmcsPaymentMethod[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface WhmcsGetPayMethodsParams {
|
||||
export interface WhmcsGetPayMethodsParams extends Record<string, unknown> {
|
||||
clientid: number;
|
||||
paymethodid?: number;
|
||||
[key: string]: unknown;
|
||||
type?: "BankAccount" | "CreditCard";
|
||||
}
|
||||
|
||||
// Payment Gateway Types
|
||||
@ -353,7 +354,8 @@ export interface WhmcsCreateInvoiceParams {
|
||||
| "Cancelled"
|
||||
| "Refunded"
|
||||
| "Collections"
|
||||
| "Overdue";
|
||||
| "Overdue"
|
||||
| "Payment Pending";
|
||||
sendnotification?: boolean;
|
||||
paymentmethod?: string;
|
||||
taxrate?: number;
|
||||
|
||||
@ -20,12 +20,12 @@ import {
|
||||
import type { User as PrismaUser } from "@prisma/client";
|
||||
import type { Request } from "express";
|
||||
import { PrismaService } from "@bff/infra/database/prisma.service";
|
||||
import { TokenBlacklistService } from "@bff/modules/auth/infra/token/token-blacklist.service";
|
||||
import { AuthTokenService } from "@bff/modules/auth/infra/token/token.service";
|
||||
import { AuthRateLimitService } from "@bff/modules/auth/infra/rate-limiting/auth-rate-limit.service";
|
||||
import { SignupWorkflowService } from "@bff/modules/auth/infra/workflows/signup-workflow.service";
|
||||
import { PasswordWorkflowService } from "@bff/modules/auth/infra/workflows/password-workflow.service";
|
||||
import { WhmcsLinkWorkflowService } from "@bff/modules/auth/infra/workflows/whmcs-link-workflow.service";
|
||||
import { TokenBlacklistService } from "../infra/token/token-blacklist.service";
|
||||
import { AuthTokenService } from "../infra/token/token.service";
|
||||
import { AuthRateLimitService } from "../infra/rate-limiting/auth-rate-limit.service";
|
||||
import { SignupWorkflowService } from "../infra/workflows/workflows/signup-workflow.service";
|
||||
import { PasswordWorkflowService } from "../infra/workflows/workflows/password-workflow.service";
|
||||
import { WhmcsLinkWorkflowService } from "../infra/workflows/workflows/whmcs-link-workflow.service";
|
||||
import { mapPrismaUserToUserProfile } from "@bff/infra/utils/user-mapper.util";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@ -1,132 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Param,
|
||||
UseGuards,
|
||||
Query,
|
||||
BadRequestException,
|
||||
UsePipes,
|
||||
} from "@nestjs/common";
|
||||
import { AdminGuard } from "./guards/admin.guard";
|
||||
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service";
|
||||
import { UsersService } from "@bff/modules/users/users.service";
|
||||
import { TokenMigrationService } from "@bff/modules/auth/infra/token/token-migration.service";
|
||||
import { ZodValidationPipe } from "@bff/core/validation";
|
||||
import {
|
||||
auditLogQuerySchema,
|
||||
dryRunQuerySchema,
|
||||
type AuditLogQuery,
|
||||
type DryRunQuery,
|
||||
} from "@customer-portal/domain";
|
||||
import { z } from "zod";
|
||||
|
||||
@UseGuards(AdminGuard)
|
||||
@Controller("auth/admin")
|
||||
export class AuthAdminController {
|
||||
constructor(
|
||||
private auditService: AuditService,
|
||||
private usersService: UsersService,
|
||||
private tokenMigrationService: TokenMigrationService
|
||||
) {}
|
||||
|
||||
@Get("audit-logs")
|
||||
@UsePipes(new ZodValidationPipe(auditLogQuerySchema))
|
||||
async getAuditLogs(@Query() query: AuditLogQuery) {
|
||||
const { logs, total } = await this.auditService.getAuditLogs({
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
action: query.action as AuditAction | undefined,
|
||||
userId: query.userId,
|
||||
});
|
||||
|
||||
return {
|
||||
logs,
|
||||
pagination: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / query.limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post("unlock-account/:userId")
|
||||
async unlockAccount(@Param("userId") userId: string) {
|
||||
const user = await this.usersService.findById(userId);
|
||||
if (!user) {
|
||||
throw new BadRequestException("User not found");
|
||||
}
|
||||
|
||||
await this.usersService.update(userId, {
|
||||
failedLoginAttempts: 0,
|
||||
lockedUntil: null,
|
||||
});
|
||||
|
||||
await this.auditService.log({
|
||||
userId,
|
||||
action: AuditAction.ACCOUNT_UNLOCKED,
|
||||
resource: "auth",
|
||||
details: { adminAction: true, email: user.email },
|
||||
success: true,
|
||||
});
|
||||
|
||||
return { message: "Account unlocked successfully" };
|
||||
}
|
||||
|
||||
@Get("security-stats")
|
||||
async getSecurityStats() {
|
||||
return this.auditService.getSecurityStats();
|
||||
}
|
||||
|
||||
@Get("token-migration/status")
|
||||
async getTokenMigrationStatus() {
|
||||
return this.tokenMigrationService.getMigrationStatus();
|
||||
}
|
||||
|
||||
@Post("token-migration/run")
|
||||
@UsePipes(new ZodValidationPipe(dryRunQuerySchema))
|
||||
async runTokenMigration(@Query() query: DryRunQuery) {
|
||||
const isDryRun = query.dryRun ?? true;
|
||||
const stats = await this.tokenMigrationService.migrateExistingTokens(isDryRun);
|
||||
|
||||
await this.auditService.log({
|
||||
action: AuditAction.SYSTEM_MAINTENANCE,
|
||||
resource: "auth",
|
||||
details: {
|
||||
operation: "token_migration",
|
||||
dryRun: isDryRun,
|
||||
stats,
|
||||
},
|
||||
success: true,
|
||||
});
|
||||
|
||||
return {
|
||||
message: isDryRun ? "Migration dry run completed" : "Migration completed",
|
||||
stats,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("token-migration/cleanup")
|
||||
@UsePipes(new ZodValidationPipe(dryRunQuerySchema))
|
||||
async cleanupOrphanedTokens(@Query() query: DryRunQuery) {
|
||||
const isDryRun = query.dryRun ?? true;
|
||||
const stats = await this.tokenMigrationService.cleanupOrphanedTokens(isDryRun);
|
||||
|
||||
await this.auditService.log({
|
||||
action: AuditAction.SYSTEM_MAINTENANCE,
|
||||
resource: "auth",
|
||||
details: {
|
||||
operation: "token_cleanup",
|
||||
dryRun: isDryRun,
|
||||
stats,
|
||||
},
|
||||
success: true,
|
||||
});
|
||||
|
||||
return {
|
||||
message: isDryRun ? "Cleanup dry run completed" : "Cleanup completed",
|
||||
stats,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,6 @@ import { ConfigService } from "@nestjs/config";
|
||||
import { APP_GUARD } from "@nestjs/core";
|
||||
import { AuthFacade } from "./application/auth.facade";
|
||||
import { AuthController } from "./presentation/http/auth.controller";
|
||||
import { AuthAdminController } from "./auth-admin.controller";
|
||||
import { UsersModule } from "@bff/modules/users/users.module";
|
||||
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module";
|
||||
import { IntegrationsModule } from "@bff/integrations/integrations.module";
|
||||
@ -15,10 +14,9 @@ import { GlobalAuthGuard } from "./presentation/http/guards/global-auth.guard";
|
||||
import { TokenBlacklistService } from "./infra/token/token-blacklist.service";
|
||||
import { EmailModule } from "@bff/infra/email/email.module";
|
||||
import { AuthTokenService } from "./infra/token/token.service";
|
||||
import { TokenMigrationService } from "./infra/token/token-migration.service";
|
||||
import { SignupWorkflowService } from "./infra/workflows/signup-workflow.service";
|
||||
import { PasswordWorkflowService } from "./infra/workflows/password-workflow.service";
|
||||
import { WhmcsLinkWorkflowService } from "./infra/workflows/whmcs-link-workflow.service";
|
||||
import { SignupWorkflowService } from "./infra/workflows/workflows/signup-workflow.service";
|
||||
import { PasswordWorkflowService } from "./infra/workflows/workflows/password-workflow.service";
|
||||
import { WhmcsLinkWorkflowService } from "./infra/workflows/workflows/whmcs-link-workflow.service";
|
||||
import { FailedLoginThrottleGuard } from "./presentation/http/guards/failed-login-throttle.guard";
|
||||
import { LoginResultInterceptor } from "./presentation/http/interceptors/login-result.interceptor";
|
||||
import { AuthRateLimitService } from "./infra/rate-limiting/auth-rate-limit.service";
|
||||
@ -38,14 +36,13 @@ import { AuthRateLimitService } from "./infra/rate-limiting/auth-rate-limit.serv
|
||||
IntegrationsModule,
|
||||
EmailModule,
|
||||
],
|
||||
controllers: [AuthController, AuthAdminController],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
AuthFacade,
|
||||
JwtStrategy,
|
||||
LocalStrategy,
|
||||
TokenBlacklistService,
|
||||
AuthTokenService,
|
||||
TokenMigrationService,
|
||||
SignupWorkflowService,
|
||||
PasswordWorkflowService,
|
||||
WhmcsLinkWorkflowService,
|
||||
@ -57,6 +54,6 @@ import { AuthRateLimitService } from "./infra/rate-limiting/auth-rate-limit.serv
|
||||
useClass: GlobalAuthGuard,
|
||||
},
|
||||
],
|
||||
exports: [AuthFacade, TokenBlacklistService, AuthTokenService, TokenMigrationService],
|
||||
exports: [AuthFacade, TokenBlacklistService, AuthTokenService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@ -4,7 +4,7 @@ import { JwtService } from "@nestjs/jwt";
|
||||
import { createHash } from "crypto";
|
||||
import { Redis } from "ioredis";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { parseJwtExpiry } from "../utils/jwt-expiry.util";
|
||||
import { parseJwtExpiry } from "../../utils/jwt-expiry.util";
|
||||
|
||||
@Injectable()
|
||||
export class TokenBlacklistService {
|
||||
|
||||
@ -8,8 +8,8 @@ import { UsersService } from "@bff/modules/users/users.service";
|
||||
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service";
|
||||
import { EmailService } from "@bff/infra/email/email.service";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import { AuthTokenService } from "../token.service";
|
||||
import { AuthRateLimitService } from "../auth-rate-limit.service";
|
||||
import { AuthTokenService } from "../../token/token.service";
|
||||
import { AuthRateLimitService } from "../../rate-limiting/auth-rate-limit.service";
|
||||
import { type AuthTokens, type UserProfile } from "@customer-portal/domain";
|
||||
import { mapPrismaUserToUserProfile } from "@bff/infra/utils/user-mapper.util";
|
||||
|
||||
|
||||
@ -15,8 +15,8 @@ import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
|
||||
import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service";
|
||||
import { PrismaService } from "@bff/infra/database/prisma.service";
|
||||
import { AuthTokenService } from "../token.service";
|
||||
import { AuthRateLimitService } from "../auth-rate-limit.service";
|
||||
import { AuthTokenService } from "../../token/token.service";
|
||||
import { AuthRateLimitService } from "../../rate-limiting/auth-rate-limit.service";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import {
|
||||
signupRequestSchema,
|
||||
|
||||
@ -17,7 +17,7 @@ import { LocalAuthGuard } from "./guards/local-auth.guard";
|
||||
import { AuthThrottleGuard } from "./guards/auth-throttle.guard";
|
||||
import { FailedLoginThrottleGuard } from "./guards/failed-login-throttle.guard";
|
||||
import { LoginResultInterceptor } from "./interceptors/login-result.interceptor";
|
||||
import { Public } from "./decorators/public.decorator";
|
||||
import { Public } from "../../decorators/public.decorator";
|
||||
import { ZodValidationPipe } from "@bff/core/validation";
|
||||
|
||||
// Import Zod schemas from domain
|
||||
@ -49,19 +49,35 @@ import type { AuthTokens } from "@customer-portal/domain";
|
||||
|
||||
type RequestWithCookies = Request & { cookies?: Record<string, string | undefined> };
|
||||
|
||||
const EXTRACT_BEARER = (req: RequestWithCookies): string | undefined => {
|
||||
const authHeader = req.headers?.authorization;
|
||||
if (typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.slice(7);
|
||||
const resolveAuthorizationHeader = (req: RequestWithCookies): string | undefined => {
|
||||
const rawHeader = req.headers?.authorization;
|
||||
|
||||
if (typeof rawHeader === "string") {
|
||||
return rawHeader;
|
||||
}
|
||||
if (Array.isArray(authHeader) && authHeader.length > 0 && authHeader[0]?.startsWith("Bearer ")) {
|
||||
return authHeader[0]?.slice(7);
|
||||
|
||||
if (Array.isArray(rawHeader)) {
|
||||
const headerValues: string[] = rawHeader;
|
||||
for (const candidate of headerValues) {
|
||||
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const extractBearerToken = (req: RequestWithCookies): string | undefined => {
|
||||
const authHeader = resolveAuthorizationHeader(req);
|
||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.slice(7);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const extractTokenFromRequest = (req: RequestWithCookies): string | undefined => {
|
||||
const headerToken = EXTRACT_BEARER(req);
|
||||
const headerToken = extractBearerToken(req);
|
||||
if (headerToken) {
|
||||
return headerToken;
|
||||
}
|
||||
@ -225,7 +241,7 @@ export class AuthController {
|
||||
@Post("request-password-reset")
|
||||
@Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 attempts per 15 minutes (standard for password operations)
|
||||
@UsePipes(new ZodValidationPipe(passwordResetRequestSchema))
|
||||
async requestPasswordReset(@Body() body: PasswordResetRequestInput) {
|
||||
async requestPasswordReset(@Body() body: PasswordResetRequestInput, @Req() req: Request) {
|
||||
await this.authFacade.requestPasswordReset(body.email, req);
|
||||
return { message: "If an account exists, a reset email has been sent" };
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { UserRole } from "@prisma/client";
|
||||
import type { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class AdminGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<Request & { user?: { role?: UserRole } }>();
|
||||
const user = request.user;
|
||||
|
||||
if (!user) {
|
||||
throw new ForbiddenException("Authentication required");
|
||||
}
|
||||
|
||||
if (user.role !== UserRole.ADMIN) {
|
||||
throw new ForbiddenException("Admin access required");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { Injectable, ExecutionContext } from "@nestjs/common";
|
||||
import type { Request } from "express";
|
||||
import { AuthRateLimitService } from "@bff/modules/auth/infra/rate-limiting/auth-rate-limit.service";
|
||||
import { AuthRateLimitService } from "../../../infra/rate-limiting/auth-rate-limit.service";
|
||||
|
||||
@Injectable()
|
||||
export class FailedLoginThrottleGuard {
|
||||
|
||||
@ -11,8 +11,8 @@ import { ExtractJwt } from "passport-jwt";
|
||||
|
||||
import type { Request } from "express";
|
||||
|
||||
import { TokenBlacklistService } from "@bff/modules/auth/infra/token/token-blacklist.service";
|
||||
import { IS_PUBLIC_KEY } from "@bff/modules/auth/decorators/public.decorator";
|
||||
import { TokenBlacklistService } from "../../../infra/token/token-blacklist.service";
|
||||
import { IS_PUBLIC_KEY } from "../../../decorators/public.decorator";
|
||||
|
||||
type RequestWithCookies = Request & { cookies?: Record<string, string | undefined> };
|
||||
|
||||
|
||||
@ -124,6 +124,10 @@ export class OrderFulfillmentOrchestrator {
|
||||
}
|
||||
|
||||
// Step 3: Execute the main fulfillment workflow as a distributed transaction
|
||||
let mappingResult: OrderItemMappingResult | undefined;
|
||||
let whmcsCreateResult: { orderId: number } | undefined;
|
||||
let whmcsAcceptResult: WhmcsOrderResult | undefined;
|
||||
|
||||
const fulfillmentResult =
|
||||
await this.distributedTransactionService.executeDistributedTransaction(
|
||||
[
|
||||
@ -146,14 +150,22 @@ export class OrderFulfillmentOrchestrator {
|
||||
},
|
||||
critical: true,
|
||||
},
|
||||
{
|
||||
id: "order_details",
|
||||
description: "Retain order details in context",
|
||||
execute: () => Promise.resolve(context.orderDetails),
|
||||
critical: false,
|
||||
},
|
||||
{
|
||||
id: "mapping",
|
||||
description: "Map OrderItems to WHMCS format",
|
||||
execute: async () => {
|
||||
execute: () => {
|
||||
if (!context.orderDetails) {
|
||||
throw new Error("Order details are required for mapping");
|
||||
return Promise.reject(new Error("Order details are required for mapping"));
|
||||
}
|
||||
return this.orderWhmcsMapper.mapOrderItemsToWhmcs(context.orderDetails.items);
|
||||
const result = this.orderWhmcsMapper.mapOrderItemsToWhmcs(context.orderDetails.items);
|
||||
mappingResult = result;
|
||||
return Promise.resolve(result);
|
||||
},
|
||||
critical: true,
|
||||
},
|
||||
@ -161,7 +173,9 @@ export class OrderFulfillmentOrchestrator {
|
||||
id: "whmcs_create",
|
||||
description: "Create order in WHMCS",
|
||||
execute: async () => {
|
||||
const mappingResult = fulfillmentResult.stepResults?.mapping;
|
||||
if (!context.validation) {
|
||||
throw new Error("Validation context is missing");
|
||||
}
|
||||
if (!mappingResult) {
|
||||
throw new Error("Mapping result is not available");
|
||||
}
|
||||
@ -171,8 +185,8 @@ export class OrderFulfillmentOrchestrator {
|
||||
`Provisioned from Salesforce Order ${sfOrderId}`
|
||||
);
|
||||
|
||||
return await this.whmcsOrderService.addOrder({
|
||||
clientId: context.validation!.clientId,
|
||||
const result = await this.whmcsOrderService.addOrder({
|
||||
clientId: context.validation.clientId,
|
||||
items: mappingResult.whmcsItems,
|
||||
paymentMethod: "stripe",
|
||||
promoCode: "1st Month Free (Monthly Plan)",
|
||||
@ -181,21 +195,24 @@ export class OrderFulfillmentOrchestrator {
|
||||
noinvoiceemail: true,
|
||||
noemail: true,
|
||||
});
|
||||
|
||||
whmcsCreateResult = result;
|
||||
return result;
|
||||
},
|
||||
rollback: async () => {
|
||||
const createResult = fulfillmentResult.stepResults?.whmcs_create;
|
||||
if (createResult?.orderId) {
|
||||
rollback: () => {
|
||||
if (whmcsCreateResult?.orderId) {
|
||||
// Note: WHMCS doesn't have an automated cancel API
|
||||
// Manual intervention required for order cleanup
|
||||
this.logger.error(
|
||||
"WHMCS order created but fulfillment failed - manual cleanup required",
|
||||
{
|
||||
orderId: createResult.orderId,
|
||||
orderId: whmcsCreateResult.orderId,
|
||||
sfOrderId,
|
||||
action: "MANUAL_CLEANUP_REQUIRED",
|
||||
}
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
critical: true,
|
||||
},
|
||||
@ -203,28 +220,33 @@ export class OrderFulfillmentOrchestrator {
|
||||
id: "whmcs_accept",
|
||||
description: "Accept/provision order in WHMCS",
|
||||
execute: async () => {
|
||||
const createResult = fulfillmentResult.stepResults?.whmcs_create;
|
||||
if (!createResult?.orderId) {
|
||||
if (!whmcsCreateResult?.orderId) {
|
||||
throw new Error("WHMCS order ID missing before acceptance step");
|
||||
}
|
||||
|
||||
return await this.whmcsOrderService.acceptOrder(createResult.orderId, sfOrderId);
|
||||
const result = await this.whmcsOrderService.acceptOrder(
|
||||
whmcsCreateResult.orderId,
|
||||
sfOrderId
|
||||
);
|
||||
|
||||
whmcsAcceptResult = result;
|
||||
return result;
|
||||
},
|
||||
rollback: async () => {
|
||||
const acceptResult = fulfillmentResult.stepResults?.whmcs_accept;
|
||||
if (acceptResult?.orderId) {
|
||||
rollback: () => {
|
||||
if (whmcsAcceptResult?.orderId) {
|
||||
// Note: WHMCS doesn't have an automated cancel API for accepted orders
|
||||
// Manual intervention required for service termination
|
||||
this.logger.error(
|
||||
"WHMCS order accepted but fulfillment failed - manual cleanup required",
|
||||
{
|
||||
orderId: acceptResult.orderId,
|
||||
serviceIds: acceptResult.serviceIds,
|
||||
orderId: whmcsAcceptResult.orderId,
|
||||
serviceIds: whmcsAcceptResult.serviceIds,
|
||||
sfOrderId,
|
||||
action: "MANUAL_SERVICE_TERMINATION_REQUIRED",
|
||||
}
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
critical: true,
|
||||
},
|
||||
@ -238,9 +260,9 @@ export class OrderFulfillmentOrchestrator {
|
||||
orderDetails: context.orderDetails,
|
||||
configurations,
|
||||
});
|
||||
return { completed: true };
|
||||
return { completed: true as const };
|
||||
}
|
||||
return { skipped: true };
|
||||
return { skipped: true as const };
|
||||
},
|
||||
critical: false, // SIM fulfillment failure shouldn't rollback the entire order
|
||||
},
|
||||
@ -249,13 +271,12 @@ export class OrderFulfillmentOrchestrator {
|
||||
description: "Update Salesforce with success",
|
||||
execute: async () => {
|
||||
const fields = this.fieldMapService.getFieldMap();
|
||||
const whmcsResult = fulfillmentResult.stepResults?.whmcs_accept;
|
||||
|
||||
return await this.salesforceService.updateOrder({
|
||||
Id: sfOrderId,
|
||||
Status: "Completed",
|
||||
[fields.order.activationStatus]: "Activated",
|
||||
[fields.order.whmcsOrderId]: whmcsResult?.orderId?.toString(),
|
||||
[fields.order.whmcsOrderId]: whmcsAcceptResult?.orderId?.toString(),
|
||||
});
|
||||
},
|
||||
rollback: async () => {
|
||||
@ -286,8 +307,8 @@ export class OrderFulfillmentOrchestrator {
|
||||
}
|
||||
|
||||
// Update context with results
|
||||
context.mappingResult = fulfillmentResult.stepResults?.mapping;
|
||||
context.whmcsResult = fulfillmentResult.stepResults?.whmcs_accept;
|
||||
context.mappingResult = mappingResult;
|
||||
context.whmcsResult = whmcsAcceptResult;
|
||||
|
||||
this.logger.log("Transactional fulfillment completed successfully", {
|
||||
sfOrderId,
|
||||
|
||||
@ -6,7 +6,6 @@ import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import type { SalesforceOrderRecord } from "@customer-portal/domain";
|
||||
import { SalesforceFieldMapService, type SalesforceFieldMap } from "@bff/core/config/field-map";
|
||||
import { orderFulfillmentValidationSchema } from "@customer-portal/domain";
|
||||
import { sfOrderIdParamSchema } from "@customer-portal/domain";
|
||||
type OrderStringFieldKey = "activationStatus";
|
||||
|
||||
@ -36,9 +35,11 @@ export class OrderFulfillmentValidator {
|
||||
* Validates SF order, gets client ID, checks payment method, checks idempotency
|
||||
*/
|
||||
async validateFulfillmentRequest(
|
||||
sfOrderId: string,
|
||||
rawSfOrderId: string,
|
||||
idempotencyKey: string
|
||||
): Promise<OrderFulfillmentValidationResult> {
|
||||
const { sfOrderId } = sfOrderIdParamSchema.parse({ sfOrderId: rawSfOrderId });
|
||||
|
||||
this.logger.log("Starting fulfillment validation", {
|
||||
sfOrderId,
|
||||
idempotencyKey,
|
||||
|
||||
40
apps/bff/src/types/rate-limiter-flexible.d.ts
vendored
Normal file
40
apps/bff/src/types/rate-limiter-flexible.d.ts
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
import type { Redis } from "ioredis";
|
||||
|
||||
declare module "rate-limiter-flexible" {
|
||||
export interface RateLimiterOptions {
|
||||
storeClient: Redis;
|
||||
points: number;
|
||||
duration: number;
|
||||
blockDuration?: number;
|
||||
keyPrefix?: string;
|
||||
inmemoryBlockOnConsumed?: number;
|
||||
insuranceLimiter?: RateLimiterMemory;
|
||||
}
|
||||
|
||||
export class RateLimiterRes {
|
||||
remainingPoints: number;
|
||||
consumedPoints: number;
|
||||
msBeforeNext: number;
|
||||
constructor(data: {
|
||||
remainingPoints: number;
|
||||
consumedPoints: number;
|
||||
msBeforeNext: number;
|
||||
});
|
||||
}
|
||||
|
||||
export class RateLimiterRedis {
|
||||
readonly points: number;
|
||||
constructor(options: RateLimiterOptions);
|
||||
consume(key: string, points?: number): Promise<RateLimiterRes>;
|
||||
delete(key: string): Promise<void>;
|
||||
penalty(key: string, points?: number): Promise<RateLimiterRes>;
|
||||
reward(key: string, points?: number): Promise<RateLimiterRes>;
|
||||
}
|
||||
|
||||
export class RateLimiterMemory {
|
||||
readonly points: number;
|
||||
constructor(options: RateLimiterOptions);
|
||||
consume(key: string, points?: number): Promise<RateLimiterRes>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import { CreditCardIcon, BanknotesIcon, DevicePhoneMobileIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
|
||||
import type { PaymentMethod } from "@customer-portal/domain";
|
||||
import { StatusPill } from "@/components/atoms/status-pill";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
@ -53,16 +52,15 @@ const isBankAccount = (type: PaymentMethod["type"]) =>
|
||||
type === "BankAccount" || type === "RemoteBankAccount";
|
||||
|
||||
const formatCardDisplay = (method: PaymentMethod) => {
|
||||
// Show ***** and last 4 digits for any payment method with lastFour
|
||||
if (method.lastFour) {
|
||||
return `***** ${method.lastFour}`;
|
||||
if (method.cardLastFour) {
|
||||
return `***** ${method.cardLastFour}`;
|
||||
}
|
||||
|
||||
|
||||
// Fallback based on type
|
||||
if (isCreditCard(method.type)) {
|
||||
return method.cardBrand ? `${method.cardBrand.toUpperCase()} Card` : "Credit Card";
|
||||
return method.cardType ? `${method.cardType.toUpperCase()} Card` : "Credit Card";
|
||||
}
|
||||
|
||||
|
||||
if (isBankAccount(method.type)) {
|
||||
return method.bankName || "Bank Account";
|
||||
}
|
||||
@ -71,10 +69,10 @@ const formatCardDisplay = (method: PaymentMethod) => {
|
||||
};
|
||||
|
||||
const formatCardBrand = (method: PaymentMethod) => {
|
||||
if (isCreditCard(method.type) && method.cardBrand) {
|
||||
return method.cardBrand.toUpperCase();
|
||||
if (isCreditCard(method.type) && method.cardType) {
|
||||
return method.cardType.toUpperCase();
|
||||
}
|
||||
|
||||
|
||||
if (isBankAccount(method.type) && method.bankName) {
|
||||
return method.bankName;
|
||||
}
|
||||
@ -96,7 +94,7 @@ export function PaymentMethodCard({
|
||||
const cardDisplay = formatCardDisplay(paymentMethod);
|
||||
const cardBrand = formatCardBrand(paymentMethod);
|
||||
const expiry = formatExpiry(paymentMethod.expiryDate);
|
||||
const icon = getMethodIcon(paymentMethod.type, paymentMethod.cardBrand ?? paymentMethod.ccType);
|
||||
const icon = getMethodIcon(paymentMethod.type, paymentMethod.cardType);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -66,18 +66,8 @@ const PaymentMethodCard = forwardRef<HTMLDivElement, PaymentMethodCardProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const {
|
||||
type,
|
||||
description,
|
||||
gatewayDisplayName,
|
||||
isDefault,
|
||||
lastFour,
|
||||
expiryDate,
|
||||
bankName,
|
||||
accountType,
|
||||
ccType,
|
||||
cardBrand,
|
||||
} = paymentMethod;
|
||||
const { type, description, gatewayName, isDefault, expiryDate, bankName, cardType, cardLastFour } =
|
||||
paymentMethod;
|
||||
|
||||
const formatExpiryDate = (expiry?: string) => {
|
||||
if (!expiry) return null;
|
||||
@ -92,13 +82,12 @@ const PaymentMethodCard = forwardRef<HTMLDivElement, PaymentMethodCardProps>(
|
||||
};
|
||||
|
||||
const renderPaymentMethodDetails = () => {
|
||||
if (type === "BankAccount") {
|
||||
if (type === "BankAccount" || type === "RemoteBankAccount") {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium text-gray-900">{bankName || "Bank Account"}</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{accountType && <span className="capitalize">{accountType}</span>}
|
||||
{lastFour && <span> •••• {lastFour}</span>}
|
||||
{cardLastFour && <span> •••• {cardLastFour}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -108,12 +97,12 @@ const PaymentMethodCard = forwardRef<HTMLDivElement, PaymentMethodCardProps>(
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium text-gray-900">
|
||||
{cardBrand || ccType || "Credit Card"}
|
||||
{lastFour && <span className="ml-2">•••• {lastFour}</span>}
|
||||
{cardType || "Credit Card"}
|
||||
{cardLastFour && <span className="ml-2">•••• {cardLastFour}</span>}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{formatExpiryDate(expiryDate) && <span>Expires {formatExpiryDate(expiryDate)}</span>}
|
||||
{gatewayDisplayName && <span className="ml-2">• {gatewayDisplayName}</span>}
|
||||
{gatewayName && <span className="ml-2">• {gatewayName}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -139,7 +128,7 @@ const PaymentMethodCard = forwardRef<HTMLDivElement, PaymentMethodCardProps>(
|
||||
"flex-shrink-0 rounded-lg flex items-center justify-center",
|
||||
compact ? "w-8 h-8 bg-gray-100" : "w-10 h-10 bg-gray-100",
|
||||
type === "CreditCard" || type === "RemoteCreditCard"
|
||||
? getCardBrandColor(cardBrand)
|
||||
? getCardBrandColor(cardType)
|
||||
: "text-gray-600"
|
||||
)}
|
||||
>
|
||||
@ -151,7 +140,7 @@ const PaymentMethodCard = forwardRef<HTMLDivElement, PaymentMethodCardProps>(
|
||||
{renderPaymentMethodDetails()}
|
||||
|
||||
{/* Description if different from generated details */}
|
||||
{description && !description.includes(lastFour || "") && (
|
||||
{description && !description.includes(cardLastFour || "") && (
|
||||
<div className="text-sm text-gray-500 mt-1 truncate">{description}</div>
|
||||
)}
|
||||
|
||||
@ -184,8 +173,8 @@ const PaymentMethodCard = forwardRef<HTMLDivElement, PaymentMethodCardProps>(
|
||||
</div>
|
||||
|
||||
{/* Gateway info for compact view */}
|
||||
{compact && gatewayDisplayName && (
|
||||
<div className="mt-2 text-xs text-gray-500">via {gatewayDisplayName}</div>
|
||||
{compact && gatewayName && (
|
||||
<div className="mt-2 text-xs text-gray-500">via {gatewayName}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { apiClient, queryKeys, getDataOrDefault, getDataOrThrow } from "@/lib/api";
|
||||
import type { InvoiceQueryParams } from "@/lib/api/types";
|
||||
import type { QueryParams } from "@/lib/api/runtime/client";
|
||||
import type {
|
||||
Invoice,
|
||||
InvoiceList,
|
||||
@ -85,10 +86,24 @@ type SsoLinkMutationOptions = UseMutationOptions<
|
||||
{ invoiceId: number; target?: "view" | "download" | "pay" }
|
||||
>;
|
||||
|
||||
const 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;
|
||||
};
|
||||
|
||||
async function fetchInvoices(params?: InvoiceQueryParams): Promise<InvoiceList> {
|
||||
const query = params ? toQueryParams(params) : undefined;
|
||||
const response = await apiClient.GET<InvoiceList>(
|
||||
"/api/invoices",
|
||||
params ? { params: { query: params as Record<string, unknown> } } : undefined
|
||||
query ? { params: { query } } : undefined
|
||||
);
|
||||
const data = getDataOrDefault<InvoiceList>(response, emptyInvoiceList);
|
||||
const parsed = invoiceListSchema.parse(data);
|
||||
@ -113,9 +128,9 @@ export function useInvoices(
|
||||
params?: InvoiceQueryParams,
|
||||
options?: InvoicesQueryOptions
|
||||
): UseQueryResult<InvoiceList, Error> {
|
||||
const queryParams = params ? (params as Record<string, unknown>) : {};
|
||||
const queryKeyParams = params ? { ...params } : undefined;
|
||||
return useQuery({
|
||||
queryKey: queryKeys.billing.invoices(queryParams),
|
||||
queryKey: queryKeys.billing.invoices(queryKeyParams),
|
||||
queryFn: () => fetchInvoices(params),
|
||||
...options,
|
||||
});
|
||||
|
||||
@ -90,8 +90,8 @@ export function PaymentForm({
|
||||
const renderMethod = (method: PaymentMethod) => {
|
||||
const methodId = String(method.id);
|
||||
const isSelected = selectedMethod === methodId;
|
||||
const label = method.cardBrand
|
||||
? `${method.cardBrand.toUpperCase()} ${method.lastFour ? `•••• ${method.lastFour}` : ""}`.trim()
|
||||
const label = method.cardType
|
||||
? `${method.cardType.toUpperCase()} ${method.cardLastFour ? `•••• ${method.cardLastFour}` : ""}`.trim()
|
||||
: (method.description ?? method.type);
|
||||
|
||||
return (
|
||||
|
||||
@ -13,11 +13,18 @@ export class ApiError extends Error {
|
||||
|
||||
export const isApiError = (error: unknown): error is ApiError => error instanceof ApiError;
|
||||
|
||||
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
export type HttpMethod =
|
||||
| "GET"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "PATCH"
|
||||
| "DELETE"
|
||||
| "HEAD"
|
||||
| "OPTIONS";
|
||||
|
||||
type PathParams = Record<string, string | number>;
|
||||
type QueryPrimitive = string | number | boolean;
|
||||
type QueryParams = Record<
|
||||
export type QueryPrimitive = string | number | boolean;
|
||||
export type QueryParams = Record<
|
||||
string,
|
||||
QueryPrimitive | QueryPrimitive[] | readonly QueryPrimitive[] | undefined
|
||||
>;
|
||||
@ -138,11 +145,11 @@ const buildQueryString = (query?: QueryParams): string => {
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(entry => appendPrimitive(key, entry));
|
||||
(value as readonly QueryPrimitive[]).forEach(entry => appendPrimitive(key, entry));
|
||||
continue;
|
||||
}
|
||||
|
||||
appendPrimitive(key, value);
|
||||
appendPrimitive(key, value as QueryPrimitive);
|
||||
}
|
||||
|
||||
return searchParams.toString();
|
||||
|
||||
@ -44,7 +44,6 @@ modules/auth/
|
||||
- `GlobalAuthGuard`: wraps Passport JWT, checks blacklist via `TokenBlacklistService`.
|
||||
- `AuthThrottleGuard`: Nest Throttler-based guard for general rate limits.
|
||||
- `FailedLoginThrottleGuard`: uses `AuthRateLimitService` for login attempt limiting.
|
||||
- `AdminGuard`: ensures authenticated user has admin role (uses Prisma enum).
|
||||
|
||||
### Interceptors
|
||||
- `LoginResultInterceptor`: ties into `FailedLoginThrottleGuard` to clear counters on success/failure.
|
||||
@ -65,7 +64,6 @@ modules/auth/
|
||||
### Token (`infra/token`)
|
||||
- `token.service.ts`: issues/rotates access + refresh tokens, enforces Redis-backed refresh token families.
|
||||
- `token-blacklist.service.ts`: stores revoked access tokens.
|
||||
- `token-migration.service.ts`: helpers for legacy flows.
|
||||
|
||||
### Rate Limiting (`infra/rate-limiting/auth-rate-limit.service.ts`)
|
||||
- Built on `rate-limiter-flexible` with Redis storage.
|
||||
|
||||
@ -98,35 +98,7 @@ This document summarizes the implementation of the Redis-required token flow wit
|
||||
|
||||
### 5. Migration Utilities for Existing Keys
|
||||
|
||||
**New Service:** `TokenMigrationService`
|
||||
|
||||
- Migrates existing refresh tokens to new per-user structure
|
||||
- Handles orphaned token cleanup
|
||||
- Provides migration statistics and status
|
||||
- Supports dry-run mode for safe testing
|
||||
|
||||
**Admin Endpoints Added:**
|
||||
|
||||
- `GET /auth/admin/token-migration/status`: Check migration status
|
||||
- `POST /auth/admin/token-migration/run?dryRun=true`: Run migration
|
||||
- `POST /auth/admin/token-migration/cleanup?dryRun=true`: Cleanup orphaned tokens
|
||||
|
||||
**Migration Features:**
|
||||
|
||||
- Scans existing refresh token families and tokens
|
||||
- Creates per-user token sets for existing tokens
|
||||
- Identifies and removes orphaned tokens
|
||||
- Comprehensive logging and error handling
|
||||
- Audit trail for all migration operations
|
||||
|
||||
**Files Created:**
|
||||
|
||||
- `apps/bff/src/modules/auth/services/token-migration.service.ts`
|
||||
|
||||
**Files Modified:**
|
||||
|
||||
- `apps/bff/src/modules/auth/auth.module.ts`
|
||||
- `apps/bff/src/modules/auth/auth-admin.controller.ts`
|
||||
Legacy helpers (`token-migration.service.ts`) have been removed along with the admin-only migration endpoints.
|
||||
|
||||
## 🚀 Deployment Instructions
|
||||
|
||||
@ -158,28 +130,7 @@ SF_QUEUE_LONG_RUNNING_TIMEOUT_MS=600000
|
||||
|
||||
### 2. Migration Process
|
||||
|
||||
1. **Check Migration Status:**
|
||||
|
||||
```bash
|
||||
GET /auth/admin/token-migration/status
|
||||
```
|
||||
|
||||
2. **Run Dry-Run Migration:**
|
||||
|
||||
```bash
|
||||
POST /auth/admin/token-migration/run?dryRun=true
|
||||
```
|
||||
|
||||
3. **Execute Actual Migration:**
|
||||
|
||||
```bash
|
||||
POST /auth/admin/token-migration/run?dryRun=false
|
||||
```
|
||||
|
||||
4. **Cleanup Orphaned Tokens:**
|
||||
```bash
|
||||
POST /auth/admin/token-migration/cleanup?dryRun=false
|
||||
```
|
||||
Legacy admin migration endpoints were removed. If migration is needed in the future, plan a manual script or one-off job.
|
||||
|
||||
### 3. Feature Flag Rollout
|
||||
|
||||
@ -263,7 +214,6 @@ SF_QUEUE_INTERVAL_CAP=800
|
||||
- [ ] Token migration dry-run and execution
|
||||
- [ ] Per-user token limit enforcement
|
||||
- [ ] Orphaned token cleanup
|
||||
- [ ] Admin endpoint security (admin-only access)
|
||||
|
||||
## 🔄 Rollback Plan
|
||||
|
||||
|
||||
@ -5,17 +5,17 @@ export interface PaymentMethod extends WhmcsEntity {
|
||||
type: "CreditCard" | "BankAccount" | "RemoteCreditCard" | "RemoteBankAccount" | "Manual";
|
||||
description: string;
|
||||
gatewayName?: string;
|
||||
isDefault?: boolean;
|
||||
lastFour?: string;
|
||||
contactType?: string;
|
||||
contactId?: number;
|
||||
cardLastFour?: string;
|
||||
expiryDate?: string;
|
||||
bankName?: string;
|
||||
accountType?: string;
|
||||
startDate?: string;
|
||||
issueNumber?: string;
|
||||
cardType?: string;
|
||||
remoteToken?: string;
|
||||
ccType?: string;
|
||||
cardBrand?: string;
|
||||
billingContactId?: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
lastUpdated?: string;
|
||||
bankName?: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface PaymentMethodList {
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
requiredAddressSchema,
|
||||
genderEnum,
|
||||
} from "../shared/primitives";
|
||||
import { invoiceStatusSchema } from "../shared/entities";
|
||||
|
||||
const invoiceStatusEnum = z.enum(["Paid", "Unpaid", "Overdue", "Cancelled", "Collections"]);
|
||||
const subscriptionStatusEnum = z.enum([
|
||||
@ -269,7 +270,7 @@ export const invoiceItemSchema = z.object({
|
||||
export const invoiceSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
number: z.string().min(1, "Invoice number is required"),
|
||||
status: z.string().min(1, "Status is required"),
|
||||
status: invoiceStatusSchema,
|
||||
currency: z.string().length(3, "Currency must be 3 characters"),
|
||||
currencySymbol: z.string().optional(),
|
||||
total: z.number().nonnegative("Total must be non-negative"),
|
||||
|
||||
@ -55,15 +55,15 @@ const paymentMethodTypeSchema = z.enum([
|
||||
"Manual",
|
||||
]);
|
||||
|
||||
const orderStatusSchema = z.enum(["Pending", "Active", "Cancelled", "Fraud"]);
|
||||
const invoiceStatusSchema = z.enum(tupleFromEnum(INVOICE_STATUS));
|
||||
const subscriptionStatusSchema = z.enum(tupleFromEnum(SUBSCRIPTION_STATUS));
|
||||
const caseStatusSchema = z.enum(tupleFromEnum(CASE_STATUS));
|
||||
const casePrioritySchema = z.enum(tupleFromEnum(CASE_PRIORITY));
|
||||
const paymentStatusSchema = z.enum(tupleFromEnum(PAYMENT_STATUS));
|
||||
const caseTypeSchema = z.enum(["Question", "Problem", "Feature Request"]);
|
||||
export const orderStatusSchema = z.enum(["Pending", "Active", "Cancelled", "Fraud"]);
|
||||
export const invoiceStatusSchema = z.enum(tupleFromEnum(INVOICE_STATUS));
|
||||
export const subscriptionStatusSchema = z.enum(tupleFromEnum(SUBSCRIPTION_STATUS));
|
||||
export const caseStatusSchema = z.enum(tupleFromEnum(CASE_STATUS));
|
||||
export const casePrioritySchema = z.enum(tupleFromEnum(CASE_PRIORITY));
|
||||
export const paymentStatusSchema = z.enum(tupleFromEnum(PAYMENT_STATUS));
|
||||
export const caseTypeSchema = z.enum(["Question", "Problem", "Feature Request"]);
|
||||
|
||||
const subscriptionCycleSchema = z.enum([
|
||||
export const subscriptionCycleSchema = z.enum([
|
||||
"Monthly",
|
||||
"Quarterly",
|
||||
"Semi-Annually",
|
||||
@ -220,18 +220,17 @@ export const paymentMethodSchema = whmcsEntitySchema.extend({
|
||||
type: paymentMethodTypeSchema,
|
||||
description: z.string().min(1, "Payment method description is required"),
|
||||
gatewayName: z.string().optional(),
|
||||
gatewayDisplayName: z.string().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
lastFour: z.string().length(4, "Last four must be exactly 4 digits").optional(),
|
||||
contactType: z.string().optional(),
|
||||
contactId: z.number().int().positive().optional(),
|
||||
cardLastFour: z.string().length(4, "Last four must be exactly 4 digits").optional(),
|
||||
expiryDate: z.string().optional(),
|
||||
bankName: z.string().optional(),
|
||||
accountType: z.string().optional(),
|
||||
startDate: z.string().optional(),
|
||||
issueNumber: z.string().optional(),
|
||||
cardType: z.string().optional(),
|
||||
remoteToken: z.string().optional(),
|
||||
ccType: z.string().optional(),
|
||||
cardBrand: z.string().optional(),
|
||||
billingContactId: z.number().int().positive().optional(),
|
||||
createdAt: z.string().optional(),
|
||||
updatedAt: z.string().optional(),
|
||||
lastUpdated: z.string().optional(),
|
||||
bankName: z.string().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const paymentSchema = z.object({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user