File

apps/recallassess/recallassess-api/src/api/client/learning-group/learning-group.controller.ts

Prefix

api/client/learning-group

Index

Methods

Methods

Async acceptCourseInvitation
acceptCourseInvitation(learningGroupParticipantId: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Post('accept-invitation/:learningGroupParticipantId')
@ApiOperation({summary: 'Accept course invitation', description: 'Accepts a course invitation for the current participant'})
@ApiResponse({status: 200, description: 'Course invitation accepted successfully'})
@ApiResponse({status: 400, description: 'Course invitation not found or password not set'})

Accept course invitation POST /api/client/learning-group/accept-invitation/:learningGroupParticipantId

Parameters :
Name Type Optional
learningGroupParticipantId number No
auth CLAuthData No
Returns : Promise<literal type>
Async cancelParticipantLicense
cancelParticipantLicense(learningGroupParticipantId: number, dto: CancelLicenseDto, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Delete('participant/:learningGroupParticipantId')
@ApiOperation({summary: 'Cancel participant license allocation', description: 'Cancels a participant license allocation, releases the license, and preserves all progress data'})
@ApiResponse({status: 200, description: 'License cancelled successfully'})
@ApiResponse({status: 400, description: 'Enrollment not found or already cancelled'})

Cancel a participant license allocation DELETE /api/client/learning-group/participant/:learningGroupParticipantId

Parameters :
Name Type Optional
learningGroupParticipantId number No
dto CancelLicenseDto No
auth CLAuthData No
Returns : Promise<literal type>
Async checkCancellationEligibility
checkCancellationEligibility(learningGroupParticipantId: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get('participant/:learningGroupParticipantId/cancellation-eligibility')
@ApiOperation({summary: 'Check cancellation eligibility', description: 'Returns eligibility information and progress details for cancelling a participant license'})
@ApiResponse({status: 200, description: 'Returns cancellation eligibility information'})
@ApiResponse({status: 400, description: 'Enrollment not found or already cancelled'})

Check cancellation eligibility for a participant license GET /api/client/learning-group/participant/:learningGroupParticipantId/cancellation-eligibility

Parameters :
Name Type Optional
learningGroupParticipantId number No
auth CLAuthData No
Returns : unknown
Async createLearningGroup
createLearningGroup(dto: AddLearningGroupDto, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.CREATED)
@Post()
@ApiOperation({summary: 'Add a new learning group (license allocation)', description: 'Adds a learning group with assigned course and participants. This allocates licenses to the selected participants for the specified course.'})
@ApiResponse({status: 201, description: 'Learning group added successfully', type: CLLearningGroupDto})
@ApiResponse({status: 400, description: 'Invalid input data or participants not found'})

Create a new learning group (allocate license to participants) POST /api/client/learning-group

Parameters :
Name Type Optional
dto AddLearningGroupDto No
auth CLAuthData No
Async downloadPostBatHtml
downloadPostBatHtml(id: number, learningGroupParticipantId: number, auth: CLAuthData, reply: FastifyReply)
Decorators :
@HttpCode(HttpStatus.OK)
@Get(':id/participant/:learningGroupParticipantId/post-bat-download')
@ApiOperation({summary: 'Download post-BAT analysis HTML for a participant in this allocation'})
@ApiResponse({status: 200, description: 'HTML content returned successfully'})
@ApiResponse({status: 404, description: 'Learning group or enrollment not found'})

Download post-BAT HTML for a participant in this learning group. GET /api/client/learning-group/:id/participant/:learningGroupParticipantId/post-bat-download

Parameters :
Name Type Optional
id number No
learningGroupParticipantId number No
auth CLAuthData No
reply FastifyReply No
Returns : Promise<void>
Async downloadPreBatHtml
downloadPreBatHtml(id: number, learningGroupParticipantId: number, auth: CLAuthData, reply: FastifyReply)
Decorators :
@HttpCode(HttpStatus.OK)
@Get(':id/participant/:learningGroupParticipantId/pre-bat-download')
@ApiOperation({summary: 'Download pre-BAT analysis HTML for a participant in this allocation'})
@ApiResponse({status: 200, description: 'HTML content returned successfully'})
@ApiResponse({status: 404, description: 'Learning group or enrollment not found'})

Download pre-BAT HTML for a participant in this learning group (for allocation details view). GET /api/client/learning-group/:id/participant/:learningGroupParticipantId/pre-bat-download

Parameters :
Name Type Optional
id number No
learningGroupParticipantId number No
auth CLAuthData No
reply FastifyReply No
Returns : Promise<void>
Async getAllAllocatedParticipants
getAllAllocatedParticipants(auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get('allocated-participants')
@ApiOperation({summary: 'Get all participants already allocated to any course', description: 'Returns list of participants with their completion status that are already allocated to any course'})
@ApiResponse({status: 200, description: 'Returns array of participants with completion status'})

Get all participants already allocated to any course GET /api/client/learning-group/allocated-participants

Parameters :
Name Type Optional
auth CLAuthData No
Returns : Promise<literal type>
Async getAllLearningGroups
getAllLearningGroups(query: LearningGroupQueryDto, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get()
@ApiOperation({summary: 'Get all learning groups with pagination, search and filters', description: 'Returns paginated learning groups filtered by search query and status with metadata'})
@ApiResponse({status: 200, description: 'Returns paginated list of learning groups with metadata (page, limit, totalCount, totalPages, etc.)'})

Get all learning groups (license allocations) for client consumption with optional filtering and pagination GET /api/client/learning-group?page=1&limit=20&sq=sales&status=active

Parameters :
Name Type Optional
query LearningGroupQueryDto No
auth CLAuthData No
Async getAllocatedParticipants
getAllocatedParticipants(courseId: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get('course/:courseId/allocated-participants')
@ApiOperation({summary: 'Get participants already allocated to a course', description: 'Returns list of participants with completion status that are already allocated to the specified course'})
@ApiResponse({status: 200, description: 'Returns array of participants with completion status'})

Get participants already allocated to a specific course GET /api/client/learning-group/course/:courseId/allocated-participants Note: This route must be before :id route to avoid conflicts

Parameters :
Name Type Optional
courseId number No
auth CLAuthData No
Returns : Promise<literal type>
Async getCourseLicenseUtilization
getCourseLicenseUtilization(auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get('course-license-utilization')
@ApiOperation({summary: 'Get course license utilization data', description: 'Returns license utilization data per course including total allocated, used, available, and utilization percentage'})
@ApiResponse({status: 200, description: 'Returns array of course license utilization data', type: CourseLicenseUtilizationRowDto, isArray: true})

Get course license utilization data GET /api/client/learning-group/course-license-utilization Note: This route must be before :id route to avoid conflicts

Parameters :
Name Type Optional
auth CLAuthData No
Async getLearningGroupDetail
getLearningGroupDetail(id: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get(':id')
@ApiOperation({summary: 'Get learning group detail with participants', description: 'Returns a single learning group with all participant details'})
@ApiResponse({status: 200, description: 'Returns learning group detail with participants'})
@ApiResponse({status: 400, description: 'Learning group not found'})

Get a single learning group (license allocation) detail with participants GET /api/client/learning-group/:id

Parameters :
Name Type Optional
id number No
auth CLAuthData No
Returns : Promise<unknown>
Async getLicenseInfo
getLicenseInfo(auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get('licenses/info')
@ApiOperation({summary: 'Get license information', description: 'Returns license information including total, available, consumed, and allocated licenses'})
@ApiResponse({status: 200, description: 'Returns license information'})

Get license information for the company GET /api/client/learning-group/licenses/info Note: This route must be before :id route to avoid conflicts

Parameters :
Name Type Optional
auth CLAuthData No
Returns : Promise<literal type>
Async getParticipantsInCoolingPeriod
getParticipantsInCoolingPeriod(courseId: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get('course/:courseId/cooling-period-participants')
@ApiOperation({summary: 'Get participants in cooling period for a course', description: 'Returns participants who cannot be allocated to this course yet due to the enrollment cooling period.'})
@ApiResponse({status: 200, description: 'Returns participants in cooling period with next eligible date'})

Get participants in cooling period for a course (for allocate-license dialog tooltip) GET /api/client/learning-group/course/:courseId/cooling-period-participants

Parameters :
Name Type Optional
courseId number No
auth CLAuthData No
Returns : Promise<literal type>
Async getPostBatPdfUrl
getPostBatPdfUrl(id: number, learningGroupParticipantId: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get(':id/participant/:learningGroupParticipantId/post-bat-pdf-url')
@ApiOperation({summary: 'Get post-BAT PDF download URL for a participant in this allocation'})
@ApiResponse({status: 200, description: 'Presigned URL returned successfully'})
@ApiResponse({status: 404, description: 'Learning group or enrollment not found'})

Get post-BAT PDF download URL for a participant in this learning group. GET /api/client/learning-group/:id/participant/:learningGroupParticipantId/post-bat-pdf-url

Parameters :
Name Type Optional
id number No
learningGroupParticipantId number No
auth CLAuthData No
Returns : Promise<literal type>
Async getPreBatPdfUrl
getPreBatPdfUrl(id: number, learningGroupParticipantId: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get(':id/participant/:learningGroupParticipantId/pre-bat-pdf-url')
@ApiOperation({summary: 'Get pre-BAT PDF download URL for a participant in this allocation'})
@ApiResponse({status: 200, description: 'Presigned URL returned successfully'})
@ApiResponse({status: 404, description: 'Learning group or enrollment not found'})

Get pre-BAT PDF download URL for a participant in this learning group. GET /api/client/learning-group/:id/participant/:learningGroupParticipantId/pre-bat-pdf-url

Parameters :
Name Type Optional
id number No
learningGroupParticipantId number No
auth CLAuthData No
Returns : Promise<literal type>
Async getSubscriptionBillingInfo
getSubscriptionBillingInfo(auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Get('subscription/billing')
@ApiOperation({summary: 'Get subscription billing information', description: 'Returns subscription billing info; `companyActive` matches portal banners (see ParticipantSubscriptionCourseAccessService.isPortalCompanyActive).'})
@ApiResponse({status: 200, description: 'Returns subscription billing information'})

Get subscription billing information for the company GET /api/client/learning-group/subscription/billing Note: This route must be before :id route to avoid conflicts

Parameters :
Name Type Optional
auth CLAuthData No
Returns : Promise<literal type>
Async resendInvitation
resendInvitation(id: number, participantId: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Post(':id/resend-invitation/:participantId')
@ApiOperation({summary: 'Resend invitation to a specific participant', description: 'Resends invitation email to a specific participant in the learning group'})
@ApiResponse({status: 200, description: 'Invitation resent successfully'})
@ApiResponse({status: 400, description: 'Participant not found or not in INVITED status'})

Resend invitation to a specific participant POST /api/client/learning-group/:id/resend-invitation/:participantId

Parameters :
Name Type Optional
id number No
participantId number No
auth CLAuthData No
Returns : Promise<literal type>
Async resumeParticipantLicense
resumeParticipantLicense(learningGroupParticipantId: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Post('participant/:learningGroupParticipantId/resume')
@ApiOperation({summary: 'Resume cancelled participant license allocation', description: 'Resumes a cancelled participant license allocation, restoring access at the same level/status'})
@ApiResponse({status: 200, description: 'License resumed successfully'})
@ApiResponse({status: 400, description: 'Enrollment not found or not cancelled'})

Resume a cancelled participant license allocation POST /api/client/learning-group/participant/:learningGroupParticipantId/resume

Parameters :
Name Type Optional
learningGroupParticipantId number No
auth CLAuthData No
Returns : Promise<literal type>
Async sendInvitationsToAll
sendInvitationsToAll(id: number, auth: CLAuthData)
Decorators :
@HttpCode(HttpStatus.OK)
@Post(':id/send-invitations')
@ApiOperation({summary: 'Send invitations to all participants in a learning group', description: 'Sends invitation emails to all participants with INVITED status in the learning group'})
@ApiResponse({status: 200, description: 'Invitations sent successfully'})
@ApiResponse({status: 400, description: 'Learning group not found or not in PENDING status'})

Send invitations to all participants in a learning group POST /api/client/learning-group/:id/send-invitations

Parameters :
Name Type Optional
id number No
auth CLAuthData No
Returns : Promise<literal type>
import { CLAuthData, ClientAuth } from '@api/shared/decorators';
import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  HttpStatus,
  Param,
  ParseIntPipe,
  Post,
  Query,
  Res,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { FastifyReply } from 'fastify';
import {
  AddLearningGroupDto,
  CancelLicenseDto,
  CLLearningGroupDto,
  CLLearningGroupListResponse,
  CourseLicenseUtilizationRowDto,
  LearningGroupQueryDto,
} from './dto';
import { CLLearningGroupService } from './learning-group.service';

@ApiTags('Client - Learning Groups (License Allocations)')
@Controller('api/client/learning-group')
export class CLLearningGroupController {
  constructor(private learningGroupService: CLLearningGroupService) {}

  /**
   * Get all learning groups (license allocations) for client consumption with optional filtering and pagination
   * GET /api/client/learning-group?page=1&limit=20&sq=sales&status=active
   */
  @HttpCode(HttpStatus.OK)
  @Get()
  @ApiOperation({
    summary: 'Get all learning groups with pagination, search and filters',
    description:
      'Returns paginated learning groups filtered by search query and status with metadata',
  })
  @ApiResponse({
    status: 200,
    description:
      'Returns paginated list of learning groups with metadata (page, limit, totalCount, totalPages, etc.)',
  })
  async getAllLearningGroups(
    @Query() query: LearningGroupQueryDto,
    @ClientAuth() auth: CLAuthData,
  ): Promise<CLLearningGroupListResponse> {
    return this.learningGroupService.getFilteredLearningGroups(
      auth.companyId,
      query.page || 1,
      query.limit || 20,
      query.sq,
      query.status,
    );
  }

  /**
   * Get license information for the company
   * GET /api/client/learning-group/licenses/info
   * Note: This route must be before :id route to avoid conflicts
   */
  @HttpCode(HttpStatus.OK)
  @Get('licenses/info')
  @ApiOperation({
    summary: 'Get license information',
    description:
      'Returns license information including total, available, consumed, and allocated licenses',
  })
  @ApiResponse({
    status: 200,
    description: 'Returns license information',
  })
  async getLicenseInfo(@ClientAuth() auth: CLAuthData): Promise<{
    total: number;
    available: number;
    consumed: number;
    allocated: number;
    inTraining: number;
  }> {
    return this.learningGroupService.getLicenseInfo(auth.companyId);
  }

  /**
   * Get subscription billing information for the company
   * GET /api/client/learning-group/subscription/billing
   * Note: This route must be before :id route to avoid conflicts
   */
  @HttpCode(HttpStatus.OK)
  @Get('subscription/billing')
  @ApiOperation({
    summary: 'Get subscription billing information',
    description:
      'Returns subscription billing info; `companyActive` matches portal banners (see ParticipantSubscriptionCourseAccessService.isPortalCompanyActive).',
  })
  @ApiResponse({
    status: 200,
    description: 'Returns subscription billing information',
  })
  async getSubscriptionBillingInfo(@ClientAuth() auth: CLAuthData): Promise<{
    monthlyTotal: number;
    nextRenewalDate: string | null;
    licenseCount: number;
    pricePerLicense: number | null;
    packageName: string | null;
    companyActive: boolean;
    isSubscriptionExpiry: boolean;
    stripeCancelAtPeriodEnd: boolean;
  }> {
    return this.learningGroupService.getSubscriptionBillingInfo(auth.companyId);
  }

  /**
   * Get course license utilization data
   * GET /api/client/learning-group/course-license-utilization
   * Note: This route must be before :id route to avoid conflicts
   */
  @HttpCode(HttpStatus.OK)
  @Get('course-license-utilization')
  @ApiOperation({
    summary: 'Get course license utilization data',
    description:
      'Returns license utilization data per course including total allocated, used, available, and utilization percentage',
  })
  @ApiResponse({
    status: 200,
    description: 'Returns array of course license utilization data',
    type: CourseLicenseUtilizationRowDto,
    isArray: true,
  })
  async getCourseLicenseUtilization(
    @ClientAuth() auth: CLAuthData,
  ): Promise<CourseLicenseUtilizationRowDto[]> {
    return this.learningGroupService.getCourseLicenseUtilization(auth.companyId);
  }

  /**
   * Get participants already allocated to a specific course
   * GET /api/client/learning-group/course/:courseId/allocated-participants
   * Note: This route must be before :id route to avoid conflicts
   */
  @HttpCode(HttpStatus.OK)
  @Get('course/:courseId/allocated-participants')
  @ApiOperation({
    summary: 'Get participants already allocated to a course',
    description:
      'Returns list of participants with completion status that are already allocated to the specified course',
  })
  @ApiResponse({
    status: 200,
    description: 'Returns array of participants with completion status',
  })
  async getAllocatedParticipants(
    @Param('courseId', ParseIntPipe) courseId: number,
    @ClientAuth() auth: CLAuthData,
  ): Promise<{
    participants: Array<{
      participantId: number;
      participantName: string;
      participantEmail: string;
      isCompleted: boolean;
    }>;
  }> {
    return this.learningGroupService.getAllocatedParticipantsForCourse(auth.companyId, courseId);
  }

  /**
   * Get participants in cooling period for a course (for allocate-license dialog tooltip)
   * GET /api/client/learning-group/course/:courseId/cooling-period-participants
   */
  @HttpCode(HttpStatus.OK)
  @Get('course/:courseId/cooling-period-participants')
  @ApiOperation({
    summary: 'Get participants in cooling period for a course',
    description:
      'Returns participants who cannot be allocated to this course yet due to the enrollment cooling period.',
  })
  @ApiResponse({
    status: 200,
    description: 'Returns participants in cooling period with next eligible date',
  })
  async getParticipantsInCoolingPeriod(
    @Param('courseId', ParseIntPipe) courseId: number,
    @ClientAuth() auth: CLAuthData,
  ): Promise<{
    participants: Array<{
      participantId: number;
      nextEligibleDate: string;
      lastEnrollmentCourseName: string;
    }>;
    coolingPeriodDays: number;
  }> {
    return this.learningGroupService.getParticipantsInCoolingPeriod(auth.companyId, courseId);
  }

  /**
   * Get all participants already allocated to any course
   * GET /api/client/learning-group/allocated-participants
   */
  @HttpCode(HttpStatus.OK)
  @Get('allocated-participants')
  @ApiOperation({
    summary: 'Get all participants already allocated to any course',
    description:
      'Returns list of participants with their completion status that are already allocated to any course',
  })
  @ApiResponse({
    status: 200,
    description: 'Returns array of participants with completion status',
  })
  async getAllAllocatedParticipants(
    @ClientAuth() auth: CLAuthData,
  ): Promise<{
    participants: Array<{
      participantId: number;
      isCompleted: boolean;
      courseId: number;
      courseName: string;
    }>;
  }> {
    return this.learningGroupService.getAllAllocatedParticipants(auth.companyId);
  }

  /**
   * Create a new learning group (allocate license to participants)
   * POST /api/client/learning-group
   */
  @HttpCode(HttpStatus.CREATED)
  @Post()
  @ApiOperation({
    summary: 'Add a new learning group (license allocation)',
    description:
      'Adds a learning group with assigned course and participants. This allocates licenses to the selected participants for the specified course.',
  })
  @ApiResponse({
    status: 201,
    description: 'Learning group added successfully',
    type: CLLearningGroupDto,
  })
  @ApiResponse({
    status: 400,
    description: 'Invalid input data or participants not found',
  })
  async createLearningGroup(
    @Body() dto: AddLearningGroupDto,
    @ClientAuth() auth: CLAuthData,
  ): Promise<CLLearningGroupDto> {
    return this.learningGroupService.createLearningGroup(auth.companyId, auth.participantId, dto);
  }

  /**
   * Get a single learning group (license allocation) detail with participants
   * GET /api/client/learning-group/:id
   */
  @HttpCode(HttpStatus.OK)
  @Get(':id')
  @ApiOperation({
    summary: 'Get learning group detail with participants',
    description: 'Returns a single learning group with all participant details',
  })
  @ApiResponse({
    status: 200,
    description: 'Returns learning group detail with participants',
  })
  @ApiResponse({
    status: 400,
    description: 'Learning group not found',
  })
  async getLearningGroupDetail(
    @Param('id', ParseIntPipe) id: number,
    @ClientAuth() auth: CLAuthData,
  ): Promise<import('./dto').CLLearningGroupDetailDto> {
    const { CLLearningGroupDetailDto } = await import('./dto');
    return this.learningGroupService.getLearningGroupDetail(auth.companyId, id);
  }

  /**
   * Send invitations to all participants in a learning group
   * POST /api/client/learning-group/:id/send-invitations
   */
  @HttpCode(HttpStatus.OK)
  @Post(':id/send-invitations')
  @ApiOperation({
    summary: 'Send invitations to all participants in a learning group',
    description:
      'Sends invitation emails to all participants with INVITED status in the learning group',
  })
  @ApiResponse({
    status: 200,
    description: 'Invitations sent successfully',
  })
  @ApiResponse({
    status: 400,
    description: 'Learning group not found or not in PENDING status',
  })
  async sendInvitationsToAll(
    @Param('id', ParseIntPipe) id: number,
    @ClientAuth() auth: CLAuthData,
  ): Promise<{ message: string; invitationsSent: number }> {
    return this.learningGroupService.sendInvitationsToAll(auth.companyId, id, auth.participantId);
  }

  /**
   * Resend invitation to a specific participant
   * POST /api/client/learning-group/:id/resend-invitation/:participantId
   */
  @HttpCode(HttpStatus.OK)
  @Post(':id/resend-invitation/:participantId')
  @ApiOperation({
    summary: 'Resend invitation to a specific participant',
    description: 'Resends invitation email to a specific participant in the learning group',
  })
  @ApiResponse({
    status: 200,
    description: 'Invitation resent successfully',
  })
  @ApiResponse({
    status: 400,
    description: 'Participant not found or not in INVITED status',
  })
  async resendInvitation(
    @Param('id', ParseIntPipe) id: number,
    @Param('participantId', ParseIntPipe) participantId: number,
    @ClientAuth() auth: CLAuthData,
  ): Promise<{ message: string }> {
    return this.learningGroupService.resendInvitation(
      auth.companyId,
      id,
      participantId,
      auth.participantId,
    );
  }

  /**
   * Accept course invitation
   * POST /api/client/learning-group/accept-invitation/:learningGroupParticipantId
   */
  @HttpCode(HttpStatus.OK)
  @Post('accept-invitation/:learningGroupParticipantId')
  @ApiOperation({
    summary: 'Accept course invitation',
    description: 'Accepts a course invitation for the current participant',
  })
  @ApiResponse({
    status: 200,
    description: 'Course invitation accepted successfully',
  })
  @ApiResponse({
    status: 400,
    description: 'Course invitation not found or password not set',
  })
  async acceptCourseInvitation(
    @Param('learningGroupParticipantId', ParseIntPipe) learningGroupParticipantId: number,
    @ClientAuth() auth: CLAuthData,
  ): Promise<{ success: boolean; message: string }> {
    return this.learningGroupService.acceptCourseInvitation(
      auth.participantId,
      learningGroupParticipantId,
    );
  }

  /**
   * Check cancellation eligibility for a participant license
   * GET /api/client/learning-group/participant/:learningGroupParticipantId/cancellation-eligibility
   */
  @HttpCode(HttpStatus.OK)
  @Get('participant/:learningGroupParticipantId/cancellation-eligibility')
  @ApiOperation({
    summary: 'Check cancellation eligibility',
    description: 'Returns eligibility information and progress details for cancelling a participant license',
  })
  @ApiResponse({
    status: 200,
    description: 'Returns cancellation eligibility information',
  })
  @ApiResponse({
    status: 400,
    description: 'Enrollment not found or already cancelled',
  })
  async checkCancellationEligibility(
    @Param('learningGroupParticipantId', ParseIntPipe) learningGroupParticipantId: number,
    @ClientAuth() auth: CLAuthData,
  ) {
    return this.learningGroupService.checkCancellationEligibility(
      learningGroupParticipantId,
      auth.companyId,
    );
  }

  /**
   * Cancel a participant license allocation
   * DELETE /api/client/learning-group/participant/:learningGroupParticipantId
   */
  @HttpCode(HttpStatus.OK)
  @Delete('participant/:learningGroupParticipantId')
  @ApiOperation({
    summary: 'Cancel participant license allocation',
    description: 'Cancels a participant license allocation, releases the license, and preserves all progress data',
  })
  @ApiResponse({
    status: 200,
    description: 'License cancelled successfully',
  })
  @ApiResponse({
    status: 400,
    description: 'Enrollment not found or already cancelled',
  })
  async cancelParticipantLicense(
    @Param('learningGroupParticipantId', ParseIntPipe) learningGroupParticipantId: number,
    @Body() dto: CancelLicenseDto,
    @ClientAuth() auth: CLAuthData,
  ): Promise<{ success: boolean; message: string }> {
    return this.learningGroupService.cancelParticipantLicense(
      learningGroupParticipantId,
      auth.companyId,
      auth.participantId,
      dto.reason,
      dto.notes,
    );
  }

  /**
   * Resume a cancelled participant license allocation
   * POST /api/client/learning-group/participant/:learningGroupParticipantId/resume
   */
  @HttpCode(HttpStatus.OK)
  @Post('participant/:learningGroupParticipantId/resume')
  @ApiOperation({
    summary: 'Resume cancelled participant license allocation',
    description: 'Resumes a cancelled participant license allocation, restoring access at the same level/status',
  })
  @ApiResponse({
    status: 200,
    description: 'License resumed successfully',
  })
  @ApiResponse({
    status: 400,
    description: 'Enrollment not found or not cancelled',
  })
  async resumeParticipantLicense(
    @Param('learningGroupParticipantId', ParseIntPipe) learningGroupParticipantId: number,
    @ClientAuth() auth: CLAuthData,
  ): Promise<{ success: boolean; message: string; restoredStatus: string }> {
    return this.learningGroupService.resumeParticipantLicense(
      learningGroupParticipantId,
      auth.companyId,
      auth.participantId,
    );
  }

  /**
   * Download pre-BAT HTML for a participant in this learning group (for allocation details view).
   * GET /api/client/learning-group/:id/participant/:learningGroupParticipantId/pre-bat-download
   */
  @HttpCode(HttpStatus.OK)
  @Get(':id/participant/:learningGroupParticipantId/pre-bat-download')
  @ApiOperation({ summary: 'Download pre-BAT analysis HTML for a participant in this allocation' })
  @ApiResponse({ status: 200, description: 'HTML content returned successfully' })
  @ApiResponse({ status: 404, description: 'Learning group or enrollment not found' })
  async downloadPreBatHtml(
    @Param('id', ParseIntPipe) id: number,
    @Param('learningGroupParticipantId', ParseIntPipe) learningGroupParticipantId: number,
    @ClientAuth() auth: CLAuthData,
    @Res() reply: FastifyReply,
  ): Promise<void> {
    const html = await this.learningGroupService.generatePreBatHtmlForParticipant(
      auth.companyId,
      id,
      learningGroupParticipantId,
    );
    reply.header('Content-Type', 'text/html; charset=utf-8');
    reply.send(html);
  }

  /**
   * Get pre-BAT PDF download URL for a participant in this learning group.
   * GET /api/client/learning-group/:id/participant/:learningGroupParticipantId/pre-bat-pdf-url
   */
  @HttpCode(HttpStatus.OK)
  @Get(':id/participant/:learningGroupParticipantId/pre-bat-pdf-url')
  @ApiOperation({ summary: 'Get pre-BAT PDF download URL for a participant in this allocation' })
  @ApiResponse({ status: 200, description: 'Presigned URL returned successfully' })
  @ApiResponse({ status: 404, description: 'Learning group or enrollment not found' })
  async getPreBatPdfUrl(
    @Param('id', ParseIntPipe) id: number,
    @Param('learningGroupParticipantId', ParseIntPipe) learningGroupParticipantId: number,
    @ClientAuth() auth: CLAuthData,
  ): Promise<{ url: string }> {
    const url = await this.learningGroupService.getPreBatPdfUrlForParticipant(
      auth.companyId,
      id,
      learningGroupParticipantId,
    );
    return { url };
  }

  /**
   * Download post-BAT HTML for a participant in this learning group.
   * GET /api/client/learning-group/:id/participant/:learningGroupParticipantId/post-bat-download
   */
  @HttpCode(HttpStatus.OK)
  @Get(':id/participant/:learningGroupParticipantId/post-bat-download')
  @ApiOperation({ summary: 'Download post-BAT analysis HTML for a participant in this allocation' })
  @ApiResponse({ status: 200, description: 'HTML content returned successfully' })
  @ApiResponse({ status: 404, description: 'Learning group or enrollment not found' })
  async downloadPostBatHtml(
    @Param('id', ParseIntPipe) id: number,
    @Param('learningGroupParticipantId', ParseIntPipe) learningGroupParticipantId: number,
    @ClientAuth() auth: CLAuthData,
    @Res() reply: FastifyReply,
  ): Promise<void> {
    const html = await this.learningGroupService.generatePostBatHtmlForParticipant(
      auth.companyId,
      id,
      learningGroupParticipantId,
    );
    reply.header('Content-Type', 'text/html; charset=utf-8');
    reply.send(html);
  }

  /**
   * Get post-BAT PDF download URL for a participant in this learning group.
   * GET /api/client/learning-group/:id/participant/:learningGroupParticipantId/post-bat-pdf-url
   */
  @HttpCode(HttpStatus.OK)
  @Get(':id/participant/:learningGroupParticipantId/post-bat-pdf-url')
  @ApiOperation({ summary: 'Get post-BAT PDF download URL for a participant in this allocation' })
  @ApiResponse({ status: 200, description: 'Presigned URL returned successfully' })
  @ApiResponse({ status: 404, description: 'Learning group or enrollment not found' })
  async getPostBatPdfUrl(
    @Param('id', ParseIntPipe) id: number,
    @Param('learningGroupParticipantId', ParseIntPipe) learningGroupParticipantId: number,
    @ClientAuth() auth: CLAuthData,
  ): Promise<{ url: string }> {
    const url = await this.learningGroupService.getPostBatPdfUrlForParticipant(
      auth.companyId,
      id,
      learningGroupParticipantId,
    );
    return { url };
  }
}

results matching ""

    No results matching ""