import { Body, Controller, Delete, Get, HttpCode, Param, ParseUUIDPipe, Patch, Post, UploadedFile, UseInterceptors, } from '@nestjs/common'; import { CurrentUser } from '@edr/api-common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; import { actorLabel } from '../warehouses/current-actor.util'; import { BookingStaff } from '../../common/booking-guards'; import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { hasFreightPermission } from '../../common/freight-permission.util'; import { resolveAuthUserId } from '../../common/resolve-auth-user-id'; import { GlExchangeService, type GlExchangeActor, type GlExchangeSide, } from './gl-exchange.service'; /** Either GL desk may read and post; ownership decides who may edit. */ const GL_EXCHANGE_PERMS = [ FREIGHT_PERMS.contracts.clearanceEtActions, FREIGHT_PERMS.contracts.clearanceDjActions, ]; /** Multipart bodies arrive as strings — "true"/"1" mean checked. */ const asBool = (raw: string | boolean | undefined): boolean => raw === true || raw === 'true' || raw === '1'; @ApiTags('gl-exchange') @ApiBearerAuth() @Controller('gl-exchange') export class GlExchangeController { constructor(private readonly exchangeService: GlExchangeService) {} @Get(':entityId') @BookingStaff(GL_EXCHANGE_PERMS) @ApiOperation({ summary: 'GL ET ↔ GL DJ shared documents for a booking or contract', }) list( @Param('entityId', ParseUUIDPipe) entityId: string, @CurrentUser() user: TCurrentUser, ) { return this.exchangeService.list(entityId, resolveAuthUserId(user)); } @Post(':entityId') @BookingStaff(GL_EXCHANGE_PERMS) @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Share a document with the other GL desk' }) upload( @Param('entityId', ParseUUIDPipe) entityId: string, @UploadedFile() file: Express.Multer.File | undefined, @Body('title') title: string, @Body('visibleToCustomer') visibleToCustomer: string | undefined, @CurrentUser() user: TCurrentUser, ) { return this.exchangeService.upload( entityId, file, { title, visibleToCustomer: asBool(visibleToCustomer) }, this.actor(user), ); } @Patch('documents/:documentId') @BookingStaff(GL_EXCHANGE_PERMS) @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Uploader edits a shared document (title, visibility, file)', }) update( @Param('documentId', ParseUUIDPipe) documentId: string, @UploadedFile() file: Express.Multer.File | undefined, @Body('title') title: string | undefined, @Body('visibleToCustomer') visibleToCustomer: string | undefined, @CurrentUser() user: TCurrentUser, ) { return this.exchangeService.update( documentId, { title, visibleToCustomer: visibleToCustomer == null ? undefined : asBool(visibleToCustomer), }, file, resolveAuthUserId(user), ); } @Delete('documents/:documentId') @BookingStaff(GL_EXCHANGE_PERMS) @HttpCode(204) @ApiOperation({ summary: 'Uploader removes a shared document' }) async remove( @Param('documentId', ParseUUIDPipe) documentId: string, @CurrentUser() user: TCurrentUser, ) { await this.exchangeService.remove(documentId, resolveAuthUserId(user)); } /** * Which desk is posting. A user holding only the Djibouti actions permission * is Djibouti; everyone else (GL Ethiopia, and super admins who hold both) * posts as Ethiopia. */ private actor(user: TCurrentUser): GlExchangeActor { const side: GlExchangeSide = !hasFreightPermission(user, FREIGHT_PERMS.contracts.clearanceEtActions) && hasFreightPermission(user, FREIGHT_PERMS.contracts.clearanceDjActions) ? 'DJ' : 'ET'; return { userId: resolveAuthUserId(user), name: actorLabel(user) ?? null, side, }; } }