xpeditis2.0/apps/backend/src/application/controllers/admin/csv-rates.controller.ts
2025-10-27 20:54:01 +01:00

332 lines
9.9 KiB
TypeScript

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<CsvRateUploadResponseDto> {
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<CsvRateConfigDto[]> {
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<CsvRateConfigDto> {
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<CsvFileValidationDto> {
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<void> {
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}`);
}
}