- Added optional userId parameter to payment capture methods in WhmcsService and WhmcsInvoiceService to improve tracking and management of user-related transactions. - Updated invoice retrieval and user profile services to utilize parseUuidOrThrow for user ID validation, ensuring consistent error messaging for invalid formats. - Refactored SIM billing and activation services to include userId in one-time charge creation, enhancing billing traceability. - Adjusted validation logic in various services to improve clarity and maintainability, ensuring robust handling of user IDs throughout the application.
197 lines
5.5 KiB
TypeScript
197 lines
5.5 KiB
TypeScript
/**
|
|
* Domain Toolkit - Validation Helpers
|
|
*
|
|
* Common validation utilities that can be reused across all domains.
|
|
*/
|
|
|
|
import { z } from "zod";
|
|
|
|
// ============================================================================
|
|
// ID Validation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Validate that an ID is a positive integer
|
|
*/
|
|
export function isValidPositiveId(id: number): boolean {
|
|
return Number.isInteger(id) && id > 0;
|
|
}
|
|
|
|
/**
|
|
* Validate Salesforce ID (15 or 18 characters, alphanumeric)
|
|
*/
|
|
export function isValidSalesforceId(id: string): boolean {
|
|
return /^[A-Za-z0-9]{15,18}$/.test(id);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pagination Validation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Validate and sanitize pagination parameters
|
|
*/
|
|
export function sanitizePagination(options: {
|
|
page?: number;
|
|
limit?: number;
|
|
minLimit?: number;
|
|
maxLimit?: number;
|
|
defaultLimit?: number;
|
|
}): {
|
|
page: number;
|
|
limit: number;
|
|
} {
|
|
const { page = 1, limit = options.defaultLimit ?? 10, minLimit = 1, maxLimit = 100 } = options;
|
|
|
|
return {
|
|
page: Math.max(1, Math.floor(page)),
|
|
limit: Math.max(minLimit, Math.min(maxLimit, Math.floor(limit))),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if pagination offset is valid
|
|
*/
|
|
export function isValidPaginationOffset(offset: number): boolean {
|
|
return Number.isInteger(offset) && offset >= 0;
|
|
}
|
|
|
|
// ============================================================================
|
|
// String Validation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if string is non-empty after trimming
|
|
*/
|
|
export function isNonEmptyString(value: unknown): value is string {
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if value is a valid enum member
|
|
*/
|
|
export function isValidEnumValue<T extends Record<string, string | number>>(
|
|
value: unknown,
|
|
enumObj: T
|
|
): value is T[keyof T] {
|
|
return Object.values(enumObj).some(enumValue => enumValue === value);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Array Validation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if array is non-empty
|
|
*/
|
|
export function isNonEmptyArray<T>(value: unknown): value is T[] {
|
|
return Array.isArray(value) && value.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if all array items are unique
|
|
*/
|
|
export function hasUniqueItems<T>(items: T[]): boolean {
|
|
return new Set(items).size === items.length;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Number Validation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if number is within range (inclusive)
|
|
*/
|
|
export function isInRange(value: number, min: number, max: number): boolean {
|
|
return value >= min && value <= max;
|
|
}
|
|
|
|
/**
|
|
* Check if number is a valid positive integer
|
|
*/
|
|
export function isPositiveInteger(value: unknown): value is number {
|
|
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if number is a valid non-negative integer
|
|
*/
|
|
export function isNonNegativeInteger(value: unknown): value is number {
|
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Date Validation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if string is a valid ISO date time
|
|
*/
|
|
export function isValidIsoDateTime(value: string): boolean {
|
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
|
|
if (!isoRegex.test(value)) return false;
|
|
|
|
const date = new Date(value);
|
|
return !isNaN(date.getTime());
|
|
}
|
|
|
|
/**
|
|
* Check if string is a valid date in YYYYMMDD format
|
|
*/
|
|
export function isValidYYYYMMDD(value: string): boolean {
|
|
return /^\d{8}$/.test(value);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Zod Schema Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Create a schema for a positive integer ID
|
|
*/
|
|
export const positiveIdSchema = z.number().int().positive();
|
|
|
|
/**
|
|
* Create a schema for pagination parameters
|
|
*/
|
|
export function createPaginationSchema(options?: {
|
|
minLimit?: number;
|
|
maxLimit?: number;
|
|
defaultLimit?: number;
|
|
}) {
|
|
const { minLimit = 1, maxLimit = 100, defaultLimit = 10 } = options ?? {};
|
|
|
|
return z.object({
|
|
page: z.coerce.number().int().positive().optional().default(1),
|
|
limit: z.coerce.number().int().min(minLimit).max(maxLimit).optional().default(defaultLimit),
|
|
offset: z.coerce.number().int().nonnegative().optional(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a schema for sortable queries
|
|
*/
|
|
export const sortableQuerySchema = z.object({
|
|
sortBy: z.string().optional(),
|
|
sortOrder: z.enum(["asc", "desc"]).optional(),
|
|
});
|
|
|
|
// ============================================================================
|
|
// Type Guards
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Type guard for checking if value is a record
|
|
*/
|
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
/**
|
|
* Type guard for checking if error is a Zod error
|
|
*/
|
|
export function isZodError(error: unknown): error is z.ZodError {
|
|
if (!isRecord(error)) return false;
|
|
return Array.isArray(error.issues);
|
|
}
|