864 lines
26 KiB
TypeScript
864 lines
26 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Patch,
|
|
Delete,
|
|
Param,
|
|
Body,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Logger,
|
|
UsePipes,
|
|
ValidationPipe,
|
|
NotFoundException,
|
|
BadRequestException,
|
|
ParseUUIDPipe,
|
|
UseGuards,
|
|
Inject,
|
|
} from '@nestjs/common';
|
|
import {
|
|
ApiTags,
|
|
ApiOperation,
|
|
ApiResponse,
|
|
ApiNotFoundResponse,
|
|
ApiParam,
|
|
ApiBearerAuth,
|
|
} from '@nestjs/swagger';
|
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
|
import { RolesGuard } from '../guards/roles.guard';
|
|
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
|
|
import { Roles } from '../decorators/roles.decorator';
|
|
|
|
// User imports
|
|
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
|
import { UserMapper } from '../mappers/user.mapper';
|
|
import { UpdateUserDto, UserResponseDto, UserListResponseDto } from '../dto/user.dto';
|
|
|
|
// Organization imports
|
|
import {
|
|
OrganizationRepository,
|
|
ORGANIZATION_REPOSITORY,
|
|
} from '@domain/ports/out/organization.repository';
|
|
import { OrganizationMapper } from '../mappers/organization.mapper';
|
|
import { OrganizationResponseDto, OrganizationListResponseDto } from '../dto/organization.dto';
|
|
|
|
// CSV Booking imports
|
|
import { TypeOrmCsvBookingRepository } from '@infrastructure/persistence/typeorm/repositories/csv-booking.repository';
|
|
import { CsvBookingService } from '../services/csv-booking.service';
|
|
|
|
// SIRET verification imports
|
|
import {
|
|
SiretVerificationPort,
|
|
SIRET_VERIFICATION_PORT,
|
|
} from '@domain/ports/out/siret-verification.port';
|
|
|
|
// Email imports
|
|
import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port';
|
|
|
|
/**
|
|
* Admin Controller
|
|
*
|
|
* Dedicated controller for admin-only endpoints that provide access to ALL data
|
|
* in the database without organization filtering.
|
|
*
|
|
* All endpoints require ADMIN role.
|
|
*/
|
|
@ApiTags('Admin')
|
|
@Controller('admin')
|
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
@Roles('admin')
|
|
@ApiBearerAuth()
|
|
export class AdminController {
|
|
private readonly logger = new Logger(AdminController.name);
|
|
|
|
constructor(
|
|
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
|
|
@Inject(ORGANIZATION_REPOSITORY)
|
|
private readonly organizationRepository: OrganizationRepository,
|
|
private readonly csvBookingRepository: TypeOrmCsvBookingRepository,
|
|
private readonly csvBookingService: CsvBookingService,
|
|
@Inject(SIRET_VERIFICATION_PORT)
|
|
private readonly siretVerificationPort: SiretVerificationPort,
|
|
@Inject(EMAIL_PORT) private readonly emailPort: EmailPort
|
|
) {}
|
|
|
|
// ==================== USERS ENDPOINTS ====================
|
|
|
|
/**
|
|
* Get ALL users from database (admin only)
|
|
*
|
|
* Returns all users regardless of status (active/inactive) or organization
|
|
*/
|
|
@Get('users')
|
|
@ApiOperation({
|
|
summary: 'Get all users (Admin only)',
|
|
description:
|
|
'Retrieve ALL users from the database without any filters. Includes active and inactive users from all organizations.',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'All users retrieved successfully',
|
|
type: UserListResponseDto,
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
@ApiResponse({
|
|
status: 403,
|
|
description: 'Forbidden - requires admin role',
|
|
})
|
|
async getAllUsers(@CurrentUser() user: UserPayload): Promise<UserListResponseDto> {
|
|
this.logger.log(`[ADMIN: ${user.email}] Fetching ALL users from database`);
|
|
|
|
let users = await this.userRepository.findAll();
|
|
|
|
// Security: Non-admin users (MANAGER and below) cannot see ADMIN users
|
|
if (user.role !== 'ADMIN') {
|
|
users = users.filter(u => u.role !== 'ADMIN');
|
|
this.logger.log(`[SECURITY] Non-admin user ${user.email} - filtered out ADMIN users`);
|
|
}
|
|
|
|
const userDtos = UserMapper.toDtoArray(users);
|
|
|
|
this.logger.log(`[ADMIN] Retrieved ${users.length} users from database`);
|
|
|
|
return {
|
|
users: userDtos,
|
|
total: users.length,
|
|
page: 1,
|
|
pageSize: users.length,
|
|
totalPages: 1,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get user by ID (admin only)
|
|
*/
|
|
@Get('users/:id')
|
|
@ApiOperation({
|
|
summary: 'Get user by ID (Admin only)',
|
|
description: 'Retrieve a specific user by ID',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'User ID (UUID)',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'User retrieved successfully',
|
|
type: UserResponseDto,
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'User not found',
|
|
})
|
|
async getUserById(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<UserResponseDto> {
|
|
this.logger.log(`[ADMIN: ${user.email}] Fetching user: ${id}`);
|
|
|
|
const foundUser = await this.userRepository.findById(id);
|
|
if (!foundUser) {
|
|
throw new NotFoundException(`User ${id} not found`);
|
|
}
|
|
|
|
return UserMapper.toDto(foundUser);
|
|
}
|
|
|
|
/**
|
|
* Update user (admin only)
|
|
*/
|
|
@Patch('users/:id')
|
|
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
|
@ApiOperation({
|
|
summary: 'Update user (Admin only)',
|
|
description: 'Update user details (any user, any organization)',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'User ID (UUID)',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'User updated successfully',
|
|
type: UserResponseDto,
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'User not found',
|
|
})
|
|
async updateUser(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@Body() dto: UpdateUserDto,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<UserResponseDto> {
|
|
this.logger.log(`[ADMIN: ${user.email}] Updating user: ${id}`);
|
|
|
|
const foundUser = await this.userRepository.findById(id);
|
|
if (!foundUser) {
|
|
throw new NotFoundException(`User ${id} not found`);
|
|
}
|
|
|
|
// Security: Prevent users from changing their own role
|
|
if (dto.role && id === user.id) {
|
|
this.logger.warn(`[SECURITY] User ${user.email} attempted to change their own role`);
|
|
throw new BadRequestException('You cannot change your own role');
|
|
}
|
|
|
|
// Apply updates
|
|
if (dto.firstName) {
|
|
foundUser.updateFirstName(dto.firstName);
|
|
}
|
|
if (dto.lastName) {
|
|
foundUser.updateLastName(dto.lastName);
|
|
}
|
|
if (dto.role) {
|
|
foundUser.updateRole(dto.role);
|
|
}
|
|
if (dto.isActive !== undefined) {
|
|
if (dto.isActive) {
|
|
foundUser.activate();
|
|
} else {
|
|
foundUser.deactivate();
|
|
}
|
|
}
|
|
|
|
const updatedUser = await this.userRepository.update(foundUser);
|
|
this.logger.log(`[ADMIN] User updated successfully: ${updatedUser.id}`);
|
|
|
|
return UserMapper.toDto(updatedUser);
|
|
}
|
|
|
|
/**
|
|
* Delete user (admin only)
|
|
*/
|
|
@Delete('users/:id')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
@ApiOperation({
|
|
summary: 'Delete user (Admin only)',
|
|
description: 'Permanently delete a user from the database',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'User ID (UUID)',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.NO_CONTENT,
|
|
description: 'User deleted successfully',
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'User not found',
|
|
})
|
|
async deleteUser(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<void> {
|
|
this.logger.log(`[ADMIN: ${user.email}] Deleting user: ${id}`);
|
|
|
|
const foundUser = await this.userRepository.findById(id);
|
|
if (!foundUser) {
|
|
throw new NotFoundException(`User ${id} not found`);
|
|
}
|
|
|
|
await this.userRepository.deleteById(id);
|
|
this.logger.log(`[ADMIN] User deleted successfully: ${id}`);
|
|
}
|
|
|
|
// ==================== ORGANIZATIONS ENDPOINTS ====================
|
|
|
|
/**
|
|
* Get ALL organizations from database (admin only)
|
|
*
|
|
* Returns all organizations regardless of status (active/inactive)
|
|
*/
|
|
@Get('organizations')
|
|
@ApiOperation({
|
|
summary: 'Get all organizations (Admin only)',
|
|
description:
|
|
'Retrieve ALL organizations from the database without any filters. Includes active and inactive organizations.',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'All organizations retrieved successfully',
|
|
type: OrganizationListResponseDto,
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
@ApiResponse({
|
|
status: 403,
|
|
description: 'Forbidden - requires admin role',
|
|
})
|
|
async getAllOrganizations(
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<OrganizationListResponseDto> {
|
|
this.logger.log(`[ADMIN: ${user.email}] Fetching ALL organizations from database`);
|
|
|
|
const organizations = await this.organizationRepository.findAll();
|
|
const orgDtos = OrganizationMapper.toDtoArray(organizations);
|
|
|
|
this.logger.log(`[ADMIN] Retrieved ${organizations.length} organizations from database`);
|
|
|
|
return {
|
|
organizations: orgDtos,
|
|
total: organizations.length,
|
|
page: 1,
|
|
pageSize: organizations.length,
|
|
totalPages: 1,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get organization by ID (admin only)
|
|
*/
|
|
@Get('organizations/:id')
|
|
@ApiOperation({
|
|
summary: 'Get organization by ID (Admin only)',
|
|
description: 'Retrieve a specific organization by ID',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'Organization ID (UUID)',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'Organization retrieved successfully',
|
|
type: OrganizationResponseDto,
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'Organization not found',
|
|
})
|
|
async getOrganizationById(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<OrganizationResponseDto> {
|
|
this.logger.log(`[ADMIN: ${user.email}] Fetching organization: ${id}`);
|
|
|
|
const organization = await this.organizationRepository.findById(id);
|
|
if (!organization) {
|
|
throw new NotFoundException(`Organization ${id} not found`);
|
|
}
|
|
|
|
return OrganizationMapper.toDto(organization);
|
|
}
|
|
|
|
/**
|
|
* Verify SIRET number for an organization (admin only)
|
|
*
|
|
* Calls Pappers API to verify the SIRET, then marks the organization as verified.
|
|
*/
|
|
@Post('organizations/:id/verify-siret')
|
|
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
|
@ApiOperation({
|
|
summary: 'Verify organization SIRET (Admin only)',
|
|
description:
|
|
'Verify the SIRET number of an organization via Pappers API and mark it as verified. Required before the organization can make purchases.',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'Organization ID (UUID)',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'SIRET verification result',
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
verified: { type: 'boolean' },
|
|
companyName: { type: 'string' },
|
|
address: { type: 'string' },
|
|
message: { type: 'string' },
|
|
},
|
|
},
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'Organization not found',
|
|
})
|
|
async verifySiret(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: UserPayload) {
|
|
this.logger.log(`[ADMIN: ${user.email}] Verifying SIRET for organization: ${id}`);
|
|
|
|
const organization = await this.organizationRepository.findById(id);
|
|
if (!organization) {
|
|
throw new NotFoundException(`Organization ${id} not found`);
|
|
}
|
|
|
|
const siret = organization.siret;
|
|
if (!siret) {
|
|
throw new BadRequestException(
|
|
'Organization has no SIRET number. Please set a SIRET number before verification.'
|
|
);
|
|
}
|
|
|
|
const result = await this.siretVerificationPort.verify(siret);
|
|
|
|
if (!result.valid) {
|
|
this.logger.warn(`[ADMIN] SIRET verification failed for ${siret}`);
|
|
return {
|
|
verified: false,
|
|
message: `Le numero SIRET ${siret} est invalide ou introuvable.`,
|
|
};
|
|
}
|
|
|
|
// Mark as verified and save
|
|
organization.markSiretVerified();
|
|
await this.organizationRepository.update(organization);
|
|
|
|
this.logger.log(`[ADMIN] SIRET verified successfully for organization: ${id}`);
|
|
|
|
return {
|
|
verified: true,
|
|
companyName: result.companyName,
|
|
address: result.address,
|
|
message: `SIRET ${siret} verifie avec succes.`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Manually approve SIRET/SIREN for an organization (admin only)
|
|
*
|
|
* Marks the organization's SIRET as verified without calling the external API.
|
|
*/
|
|
@Post('organizations/:id/approve-siret')
|
|
@ApiOperation({
|
|
summary: 'Approve SIRET/SIREN (Admin only)',
|
|
description:
|
|
'Manually approve the SIRET/SIREN of an organization. Marks it as verified without calling Pappers API.',
|
|
})
|
|
@ApiParam({ name: 'id', description: 'Organization ID (UUID)' })
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'SIRET approved successfully',
|
|
})
|
|
@ApiNotFoundResponse({ description: 'Organization not found' })
|
|
async approveSiret(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: UserPayload) {
|
|
this.logger.log(`[ADMIN: ${user.email}] Manually approving SIRET for organization: ${id}`);
|
|
|
|
const organization = await this.organizationRepository.findById(id);
|
|
if (!organization) {
|
|
throw new NotFoundException(`Organization ${id} not found`);
|
|
}
|
|
|
|
if (!organization.siret && !organization.siren) {
|
|
throw new BadRequestException(
|
|
"L'organisation n'a ni SIRET ni SIREN. Veuillez en renseigner un avant l'approbation."
|
|
);
|
|
}
|
|
|
|
organization.markSiretVerified();
|
|
await this.organizationRepository.update(organization);
|
|
|
|
this.logger.log(`[ADMIN] SIRET manually approved for organization: ${id}`);
|
|
|
|
return {
|
|
approved: true,
|
|
message: 'SIRET/SIREN approuve manuellement avec succes.',
|
|
organizationId: id,
|
|
organizationName: organization.name,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reject SIRET/SIREN for an organization (admin only)
|
|
*
|
|
* Resets the verification flag to false.
|
|
*/
|
|
@Post('organizations/:id/reject-siret')
|
|
@ApiOperation({
|
|
summary: 'Reject SIRET/SIREN (Admin only)',
|
|
description:
|
|
'Reject the SIRET/SIREN of an organization. Resets the verification status to unverified.',
|
|
})
|
|
@ApiParam({ name: 'id', description: 'Organization ID (UUID)' })
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'SIRET rejected successfully',
|
|
})
|
|
@ApiNotFoundResponse({ description: 'Organization not found' })
|
|
async rejectSiret(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: UserPayload) {
|
|
this.logger.log(`[ADMIN: ${user.email}] Rejecting SIRET for organization: ${id}`);
|
|
|
|
const organization = await this.organizationRepository.findById(id);
|
|
if (!organization) {
|
|
throw new NotFoundException(`Organization ${id} not found`);
|
|
}
|
|
|
|
// Reset SIRET verification to false by updating the SIRET (which resets siretVerified)
|
|
// If no SIRET, just update directly
|
|
if (organization.siret) {
|
|
organization.updateSiret(organization.siret); // This resets siretVerified to false
|
|
}
|
|
await this.organizationRepository.update(organization);
|
|
|
|
this.logger.log(`[ADMIN] SIRET rejected for organization: ${id}`);
|
|
|
|
return {
|
|
rejected: true,
|
|
message: "SIRET/SIREN rejete. L'organisation ne pourra pas effectuer d'achats.",
|
|
organizationId: id,
|
|
organizationName: organization.name,
|
|
};
|
|
}
|
|
|
|
// ==================== CSV BOOKINGS ENDPOINTS ====================
|
|
|
|
/**
|
|
* Get ALL csv bookings from database (admin only)
|
|
*
|
|
* Returns all csv bookings from all organizations
|
|
*/
|
|
@Get('bookings')
|
|
@ApiOperation({
|
|
summary: 'Get all CSV bookings (Admin only)',
|
|
description:
|
|
'Retrieve ALL CSV bookings from the database without any filters. Includes bookings from all organizations and all statuses.',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'All CSV bookings retrieved successfully',
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
@ApiResponse({
|
|
status: 403,
|
|
description: 'Forbidden - requires admin role',
|
|
})
|
|
async getAllBookings(@CurrentUser() user: UserPayload) {
|
|
this.logger.log(`[ADMIN: ${user.email}] Fetching ALL csv bookings from database`);
|
|
|
|
const csvBookings = await this.csvBookingRepository.findAll();
|
|
const bookingDtos = csvBookings.map(booking => this.csvBookingToDto(booking));
|
|
|
|
this.logger.log(`[ADMIN] Retrieved ${csvBookings.length} csv bookings from database`);
|
|
|
|
return {
|
|
bookings: bookingDtos,
|
|
total: csvBookings.length,
|
|
page: 1,
|
|
pageSize: csvBookings.length,
|
|
totalPages: csvBookings.length > 0 ? 1 : 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get csv booking by ID (admin only)
|
|
*/
|
|
@Get('bookings/:id')
|
|
@ApiOperation({
|
|
summary: 'Get CSV booking by ID (Admin only)',
|
|
description: 'Retrieve a specific CSV booking by ID',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'Booking ID (UUID)',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'CSV booking retrieved successfully',
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'CSV booking not found',
|
|
})
|
|
async getBookingById(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: UserPayload) {
|
|
this.logger.log(`[ADMIN: ${user.email}] Fetching csv booking: ${id}`);
|
|
|
|
const csvBooking = await this.csvBookingRepository.findById(id);
|
|
if (!csvBooking) {
|
|
throw new NotFoundException(`CSV booking ${id} not found`);
|
|
}
|
|
|
|
return this.csvBookingToDto(csvBooking);
|
|
}
|
|
|
|
/**
|
|
* Update csv booking (admin only)
|
|
*/
|
|
@Patch('bookings/:id')
|
|
@ApiOperation({
|
|
summary: 'Update CSV booking (Admin only)',
|
|
description: 'Update CSV booking status or details',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'Booking ID (UUID)',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'CSV booking updated successfully',
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'CSV booking not found',
|
|
})
|
|
async updateBooking(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@Body() updateDto: any,
|
|
@CurrentUser() user: UserPayload
|
|
) {
|
|
this.logger.log(`[ADMIN: ${user.email}] Updating csv booking: ${id}`);
|
|
|
|
const csvBooking = await this.csvBookingRepository.findById(id);
|
|
if (!csvBooking) {
|
|
throw new NotFoundException(`CSV booking ${id} not found`);
|
|
}
|
|
|
|
// Apply updates to the domain entity
|
|
// Note: This is a simplified version. You may want to add proper domain methods
|
|
const updatedBooking = await this.csvBookingRepository.update(csvBooking);
|
|
|
|
this.logger.log(`[ADMIN] CSV booking updated successfully: ${id}`);
|
|
return this.csvBookingToDto(updatedBooking);
|
|
}
|
|
|
|
/**
|
|
* Resend carrier email for a booking (admin only)
|
|
*
|
|
* Manually sends the booking request email to the carrier.
|
|
* Useful when the automatic email failed (SMTP error) or for testing without Stripe.
|
|
*/
|
|
@Post('bookings/:id/resend-carrier-email')
|
|
@ApiOperation({
|
|
summary: 'Resend carrier email (Admin only)',
|
|
description:
|
|
'Manually resend the booking request email to the carrier. Works regardless of payment status.',
|
|
})
|
|
@ApiParam({ name: 'id', description: 'Booking ID (UUID)' })
|
|
@ApiResponse({ status: 200, description: 'Email sent to carrier' })
|
|
@ApiNotFoundResponse({ description: 'Booking not found' })
|
|
async resendCarrierEmail(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: UserPayload
|
|
) {
|
|
this.logger.log(`[ADMIN: ${user.email}] Resending carrier email for booking: ${id}`);
|
|
await this.csvBookingService.resendCarrierEmail(id);
|
|
return { success: true, message: 'Email sent to carrier' };
|
|
}
|
|
|
|
/**
|
|
* Validate bank transfer for a booking (admin only)
|
|
*
|
|
* Transitions booking from PENDING_BANK_TRANSFER → PENDING and sends email to carrier
|
|
*/
|
|
@Post('bookings/:id/validate-transfer')
|
|
@ApiOperation({
|
|
summary: 'Validate bank transfer (Admin only)',
|
|
description:
|
|
'Admin confirms that the bank wire transfer has been received. Activates the booking and sends email to carrier.',
|
|
})
|
|
@ApiParam({ name: 'id', description: 'Booking ID (UUID)' })
|
|
@ApiResponse({ status: 200, description: 'Bank transfer validated, booking activated' })
|
|
@ApiNotFoundResponse({ description: 'Booking not found' })
|
|
async validateBankTransfer(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: UserPayload
|
|
) {
|
|
this.logger.log(`[ADMIN: ${user.email}] Validating bank transfer for booking: ${id}`);
|
|
return this.csvBookingService.validateBankTransfer(id);
|
|
}
|
|
|
|
/**
|
|
* Delete csv booking (admin only)
|
|
*/
|
|
@Delete('bookings/:id')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
@ApiOperation({
|
|
summary: 'Delete CSV booking (Admin only)',
|
|
description: 'Permanently delete a CSV booking from the database',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'Booking ID (UUID)',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.NO_CONTENT,
|
|
description: 'CSV booking deleted successfully',
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'CSV booking not found',
|
|
})
|
|
async deleteBooking(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<void> {
|
|
this.logger.log(`[ADMIN: ${user.email}] Deleting csv booking: ${id}`);
|
|
|
|
const csvBooking = await this.csvBookingRepository.findById(id);
|
|
if (!csvBooking) {
|
|
throw new NotFoundException(`CSV booking ${id} not found`);
|
|
}
|
|
|
|
await this.csvBookingRepository.delete(id);
|
|
this.logger.log(`[ADMIN] CSV booking deleted successfully: ${id}`);
|
|
}
|
|
|
|
/**
|
|
* Helper method to convert CSV booking domain entity to DTO
|
|
*/
|
|
private csvBookingToDto(booking: any) {
|
|
const primaryCurrency = booking.primaryCurrency as 'USD' | 'EUR';
|
|
|
|
return {
|
|
id: booking.id,
|
|
bookingNumber: booking.bookingNumber || null,
|
|
userId: booking.userId,
|
|
organizationId: booking.organizationId,
|
|
carrierName: booking.carrierName,
|
|
carrierEmail: booking.carrierEmail,
|
|
origin: booking.origin.getValue(),
|
|
destination: booking.destination.getValue(),
|
|
volumeCBM: booking.volumeCBM,
|
|
weightKG: booking.weightKG,
|
|
palletCount: booking.palletCount,
|
|
priceUSD: booking.priceUSD,
|
|
priceEUR: booking.priceEUR,
|
|
primaryCurrency: booking.primaryCurrency,
|
|
transitDays: booking.transitDays,
|
|
containerType: booking.containerType,
|
|
status: booking.status,
|
|
documents: booking.documents || [],
|
|
confirmationToken: booking.confirmationToken,
|
|
requestedAt: booking.requestedAt,
|
|
respondedAt: booking.respondedAt || null,
|
|
notes: booking.notes,
|
|
rejectionReason: booking.rejectionReason,
|
|
routeDescription: booking.getRouteDescription(),
|
|
isExpired: booking.isExpired(),
|
|
price: booking.getPriceInCurrency(primaryCurrency),
|
|
};
|
|
}
|
|
|
|
// ==================== EMAIL TEST ENDPOINT ====================
|
|
|
|
/**
|
|
* Send a test email to verify SMTP configuration (admin only)
|
|
*
|
|
* Returns the exact SMTP error in the response instead of only logging it.
|
|
*/
|
|
@Post('test-email')
|
|
@ApiOperation({
|
|
summary: 'Send test email (Admin only)',
|
|
description:
|
|
'Sends a simple test email to the given address. Returns the exact SMTP error if delivery fails — useful for diagnosing Brevo/SMTP issues.',
|
|
})
|
|
@ApiResponse({ status: 200, description: 'Email sent successfully' })
|
|
@ApiResponse({ status: 400, description: 'SMTP error — check the message field' })
|
|
async sendTestEmail(
|
|
@Body() body: { to: string },
|
|
@CurrentUser() user: UserPayload
|
|
) {
|
|
if (!body?.to) {
|
|
throw new BadRequestException('Field "to" is required');
|
|
}
|
|
|
|
this.logger.log(`[ADMIN: ${user.email}] Sending test email to ${body.to}`);
|
|
|
|
try {
|
|
await this.emailPort.send({
|
|
to: body.to,
|
|
subject: '[Xpeditis] Test SMTP',
|
|
html: `<p>Email de test envoyé depuis le panel admin par <strong>${user.email}</strong>.</p><p>Si vous lisez ceci, la configuration SMTP fonctionne correctement.</p>`,
|
|
text: `Email de test envoyé par ${user.email}. Si vous lisez ceci, le SMTP fonctionne.`,
|
|
});
|
|
|
|
this.logger.log(`[ADMIN] Test email sent successfully to ${body.to}`);
|
|
return { success: true, message: `Email envoyé avec succès à ${body.to}` };
|
|
} catch (error: any) {
|
|
this.logger.error(`[ADMIN] Test email FAILED to ${body.to}: ${error?.message}`, error?.stack);
|
|
throw new BadRequestException(
|
|
`Échec SMTP — ${error?.message ?? 'erreur inconnue'}. ` +
|
|
`Code: ${error?.code ?? 'N/A'}, Response: ${error?.response ?? 'N/A'}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// ==================== DOCUMENTS ENDPOINTS ====================
|
|
|
|
/**
|
|
* Get ALL documents from all organizations (admin only)
|
|
*
|
|
* Returns documents grouped by organization
|
|
*/
|
|
@Get('documents')
|
|
@ApiOperation({
|
|
summary: 'Get all documents (Admin only)',
|
|
description: 'Retrieve ALL documents from all organizations in the database.',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'All documents retrieved successfully',
|
|
})
|
|
@ApiResponse({
|
|
status: 401,
|
|
description: 'Unauthorized - missing or invalid token',
|
|
})
|
|
@ApiResponse({
|
|
status: 403,
|
|
description: 'Forbidden - requires admin role',
|
|
})
|
|
async getAllDocuments(@CurrentUser() user: UserPayload): Promise<any> {
|
|
this.logger.log(`[ADMIN: ${user.email}] Fetching ALL documents from database`);
|
|
|
|
// Get all organizations
|
|
const organizations = await this.organizationRepository.findAll();
|
|
|
|
// Extract documents from all organizations
|
|
const allDocuments = organizations.flatMap(org =>
|
|
org.documents.map(doc => ({
|
|
...doc,
|
|
organizationId: org.id,
|
|
organizationName: org.name,
|
|
}))
|
|
);
|
|
|
|
this.logger.log(
|
|
`[ADMIN] Retrieved ${allDocuments.length} documents from ${organizations.length} organizations`
|
|
);
|
|
|
|
return {
|
|
documents: allDocuments,
|
|
total: allDocuments.length,
|
|
organizationCount: organizations.length,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get documents for a specific organization (admin only)
|
|
*/
|
|
@Get('organizations/:id/documents')
|
|
@ApiOperation({
|
|
summary: 'Get organization documents (Admin only)',
|
|
description: 'Retrieve all documents for a specific organization',
|
|
})
|
|
@ApiParam({
|
|
name: 'id',
|
|
description: 'Organization ID (UUID)',
|
|
})
|
|
@ApiResponse({
|
|
status: HttpStatus.OK,
|
|
description: 'Organization documents retrieved successfully',
|
|
})
|
|
@ApiNotFoundResponse({
|
|
description: 'Organization not found',
|
|
})
|
|
async getOrganizationDocuments(
|
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: UserPayload
|
|
): Promise<any> {
|
|
this.logger.log(`[ADMIN: ${user.email}] Fetching documents for organization: ${id}`);
|
|
|
|
const organization = await this.organizationRepository.findById(id);
|
|
if (!organization) {
|
|
throw new NotFoundException(`Organization ${id} not found`);
|
|
}
|
|
|
|
return {
|
|
organizationId: organization.id,
|
|
organizationName: organization.name,
|
|
documents: organization.documents,
|
|
total: organization.documents.length,
|
|
};
|
|
}
|
|
}
|