Add customers management module with database schema and validation
- Introduced `CustomersModule` to manage customer data, including read and write controllers. - Created database migrations for the `customers` and `customer_contacts` tables, including constraints and unique indexes. - Implemented validation for customer fields such as name, code, and address with corresponding utility functions. - Developed service and repository layers for handling customer data operations. - Added unit tests for the customers service, repository, and controllers to ensure functionality and correctness. - Updated application module to include the new `CustomersModule` for better organization.
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Matches,
|
||||
Max,
|
||||
MaxLength,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { PaginationQueryDto } from '../../../../common/http/response';
|
||||
import { CORE_STATUSES } from '../../../../common/value-objects/status/status';
|
||||
import {
|
||||
CONTACT_JOB_TITLE_MAX_LENGTH,
|
||||
CONTACT_NAME_MAX_LENGTH,
|
||||
CONTACT_NOTES_MAX_LENGTH,
|
||||
CUSTOMER_ADDRESS_MAX_LENGTH,
|
||||
CUSTOMER_CODE_MAX_LENGTH,
|
||||
CUSTOMER_CODE_PATTERN,
|
||||
CUSTOMER_NAME_MAX_LENGTH,
|
||||
CUSTOMER_NAME_PATTERN,
|
||||
CUSTOMER_NFC_ID_MAX_LENGTH,
|
||||
} from '../customer-fields';
|
||||
|
||||
export class CreateCustomerContactDto {
|
||||
@ApiProperty({ example: 'Jean Luc', maxLength: CONTACT_NAME_MAX_LENGTH })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(CONTACT_NAME_MAX_LENGTH)
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Purchasing Manager' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(CONTACT_JOB_TITLE_MAX_LENGTH)
|
||||
jobTitle?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '+6281234567890' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '+6281234567891' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
mobilePhone?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Call after 9am' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(CONTACT_NOTES_MAX_LENGTH)
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class UpdateCustomerContactDto {
|
||||
@ApiPropertyOptional({ example: 'Jean Luc' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(CONTACT_NAME_MAX_LENGTH)
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Purchasing Manager', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(CONTACT_JOB_TITLE_MAX_LENGTH)
|
||||
jobTitle?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ example: '+6281234567890', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ example: '+6281234567891', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mobilePhone?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Call after 9am', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(CONTACT_NOTES_MAX_LENGTH)
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export class CustomerContactDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
customerId!: string;
|
||||
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
jobTitle!: string | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
phone!: string | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
mobilePhone!: string | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
notes!: string | null;
|
||||
}
|
||||
|
||||
export class CreateCustomerDto {
|
||||
@ApiProperty({ example: 'CUST_01', maxLength: CUSTOMER_CODE_MAX_LENGTH })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(CUSTOMER_CODE_MAX_LENGTH)
|
||||
@Matches(CUSTOMER_CODE_PATTERN, {
|
||||
message: 'code must contain only letters, numbers, and underscores',
|
||||
})
|
||||
code!: string;
|
||||
|
||||
@ApiProperty({ example: 'Acme Corp', maxLength: CUSTOMER_NAME_MAX_LENGTH })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(CUSTOMER_NAME_MAX_LENGTH)
|
||||
@Matches(CUSTOMER_NAME_PATTERN, {
|
||||
message: 'name must contain only letters and spaces',
|
||||
})
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: '+6281234567890' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone!: string;
|
||||
|
||||
@ApiProperty({ example: 'Jl Sudirman No 1' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(CUSTOMER_ADDRESS_MAX_LENGTH)
|
||||
address!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: -6.2 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
latitude?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 106.8 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
longitude?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 'NFC-001' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(CUSTOMER_NFC_ID_MAX_LENGTH)
|
||||
nfcId?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: CORE_STATUSES })
|
||||
@IsOptional()
|
||||
@IsIn([...CORE_STATUSES])
|
||||
status?: string;
|
||||
|
||||
@ApiPropertyOptional({ type: [CreateCustomerContactDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateCustomerContactDto)
|
||||
contacts?: CreateCustomerContactDto[];
|
||||
}
|
||||
|
||||
export class UpdateCustomerDto {
|
||||
@ApiPropertyOptional({ example: 'CUST_01' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(CUSTOMER_CODE_MAX_LENGTH)
|
||||
@Matches(CUSTOMER_CODE_PATTERN, {
|
||||
message: 'code must contain only letters, numbers, and underscores',
|
||||
})
|
||||
code?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Acme Corp' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(CUSTOMER_NAME_MAX_LENGTH)
|
||||
@Matches(CUSTOMER_NAME_PATTERN, {
|
||||
message: 'name must contain only letters and spaces',
|
||||
})
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '+6281234567890' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Jl Sudirman No 1' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(CUSTOMER_ADDRESS_MAX_LENGTH)
|
||||
address?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: -6.2, nullable: true })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
latitude?: number | null;
|
||||
|
||||
@ApiPropertyOptional({ example: 106.8, nullable: true })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
longitude?: number | null;
|
||||
|
||||
@ApiPropertyOptional({ example: 'NFC-001', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(CUSTOMER_NFC_ID_MAX_LENGTH)
|
||||
nfcId?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ type: [CreateCustomerContactDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateCustomerContactDto)
|
||||
contacts?: CreateCustomerContactDto[];
|
||||
}
|
||||
|
||||
export class UpdateCustomerStatusDto {
|
||||
@ApiProperty({ enum: CORE_STATUSES })
|
||||
@IsIn([...CORE_STATUSES])
|
||||
status!: string;
|
||||
}
|
||||
|
||||
export class BulkIdsDto {
|
||||
@ApiProperty({ type: [String], format: 'uuid' })
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsUUID('4', { each: true })
|
||||
ids!: string[];
|
||||
}
|
||||
|
||||
export class BulkStatusDto {
|
||||
@ApiProperty({ type: [String], format: 'uuid' })
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsUUID('4', { each: true })
|
||||
ids!: string[];
|
||||
|
||||
@ApiProperty({ enum: CORE_STATUSES })
|
||||
@IsIn([...CORE_STATUSES])
|
||||
status!: string;
|
||||
}
|
||||
|
||||
export class ListCustomersQueryDto extends PaginationQueryDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nfcId?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: CORE_STATUSES })
|
||||
@IsOptional()
|
||||
@IsIn([...CORE_STATUSES])
|
||||
status?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Case-insensitive match on code, name, or address',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export class CustomerDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
code!: string;
|
||||
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: '+6281234567890' })
|
||||
phone!: string;
|
||||
|
||||
@ApiProperty()
|
||||
address!: string;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
latitude!: number | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
longitude!: number | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
nfcId!: string | null;
|
||||
|
||||
@ApiPropertyOptional({ type: [CustomerContactDto] })
|
||||
contacts?: CustomerContactDto[];
|
||||
|
||||
@ApiProperty({ enum: CORE_STATUSES })
|
||||
status!: string;
|
||||
|
||||
@ApiProperty({ description: 'Unix ms' })
|
||||
createdAt!: number;
|
||||
|
||||
@ApiProperty({ description: 'Unix ms' })
|
||||
updatedAt!: number;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
createdBy!: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
updatedBy!: string;
|
||||
}
|
||||
Reference in New Issue
Block a user