Refactor subscriptions controller and service to enhance validation and error handling
- Introduced a new pagination schema for subscription invoices to improve query validation. - Updated the subscriptions controller to utilize the new schema for invoice queries, ensuring better data integrity. - Enhanced error handling in the subscriptions service by replacing generic error messages with specific BadRequestException for invalid subscription IDs. - Refactored WHMCS raw types to implement normalization functions for better data validation and consistency.
This commit is contained in:
parent
7cefee4c75
commit
82bb590023
@ -22,11 +22,9 @@ import {
|
||||
subscriptionQuerySchema,
|
||||
type SubscriptionQuery,
|
||||
} from "@customer-portal/domain/subscriptions";
|
||||
import {
|
||||
InvoiceList,
|
||||
invoiceListQuerySchema,
|
||||
type InvoiceListQuery,
|
||||
} from "@customer-portal/domain/billing";
|
||||
import { InvoiceList } from "@customer-portal/domain/billing";
|
||||
import { createPaginationSchema } from "@customer-portal/domain/toolkit/validation/helpers";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
simTopupRequestSchema,
|
||||
simChangePlanRequestSchema,
|
||||
@ -43,6 +41,13 @@ import {
|
||||
import { ZodValidationPipe } from "@bff/core/validation";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types";
|
||||
|
||||
const subscriptionInvoiceQuerySchema = createPaginationSchema({
|
||||
defaultLimit: 10,
|
||||
maxLimit: 100,
|
||||
minLimit: 1,
|
||||
});
|
||||
type SubscriptionInvoiceQuery = z.infer<typeof subscriptionInvoiceQuerySchema>;
|
||||
|
||||
@Controller("subscriptions")
|
||||
export class SubscriptionsController {
|
||||
constructor(
|
||||
@ -75,24 +80,14 @@ export class SubscriptionsController {
|
||||
@Request() req: RequestWithUser,
|
||||
@Param("id", ParseIntPipe) subscriptionId: number
|
||||
): Promise<Subscription> {
|
||||
if (subscriptionId <= 0) {
|
||||
throw new BadRequestException("Subscription ID must be a positive number");
|
||||
}
|
||||
|
||||
return this.subscriptionsService.getSubscriptionById(req.user.id, subscriptionId);
|
||||
}
|
||||
|
||||
@Get(":id/invoices")
|
||||
@UsePipes(new ZodValidationPipe(invoiceListQuerySchema))
|
||||
async getSubscriptionInvoices(
|
||||
@Request() req: RequestWithUser,
|
||||
@Param("id", ParseIntPipe) subscriptionId: number,
|
||||
@Query() query: InvoiceListQuery
|
||||
@Query(new ZodValidationPipe(subscriptionInvoiceQuerySchema)) query: SubscriptionInvoiceQuery
|
||||
): Promise<InvoiceList> {
|
||||
if (subscriptionId <= 0) {
|
||||
throw new BadRequestException("Subscription ID must be a positive number");
|
||||
}
|
||||
|
||||
return this.subscriptionsService.getSubscriptionInvoices(req.user.id, subscriptionId, query);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException, Inject, BadRequestException } from "@nestjs/common";
|
||||
import {
|
||||
Subscription,
|
||||
SubscriptionList,
|
||||
@ -82,7 +82,7 @@ export class SubscriptionsService {
|
||||
try {
|
||||
// Validate subscription ID
|
||||
if (!subscriptionId || subscriptionId < 1) {
|
||||
throw new Error("Invalid subscription ID");
|
||||
throw new BadRequestException("Subscription ID must be a positive number");
|
||||
}
|
||||
|
||||
// Get WHMCS client ID from user mapping
|
||||
@ -111,7 +111,7 @@ export class SubscriptionsService {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
|
||||
if (error instanceof NotFoundException) {
|
||||
if (error instanceof NotFoundException || error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,42 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
const normalizeRequiredNumber = z.preprocess(
|
||||
value => {
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
z.number()
|
||||
);
|
||||
|
||||
const normalizeOptionalNumber = z.preprocess(
|
||||
value => {
|
||||
if (value === undefined || value === null || value === "") return undefined;
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
z.number().optional()
|
||||
);
|
||||
|
||||
const optionalStringField = () =>
|
||||
z
|
||||
.union([z.string(), z.number()])
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform(value => {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : undefined;
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Request Parameter Types
|
||||
// ============================================================================
|
||||
@ -44,25 +80,25 @@ export const whmcsCustomFieldsContainerSchema = z.object({
|
||||
|
||||
// Raw WHMCS Product/Service (Subscription)
|
||||
export const whmcsProductRawSchema = z.object({
|
||||
id: z.number(),
|
||||
clientid: z.number(),
|
||||
serviceid: z.number().optional(),
|
||||
pid: z.number().optional(),
|
||||
orderid: z.number().optional(),
|
||||
ordernumber: z.string().optional(),
|
||||
id: normalizeRequiredNumber,
|
||||
clientid: normalizeRequiredNumber,
|
||||
serviceid: normalizeOptionalNumber,
|
||||
pid: normalizeOptionalNumber,
|
||||
orderid: normalizeOptionalNumber,
|
||||
ordernumber: optionalStringField(),
|
||||
regdate: z.string(),
|
||||
name: z.string(),
|
||||
translated_name: z.string().optional(),
|
||||
groupname: z.string().optional(),
|
||||
translated_groupname: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
dedicatedip: z.string().optional(),
|
||||
serverid: z.number().optional(),
|
||||
servername: z.string().optional(),
|
||||
serverip: z.string().optional(),
|
||||
serverhostname: z.string().optional(),
|
||||
domain: optionalStringField(),
|
||||
dedicatedip: optionalStringField(),
|
||||
serverid: normalizeOptionalNumber,
|
||||
servername: optionalStringField(),
|
||||
serverip: optionalStringField(),
|
||||
serverhostname: optionalStringField(),
|
||||
suspensionreason: z.string().optional(),
|
||||
promoid: z.number().optional(),
|
||||
promoid: normalizeOptionalNumber,
|
||||
subscriptionid: z.string().optional(),
|
||||
|
||||
// Pricing
|
||||
@ -84,10 +120,10 @@ export const whmcsProductRawSchema = z.object({
|
||||
|
||||
// Notes
|
||||
notes: z.string().optional(),
|
||||
diskusage: z.number().optional(),
|
||||
disklimit: z.number().optional(),
|
||||
bwusage: z.number().optional(),
|
||||
bwlimit: z.number().optional(),
|
||||
diskusage: normalizeOptionalNumber,
|
||||
disklimit: normalizeOptionalNumber,
|
||||
bwusage: normalizeOptionalNumber,
|
||||
bwlimit: normalizeOptionalNumber,
|
||||
lastupdate: z.string().optional(),
|
||||
|
||||
// Custom fields
|
||||
@ -115,15 +151,21 @@ export type WhmcsCustomField = z.infer<typeof whmcsCustomFieldSchema>;
|
||||
export const whmcsProductListResponseSchema = z.object({
|
||||
result: z.enum(["success", "error"]).optional(),
|
||||
message: z.string().optional(),
|
||||
clientid: z.number().optional(),
|
||||
serviceid: z.number().optional(),
|
||||
pid: z.number().optional(),
|
||||
domain: z.string().optional(),
|
||||
totalresults: z.number().optional(),
|
||||
startnumber: z.number().optional(),
|
||||
numreturned: z.number().optional(),
|
||||
clientid: z.union([z.number(), z.string()]).optional(),
|
||||
serviceid: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
pid: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
domain: z.string().nullable().optional(),
|
||||
totalresults: z.union([z.number(), z.string()]).optional(),
|
||||
startnumber: normalizeOptionalNumber,
|
||||
numreturned: normalizeOptionalNumber,
|
||||
products: z.object({
|
||||
product: z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional(),
|
||||
product: z
|
||||
.preprocess(value => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}, z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional())
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user