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:
shancheas
2026-08-25 09:54:16 +07:00
parent ddbe8a9ef8
commit 34d4f2c120
82 changed files with 12609 additions and 2 deletions
@@ -0,0 +1,199 @@
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 '../shared/sales-fields';
import { SALES_INVOICE_PRIVILEGE_KEY } from './sales-invoices-read.controller';
import { SalesInvoicesService } from './sales-invoices.service';
import {
BulkIdsDto,
BulkStatusDto,
CreateSalesInvoiceDto,
SalesInvoiceDto,
UpdateSalesInvoiceDto,
UpdateSalesInvoiceStatusDto,
} from './dto/sales-invoice.dto';
@ApiTags('sales-invoices')
@ApiBearerAuth(BEARER_AUTH_NAME)
@Controller('sales-invoices')
export class SalesInvoicesWriteController {
constructor(private readonly salesInvoicesService: SalesInvoicesService) {}
@Post('import')
@RequirePrivilege(SALES_INVOICE_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 sales invoices 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.salesInvoicesService.importCsv(csv, userId);
}
@Post('bulk-delete')
@HttpCode(200)
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'delete')
@ApiOperation({ summary: 'Bulk delete sales invoices' })
@ApiOkResponse({
schema: { properties: { deleted: { type: 'number' } } },
})
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
bulkDelete(@Body() dto: BulkIdsDto): Promise<{ deleted: number }> {
return this.salesInvoicesService.bulkDelete(dto.ids);
}
@Post('bulk-status')
@HttpCode(200)
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'update')
@ApiOperation({ summary: 'Bulk update sales invoice status' })
@ApiOkResponse({
schema: { properties: { updated: { type: 'number' } } },
})
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
bulkStatus(
@Body() dto: BulkStatusDto,
@CurrentUser('id') userId: string,
): Promise<{ updated: number }> {
return this.salesInvoicesService.bulkUpdateStatus(
dto.ids,
dto.status,
userId,
);
}
@Post()
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'create')
@ApiOperation({ summary: 'Create sales invoice' })
@ApiCreatedResponse({ type: SalesInvoiceDto })
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
create(
@Body() dto: CreateSalesInvoiceDto,
@CurrentUser('id') userId: string,
): Promise<SalesInvoiceDto> {
return this.salesInvoicesService.create({
code: dto.code,
salesOrderId: dto.salesOrderId,
packingSlipId: dto.packingSlipId,
date: dto.date,
salesPersonId: dto.salesPersonId,
branchId: dto.branchId,
divisionId: dto.divisionId,
customerId: dto.customerId,
notes: dto.notes,
products: dto.products,
status: dto.status,
userId,
});
}
@Patch(':id/status')
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'update')
@ApiOperation({ summary: 'Update sales invoice status' })
@ApiOkResponse({ type: SalesInvoiceDto })
@ApiNotFoundResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
updateStatus(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateSalesInvoiceStatusDto,
@CurrentUser('id') userId: string,
): Promise<SalesInvoiceDto> {
return this.salesInvoicesService.updateStatus(id, dto.status, userId);
}
@Patch(':id')
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'update')
@ApiOperation({ summary: 'Update sales invoice (not status)' })
@ApiOkResponse({ type: SalesInvoiceDto })
@ApiNotFoundResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateSalesInvoiceDto,
@CurrentUser('id') userId: string,
): Promise<SalesInvoiceDto> {
return this.salesInvoicesService.update(id, {
code: dto.code,
salesOrderId: dto.salesOrderId,
packingSlipId: dto.packingSlipId,
date: dto.date,
salesPersonId: dto.salesPersonId,
branchId: dto.branchId,
divisionId: dto.divisionId,
customerId: dto.customerId,
notes: dto.notes,
products: dto.products,
userId,
});
}
@Delete(':id')
@HttpCode(204)
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'delete')
@ApiOperation({ summary: 'Delete sales invoice' })
@ApiNoContentResponse()
@ApiNotFoundResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
async delete(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
await this.salesInvoicesService.delete(id);
}
}