import { Controller, Get, Post, Patch, Param, Body, Query, HttpCode, HttpStatus, Logger, UsePipes, ValidationPipe, NotFoundException, ParseUUIDPipe, ParseIntPipe, DefaultValuePipe, UseGuards, ForbiddenException, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBadRequestResponse, ApiNotFoundResponse, ApiQuery, ApiParam, ApiBearerAuth, } from '@nestjs/swagger'; import { CreateOrganizationDto, UpdateOrganizationDto, OrganizationResponseDto, OrganizationListResponseDto, } from '../dto/organization.dto'; import { OrganizationMapper } from '../mappers/organization.mapper'; import { OrganizationRepository } from '../../domain/ports/out/organization.repository'; import { Organization, OrganizationType } from '../../domain/entities/organization.entity'; 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'; import { v4 as uuidv4 } from 'uuid'; /** * Organizations Controller * * Manages organization CRUD operations: * - Create organization (admin only) * - Get organization details * - Update organization (admin/manager) * - List organizations */ @ApiTags('Organizations') @Controller('api/v1/organizations') @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() export class OrganizationsController { private readonly logger = new Logger(OrganizationsController.name); constructor( private readonly organizationRepository: OrganizationRepository, ) {} /** * Create a new organization * * Admin-only endpoint to create a new organization. */ @Post() @HttpCode(HttpStatus.CREATED) @Roles('admin') @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) @ApiOperation({ summary: 'Create new organization', description: 'Create a new organization (freight forwarder, carrier, or shipper). Admin-only.', }) @ApiResponse({ status: HttpStatus.CREATED, description: 'Organization created successfully', type: OrganizationResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiResponse({ status: 403, description: 'Forbidden - requires admin role', }) @ApiBadRequestResponse({ description: 'Invalid request parameters', }) async createOrganization( @Body() dto: CreateOrganizationDto, @CurrentUser() user: UserPayload, ): Promise { this.logger.log( `[Admin: ${user.email}] Creating organization: ${dto.name} (${dto.type})`, ); try { // Check for duplicate name const existingByName = await this.organizationRepository.findByName(dto.name); if (existingByName) { throw new ForbiddenException( `Organization with name "${dto.name}" already exists`, ); } // Check for duplicate SCAC if provided if (dto.scac) { const existingBySCAC = await this.organizationRepository.findBySCAC(dto.scac); if (existingBySCAC) { throw new ForbiddenException( `Organization with SCAC "${dto.scac}" already exists`, ); } } // Create organization entity const organization = Organization.create({ id: uuidv4(), name: dto.name, type: dto.type, scac: dto.scac, address: OrganizationMapper.mapDtoToAddress(dto.address), logoUrl: dto.logoUrl, documents: [], isActive: true, }); // Save to database const savedOrg = await this.organizationRepository.save(organization); this.logger.log( `Organization created successfully: ${savedOrg.name} (${savedOrg.id})`, ); return OrganizationMapper.toDto(savedOrg); } catch (error: any) { this.logger.error( `Organization creation failed: ${error?.message || 'Unknown error'}`, error?.stack, ); throw error; } } /** * Get organization by ID * * Retrieve details of a specific organization. * Users can only view their own organization unless they are admins. */ @Get(':id') @ApiOperation({ summary: 'Get organization by ID', description: 'Retrieve organization details. Users can view their own organization, admins can view any.', }) @ApiParam({ name: 'id', description: 'Organization ID (UUID)', example: '550e8400-e29b-41d4-a716-446655440000', }) @ApiResponse({ status: HttpStatus.OK, description: 'Organization details retrieved successfully', type: OrganizationResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiNotFoundResponse({ description: 'Organization not found', }) async getOrganization( @Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: UserPayload, ): Promise { this.logger.log(`[User: ${user.email}] Fetching organization: ${id}`); const organization = await this.organizationRepository.findById(id); if (!organization) { throw new NotFoundException(`Organization ${id} not found`); } // Authorization: Users can only view their own organization (unless admin) if (user.role !== 'admin' && organization.id !== user.organizationId) { throw new ForbiddenException('You can only view your own organization'); } return OrganizationMapper.toDto(organization); } /** * Update organization * * Update organization details (name, address, logo, status). * Requires admin or manager role. */ @Patch(':id') @Roles('admin', 'manager') @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) @ApiOperation({ summary: 'Update organization', description: 'Update organization details (name, address, logo, status). Requires admin or manager role.', }) @ApiParam({ name: 'id', description: 'Organization ID (UUID)', }) @ApiResponse({ status: HttpStatus.OK, description: 'Organization updated successfully', type: OrganizationResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) @ApiResponse({ status: 403, description: 'Forbidden - requires admin or manager role', }) @ApiNotFoundResponse({ description: 'Organization not found', }) async updateOrganization( @Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateOrganizationDto, @CurrentUser() user: UserPayload, ): Promise { this.logger.log( `[User: ${user.email}] Updating organization: ${id}`, ); const organization = await this.organizationRepository.findById(id); if (!organization) { throw new NotFoundException(`Organization ${id} not found`); } // Authorization: Managers can only update their own organization if (user.role === 'manager' && organization.id !== user.organizationId) { throw new ForbiddenException('You can only update your own organization'); } // Update fields if (dto.name) { organization.updateName(dto.name); } if (dto.address) { organization.updateAddress(OrganizationMapper.mapDtoToAddress(dto.address)); } if (dto.logoUrl !== undefined) { organization.updateLogoUrl(dto.logoUrl); } if (dto.isActive !== undefined) { if (dto.isActive) { organization.activate(); } else { organization.deactivate(); } } // Save updated organization const updatedOrg = await this.organizationRepository.save(organization); this.logger.log(`Organization updated successfully: ${updatedOrg.id}`); return OrganizationMapper.toDto(updatedOrg); } /** * List organizations * * Retrieve a paginated list of organizations. * Admins can see all, others see only their own. */ @Get() @ApiOperation({ summary: 'List organizations', description: 'Retrieve a paginated list of organizations. Admins see all, others see only their own.', }) @ApiQuery({ name: 'page', required: false, description: 'Page number (1-based)', example: 1, }) @ApiQuery({ name: 'pageSize', required: false, description: 'Number of items per page', example: 20, }) @ApiQuery({ name: 'type', required: false, description: 'Filter by organization type', enum: OrganizationType, }) @ApiResponse({ status: HttpStatus.OK, description: 'Organizations list retrieved successfully', type: OrganizationListResponseDto, }) @ApiResponse({ status: 401, description: 'Unauthorized - missing or invalid token', }) async listOrganizations( @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number, @Query('type') type: OrganizationType | undefined, @CurrentUser() user: UserPayload, ): Promise { this.logger.log( `[User: ${user.email}] Listing organizations: page=${page}, pageSize=${pageSize}, type=${type}`, ); // Fetch organizations let organizations: Organization[]; if (user.role === 'admin') { // Admins can see all organizations organizations = await this.organizationRepository.findAll(); } else { // Others see only their own organization const userOrg = await this.organizationRepository.findById(user.organizationId); organizations = userOrg ? [userOrg] : []; } // Filter by type if provided const filteredOrgs = type ? organizations.filter(org => org.type === type) : organizations; // Paginate const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedOrgs = filteredOrgs.slice(startIndex, endIndex); // Convert to DTOs const orgDtos = OrganizationMapper.toDtoArray(paginatedOrgs); const totalPages = Math.ceil(filteredOrgs.length / pageSize); return { organizations: orgDtos, total: filteredOrgs.length, page, pageSize, totalPages, }; } }