apps/recallassess/recallassess-api/src/api/client/learning-group/learning-group.controller.ts
api/client/learning-group
Methods |
|
| Async acceptCourseInvitation | |||||||||
acceptCourseInvitation(learningGroupParticipantId: number, auth: CLAuthData)
|
|||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||
|
Accept course invitation POST /api/client/learning-group/accept-invitation/:learningGroupParticipantId
Parameters :
Returns :
Promise<literal type>
|
| Async cancelParticipantLicense | ||||||||||||
cancelParticipantLicense(learningGroupParticipantId: number, dto: CancelLicenseDto, auth: CLAuthData)
|
||||||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
||||||||||||
|
Cancel a participant license allocation DELETE /api/client/learning-group/participant/:learningGroupParticipantId
Parameters :
Returns :
Promise<literal type>
|
| Async checkCancellationEligibility | |||||||||
checkCancellationEligibility(learningGroupParticipantId: number, auth: CLAuthData)
|
|||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||
|
Check cancellation eligibility for a participant license GET /api/client/learning-group/participant/:learningGroupParticipantId/cancellation-eligibility
Parameters :
Returns :
unknown
|
| Async createLearningGroup | |||||||||
createLearningGroup(dto: AddLearningGroupDto, auth: CLAuthData)
|
|||||||||
Decorators :
@HttpCode(HttpStatus.CREATED)
|
|||||||||
|
Create a new learning group (allocate license to participants) POST /api/client/learning-group
Parameters :
Returns :
Promise<CLLearningGroupDto>
|
| Async downloadPostBatHtml | |||||||||||||||
downloadPostBatHtml(id: number, learningGroupParticipantId: number, auth: CLAuthData, reply: FastifyReply)
|
|||||||||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||||||||
|
Download post-BAT HTML for a participant in this learning group. GET /api/client/learning-group/:id/participant/:learningGroupParticipantId/post-bat-download
Parameters :
Returns :
Promise<void>
|
| Async downloadPreBatHtml | |||||||||||||||
downloadPreBatHtml(id: number, learningGroupParticipantId: number, auth: CLAuthData, reply: FastifyReply)
|
|||||||||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||||||||
|
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 :
Returns :
Promise<void>
|
| Async getAllAllocatedParticipants | ||||||
getAllAllocatedParticipants(auth: CLAuthData)
|
||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
||||||
|
Get all participants already allocated to any course GET /api/client/learning-group/allocated-participants
Parameters :
Returns :
Promise<literal type>
|
| Async getAllLearningGroups | |||||||||
getAllLearningGroups(query: LearningGroupQueryDto, auth: CLAuthData)
|
|||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||
|
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 :
Returns :
Promise<CLLearningGroupListResponse>
|
| Async getAllocatedParticipants | |||||||||
getAllocatedParticipants(courseId: number, auth: CLAuthData)
|
|||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||
|
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 :
Returns :
Promise<literal type>
|
| Async getCourseLicenseUtilization | ||||||
getCourseLicenseUtilization(auth: CLAuthData)
|
||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
||||||
|
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 :
|
| Async getLearningGroupDetail | |||||||||
getLearningGroupDetail(id: number, auth: CLAuthData)
|
|||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||
|
Get a single learning group (license allocation) detail with participants GET /api/client/learning-group/:id
Parameters :
Returns :
Promise<unknown>
|
| Async getLicenseInfo | ||||||
getLicenseInfo(auth: CLAuthData)
|
||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
||||||
|
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 :
Returns :
Promise<literal type>
|
| Async getParticipantsInCoolingPeriod | |||||||||
getParticipantsInCoolingPeriod(courseId: number, auth: CLAuthData)
|
|||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||
|
Get participants in cooling period for a course (for allocate-license dialog tooltip) GET /api/client/learning-group/course/:courseId/cooling-period-participants
Parameters :
Returns :
Promise<literal type>
|
| Async getPostBatPdfUrl | ||||||||||||
getPostBatPdfUrl(id: number, learningGroupParticipantId: number, auth: CLAuthData)
|
||||||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
||||||||||||
|
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 :
Returns :
Promise<literal type>
|
| Async getPreBatPdfUrl | ||||||||||||
getPreBatPdfUrl(id: number, learningGroupParticipantId: number, auth: CLAuthData)
|
||||||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
||||||||||||
|
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 :
Returns :
Promise<literal type>
|
| Async getSubscriptionBillingInfo | ||||||
getSubscriptionBillingInfo(auth: CLAuthData)
|
||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
||||||
|
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 :
Returns :
Promise<literal type>
|
| Async resendInvitation | ||||||||||||
resendInvitation(id: number, participantId: number, auth: CLAuthData)
|
||||||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
||||||||||||
|
Resend invitation to a specific participant POST /api/client/learning-group/:id/resend-invitation/:participantId
Parameters :
Returns :
Promise<literal type>
|
| Async resumeParticipantLicense | |||||||||
resumeParticipantLicense(learningGroupParticipantId: number, auth: CLAuthData)
|
|||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||
|
Resume a cancelled participant license allocation POST /api/client/learning-group/participant/:learningGroupParticipantId/resume
Parameters :
Returns :
Promise<literal type>
|
| Async sendInvitationsToAll | |||||||||
sendInvitationsToAll(id: number, auth: CLAuthData)
|
|||||||||
Decorators :
@HttpCode(HttpStatus.OK)
|
|||||||||
|
Send invitations to all participants in a learning group POST /api/client/learning-group/:id/send-invitations
Parameters :
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 };
}
}