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,
|
subscriptionQuerySchema,
|
||||||
type SubscriptionQuery,
|
type SubscriptionQuery,
|
||||||
} from "@customer-portal/domain/subscriptions";
|
} from "@customer-portal/domain/subscriptions";
|
||||||
import {
|
import { InvoiceList } from "@customer-portal/domain/billing";
|
||||||
InvoiceList,
|
import { createPaginationSchema } from "@customer-portal/domain/toolkit/validation/helpers";
|
||||||
invoiceListQuerySchema,
|
import type { z } from "zod";
|
||||||
type InvoiceListQuery,
|
|
||||||
} from "@customer-portal/domain/billing";
|
|
||||||
import {
|
import {
|
||||||
simTopupRequestSchema,
|
simTopupRequestSchema,
|
||||||
simChangePlanRequestSchema,
|
simChangePlanRequestSchema,
|
||||||
@ -43,6 +41,13 @@ import {
|
|||||||
import { ZodValidationPipe } from "@bff/core/validation";
|
import { ZodValidationPipe } from "@bff/core/validation";
|
||||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types";
|
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")
|
@Controller("subscriptions")
|
||||||
export class SubscriptionsController {
|
export class SubscriptionsController {
|
||||||
constructor(
|
constructor(
|
||||||
@ -75,24 +80,14 @@ export class SubscriptionsController {
|
|||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Param("id", ParseIntPipe) subscriptionId: number
|
@Param("id", ParseIntPipe) subscriptionId: number
|
||||||
): Promise<Subscription> {
|
): Promise<Subscription> {
|
||||||
if (subscriptionId <= 0) {
|
|
||||||
throw new BadRequestException("Subscription ID must be a positive number");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.subscriptionsService.getSubscriptionById(req.user.id, subscriptionId);
|
return this.subscriptionsService.getSubscriptionById(req.user.id, subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(":id/invoices")
|
@Get(":id/invoices")
|
||||||
@UsePipes(new ZodValidationPipe(invoiceListQuerySchema))
|
|
||||||
async getSubscriptionInvoices(
|
async getSubscriptionInvoices(
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser,
|
||||||
@Param("id", ParseIntPipe) subscriptionId: number,
|
@Param("id", ParseIntPipe) subscriptionId: number,
|
||||||
@Query() query: InvoiceListQuery
|
@Query(new ZodValidationPipe(subscriptionInvoiceQuerySchema)) query: SubscriptionInvoiceQuery
|
||||||
): Promise<InvoiceList> {
|
): Promise<InvoiceList> {
|
||||||
if (subscriptionId <= 0) {
|
|
||||||
throw new BadRequestException("Subscription ID must be a positive number");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.subscriptionsService.getSubscriptionInvoices(req.user.id, subscriptionId, query);
|
return this.subscriptionsService.getSubscriptionInvoices(req.user.id, subscriptionId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||||
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
|
import { Injectable, NotFoundException, Inject, BadRequestException } from "@nestjs/common";
|
||||||
import {
|
import {
|
||||||
Subscription,
|
Subscription,
|
||||||
SubscriptionList,
|
SubscriptionList,
|
||||||
@ -82,7 +82,7 @@ export class SubscriptionsService {
|
|||||||
try {
|
try {
|
||||||
// Validate subscription ID
|
// Validate subscription ID
|
||||||
if (!subscriptionId || subscriptionId < 1) {
|
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
|
// Get WHMCS client ID from user mapping
|
||||||
@ -111,7 +111,7 @@ export class SubscriptionsService {
|
|||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error instanceof NotFoundException) {
|
if (error instanceof NotFoundException || error instanceof BadRequestException) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,42 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
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
|
// Request Parameter Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -44,25 +80,25 @@ export const whmcsCustomFieldsContainerSchema = z.object({
|
|||||||
|
|
||||||
// Raw WHMCS Product/Service (Subscription)
|
// Raw WHMCS Product/Service (Subscription)
|
||||||
export const whmcsProductRawSchema = z.object({
|
export const whmcsProductRawSchema = z.object({
|
||||||
id: z.number(),
|
id: normalizeRequiredNumber,
|
||||||
clientid: z.number(),
|
clientid: normalizeRequiredNumber,
|
||||||
serviceid: z.number().optional(),
|
serviceid: normalizeOptionalNumber,
|
||||||
pid: z.number().optional(),
|
pid: normalizeOptionalNumber,
|
||||||
orderid: z.number().optional(),
|
orderid: normalizeOptionalNumber,
|
||||||
ordernumber: z.string().optional(),
|
ordernumber: optionalStringField(),
|
||||||
regdate: z.string(),
|
regdate: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
translated_name: z.string().optional(),
|
translated_name: z.string().optional(),
|
||||||
groupname: z.string().optional(),
|
groupname: z.string().optional(),
|
||||||
translated_groupname: z.string().optional(),
|
translated_groupname: z.string().optional(),
|
||||||
domain: z.string().optional(),
|
domain: optionalStringField(),
|
||||||
dedicatedip: z.string().optional(),
|
dedicatedip: optionalStringField(),
|
||||||
serverid: z.number().optional(),
|
serverid: normalizeOptionalNumber,
|
||||||
servername: z.string().optional(),
|
servername: optionalStringField(),
|
||||||
serverip: z.string().optional(),
|
serverip: optionalStringField(),
|
||||||
serverhostname: z.string().optional(),
|
serverhostname: optionalStringField(),
|
||||||
suspensionreason: z.string().optional(),
|
suspensionreason: z.string().optional(),
|
||||||
promoid: z.number().optional(),
|
promoid: normalizeOptionalNumber,
|
||||||
subscriptionid: z.string().optional(),
|
subscriptionid: z.string().optional(),
|
||||||
|
|
||||||
// Pricing
|
// Pricing
|
||||||
@ -84,10 +120,10 @@ export const whmcsProductRawSchema = z.object({
|
|||||||
|
|
||||||
// Notes
|
// Notes
|
||||||
notes: z.string().optional(),
|
notes: z.string().optional(),
|
||||||
diskusage: z.number().optional(),
|
diskusage: normalizeOptionalNumber,
|
||||||
disklimit: z.number().optional(),
|
disklimit: normalizeOptionalNumber,
|
||||||
bwusage: z.number().optional(),
|
bwusage: normalizeOptionalNumber,
|
||||||
bwlimit: z.number().optional(),
|
bwlimit: normalizeOptionalNumber,
|
||||||
lastupdate: z.string().optional(),
|
lastupdate: z.string().optional(),
|
||||||
|
|
||||||
// Custom fields
|
// Custom fields
|
||||||
@ -115,15 +151,21 @@ export type WhmcsCustomField = z.infer<typeof whmcsCustomFieldSchema>;
|
|||||||
export const whmcsProductListResponseSchema = z.object({
|
export const whmcsProductListResponseSchema = z.object({
|
||||||
result: z.enum(["success", "error"]).optional(),
|
result: z.enum(["success", "error"]).optional(),
|
||||||
message: z.string().optional(),
|
message: z.string().optional(),
|
||||||
clientid: z.number().optional(),
|
clientid: z.union([z.number(), z.string()]).optional(),
|
||||||
serviceid: z.number().optional(),
|
serviceid: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||||
pid: z.number().optional(),
|
pid: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||||
domain: z.string().optional(),
|
domain: z.string().nullable().optional(),
|
||||||
totalresults: z.number().optional(),
|
totalresults: z.union([z.number(), z.string()]).optional(),
|
||||||
startnumber: z.number().optional(),
|
startnumber: normalizeOptionalNumber,
|
||||||
numreturned: z.number().optional(),
|
numreturned: normalizeOptionalNumber,
|
||||||
products: z.object({
|
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(),
|
}).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user