forked from Yara724/api
Validation for prices and objection
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
} from "@nestjs/common";
|
||||
import { Observable } from "rxjs";
|
||||
import { normalizeUnicodeDigitsDeep } from "src/utils/unicode-digits";
|
||||
|
||||
/**
|
||||
* Normalizes Unicode decimal digits in JSON request bodies to ASCII before
|
||||
* validation and handlers run (Persian/Arabic-Indic → 0-9).
|
||||
*/
|
||||
@Injectable()
|
||||
export class UnicodeDigitsNormalizeInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const contentType = String(req.headers?.["content-type"] ?? "");
|
||||
|
||||
if (
|
||||
req.body &&
|
||||
typeof req.body === "object" &&
|
||||
!Buffer.isBuffer(req.body) &&
|
||||
!contentType.includes("multipart/form-data")
|
||||
) {
|
||||
req.body = normalizeUnicodeDigitsDeep(req.body);
|
||||
}
|
||||
|
||||
return next.handle();
|
||||
}
|
||||
}
|
||||
24
src/common/validators/has-objection-entries.validator.ts
Normal file
24
src/common/validators/has-objection-entries.validator.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
ValidationArguments,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from "class-validator";
|
||||
|
||||
@ValidatorConstraint({ name: "hasObjectionEntries", async: false })
|
||||
export class HasObjectionEntriesConstraint
|
||||
implements ValidatorConstraintInterface
|
||||
{
|
||||
validate(_value: unknown, args: ValidationArguments): boolean {
|
||||
const o = args.object as {
|
||||
objectionParts?: unknown[];
|
||||
newParts?: unknown[];
|
||||
};
|
||||
const partsLen = Array.isArray(o.objectionParts) ? o.objectionParts.length : 0;
|
||||
const newLen = Array.isArray(o.newParts) ? o.newParts.length : 0;
|
||||
return partsLen > 0 || newLen > 0;
|
||||
}
|
||||
|
||||
defaultMessage(): string {
|
||||
return "Provide at least one entry in objectionParts and/or newParts.";
|
||||
}
|
||||
}
|
||||
37
src/common/validators/money-amount-string.validator.ts
Normal file
37
src/common/validators/money-amount-string.validator.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from "class-validator";
|
||||
import { normalizeMoneyAmountString } from "src/utils/unicode-digits";
|
||||
|
||||
@ValidatorConstraint({ name: "isMoneyAmountString", async: false })
|
||||
export class IsMoneyAmountStringConstraint
|
||||
implements ValidatorConstraintInterface
|
||||
{
|
||||
validate(value: unknown): boolean {
|
||||
if (value == null || value === "") return true;
|
||||
if (typeof value !== "string") return false;
|
||||
const n = normalizeMoneyAmountString(value);
|
||||
if (!n) return false;
|
||||
return /^\d+(\.\d+)?$/.test(n);
|
||||
}
|
||||
|
||||
defaultMessage(): string {
|
||||
return "Must be a non-negative amount (digits only, optional decimal).";
|
||||
}
|
||||
}
|
||||
|
||||
export function IsMoneyAmountString(validationOptions?: ValidationOptions) {
|
||||
return (object: object, propertyName: string) => {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsMoneyAmountStringConstraint,
|
||||
});
|
||||
};
|
||||
}
|
||||
29
src/common/validators/objection-part-content.validator.ts
Normal file
29
src/common/validators/objection-part-content.validator.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
ValidationArguments,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from "class-validator";
|
||||
|
||||
@ValidatorConstraint({ name: "objectionPartHasContent", async: false })
|
||||
export class ObjectionPartHasContentConstraint
|
||||
implements ValidatorConstraintInterface
|
||||
{
|
||||
validate(_value: unknown, args: ValidationArguments): boolean {
|
||||
const o = args.object as {
|
||||
reason?: string;
|
||||
partPrice?: string;
|
||||
partSalary?: string;
|
||||
};
|
||||
const hasReason =
|
||||
typeof o.reason === "string" && o.reason.trim().length >= 3;
|
||||
const hasPrice =
|
||||
o.partPrice != null && String(o.partPrice).trim() !== "";
|
||||
const hasSalary =
|
||||
o.partSalary != null && String(o.partSalary).trim() !== "";
|
||||
return hasReason || hasPrice || hasSalary;
|
||||
}
|
||||
|
||||
defaultMessage(): string {
|
||||
return "Each objection line needs a reason (min 3 characters) and/or proposed partPrice / partSalary.";
|
||||
}
|
||||
}
|
||||
66
src/common/validators/repair-line-amount-toman.validator.ts
Normal file
66
src/common/validators/repair-line-amount-toman.validator.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from "class-validator";
|
||||
import {
|
||||
REPAIR_LINE_AMOUNT_TOMAN,
|
||||
} from "src/constants/repair-amount-limits";
|
||||
import { parseMoneyAmountToman } from "src/utils/unicode-digits";
|
||||
|
||||
export type RepairLineAmountTomanOptions = {
|
||||
/** When true, `0` is valid (e.g. expert `price` / `salary` split where one side is zero). */
|
||||
allowZero?: boolean;
|
||||
};
|
||||
|
||||
@ValidatorConstraint({ name: "isRepairLineAmountToman", async: false })
|
||||
export class IsRepairLineAmountTomanConstraint
|
||||
implements ValidatorConstraintInterface
|
||||
{
|
||||
validate(value: unknown, args: ValidationArguments): boolean {
|
||||
const [minToman, maxToman, allowZero] = args.constraints as [
|
||||
number,
|
||||
number,
|
||||
boolean | undefined,
|
||||
];
|
||||
if (value == null || value === "") return true;
|
||||
const amount = parseMoneyAmountToman(value);
|
||||
if (amount == null) return false;
|
||||
if (allowZero && amount === 0) return true;
|
||||
return amount >= minToman && amount <= maxToman;
|
||||
}
|
||||
|
||||
defaultMessage(args: ValidationArguments): string {
|
||||
const [minToman, maxToman, allowZero] = args.constraints as [
|
||||
number,
|
||||
number,
|
||||
boolean | undefined,
|
||||
];
|
||||
const zeroHint = allowZero ? " Use 0 where one side of price/salary is unused." : "";
|
||||
return (
|
||||
`Amount must be in Toman between ${minToman.toLocaleString("en-US")} and ${maxToman.toLocaleString("en-US")}.${zeroHint}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Optional repair-line amount in Toman (digits only after unicode normalize). */
|
||||
export function IsRepairLineAmountToman(
|
||||
options?: ValidationOptions & RepairLineAmountTomanOptions,
|
||||
) {
|
||||
const { allowZero, ...validationOptions } = options ?? {};
|
||||
return (object: object, propertyName: string) => {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [
|
||||
REPAIR_LINE_AMOUNT_TOMAN.MIN,
|
||||
REPAIR_LINE_AMOUNT_TOMAN.MAX,
|
||||
allowZero === true,
|
||||
],
|
||||
validator: IsRepairLineAmountTomanConstraint,
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user