Add products and sales management modules with database schema and validation
- Introduced `ProductsModule` to manage product data, including read and write controllers. - Created database migrations for the `products`, `sales_requests`, `sales_orders`, `sales_invoices`, and related tables, including constraints and unique indexes. - Implemented validation for product fields such as code, name, unit, and brand with corresponding utility functions. - Developed service and repository layers for handling product and sales data operations. - Added unit tests for the products and sales services, repositories, and controllers to ensure functionality and correctness. - Updated application module to include the new `ProductsModule` and related sales modules for better organization.
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
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 {
|
||||
DOCUMENT_ADDRESS_MAX_LENGTH,
|
||||
DOCUMENT_CODE_MAX_LENGTH,
|
||||
DOCUMENT_CODE_PATTERN,
|
||||
DOCUMENT_NOTES_MAX_LENGTH,
|
||||
IMAGE_DESCRIPTION_MAX_LENGTH,
|
||||
IMAGE_URL_MAX_LENGTH,
|
||||
SALES_REQUEST_STATUSES,
|
||||
} from '../../shared/sales-fields';
|
||||
|
||||
export class SalesLineDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID('4')
|
||||
productId!: string;
|
||||
|
||||
@ApiProperty({ example: '2.0000' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
quantity!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '12500.0000' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
price?: string;
|
||||
}
|
||||
|
||||
export class SalesImageDto {
|
||||
@ApiProperty({ example: 'https://cdn.example.com/a.png' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(IMAGE_URL_MAX_LENGTH)
|
||||
url!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(IMAGE_DESCRIPTION_MAX_LENGTH)
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class CreateSalesRequestDto {
|
||||
@ApiPropertyOptional({ example: 'SR-20260824-0001' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(DOCUMENT_CODE_MAX_LENGTH)
|
||||
@Matches(DOCUMENT_CODE_PATTERN)
|
||||
code?: string;
|
||||
|
||||
@ApiProperty({ example: '2026-08-24T10:00:00+07:00' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
date!: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID('4')
|
||||
salesPersonId!: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID('4')
|
||||
branchId!: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID('4')
|
||||
divisionId!: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID('4')
|
||||
customerId!: string;
|
||||
|
||||
@ApiProperty({ example: 'Jl Sudirman 1' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(DOCUMENT_ADDRESS_MAX_LENGTH)
|
||||
address!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
latitude?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
longitude?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(DOCUMENT_NOTES_MAX_LENGTH)
|
||||
notes?: string;
|
||||
|
||||
@ApiProperty({ type: [SalesLineDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SalesLineDto)
|
||||
products!: SalesLineDto[];
|
||||
|
||||
@ApiPropertyOptional({ type: [SalesImageDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SalesImageDto)
|
||||
images?: SalesImageDto[];
|
||||
|
||||
@ApiPropertyOptional({ enum: SALES_REQUEST_STATUSES })
|
||||
@IsOptional()
|
||||
@IsIn([...SALES_REQUEST_STATUSES])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class UpdateSalesRequestDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(DOCUMENT_CODE_MAX_LENGTH)
|
||||
@Matches(DOCUMENT_CODE_PATTERN)
|
||||
code?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
date?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
salesPersonId?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
branchId?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
divisionId?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
customerId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(DOCUMENT_ADDRESS_MAX_LENGTH)
|
||||
address?: string;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
latitude?: number | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
longitude?: number | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ type: [SalesLineDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SalesLineDto)
|
||||
products?: SalesLineDto[];
|
||||
|
||||
@ApiPropertyOptional({ type: [SalesImageDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SalesImageDto)
|
||||
images?: SalesImageDto[];
|
||||
}
|
||||
|
||||
export class UpdateSalesRequestStatusDto {
|
||||
@ApiProperty({ enum: SALES_REQUEST_STATUSES })
|
||||
@IsIn([...SALES_REQUEST_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: SALES_REQUEST_STATUSES })
|
||||
@IsIn([...SALES_REQUEST_STATUSES])
|
||||
status!: string;
|
||||
}
|
||||
|
||||
export class ListSalesRequestsQueryDto extends PaginationQueryDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: SALES_REQUEST_STATUSES })
|
||||
@IsOptional()
|
||||
@IsIn([...SALES_REQUEST_STATUSES])
|
||||
status?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
customerId?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
salesPersonId?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
branchId?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
divisionId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export class SalesRequestDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
id!: string;
|
||||
@ApiProperty()
|
||||
code!: string;
|
||||
@ApiProperty()
|
||||
date!: number;
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
salesPersonId!: string;
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
branchId!: string;
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
divisionId!: string;
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
customerId!: string;
|
||||
@ApiProperty()
|
||||
address!: string;
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
latitude!: number | null;
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
longitude!: number | null;
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
notes!: string | null;
|
||||
@ApiProperty({ enum: SALES_REQUEST_STATUSES })
|
||||
status!: string;
|
||||
@ApiProperty()
|
||||
createdAt!: number;
|
||||
@ApiProperty()
|
||||
updatedAt!: number;
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
createdBy!: string;
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
updatedBy!: string;
|
||||
}
|
||||
Reference in New Issue
Block a user