import { Controller, Post, Get, Delete, Param, Body, UseGuards, UseInterceptors, UploadedFile, HttpCode, HttpStatus, Logger, BadRequestException, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiConsumes, ApiBody, } from '@nestjs/swagger'; import { diskStorage } from 'multer'; import { extname } from 'path'; import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; import { RolesGuard } from '../../guards/roles.guard'; import { Roles } from '../../decorators/roles.decorator'; import { CurrentUser, UserPayload } from '../../decorators/current-user.decorator'; import { CsvRateLoaderAdapter } from '@infrastructure/carriers/csv-loader/csv-rate-loader.adapter'; import { TypeOrmCsvRateConfigRepository } from '@infrastructure/persistence/typeorm/repositories/typeorm-csv-rate-config.repository'; import { CsvRateUploadDto, CsvRateUploadResponseDto, CsvRateConfigDto, CsvFileValidationDto, } from '../../dto/csv-rate-upload.dto'; import { CsvRateMapper } from '../../mappers/csv-rate.mapper'; /** * CSV Rates Admin Controller * * ADMIN-ONLY endpoints for managing CSV rate files * Protected by JWT + Roles guard */ @ApiTags('Admin - CSV Rates') @Controller('admin/csv-rates') @ApiBearerAuth() @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN') // ⚠️ ONLY ADMIN can access these endpoints export class CsvRatesAdminController { private readonly logger = new Logger(CsvRatesAdminController.name); constructor( private readonly csvLoader: CsvRateLoaderAdapter, private readonly csvConfigRepository: TypeOrmCsvRateConfigRepository, private readonly csvRateMapper: CsvRateMapper ) {} /** * Upload CSV rate file (ADMIN only) */ @Post('upload') @HttpCode(HttpStatus.CREATED) @UseInterceptors( FileInterceptor('file', { storage: diskStorage({ destination: './apps/backend/src/infrastructure/storage/csv-storage/rates', filename: (req, file, cb) => { // Generate filename: company-name.csv const companyName = req.body.companyName || 'unknown'; const sanitized = companyName .toLowerCase() .replace(/\s+/g, '-') .replace(/[^a-z0-9-]/g, ''); const filename = `${sanitized}.csv`; cb(null, filename); }, }), fileFilter: (req, file, cb) => { // Only allow CSV files if (extname(file.originalname).toLowerCase() !== '.csv') { return cb(new BadRequestException('Only CSV files are allowed'), false); } cb(null, true); }, limits: { fileSize: 10 * 1024 * 1024, // 10MB max }, }) ) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Upload CSV rate file (ADMIN only)', description: 'Upload a CSV file containing shipping rates for a carrier company. File must be valid CSV format with required columns. Maximum file size: 10MB.', }) @ApiBody({ schema: { type: 'object', required: ['companyName', 'file'], properties: { companyName: { type: 'string', description: 'Carrier company name', example: 'SSC Consolidation', }, file: { type: 'string', format: 'binary', description: 'CSV file to upload', }, }, }, }) @ApiResponse({ status: HttpStatus.CREATED, description: 'CSV file uploaded and validated successfully', type: CsvRateUploadResponseDto, }) @ApiResponse({ status: 400, description: 'Invalid file format or validation failed', }) @ApiResponse({ status: 403, description: 'Forbidden - Admin role required', }) async uploadCsv( @UploadedFile() file: Express.Multer.File, @Body() dto: CsvRateUploadDto, @CurrentUser() user: UserPayload ): Promise { this.logger.log(`[Admin: ${user.email}] Uploading CSV for company: ${dto.companyName}`); if (!file) { throw new BadRequestException('File is required'); } try { // Validate CSV file structure const validation = await this.csvLoader.validateCsvFile(file.filename); if (!validation.valid) { this.logger.error( `CSV validation failed for ${dto.companyName}: ${validation.errors.join(', ')}` ); throw new BadRequestException({ message: 'CSV validation failed', errors: validation.errors, }); } // Load rates to verify parsing const rates = await this.csvLoader.loadRatesFromCsv(file.filename); const ratesCount = rates.length; this.logger.log(`Successfully parsed ${ratesCount} rates from ${file.filename}`); // Check if config exists for this company const existingConfig = await this.csvConfigRepository.findByCompanyName(dto.companyName); if (existingConfig) { // Update existing configuration await this.csvConfigRepository.update(existingConfig.id, { csvFilePath: file.filename, uploadedAt: new Date(), uploadedBy: user.id, rowCount: ratesCount, lastValidatedAt: new Date(), metadata: { ...existingConfig.metadata, lastUpload: { timestamp: new Date().toISOString(), by: user.email, ratesCount, }, }, }); this.logger.log(`Updated CSV config for company: ${dto.companyName}`); } else { // Create new configuration await this.csvConfigRepository.create({ companyName: dto.companyName, csvFilePath: file.filename, type: 'CSV_ONLY', hasApi: false, apiConnector: null, isActive: true, uploadedAt: new Date(), uploadedBy: user.id, rowCount: ratesCount, lastValidatedAt: new Date(), metadata: { uploadedBy: user.email, description: `${dto.companyName} shipping rates`, }, }); this.logger.log(`Created new CSV config for company: ${dto.companyName}`); } return { success: true, ratesCount, csvFilePath: file.filename, companyName: dto.companyName, uploadedAt: new Date(), }; } catch (error: any) { this.logger.error(`CSV upload failed: ${error?.message || 'Unknown error'}`, error?.stack); throw error; } } /** * Get all CSV rate configurations */ @Get('config') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get all CSV rate configurations (ADMIN only)', description: 'Returns list of all CSV rate configurations with upload details.', }) @ApiResponse({ status: HttpStatus.OK, description: 'List of CSV rate configurations', type: [CsvRateConfigDto], }) async getAllConfigs(): Promise { this.logger.log('Fetching all CSV rate configs (admin)'); const configs = await this.csvConfigRepository.findAll(); return this.csvRateMapper.mapConfigEntitiesToDtos(configs); } /** * Get configuration for specific company */ @Get('config/:companyName') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get CSV configuration for specific company (ADMIN only)', description: 'Returns CSV rate configuration details for a specific carrier.', }) @ApiResponse({ status: HttpStatus.OK, description: 'CSV rate configuration', type: CsvRateConfigDto, }) @ApiResponse({ status: 404, description: 'Company configuration not found', }) async getConfigByCompany(@Param('companyName') companyName: string): Promise { this.logger.log(`Fetching CSV config for company: ${companyName}`); const config = await this.csvConfigRepository.findByCompanyName(companyName); if (!config) { throw new BadRequestException(`No CSV configuration found for company: ${companyName}`); } return this.csvRateMapper.mapConfigEntityToDto(config); } /** * Validate CSV file */ @Post('validate/:companyName') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Validate CSV file for company (ADMIN only)', description: 'Validates the CSV file structure and data for a specific company without uploading.', }) @ApiResponse({ status: HttpStatus.OK, description: 'Validation result', type: CsvFileValidationDto, }) async validateCsvFile(@Param('companyName') companyName: string): Promise { this.logger.log(`Validating CSV file for company: ${companyName}`); const config = await this.csvConfigRepository.findByCompanyName(companyName); if (!config) { throw new BadRequestException(`No CSV configuration found for company: ${companyName}`); } const result = await this.csvLoader.validateCsvFile(config.csvFilePath); // Update validation timestamp if (result.valid && result.rowCount) { await this.csvConfigRepository.updateValidationInfo(companyName, result.rowCount, result); } return result; } /** * Delete CSV rate configuration */ @Delete('config/:companyName') @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Delete CSV rate configuration (ADMIN only)', description: 'Deletes the CSV rate configuration for a company. Note: This does not delete the actual CSV file.', }) @ApiResponse({ status: HttpStatus.NO_CONTENT, description: 'Configuration deleted successfully', }) @ApiResponse({ status: 404, description: 'Company configuration not found', }) async deleteConfig( @Param('companyName') companyName: string, @CurrentUser() user: UserPayload ): Promise { this.logger.warn(`[Admin: ${user.email}] Deleting CSV config for company: ${companyName}`); await this.csvConfigRepository.delete(companyName); this.logger.log(`Deleted CSV config for company: ${companyName}`); } }