332 lines
9.9 KiB
TypeScript
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}`);
|
|
}
|
|
}
|