apps/recallassess/recallassess-api/src/api/client/shared/participant-subscription-course-access.service.ts
Methods |
constructor(prisma: BNestPrismaService)
|
||||||
|
Parameters :
|
| Async assertAllowsCompletedStageRead | |||||||||||||||
assertAllowsCompletedStageRead(participantId: number, learningGroupParticipantId: number, stageCompleted: boolean, enrollmentStatus?: string | null)
|
|||||||||||||||
|
Stage-result read access when company subscription is inactive: allow if the specific stage is already completed (or enrollment fully completed).
Parameters :
Returns :
Promise<void>
|
| Async assertAllowsContinuingCourseAccess | ||||||||||||
assertAllowsContinuingCourseAccess(participantId: number, learningGroupParticipantId: number, enrollmentStatus?: string | null)
|
||||||||||||
|
Block starting/continuing a course when the company subscription is inactive, unless the enrollment is fully completed (read-only access to results).
Parameters :
Returns :
Promise<void>
|
| Async assertAllowsElearningRead | ||||||||||||
assertAllowsElearningRead(participantId: number, learningGroupParticipantId: number, enrollmentStatus?: string | null)
|
||||||||||||
|
E-learning read access when subscription is inactive: allow if enrollment is fully completed OR e-learning stage is completed.
Parameters :
Returns :
Promise<void>
|
| Async assertAllowsKnowledgeReviewRead | ||||||||||||||||||
assertAllowsKnowledgeReviewRead(participantId: number, learningGroupParticipantId: number, enrollmentStatus: string, courseId: number, knowledgeReviewId: number)
|
||||||||||||||||||
|
Knowledge review GET: allow read when subscription inactive if the course is completed, or if the standalone KR was already submitted (review results mid-journey).
Parameters :
Returns :
Promise<void>
|
| Async assertCompanySubscriptionActive | ||||||
assertCompanySubscriptionActive(participantId: number)
|
||||||
|
New allocations / accepting invitations while subscription is inactive.
Parameters :
Returns :
Promise<void>
|
| Async isCompanyActiveForParticipant | ||||||
isCompanyActiveForParticipant(participantId: number)
|
||||||
|
Delegates to isPortalCompanyActive for the participant's company.
Parameters :
Returns :
Promise<boolean>
|
| Async isPortalCompanyActive | ||||||
isPortalCompanyActive(companyId: number)
|
||||||
|
Portal "company is OK for training" — same for FREE_TRIAL and paid plans: no special trial bypass.
Parameters :
Returns :
Promise<boolean>
|
import { BNestPrismaService } from "@bish-nest/core/services";
import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { ELearningProgressStatus, ParticipantLearningProgressStatus, SubscriptionStatus } from "@prisma/client";
/**
* Shown when the participant tries to start/continue training while the company subscription is inactive.
* Completed enrollments may still view results (read-only).
*/
export const SUBSCRIPTION_EXPIRED_COURSE_ACCESS_MESSAGE =
"Your company subscription has expired. You can still open completed courses to review the results. To start or continue training, please ask your administrator to renew the subscription";
@Injectable()
export class ParticipantSubscriptionCourseAccessService {
constructor(private readonly prisma: BNestPrismaService) {}
/**
* Portal "company is OK for training" — **same for FREE_TRIAL and paid plans**: no special trial bypass.
* - `company.is_subscription_expiry` must be false.
* - Company must have a **current ACTIVE** subscription row with a non-null `next_billing_date`.
*/
async isPortalCompanyActive(companyId: number): Promise<boolean> {
const company = await this.prisma.client.company.findUnique({
where: { id: companyId },
select: {
is_subscription_expiry: true,
},
});
if (!company) return false;
if (company.is_subscription_expiry) return false;
const hasCurrentActive = await this.prisma.client.subscription.findFirst({
where: {
company_id: companyId,
is_current: true,
status: SubscriptionStatus.ACTIVE,
next_billing_date: { not: null },
},
select: { id: true },
});
return !!hasCurrentActive;
}
/** Delegates to {@link isPortalCompanyActive} for the participant's company. */
async isCompanyActiveForParticipant(participantId: number): Promise<boolean> {
const participant = await this.prisma.client.participant.findUnique({
where: { id: participantId },
select: {
company_id: true,
},
});
if (!participant?.company_id) return false;
return this.isPortalCompanyActive(participant.company_id);
}
/**
* Block starting/continuing a course when the company subscription is inactive,
* unless the enrollment is fully completed (read-only access to results).
*/
async assertAllowsContinuingCourseAccess(
participantId: number,
learningGroupParticipantId: number,
enrollmentStatus?: string | null,
): Promise<void> {
if (await this.isCompanyActiveForParticipant(participantId)) {
return;
}
let status = enrollmentStatus;
if (status === undefined || status === null) {
const row = await this.prisma.client.learningGroupParticipant.findFirst({
where: { id: learningGroupParticipantId, participant_id: participantId },
select: { status: true },
});
if (!row) {
throw new NotFoundException("Course enrollment not found or access denied");
}
status = row.status;
}
if (status === ParticipantLearningProgressStatus.COMPLETED) {
return;
}
throw new ForbiddenException(SUBSCRIPTION_EXPIRED_COURSE_ACCESS_MESSAGE);
}
/**
* Knowledge review GET: allow read when subscription inactive if the course is completed,
* or if the standalone KR was already submitted (review results mid-journey).
*/
async assertAllowsKnowledgeReviewRead(
participantId: number,
learningGroupParticipantId: number,
enrollmentStatus: string,
courseId: number,
knowledgeReviewId: number,
): Promise<void> {
if (await this.isCompanyActiveForParticipant(participantId)) {
return;
}
if (enrollmentStatus === ParticipantLearningProgressStatus.COMPLETED) {
return;
}
const krDone = await this.prisma.client.knowledgeReviewParticipant.findFirst({
where: {
participant_id: participantId,
knowledge_review_id: knowledgeReviewId,
course_id: courseId,
course_module_page_id: null,
} as any,
});
if (krDone) {
return;
}
throw new ForbiddenException(SUBSCRIPTION_EXPIRED_COURSE_ACCESS_MESSAGE);
}
/**
* Stage-result read access when company subscription is inactive:
* allow if the specific stage is already completed (or enrollment fully completed).
*/
async assertAllowsCompletedStageRead(
participantId: number,
learningGroupParticipantId: number,
stageCompleted: boolean,
enrollmentStatus?: string | null,
): Promise<void> {
if (await this.isCompanyActiveForParticipant(participantId)) {
return;
}
if (stageCompleted) {
return;
}
let status = enrollmentStatus;
if (status === undefined || status === null) {
const row = await this.prisma.client.learningGroupParticipant.findFirst({
where: { id: learningGroupParticipantId, participant_id: participantId },
select: { status: true },
});
if (!row) {
throw new NotFoundException("Course enrollment not found or access denied");
}
status = row.status;
}
if (status === ParticipantLearningProgressStatus.COMPLETED) {
return;
}
throw new ForbiddenException(SUBSCRIPTION_EXPIRED_COURSE_ACCESS_MESSAGE);
}
/**
* E-learning read access when subscription is inactive:
* allow if enrollment is fully completed OR e-learning stage is completed.
*/
async assertAllowsElearningRead(
participantId: number,
learningGroupParticipantId: number,
enrollmentStatus?: string | null,
): Promise<void> {
if (await this.isCompanyActiveForParticipant(participantId)) {
return;
}
const enrollment = await this.prisma.client.learningGroupParticipant.findFirst({
where: { id: learningGroupParticipantId, participant_id: participantId },
select: {
status: true,
course_id: true,
learning_group_id: true,
},
});
if (!enrollment) {
throw new NotFoundException("Course enrollment not found or access denied");
}
const status = enrollmentStatus ?? enrollment.status;
if (status === ParticipantLearningProgressStatus.COMPLETED) {
return;
}
const eLearningParticipant = await this.prisma.client.eLearningParticipant.findUnique({
where: {
participant_id_course_id_learning_group_id: {
participant_id: participantId,
course_id: enrollment.course_id,
learning_group_id: enrollment.learning_group_id,
},
},
select: {
status: true,
course_modules_completed: true,
total_course_modules: true,
},
});
const isElearningCompleted =
eLearningParticipant?.status === ELearningProgressStatus.COMPLETED ||
((eLearningParticipant?.course_modules_completed ?? 0) > 0 &&
(eLearningParticipant?.total_course_modules ?? 0) > 0 &&
(eLearningParticipant?.course_modules_completed ?? 0) >=
(eLearningParticipant?.total_course_modules ?? 0));
if (isElearningCompleted) {
return;
}
throw new ForbiddenException(SUBSCRIPTION_EXPIRED_COURSE_ACCESS_MESSAGE);
}
/**
* New allocations / accepting invitations while subscription is inactive.
*/
async assertCompanySubscriptionActive(participantId: number): Promise<void> {
if (!(await this.isCompanyActiveForParticipant(participantId))) {
throw new ForbiddenException(SUBSCRIPTION_EXPIRED_COURSE_ACCESS_MESSAGE);
}
}
}