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:
shancheas
2026-08-24 14:23:20 +07:00
parent d28506e878
commit cdcc508947
20 changed files with 4123 additions and 2 deletions
@@ -0,0 +1,227 @@
import {
BadRequestException,
Body,
Controller,
Delete,
HttpCode,
Param,
ParseUUIDPipe,
Patch,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiCreatedResponse,
ApiForbiddenResponse,
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { CurrentUser } from '../../../common/decorators/current-user.decorator';
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
import { isAllowedCsvUpload } from './customer-fields';
import { CUSTOMER_PRIVILEGE_KEY } from './customers-read.controller';
import { CustomersService } from './customers.service';
import {
BulkIdsDto,
BulkStatusDto,
CreateCustomerContactDto,
CreateCustomerDto,
CustomerDto,
UpdateCustomerContactDto,
UpdateCustomerDto,
UpdateCustomerStatusDto,
} from './dto/customer.dto';
@ApiTags('customers')
@ApiBearerAuth(BEARER_AUTH_NAME)
@Controller('customers')
export class CustomersWriteController {
constructor(private readonly customersService: CustomersService) {}
@Post('import')
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'import')
@UseInterceptors(
FileInterceptor('file', {
limits: { fileSize: 1_048_576 },
fileFilter: (_req, file, cb) => {
if (!isAllowedCsvUpload(file)) {
cb(new BadRequestException('Only CSV files are allowed'), false);
return;
}
cb(null, true);
},
}),
)
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
file: { type: 'string', format: 'binary' },
},
required: ['file'],
},
})
@ApiOperation({ summary: 'Import customers from CSV' })
@ApiOkResponse({
schema: { properties: { imported: { type: 'number' } } },
})
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
importCsv(
@UploadedFile() file: { buffer?: Buffer } | undefined,
@CurrentUser('id') userId: string,
): Promise<{ imported: number }> {
const csv = file?.buffer?.toString('utf8') ?? '';
return this.customersService.importCsv(csv, userId);
}
@Post('bulk-delete')
@HttpCode(200)
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'delete')
@ApiOperation({ summary: 'Bulk delete customers' })
@ApiOkResponse({
schema: { properties: { deleted: { type: 'number' } } },
})
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
bulkDelete(@Body() dto: BulkIdsDto): Promise<{ deleted: number }> {
return this.customersService.bulkDelete(dto.ids);
}
@Post('bulk-status')
@HttpCode(200)
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
@ApiOperation({ summary: 'Bulk update customer status' })
@ApiOkResponse({
schema: { properties: { updated: { type: 'number' } } },
})
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
bulkStatus(
@Body() dto: BulkStatusDto,
@CurrentUser('id') userId: string,
): Promise<{ updated: number }> {
return this.customersService.bulkUpdateStatus(dto.ids, dto.status, userId);
}
@Post()
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'create')
@ApiOperation({ summary: 'Create customer' })
@ApiCreatedResponse({ type: CustomerDto })
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
create(
@Body() dto: CreateCustomerDto,
@CurrentUser('id') userId: string,
): Promise<CustomerDto> {
return this.customersService.create({
...dto,
userId,
});
}
@Post(':id/contacts')
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
@ApiOperation({ summary: 'Add a customer contact' })
@ApiOkResponse({ type: CustomerDto })
@ApiNotFoundResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
addContact(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: CreateCustomerContactDto,
@CurrentUser('id') userId: string,
): Promise<CustomerDto> {
return this.customersService.addContact(id, dto, userId);
}
@Patch(':id/contacts/:contactId')
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
@ApiOperation({ summary: 'Update a customer contact' })
@ApiOkResponse({ type: CustomerDto })
@ApiNotFoundResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
updateContact(
@Param('id', ParseUUIDPipe) id: string,
@Param('contactId', ParseUUIDPipe) contactId: string,
@Body() dto: UpdateCustomerContactDto,
@CurrentUser('id') userId: string,
): Promise<CustomerDto> {
return this.customersService.updateContact(id, contactId, {
...dto,
userId,
});
}
@Delete(':id/contacts/:contactId')
@HttpCode(204)
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
@ApiOperation({ summary: 'Delete a customer contact' })
@ApiNoContentResponse()
@ApiNotFoundResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
async deleteContact(
@Param('id', ParseUUIDPipe) id: string,
@Param('contactId', ParseUUIDPipe) contactId: string,
): Promise<void> {
await this.customersService.deleteContact(id, contactId);
}
@Patch(':id/status')
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
@ApiOperation({ summary: 'Update customer status' })
@ApiOkResponse({ type: CustomerDto })
@ApiNotFoundResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
updateStatus(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateCustomerStatusDto,
@CurrentUser('id') userId: string,
): Promise<CustomerDto> {
return this.customersService.updateStatus(id, dto.status, userId);
}
@Patch(':id')
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
@ApiOperation({ summary: 'Update customer (not status)' })
@ApiOkResponse({ type: CustomerDto })
@ApiNotFoundResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateCustomerDto,
@CurrentUser('id') userId: string,
): Promise<CustomerDto> {
return this.customersService.update(id, {
...dto,
userId,
});
}
@Delete(':id')
@HttpCode(204)
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'delete')
@ApiOperation({ summary: 'Delete customer' })
@ApiNoContentResponse()
@ApiNotFoundResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
async delete(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
await this.customersService.delete(id);
}
}