mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
fix: 194 and 183 plane takss
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
validate,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
import { IsTin, normalizeTin } from './is-tin.validator';
|
||||
|
||||
class Required {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsTin({ message: 'TIN must be exactly 10 digits' })
|
||||
tin!: string;
|
||||
}
|
||||
|
||||
class Optional {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsTin({ message: 'TIN must be exactly 10 digits' })
|
||||
tin?: string;
|
||||
}
|
||||
|
||||
async function errs(cls: any, tin: any) {
|
||||
const o = new cls();
|
||||
o.tin = tin;
|
||||
return (await validate(o)).length;
|
||||
}
|
||||
|
||||
describe('IsTin', () => {
|
||||
it('accepts a real 10-digit TIN', async () => {
|
||||
expect(await errs(Required, '0012345678')).toBe(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['letters', 'ABCDEFGHIJ'],
|
||||
['symbols', '!!!!!!!!!!'],
|
||||
['too short', '123'],
|
||||
['too long', '12345678901'],
|
||||
['draft TIN', 'D123456789'],
|
||||
['spaced', '012 345678'],
|
||||
])('rejects %s', async (_label, value) => {
|
||||
expect(await errs(Required, value)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('rejects empty on the required DTO but allows omission on the optional one', async () => {
|
||||
expect(await errs(Required, '')).toBeGreaterThan(0);
|
||||
expect(await errs(Optional, undefined)).toBe(0);
|
||||
});
|
||||
|
||||
it('normalizes messy input', () => {
|
||||
expect(normalizeTin(' 001-234-5678 ')).toBe('0012345678');
|
||||
expect(normalizeTin('')).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
|
||||
/** An Ethiopian TIN is exactly 10 digits. */
|
||||
export const TIN_REGEX = /^\d{10}$/;
|
||||
|
||||
/**
|
||||
* Draft companies carry a placeholder TIN ("D" + 9 digits) minted server-side by
|
||||
* CompaniesService.generateDraftTin(), because the column is NOT NULL + unique.
|
||||
* Those never travel through a DTO, so this constraint deliberately rejects them
|
||||
* — a "D…" value arriving on a request body is client-supplied and invalid.
|
||||
*/
|
||||
@ValidatorConstraint({ name: 'IsTin', async: false })
|
||||
export class IsTinConstraint implements ValidatorConstraintInterface {
|
||||
validate(value: unknown): boolean {
|
||||
// Empty is allowed here; pair with @IsOptional / @IsNotEmpty as needed.
|
||||
if (value === undefined || value === null || value === '') return true;
|
||||
if (typeof value !== 'string') return false;
|
||||
return TIN_REGEX.test(value);
|
||||
}
|
||||
|
||||
defaultMessage(args: ValidationArguments): string {
|
||||
return `${args.property} must be exactly 10 digits`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Class-validator decorator enforcing the 10-digit TIN format. */
|
||||
export function IsTin(validationOptions?: ValidationOptions) {
|
||||
return function (object: object, propertyName: string) {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsTinConstraint,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip everything that isn't a digit and cap at 10 characters. Tolerant —
|
||||
* never throws; returns the value unchanged when empty/nullish.
|
||||
*/
|
||||
export function normalizeTin(
|
||||
value: string | null | undefined,
|
||||
): string | null | undefined {
|
||||
if (value === undefined || value === null || value === '') return value;
|
||||
return value.replace(/\D/g, '').slice(0, 10);
|
||||
}
|
||||
@@ -67,6 +67,8 @@ export class CompaniesRepository extends BaseRepository<Company> {
|
||||
kind,
|
||||
status,
|
||||
onboardingCompleted,
|
||||
sortBy = 'name',
|
||||
sortOrder = 'ASC',
|
||||
} = query;
|
||||
|
||||
const qb = this.repository
|
||||
@@ -113,8 +115,12 @@ export class CompaniesRepository extends BaseRepository<Company> {
|
||||
);
|
||||
}
|
||||
|
||||
// sortBy is whitelisted by @IsIn on the DTO, so it is safe to interpolate.
|
||||
const [items, total] = await qb
|
||||
.orderBy('company.name', 'ASC')
|
||||
.orderBy(`company.${sortBy}`, sortOrder)
|
||||
// Names are not unique and createdAt can tie on bulk imports; the id
|
||||
// tiebreaker keeps paging stable instead of dropping/repeating rows.
|
||||
.addOrderBy('company.id', 'ASC')
|
||||
.skip((page - 1) * pageSize)
|
||||
.take(pageSize)
|
||||
.getManyAndCount();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Type } from 'class-transformer';
|
||||
import { CompanyType } from '../entities/company.entity';
|
||||
import { ProfileType } from '../entities/company-profile.entity';
|
||||
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
||||
import { IsTin } from '../../../common/validators/is-tin.validator';
|
||||
|
||||
export class CompanyProfileInputDto {
|
||||
@IsEnum(ProfileType)
|
||||
@@ -45,7 +46,7 @@ export class CreateCompanyWithProfileDto {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(10)
|
||||
@IsTin({ message: 'TIN must be exactly 10 digits' })
|
||||
tin?: string;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, IsEnum, MaxLength, Length, IsEmail } from 'class-validator';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsEnum, MaxLength, IsEmail } from 'class-validator';
|
||||
import { CompanyType, CompanyStatus } from '../entities/company.entity';
|
||||
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
||||
import { IsTin } from '../../../common/validators/is-tin.validator';
|
||||
|
||||
export class CreateCompanyDto {
|
||||
@IsString()
|
||||
@@ -17,7 +18,7 @@ export class CreateCompanyDto {
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(10, 10, { message: 'TIN must be exactly 10 digits' })
|
||||
@IsTin({ message: 'TIN must be exactly 10 digits' })
|
||||
tin!: string;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { IsString, IsNotEmpty, Length } from "class-validator";
|
||||
import { IsString, IsNotEmpty } from "class-validator";
|
||||
import { IsTin } from "../../../common/validators/is-tin.validator";
|
||||
|
||||
export class FetchETradeDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(10, 10, { message: "TIN must be exactly 10 digits" })
|
||||
@IsTin({ message: "TIN must be exactly 10 digits" })
|
||||
tin!: string;
|
||||
}
|
||||
|
||||
@@ -47,4 +47,19 @@ export class ListCompaniesQueryDto {
|
||||
@Transform(({ value }: { value: unknown }) => value === "true" || value === true)
|
||||
@IsBoolean()
|
||||
onboardingCompleted?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: ["name", "createdAt", "updatedAt"],
|
||||
default: "name",
|
||||
description: "Column to order by. Defaults to name for backwards compatibility.",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(["name", "createdAt", "updatedAt"])
|
||||
sortBy?: "name" | "createdAt" | "updatedAt";
|
||||
|
||||
@ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "ASC" })
|
||||
@IsOptional()
|
||||
@Transform(({ value }: { value: unknown }) => String(value).toUpperCase())
|
||||
@IsIn(["ASC", "DESC"])
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IsString, IsOptional, IsEmail, MaxLength, Length, IsEnum } from 'class-validator';
|
||||
import { IsString, IsOptional, IsEmail, MaxLength, IsEnum } from 'class-validator';
|
||||
import { CompanyNationality } from '../entities/company.entity';
|
||||
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
||||
import { IsTin } from '../../../common/validators/is-tin.validator';
|
||||
|
||||
export class UpdateProfileDto {
|
||||
@IsOptional()
|
||||
@@ -34,7 +35,7 @@ export class UpdateProfileDto {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(10, 10, { message: 'TIN must be exactly 10 digits' })
|
||||
@IsTin({ message: 'TIN must be exactly 10 digits' })
|
||||
tin?: string;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
Reference in New Issue
Block a user