refactor: enhance account status handling and error messaging in auth guards
- Introduced a new `AccountStatusResult` interface to standardize account status detection across systems. - Updated the `VerificationWorkflowService` to merge handoff data with discovered account status. - Enhanced error handling in `GlobalAuthGuard` and `LocalAuthGuard` to include structured error codes for better clarity in unauthorized responses. - Refined WHMCS and Salesforce integration schemas to ensure consistent data validation and coercion.
This commit is contained in:
parent
29b511e44c
commit
230a61c520
@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
**Use dedicated tools, not Bash** — Read (not `cat`/`head`/`tail`), Glob (not `find`/`ls`), Grep (not `grep`/`rg`), Edit (not `sed`/`awk`). This applies to all agents and subagents.
|
**Use dedicated tools, not Bash** — Read (not `cat`/`head`/`tail`), Glob (not `find`/`ls`), Grep (not `grep`/`rg`), Edit (not `sed`/`awk`). This applies to all agents and subagents.
|
||||||
|
|
||||||
|
**Never run destructive git commands** — `git stash`, `git checkout -- .`, `git restore .`, `git reset --hard`, `git clean`, or any command that discards uncommitted work. If you need to check pre-existing state, use `git diff` or `git status` — never stash or discard the working tree.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -21,6 +21,20 @@ import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
|||||||
import { OtpService } from "../otp/otp.service.js";
|
import { OtpService } from "../otp/otp.service.js";
|
||||||
import { GetStartedSessionService } from "../otp/get-started-session.service.js";
|
import { GetStartedSessionService } from "../otp/get-started-session.service.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of account status detection across Portal, WHMCS, and Salesforce.
|
||||||
|
* Optional fields are populated based on the detected status:
|
||||||
|
* - WHMCS_UNMAPPED: whmcsClientId, whmcsFirstName, whmcsLastName
|
||||||
|
* - SF_UNMAPPED: sfAccountId
|
||||||
|
*/
|
||||||
|
interface AccountStatusResult {
|
||||||
|
status: AccountStatus;
|
||||||
|
sfAccountId?: string;
|
||||||
|
whmcsClientId?: number;
|
||||||
|
whmcsFirstName?: string;
|
||||||
|
whmcsLastName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verification Workflow Service
|
* Verification Workflow Service
|
||||||
*
|
*
|
||||||
@ -98,10 +112,20 @@ export class VerificationWorkflowService {
|
|||||||
const sessionToken = await this.sessionService.create(normalizedEmail);
|
const sessionToken = await this.sessionService.create(normalizedEmail);
|
||||||
|
|
||||||
// Check account status across all systems
|
// Check account status across all systems
|
||||||
const accountStatus = await this.determineAccountStatus(normalizedEmail);
|
const discoveredStatus = await this.determineAccountStatus(normalizedEmail);
|
||||||
|
|
||||||
// Get prefill data (including handoff token data if provided)
|
// Get prefill data (including handoff token data if provided)
|
||||||
const prefill = await this.resolvePrefillData(normalizedEmail, accountStatus, handoffToken);
|
const { prefill, sfAccountId: handoffSfAccountId } = await this.resolvePrefillData(
|
||||||
|
normalizedEmail,
|
||||||
|
discoveredStatus,
|
||||||
|
handoffToken
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge handoff SF account ID if discovery didn't find one
|
||||||
|
const accountStatus: AccountStatusResult =
|
||||||
|
!discoveredStatus.sfAccountId && handoffSfAccountId
|
||||||
|
? { ...discoveredStatus, sfAccountId: handoffSfAccountId }
|
||||||
|
: discoveredStatus;
|
||||||
|
|
||||||
// Update session with verified status and account info
|
// Update session with verified status and account info
|
||||||
const prefillData = this.buildSessionPrefillData(prefill, accountStatus);
|
const prefillData = this.buildSessionPrefillData(prefill, accountStatus);
|
||||||
@ -177,13 +201,7 @@ export class VerificationWorkflowService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async determineAccountStatus(email: string): Promise<{
|
private async determineAccountStatus(email: string): Promise<AccountStatusResult> {
|
||||||
status: AccountStatus;
|
|
||||||
sfAccountId?: string;
|
|
||||||
whmcsClientId?: number;
|
|
||||||
whmcsFirstName?: string;
|
|
||||||
whmcsLastName?: string;
|
|
||||||
}> {
|
|
||||||
// Check Portal user first
|
// Check Portal user first
|
||||||
const portalUser = await this.usersService.findByEmailInternal(email);
|
const portalUser = await this.usersService.findByEmailInternal(email);
|
||||||
if (portalUser) {
|
if (portalUser) {
|
||||||
@ -223,12 +241,7 @@ export class VerificationWorkflowService {
|
|||||||
|
|
||||||
private getPrefillData(
|
private getPrefillData(
|
||||||
email: string,
|
email: string,
|
||||||
accountStatus: {
|
accountStatus: AccountStatusResult
|
||||||
status: AccountStatus;
|
|
||||||
sfAccountId?: string;
|
|
||||||
whmcsFirstName?: string;
|
|
||||||
whmcsLastName?: string;
|
|
||||||
}
|
|
||||||
): VerifyCodeResponse["prefill"] {
|
): VerifyCodeResponse["prefill"] {
|
||||||
if (accountStatus.status === ACCOUNT_STATUS.WHMCS_UNMAPPED) {
|
if (accountStatus.status === ACCOUNT_STATUS.WHMCS_UNMAPPED) {
|
||||||
return {
|
return {
|
||||||
@ -245,24 +258,24 @@ export class VerificationWorkflowService {
|
|||||||
|
|
||||||
private async resolvePrefillData(
|
private async resolvePrefillData(
|
||||||
email: string,
|
email: string,
|
||||||
accountStatus: { status: AccountStatus; sfAccountId?: string; whmcsClientId?: number },
|
accountStatus: AccountStatusResult,
|
||||||
handoffToken?: string
|
handoffToken?: string
|
||||||
): Promise<VerifyCodeResponse["prefill"]> {
|
): Promise<{ prefill: VerifyCodeResponse["prefill"]; sfAccountId?: string }> {
|
||||||
let prefill = this.getPrefillData(email, accountStatus);
|
const prefill = this.getPrefillData(email, accountStatus);
|
||||||
|
|
||||||
if (!handoffToken) {
|
if (!handoffToken) {
|
||||||
return prefill;
|
return { prefill };
|
||||||
}
|
}
|
||||||
|
|
||||||
const handoffData = await this.sessionService.getGuestHandoffToken(handoffToken, email);
|
const handoffData = await this.sessionService.getGuestHandoffToken(handoffToken, email);
|
||||||
|
|
||||||
if (!handoffData) {
|
if (!handoffData) {
|
||||||
return prefill;
|
return { prefill };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug({ email, handoffToken }, "Applying handoff token data to session");
|
this.logger.debug({ email, handoffToken }, "Applying handoff token data to session");
|
||||||
|
|
||||||
prefill = {
|
const mergedPrefill = {
|
||||||
...prefill,
|
...prefill,
|
||||||
firstName: handoffData.firstName,
|
firstName: handoffData.firstName,
|
||||||
lastName: handoffData.lastName,
|
lastName: handoffData.lastName,
|
||||||
@ -270,18 +283,14 @@ export class VerificationWorkflowService {
|
|||||||
address: handoffData.address,
|
address: handoffData.address,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!accountStatus.sfAccountId && handoffData.sfAccountId) {
|
|
||||||
accountStatus.sfAccountId = handoffData.sfAccountId;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sessionService.invalidateHandoffToken(handoffToken);
|
await this.sessionService.invalidateHandoffToken(handoffToken);
|
||||||
|
|
||||||
return prefill;
|
return { prefill: mergedPrefill, sfAccountId: handoffData.sfAccountId };
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildSessionPrefillData(
|
private buildSessionPrefillData(
|
||||||
prefill: VerifyCodeResponse["prefill"],
|
prefill: VerifyCodeResponse["prefill"],
|
||||||
accountStatus: { status: AccountStatus; sfAccountId?: string; whmcsClientId?: number }
|
accountStatus: AccountStatusResult
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
...(prefill?.firstName && { firstName: prefill.firstName }),
|
...(prefill?.firstName && { firstName: prefill.firstName }),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Injectable, Inject, UnauthorizedException } from "@nestjs/common";
|
|||||||
import type { CanActivate, ExecutionContext } from "@nestjs/common";
|
import type { CanActivate, ExecutionContext } from "@nestjs/common";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import { ErrorCode } from "@customer-portal/domain/common";
|
||||||
|
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
|
|
||||||
@ -94,7 +95,10 @@ export class GlobalAuthGuard implements CanActivate {
|
|||||||
if (isLogoutRoute) {
|
if (isLogoutRoute) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
throw new UnauthorizedException("Missing token");
|
throw new UnauthorizedException({
|
||||||
|
message: "Missing token",
|
||||||
|
code: ErrorCode.TOKEN_INVALID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.attachUserFromToken(request, token, route);
|
await this.attachUserFromToken(request, token, route);
|
||||||
@ -119,7 +123,10 @@ export class GlobalAuthGuard implements CanActivate {
|
|||||||
const rawRequest = context.switchToHttp().getRequest<unknown>();
|
const rawRequest = context.switchToHttp().getRequest<unknown>();
|
||||||
if (!this.isRequestWithRoute(rawRequest)) {
|
if (!this.isRequestWithRoute(rawRequest)) {
|
||||||
this.logger.error("Unable to determine HTTP request in auth guard");
|
this.logger.error("Unable to determine HTTP request in auth guard");
|
||||||
throw new UnauthorizedException("Invalid request context");
|
throw new UnauthorizedException({
|
||||||
|
message: "Invalid request context",
|
||||||
|
code: ErrorCode.TOKEN_INVALID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return rawRequest;
|
return rawRequest;
|
||||||
}
|
}
|
||||||
@ -174,20 +181,32 @@ export class GlobalAuthGuard implements CanActivate {
|
|||||||
|
|
||||||
const tokenType = (payload as { type?: unknown }).type;
|
const tokenType = (payload as { type?: unknown }).type;
|
||||||
if (typeof tokenType === "string" && tokenType !== "access") {
|
if (typeof tokenType === "string" && tokenType !== "access") {
|
||||||
throw new UnauthorizedException("Invalid access token");
|
throw new UnauthorizedException({
|
||||||
|
message: "Invalid access token",
|
||||||
|
code: ErrorCode.TOKEN_INVALID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!payload.sub || !payload.email) {
|
if (!payload.sub || !payload.email) {
|
||||||
throw new UnauthorizedException("Invalid token payload");
|
throw new UnauthorizedException({
|
||||||
|
message: "Invalid token payload",
|
||||||
|
code: ErrorCode.TOKEN_INVALID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit expiry buffer check to avoid tokens expiring mid-request
|
// Explicit expiry buffer check to avoid tokens expiring mid-request
|
||||||
if (typeof payload.exp !== "number") {
|
if (typeof payload.exp !== "number") {
|
||||||
throw new UnauthorizedException("Token missing expiration claim");
|
throw new UnauthorizedException({
|
||||||
|
message: "Token missing expiration claim",
|
||||||
|
code: ErrorCode.TOKEN_INVALID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||||
if (payload.exp < nowSeconds + 60) {
|
if (payload.exp < nowSeconds + 60) {
|
||||||
throw new UnauthorizedException("Token expired or expiring soon");
|
throw new UnauthorizedException({
|
||||||
|
message: "Token expired or expiring soon",
|
||||||
|
code: ErrorCode.SESSION_EXPIRED,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check token blacklist
|
// Then check token blacklist
|
||||||
@ -196,15 +215,21 @@ export class GlobalAuthGuard implements CanActivate {
|
|||||||
if (route) {
|
if (route) {
|
||||||
this.logger.warn(`Blacklisted token attempted access to: ${route}`);
|
this.logger.warn(`Blacklisted token attempted access to: ${route}`);
|
||||||
}
|
}
|
||||||
throw new UnauthorizedException("Token has been revoked");
|
throw new UnauthorizedException({
|
||||||
|
message: "Token has been revoked",
|
||||||
|
code: ErrorCode.TOKEN_REVOKED,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const prismaUser = await this.usersService.findByIdInternal(payload.sub);
|
const prismaUser = await this.usersService.findByIdInternal(payload.sub);
|
||||||
if (!prismaUser) {
|
if (!prismaUser) {
|
||||||
throw new UnauthorizedException("User not found");
|
throw new UnauthorizedException({ message: "User not found", code: ErrorCode.TOKEN_INVALID });
|
||||||
}
|
}
|
||||||
if (prismaUser.email !== payload.email) {
|
if (prismaUser.email !== payload.email) {
|
||||||
throw new UnauthorizedException("Token subject does not match user record");
|
throw new UnauthorizedException({
|
||||||
|
message: "Token subject does not match user record",
|
||||||
|
code: ErrorCode.TOKEN_INVALID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile: UserAuth = mapPrismaUserToDomain(prismaUser);
|
const profile: UserAuth = mapPrismaUserToDomain(prismaUser);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Injectable, UnauthorizedException } from "@nestjs/common";
|
|||||||
import type { CanActivate, ExecutionContext } from "@nestjs/common";
|
import type { CanActivate, ExecutionContext } from "@nestjs/common";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import { AuthOrchestrator } from "@bff/modules/auth/application/auth-orchestrator.service.js";
|
import { AuthOrchestrator } from "@bff/modules/auth/application/auth-orchestrator.service.js";
|
||||||
|
import { ErrorCode } from "@customer-portal/domain/common";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalAuthGuard implements CanActivate {
|
export class LocalAuthGuard implements CanActivate {
|
||||||
@ -15,12 +16,18 @@ export class LocalAuthGuard implements CanActivate {
|
|||||||
const password = typeof body["password"] === "string" ? body["password"] : "";
|
const password = typeof body["password"] === "string" ? body["password"] : "";
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
throw new UnauthorizedException("Invalid credentials");
|
throw new UnauthorizedException({
|
||||||
|
message: "Invalid credentials",
|
||||||
|
code: ErrorCode.INVALID_CREDENTIALS,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.authOrchestrator.validateUser(email, password, request);
|
const user = await this.authOrchestrator.validateUser(email, password, request);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException("Invalid credentials");
|
throw new UnauthorizedException({
|
||||||
|
message: "Invalid credentials",
|
||||||
|
code: ErrorCode.INVALID_CREDENTIALS,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach user to request (replaces Passport's req.user behavior)
|
// Attach user to request (replaces Passport's req.user behavior)
|
||||||
|
|||||||
@ -54,12 +54,12 @@ function mapItems(rawItems: unknown): InvoiceItem[] {
|
|||||||
const itemArray = Array.isArray(parsed.item) ? parsed.item : [parsed.item];
|
const itemArray = Array.isArray(parsed.item) ? parsed.item : [parsed.item];
|
||||||
|
|
||||||
return itemArray.map(item => ({
|
return itemArray.map(item => ({
|
||||||
id: Number(item.id),
|
id: item.id,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
amount: parseAmount(item.amount),
|
amount: parseAmount(item.amount),
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
serviceId: Number(item.relid) > 0 ? Number(item.relid) : undefined,
|
serviceId: item.relid > 0 ? item.relid : undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ export function transformWhmcsInvoice(
|
|||||||
|
|
||||||
// Transform to domain model
|
// Transform to domain model
|
||||||
const invoice: Invoice = {
|
const invoice: Invoice = {
|
||||||
id: Number(whmcsInvoice.invoiceid ?? 0),
|
id: whmcsInvoice.invoiceid ?? 0,
|
||||||
number: whmcsInvoice.invoicenum || `INV-${whmcsInvoice.invoiceid}`,
|
number: whmcsInvoice.invoicenum || `INV-${whmcsInvoice.invoiceid}`,
|
||||||
status: mapStatus(whmcsInvoice.status),
|
status: mapStatus(whmcsInvoice.status),
|
||||||
currency,
|
currency,
|
||||||
|
|||||||
@ -11,7 +11,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
whmcsString as s,
|
whmcsString as s,
|
||||||
whmcsNumberLike as numberLike,
|
whmcsRequiredNumber,
|
||||||
|
whmcsOptionalNumber,
|
||||||
} from "../../../common/providers/whmcs-utils/index.js";
|
} from "../../../common/providers/whmcs-utils/index.js";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -106,12 +107,12 @@ export interface WhmcsCapturePaymentParams {
|
|||||||
|
|
||||||
// Raw WHMCS Invoice Item
|
// Raw WHMCS Invoice Item
|
||||||
export const whmcsInvoiceItemRawSchema = z.object({
|
export const whmcsInvoiceItemRawSchema = z.object({
|
||||||
id: numberLike,
|
id: whmcsRequiredNumber,
|
||||||
type: s,
|
type: s,
|
||||||
relid: numberLike,
|
relid: whmcsRequiredNumber,
|
||||||
description: s,
|
description: s,
|
||||||
amount: numberLike,
|
amount: s,
|
||||||
taxed: numberLike.optional(),
|
taxed: s.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type WhmcsInvoiceItemRaw = z.infer<typeof whmcsInvoiceItemRawSchema>;
|
export type WhmcsInvoiceItemRaw = z.infer<typeof whmcsInvoiceItemRawSchema>;
|
||||||
@ -126,7 +127,7 @@ export type WhmcsInvoiceItemsRaw = z.infer<typeof whmcsInvoiceItemsRawSchema>;
|
|||||||
const whmcsInvoiceCommonSchema = z
|
const whmcsInvoiceCommonSchema = z
|
||||||
.object({
|
.object({
|
||||||
invoicenum: s.optional(),
|
invoicenum: s.optional(),
|
||||||
userid: numberLike,
|
userid: whmcsRequiredNumber,
|
||||||
date: s,
|
date: s,
|
||||||
duedate: s,
|
duedate: s,
|
||||||
subtotal: s,
|
subtotal: s,
|
||||||
@ -141,7 +142,7 @@ const whmcsInvoiceCommonSchema = z
|
|||||||
ccgateway: z.boolean().optional(),
|
ccgateway: z.boolean().optional(),
|
||||||
items: whmcsInvoiceItemsRawSchema.optional(),
|
items: whmcsInvoiceItemsRawSchema.optional(),
|
||||||
transactions: z.unknown().optional(),
|
transactions: z.unknown().optional(),
|
||||||
clientid: numberLike.optional(),
|
clientid: whmcsOptionalNumber,
|
||||||
datecreated: s.optional(),
|
datecreated: s.optional(),
|
||||||
paymentmethodname: s.optional(),
|
paymentmethodname: s.optional(),
|
||||||
currencyprefix: s.optional(),
|
currencyprefix: s.optional(),
|
||||||
@ -163,14 +164,14 @@ const whmcsInvoiceCommonSchema = z
|
|||||||
.strip();
|
.strip();
|
||||||
|
|
||||||
export const whmcsInvoiceListItemSchema = whmcsInvoiceCommonSchema.extend({
|
export const whmcsInvoiceListItemSchema = whmcsInvoiceCommonSchema.extend({
|
||||||
id: numberLike,
|
id: whmcsRequiredNumber,
|
||||||
invoiceid: numberLike.optional(),
|
invoiceid: whmcsOptionalNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Raw WHMCS Invoice (detailed GetInvoice response)
|
// Raw WHMCS Invoice (detailed GetInvoice response)
|
||||||
export const whmcsInvoiceRawSchema = whmcsInvoiceCommonSchema.extend({
|
export const whmcsInvoiceRawSchema = whmcsInvoiceCommonSchema.extend({
|
||||||
invoiceid: numberLike,
|
invoiceid: whmcsRequiredNumber,
|
||||||
id: numberLike.optional(),
|
id: whmcsOptionalNumber,
|
||||||
balance: s.optional(),
|
balance: s.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -188,9 +189,9 @@ export const whmcsInvoiceListResponseSchema = z.object({
|
|||||||
invoices: z.object({
|
invoices: z.object({
|
||||||
invoice: z.array(whmcsInvoiceListItemSchema),
|
invoice: z.array(whmcsInvoiceListItemSchema),
|
||||||
}),
|
}),
|
||||||
totalresults: numberLike,
|
totalresults: whmcsRequiredNumber,
|
||||||
numreturned: numberLike,
|
numreturned: whmcsRequiredNumber,
|
||||||
startnumber: numberLike,
|
startnumber: whmcsRequiredNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type WhmcsInvoiceListResponse = z.infer<typeof whmcsInvoiceListResponseSchema>;
|
export type WhmcsInvoiceListResponse = z.infer<typeof whmcsInvoiceListResponseSchema>;
|
||||||
@ -218,7 +219,7 @@ export type WhmcsInvoiceResponse = z.infer<typeof whmcsInvoiceResponseSchema>;
|
|||||||
*/
|
*/
|
||||||
export const whmcsCreateInvoiceResponseSchema = z.object({
|
export const whmcsCreateInvoiceResponseSchema = z.object({
|
||||||
result: z.enum(["success", "error"]),
|
result: z.enum(["success", "error"]),
|
||||||
invoiceid: numberLike,
|
invoiceid: whmcsRequiredNumber,
|
||||||
status: s,
|
status: s,
|
||||||
message: s.optional(),
|
message: s.optional(),
|
||||||
});
|
});
|
||||||
@ -234,7 +235,7 @@ export type WhmcsCreateInvoiceResponse = z.infer<typeof whmcsCreateInvoiceRespon
|
|||||||
*/
|
*/
|
||||||
export const whmcsUpdateInvoiceResponseSchema = z.object({
|
export const whmcsUpdateInvoiceResponseSchema = z.object({
|
||||||
result: z.enum(["success", "error"]),
|
result: z.enum(["success", "error"]),
|
||||||
invoiceid: numberLike,
|
invoiceid: whmcsRequiredNumber,
|
||||||
status: s,
|
status: s,
|
||||||
message: s.optional(),
|
message: s.optional(),
|
||||||
});
|
});
|
||||||
@ -250,11 +251,11 @@ export type WhmcsUpdateInvoiceResponse = z.infer<typeof whmcsUpdateInvoiceRespon
|
|||||||
*/
|
*/
|
||||||
export const whmcsCapturePaymentResponseSchema = z.object({
|
export const whmcsCapturePaymentResponseSchema = z.object({
|
||||||
result: z.enum(["success", "error"]),
|
result: z.enum(["success", "error"]),
|
||||||
invoiceid: numberLike,
|
invoiceid: whmcsRequiredNumber,
|
||||||
status: s,
|
status: s,
|
||||||
transactionid: s.optional(),
|
transactionid: s.optional(),
|
||||||
amount: numberLike.optional(),
|
amount: s.optional(),
|
||||||
fees: numberLike.optional(),
|
fees: s.optional(),
|
||||||
message: s.optional(),
|
message: s.optional(),
|
||||||
error: s.optional(),
|
error: s.optional(),
|
||||||
});
|
});
|
||||||
@ -269,7 +270,7 @@ export type WhmcsCapturePaymentResponse = z.infer<typeof whmcsCapturePaymentResp
|
|||||||
* WHMCS Currency schema
|
* WHMCS Currency schema
|
||||||
*/
|
*/
|
||||||
export const whmcsCurrencySchema = z.object({
|
export const whmcsCurrencySchema = z.object({
|
||||||
id: numberLike,
|
id: whmcsRequiredNumber,
|
||||||
code: s,
|
code: s,
|
||||||
prefix: s,
|
prefix: s,
|
||||||
suffix: s,
|
suffix: s,
|
||||||
|
|||||||
@ -54,3 +54,39 @@ export const whmcsOptionalNumber = z.preprocess((value): number | undefined => {
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, z.number().optional());
|
}, z.number().optional());
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Boolean coercion helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce a WHMCS boolean-like value to a real boolean.
|
||||||
|
*
|
||||||
|
* Truthy: `true`, `1`, `"1"`, `"true"`, `"yes"`, `"on"`
|
||||||
|
* Everything else → `false`
|
||||||
|
*/
|
||||||
|
function toBool(value: unknown): boolean {
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (typeof value === "number") return value === 1;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const n = value.trim().toLowerCase();
|
||||||
|
return n === "1" || n === "true" || n === "yes" || n === "on";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coercing required boolean — accepts boolean, number (0/1), or string, always outputs boolean.
|
||||||
|
* Use for WHMCS boolean flags that must resolve to true/false.
|
||||||
|
*/
|
||||||
|
export const whmcsBoolean = z.preprocess(toBool, z.boolean());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coercing optional boolean — accepts boolean, number, string, null, or undefined.
|
||||||
|
* Returns `undefined` for null/undefined, coerces everything else to boolean.
|
||||||
|
* Use for WHMCS boolean flags that may be absent.
|
||||||
|
*/
|
||||||
|
export const whmcsOptionalBoolean = z.preprocess((value): boolean | undefined => {
|
||||||
|
if (value === undefined || value === null) return undefined;
|
||||||
|
return toBool(value);
|
||||||
|
}, z.boolean().optional());
|
||||||
|
|||||||
@ -56,7 +56,7 @@ function normalizeAddress(client: WhmcsRawClient): Address | undefined {
|
|||||||
country: client.country ?? null,
|
country: client.country ?? null,
|
||||||
countryCode: client.countrycode ?? null,
|
countryCode: client.countrycode ?? null,
|
||||||
phoneNumber: client.phonenumberformatted ?? client.phonenumber ?? null,
|
phoneNumber: client.phonenumberformatted ?? client.phonenumber ?? null,
|
||||||
phoneCountryCode: client.phonecc == null ? null : String(client.phonecc),
|
phoneCountryCode: client.phonecc ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasValues = Object.values(address).some(v => v !== undefined && v !== null && v !== "");
|
const hasValues = Object.values(address).some(v => v !== undefined && v !== null && v !== "");
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
whmcsRequiredNumber,
|
||||||
|
whmcsOptionalNumber,
|
||||||
|
whmcsOptionalBoolean,
|
||||||
|
whmcsString,
|
||||||
|
} from "../../../common/providers/whmcs-utils/index.js";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Request Parameter Types
|
// Request Parameter Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -57,12 +64,9 @@ export interface WhmcsCreateSsoTokenParams {
|
|||||||
// Response Types
|
// Response Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const booleanLike = z.union([z.boolean(), z.number(), z.string()]);
|
|
||||||
const numberLike = z.union([z.number(), z.string()]);
|
|
||||||
|
|
||||||
export const whmcsCustomFieldSchema = z
|
export const whmcsCustomFieldSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: numberLike,
|
id: whmcsRequiredNumber,
|
||||||
value: z.string().optional().nullable(),
|
value: z.string().optional().nullable(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
type: z.string().optional(),
|
type: z.string().optional(),
|
||||||
@ -71,10 +75,10 @@ export const whmcsCustomFieldSchema = z
|
|||||||
|
|
||||||
export const whmcsUserSchema = z
|
export const whmcsUserSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: numberLike,
|
id: whmcsRequiredNumber,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
is_owner: booleanLike.optional(),
|
is_owner: whmcsOptionalBoolean,
|
||||||
})
|
})
|
||||||
.strip();
|
.strip();
|
||||||
|
|
||||||
@ -100,10 +104,10 @@ const usersSchema = z
|
|||||||
|
|
||||||
export const whmcsClientSchema = z
|
export const whmcsClientSchema = z
|
||||||
.object({
|
.object({
|
||||||
client_id: numberLike.optional(),
|
client_id: whmcsOptionalNumber,
|
||||||
owner_user_id: numberLike.optional(),
|
owner_user_id: whmcsOptionalNumber,
|
||||||
userid: numberLike.optional(),
|
userid: whmcsOptionalNumber,
|
||||||
id: numberLike,
|
id: whmcsRequiredNumber,
|
||||||
uuid: z.string().optional(),
|
uuid: z.string().optional(),
|
||||||
firstname: z.string().optional(),
|
firstname: z.string().optional(),
|
||||||
lastname: z.string().optional(),
|
lastname: z.string().optional(),
|
||||||
@ -119,25 +123,25 @@ export const whmcsClientSchema = z
|
|||||||
postcode: z.string().optional(),
|
postcode: z.string().optional(),
|
||||||
country: z.string().optional(),
|
country: z.string().optional(),
|
||||||
countrycode: z.string().optional(),
|
countrycode: z.string().optional(),
|
||||||
phonecc: numberLike.optional(),
|
phonecc: whmcsString.optional(),
|
||||||
phonenumber: z.string().optional(),
|
phonenumber: z.string().optional(),
|
||||||
phonenumberformatted: z.string().optional(),
|
phonenumberformatted: z.string().optional(),
|
||||||
telephoneNumber: z.string().optional(),
|
telephoneNumber: z.string().optional(),
|
||||||
tax_id: z.string().optional(),
|
tax_id: z.string().optional(),
|
||||||
currency: numberLike.optional(),
|
currency: whmcsOptionalNumber,
|
||||||
currency_code: z.string().optional(),
|
currency_code: z.string().optional(),
|
||||||
defaultgateway: z.string().optional(),
|
defaultgateway: z.string().optional(),
|
||||||
defaultpaymethodid: numberLike.optional(),
|
defaultpaymethodid: whmcsOptionalNumber,
|
||||||
language: z.string().optional(),
|
language: z.string().optional(),
|
||||||
status: z.string().optional(),
|
status: z.string().optional(),
|
||||||
notes: z.string().optional(),
|
notes: z.string().optional(),
|
||||||
datecreated: z.string().optional(),
|
datecreated: z.string().optional(),
|
||||||
lastlogin: z.string().optional(),
|
lastlogin: z.string().optional(),
|
||||||
email_preferences: whmcsEmailPreferencesSchema,
|
email_preferences: whmcsEmailPreferencesSchema,
|
||||||
allowSingleSignOn: booleanLike.optional(),
|
allowSingleSignOn: whmcsOptionalBoolean,
|
||||||
email_verified: booleanLike.optional(),
|
email_verified: whmcsOptionalBoolean,
|
||||||
marketing_emails_opt_in: booleanLike.optional(),
|
marketing_emails_opt_in: whmcsOptionalBoolean,
|
||||||
isOptedInToMarketingEmails: booleanLike.optional(),
|
isOptedInToMarketingEmails: whmcsOptionalBoolean,
|
||||||
phoneNumber: z.string().optional(),
|
phoneNumber: z.string().optional(),
|
||||||
customfields: customFieldsSchema,
|
customfields: customFieldsSchema,
|
||||||
users: usersSchema,
|
users: usersSchema,
|
||||||
@ -151,7 +155,7 @@ export const whmcsClientStatsSchema = z
|
|||||||
export const whmcsClientResponseSchema = z
|
export const whmcsClientResponseSchema = z
|
||||||
.object({
|
.object({
|
||||||
result: z.string().optional(),
|
result: z.string().optional(),
|
||||||
client_id: numberLike.optional(),
|
client_id: whmcsOptionalNumber,
|
||||||
client: whmcsClientSchema,
|
client: whmcsClientSchema,
|
||||||
stats: whmcsClientStatsSchema,
|
stats: whmcsClientStatsSchema,
|
||||||
})
|
})
|
||||||
@ -215,12 +219,12 @@ export type WhmcsClientStats = z.infer<typeof whmcsClientStatsSchema>;
|
|||||||
* @see https://developers.whmcs.com/api-reference/getusers/
|
* @see https://developers.whmcs.com/api-reference/getusers/
|
||||||
*/
|
*/
|
||||||
export const whmcsUserClientAssociationSchema = z.object({
|
export const whmcsUserClientAssociationSchema = z.object({
|
||||||
id: numberLike,
|
id: whmcsRequiredNumber,
|
||||||
isOwner: z.boolean().optional(),
|
isOwner: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const whmcsGetUsersUserSchema = z.object({
|
export const whmcsGetUsersUserSchema = z.object({
|
||||||
id: numberLike,
|
id: whmcsRequiredNumber,
|
||||||
firstname: z.string().optional(),
|
firstname: z.string().optional(),
|
||||||
lastname: z.string().optional(),
|
lastname: z.string().optional(),
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
@ -230,9 +234,9 @@ export const whmcsGetUsersUserSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const whmcsGetUsersResponseSchema = z.object({
|
export const whmcsGetUsersResponseSchema = z.object({
|
||||||
totalresults: numberLike,
|
totalresults: whmcsRequiredNumber,
|
||||||
startnumber: numberLike.optional(),
|
startnumber: whmcsOptionalNumber,
|
||||||
numreturned: numberLike.optional(),
|
numreturned: whmcsOptionalNumber,
|
||||||
users: z.array(whmcsGetUsersUserSchema).optional().default([]),
|
users: z.array(whmcsGetUsersUserSchema).optional().default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -14,8 +14,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { countryCodeSchema } from "../common/schema.js";
|
import { countryCodeSchema } from "../common/schema.js";
|
||||||
import {
|
import {
|
||||||
whmcsNumberLike as numberLike,
|
whmcsRequiredNumber,
|
||||||
whmcsBooleanLike as booleanLike,
|
whmcsOptionalNumber,
|
||||||
|
whmcsOptionalBoolean,
|
||||||
} from "../common/providers/whmcs-utils/index.js";
|
} from "../common/providers/whmcs-utils/index.js";
|
||||||
import {
|
import {
|
||||||
whmcsClientSchema as whmcsRawClientSchema,
|
whmcsClientSchema as whmcsRawClientSchema,
|
||||||
@ -28,23 +29,6 @@ import {
|
|||||||
|
|
||||||
const stringOrNull = z.union([z.string(), z.null()]);
|
const stringOrNull = z.union([z.string(), z.null()]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize boolean-like values to actual booleans
|
|
||||||
*/
|
|
||||||
const normalizeBoolean = (value: unknown): boolean | null | undefined => {
|
|
||||||
if (value === undefined) return undefined;
|
|
||||||
if (value === null) return null;
|
|
||||||
if (typeof value === "boolean") return value;
|
|
||||||
if (typeof value === "number") return value === 1;
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const normalized = value.trim().toLowerCase();
|
|
||||||
return (
|
|
||||||
normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Address Schemas
|
// Address Schemas
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -132,41 +116,25 @@ export const userAuthSchema = z.object({
|
|||||||
* Email preferences from WHMCS
|
* Email preferences from WHMCS
|
||||||
* Internal to Providers.Whmcs namespace
|
* Internal to Providers.Whmcs namespace
|
||||||
*/
|
*/
|
||||||
const emailPreferencesSchema = z
|
const emailPreferencesSchema = z.object({
|
||||||
.object({
|
general: whmcsOptionalBoolean,
|
||||||
general: booleanLike.optional(),
|
invoice: whmcsOptionalBoolean,
|
||||||
invoice: booleanLike.optional(),
|
support: whmcsOptionalBoolean,
|
||||||
support: booleanLike.optional(),
|
product: whmcsOptionalBoolean,
|
||||||
product: booleanLike.optional(),
|
domain: whmcsOptionalBoolean,
|
||||||
domain: booleanLike.optional(),
|
affiliate: whmcsOptionalBoolean,
|
||||||
affiliate: booleanLike.optional(),
|
});
|
||||||
})
|
|
||||||
.transform(prefs => ({
|
|
||||||
general: normalizeBoolean(prefs.general),
|
|
||||||
invoice: normalizeBoolean(prefs.invoice),
|
|
||||||
support: normalizeBoolean(prefs.support),
|
|
||||||
product: normalizeBoolean(prefs.product),
|
|
||||||
domain: normalizeBoolean(prefs.domain),
|
|
||||||
affiliate: normalizeBoolean(prefs.affiliate),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sub-user from WHMCS
|
* Sub-user from WHMCS
|
||||||
* Internal to Providers.Whmcs namespace
|
* Internal to Providers.Whmcs namespace
|
||||||
*/
|
*/
|
||||||
const subUserSchema = z
|
const subUserSchema = z.object({
|
||||||
.object({
|
id: whmcsRequiredNumber,
|
||||||
id: numberLike,
|
name: z.string(),
|
||||||
name: z.string(),
|
email: z.string(),
|
||||||
email: z.string(),
|
is_owner: whmcsOptionalBoolean,
|
||||||
is_owner: booleanLike.optional(),
|
});
|
||||||
})
|
|
||||||
.transform(user => ({
|
|
||||||
id: Number(user.id),
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
is_owner: normalizeBoolean(user.is_owner),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Billing stats from WHMCS
|
* Billing stats from WHMCS
|
||||||
@ -243,38 +211,18 @@ const nullableProfileOverrides = nullableProfileFields.reduce<Record<string, z.Z
|
|||||||
export const whmcsClientSchema = whmcsRawClientSchema
|
export const whmcsClientSchema = whmcsRawClientSchema
|
||||||
.extend({
|
.extend({
|
||||||
...nullableProfileOverrides,
|
...nullableProfileOverrides,
|
||||||
// Allow nullable numeric strings
|
defaultpaymethodid: whmcsOptionalNumber.nullable(),
|
||||||
defaultpaymethodid: numberLike.nullable().optional(),
|
currency: whmcsOptionalNumber.nullable(),
|
||||||
currency: numberLike.nullable().optional(),
|
allowSingleSignOn: whmcsOptionalBoolean.nullable(),
|
||||||
allowSingleSignOn: booleanLike.nullable().optional(),
|
email_verified: whmcsOptionalBoolean.nullable(),
|
||||||
email_verified: booleanLike.nullable().optional(),
|
marketing_emails_opt_in: whmcsOptionalBoolean.nullable(),
|
||||||
marketing_emails_opt_in: booleanLike.nullable().optional(),
|
|
||||||
address: addressSchema.nullable().optional(),
|
address: addressSchema.nullable().optional(),
|
||||||
email_preferences: emailPreferencesSchema.nullable().optional(),
|
email_preferences: emailPreferencesSchema.nullable().optional(),
|
||||||
customfields: whmcsCustomFieldsSchema,
|
customfields: whmcsCustomFieldsSchema,
|
||||||
users: whmcsUsersSchema,
|
users: whmcsUsersSchema,
|
||||||
stats: statsSchema.optional(),
|
stats: statsSchema.optional(),
|
||||||
})
|
})
|
||||||
.transform(raw => {
|
.transform(raw => ({ ...raw }));
|
||||||
const coerceNumber = (value: unknown) =>
|
|
||||||
value === null || value === undefined ? null : Number(value);
|
|
||||||
|
|
||||||
const coerceOptionalNumber = (value: unknown) =>
|
|
||||||
value === null || value === undefined ? undefined : Number(value);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...raw,
|
|
||||||
id: Number(raw.id),
|
|
||||||
client_id: coerceOptionalNumber((raw as Record<string, unknown>)["client_id"]),
|
|
||||||
owner_user_id: coerceOptionalNumber((raw as Record<string, unknown>)["owner_user_id"]),
|
|
||||||
userid: coerceOptionalNumber((raw as Record<string, unknown>)["userid"]),
|
|
||||||
allowSingleSignOn: normalizeBoolean(raw.allowSingleSignOn),
|
|
||||||
email_verified: normalizeBoolean(raw.email_verified),
|
|
||||||
marketing_emails_opt_in: normalizeBoolean(raw.marketing_emails_opt_in),
|
|
||||||
defaultpaymethodid: coerceNumber(raw.defaultpaymethodid),
|
|
||||||
currency: coerceNumber(raw.currency),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// User Schema (API Response - Normalized camelCase)
|
// User Schema (API Response - Normalized camelCase)
|
||||||
|
|||||||
@ -21,6 +21,11 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
whmcsRequiredNumber,
|
||||||
|
whmcsOptionalNumber,
|
||||||
|
} from "../../../common/providers/whmcs-utils/index.js";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// WHMCS Order Item Schema
|
// WHMCS Order Item Schema
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -91,8 +96,8 @@ export type WhmcsAddOrderPayload = z.infer<typeof whmcsAddOrderPayloadSchema>;
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export const whmcsAddOrderResponseSchema = z.object({
|
export const whmcsAddOrderResponseSchema = z.object({
|
||||||
orderid: z.union([z.string(), z.number()]),
|
orderid: whmcsRequiredNumber,
|
||||||
invoiceid: z.union([z.string(), z.number()]).optional(),
|
invoiceid: whmcsOptionalNumber,
|
||||||
serviceids: z.string().optional(),
|
serviceids: z.string().optional(),
|
||||||
addonids: z.string().optional(),
|
addonids: z.string().optional(),
|
||||||
domainids: z.string().optional(),
|
domainids: z.string().optional(),
|
||||||
@ -115,8 +120,8 @@ export type WhmcsOrderResult = z.infer<typeof whmcsOrderResultSchema>;
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export const whmcsAcceptOrderResponseSchema = z.object({
|
export const whmcsAcceptOrderResponseSchema = z.object({
|
||||||
orderid: z.union([z.string(), z.number()]).optional(),
|
orderid: whmcsOptionalNumber,
|
||||||
invoiceid: z.union([z.string(), z.number()]).optional(),
|
invoiceid: whmcsOptionalNumber,
|
||||||
serviceids: z.string().optional(),
|
serviceids: z.string().optional(),
|
||||||
addonids: z.string().optional(),
|
addonids: z.string().optional(),
|
||||||
domainids: z.string().optional(),
|
domainids: z.string().optional(),
|
||||||
|
|||||||
@ -32,18 +32,11 @@ function mapGatewayType(type: string): PaymentGateway["type"] {
|
|||||||
return GATEWAY_TYPE_MAP[normalized] ?? "manual";
|
return GATEWAY_TYPE_MAP[normalized] ?? "manual";
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceBoolean(value: boolean | number | string | undefined): boolean {
|
|
||||||
if (typeof value === "boolean") return value;
|
|
||||||
if (typeof value === "number") return value === 1;
|
|
||||||
if (typeof value === "string") return value === "1" || value.toLowerCase() === "true";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformWhmcsPaymentMethod(raw: unknown): PaymentMethod {
|
export function transformWhmcsPaymentMethod(raw: unknown): PaymentMethod {
|
||||||
const whmcs = whmcsPaymentMethodRawSchema.parse(raw);
|
const whmcs = whmcsPaymentMethodRawSchema.parse(raw);
|
||||||
|
|
||||||
const paymentMethod: PaymentMethod = {
|
const paymentMethod: PaymentMethod = {
|
||||||
id: Number(whmcs.id),
|
id: whmcs.id,
|
||||||
type: mapPaymentMethodType(whmcs.payment_type || whmcs.type || "manual"),
|
type: mapPaymentMethodType(whmcs.payment_type || whmcs.type || "manual"),
|
||||||
description: whmcs.description,
|
description: whmcs.description,
|
||||||
gatewayName: whmcs.gateway_name || whmcs.gateway,
|
gatewayName: whmcs.gateway_name || whmcs.gateway,
|
||||||
@ -53,7 +46,7 @@ export function transformWhmcsPaymentMethod(raw: unknown): PaymentMethod {
|
|||||||
bankName: whmcs.bank_name,
|
bankName: whmcs.bank_name,
|
||||||
remoteToken: whmcs.remote_token,
|
remoteToken: whmcs.remote_token,
|
||||||
lastUpdated: whmcs.last_updated,
|
lastUpdated: whmcs.last_updated,
|
||||||
isDefault: coerceBoolean(whmcs.is_default),
|
isDefault: whmcs.is_default ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return paymentMethodSchema.parse(paymentMethod);
|
return paymentMethodSchema.parse(paymentMethod);
|
||||||
@ -66,7 +59,7 @@ export function transformWhmcsPaymentGateway(raw: unknown): PaymentGateway {
|
|||||||
name: whmcs.name,
|
name: whmcs.name,
|
||||||
displayName: whmcs.display_name || whmcs.name,
|
displayName: whmcs.display_name || whmcs.name,
|
||||||
type: mapGatewayType(whmcs.type),
|
type: mapGatewayType(whmcs.type),
|
||||||
isActive: coerceBoolean(whmcs.visible),
|
isActive: whmcs.visible ?? false,
|
||||||
configuration: whmcs.configuration,
|
configuration: whmcs.configuration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
whmcsOptionalBoolean,
|
||||||
|
whmcsRequiredNumber,
|
||||||
|
} from "../../../common/providers/whmcs-utils/index.js";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Request Parameter Types
|
// Request Parameter Types
|
||||||
@ -38,7 +42,7 @@ export const whmcsPaymentMethodRawSchema = z.object({
|
|||||||
bank_name: z.string().optional(),
|
bank_name: z.string().optional(),
|
||||||
remote_token: z.string().optional(),
|
remote_token: z.string().optional(),
|
||||||
last_updated: z.string().optional(),
|
last_updated: z.string().optional(),
|
||||||
is_default: z.union([z.boolean(), z.number(), z.string()]).optional(),
|
is_default: whmcsOptionalBoolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type WhmcsPaymentMethodRaw = z.infer<typeof whmcsPaymentMethodRawSchema>;
|
export type WhmcsPaymentMethodRaw = z.infer<typeof whmcsPaymentMethodRawSchema>;
|
||||||
@ -47,7 +51,7 @@ export const whmcsPaymentGatewayRawSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
display_name: z.string().optional(),
|
display_name: z.string().optional(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
visible: z.union([z.boolean(), z.number(), z.string()]).optional(),
|
visible: whmcsOptionalBoolean,
|
||||||
configuration: z
|
configuration: z
|
||||||
.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
||||||
.optional(),
|
.optional(),
|
||||||
@ -85,7 +89,7 @@ export type WhmcsPaymentMethod = z.infer<typeof whmcsPaymentMethodSchema>;
|
|||||||
* WHMCS GetPayMethods API response schema
|
* WHMCS GetPayMethods API response schema
|
||||||
*/
|
*/
|
||||||
export const whmcsPaymentMethodListResponseSchema = z.object({
|
export const whmcsPaymentMethodListResponseSchema = z.object({
|
||||||
clientid: z.union([z.number(), z.string()]),
|
clientid: whmcsRequiredNumber,
|
||||||
paymethods: z.array(whmcsPaymentMethodSchema).optional(),
|
paymethods: z.array(whmcsPaymentMethodSchema).optional(),
|
||||||
message: z.string().optional(),
|
message: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -112,8 +112,8 @@ export function transformWhmcsCatalogProductsResponse(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(product.pid),
|
id: product.pid,
|
||||||
groupId: Number(product.gid),
|
groupId: product.gid,
|
||||||
name: product.name,
|
name: product.name,
|
||||||
description: product.description,
|
description: product.description,
|
||||||
module: product.module,
|
module: product.module,
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
whmcsString as s,
|
whmcsString as s,
|
||||||
whmcsNumberLike as numberLike,
|
whmcsRequiredNumber,
|
||||||
} from "../../../common/providers/whmcs-utils/index.js";
|
} from "../../../common/providers/whmcs-utils/index.js";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -36,8 +36,8 @@ const whmcsCatalogProductPricingCycleSchema = z.object({
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const whmcsCatalogProductSchema = z.object({
|
const whmcsCatalogProductSchema = z.object({
|
||||||
pid: numberLike,
|
pid: s,
|
||||||
gid: numberLike,
|
gid: whmcsRequiredNumber,
|
||||||
name: s,
|
name: s,
|
||||||
description: s,
|
description: s,
|
||||||
module: s,
|
module: s,
|
||||||
@ -58,7 +58,7 @@ export const whmcsCatalogProductListResponseSchema = z.object({
|
|||||||
products: z.object({
|
products: z.object({
|
||||||
product: z.array(whmcsCatalogProductSchema),
|
product: z.array(whmcsCatalogProductSchema),
|
||||||
}),
|
}),
|
||||||
totalresults: numberLike,
|
totalresults: whmcsRequiredNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type WhmcsCatalogProductListResponse = z.infer<typeof whmcsCatalogProductListResponseSchema>;
|
export type WhmcsCatalogProductListResponse = z.infer<typeof whmcsCatalogProductListResponseSchema>;
|
||||||
|
|||||||
@ -209,13 +209,7 @@ export function transformWhmcsSubscriptionListResponse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalResultsRaw = parsed.totalresults;
|
const totalResults = parsed.totalresults ?? subscriptions.length;
|
||||||
const totalResults =
|
|
||||||
typeof totalResultsRaw === "number"
|
|
||||||
? totalResultsRaw
|
|
||||||
: typeof totalResultsRaw === "string"
|
|
||||||
? Number.parseInt(totalResultsRaw, 10)
|
|
||||||
: subscriptions.length;
|
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
const normalizedStatus = subscriptionStatusSchema.parse(status);
|
const normalizedStatus = subscriptionStatusSchema.parse(status);
|
||||||
@ -228,7 +222,7 @@ export function transformWhmcsSubscriptionListResponse(
|
|||||||
|
|
||||||
return subscriptionListSchema.parse({
|
return subscriptionListSchema.parse({
|
||||||
subscriptions,
|
subscriptions,
|
||||||
totalCount: Number.isFinite(totalResults) ? totalResults : subscriptions.length,
|
totalCount: totalResults,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export const whmcsCustomFieldsContainerSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const whmcsConfigOptionSchema = z.object({
|
export const whmcsConfigOptionSchema = z.object({
|
||||||
id: numberLike.optional(),
|
id: normalizeOptionalNumber,
|
||||||
option: s.optional(),
|
option: s.optional(),
|
||||||
type: s.optional(),
|
type: s.optional(),
|
||||||
value: s.optional(),
|
value: s.optional(),
|
||||||
@ -165,11 +165,11 @@ const whmcsProductContainerSchema = z.object({
|
|||||||
export const whmcsProductListResponseSchema = z.object({
|
export const whmcsProductListResponseSchema = z.object({
|
||||||
result: z.enum(["success", "error"]).optional(),
|
result: z.enum(["success", "error"]).optional(),
|
||||||
message: s.optional(),
|
message: s.optional(),
|
||||||
clientid: numberLike.optional(),
|
clientid: normalizeOptionalNumber,
|
||||||
serviceid: z.union([numberLike, z.null()]).optional(),
|
serviceid: normalizeOptionalNumber,
|
||||||
pid: z.union([numberLike, z.null()]).optional(),
|
pid: normalizeOptionalNumber,
|
||||||
domain: s.nullable().optional(),
|
domain: s.nullable().optional(),
|
||||||
totalresults: numberLike.optional(),
|
totalresults: normalizeOptionalNumber,
|
||||||
startnumber: normalizeOptionalNumber,
|
startnumber: normalizeOptionalNumber,
|
||||||
numreturned: normalizeOptionalNumber,
|
numreturned: normalizeOptionalNumber,
|
||||||
products: z.preprocess(value => {
|
products: z.preprocess(value => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user