Skip to content

Commit afaf387

Browse files
committed
move functionality to service
1 parent 58c48b8 commit afaf387

3 files changed

Lines changed: 95 additions & 77 deletions

File tree

src/app/api/assessments/send-invitation/route.ts

Lines changed: 5 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import { z } from 'zod';
33
import { handleError } from '@/lib/utils/errors.utils';
44
import { getSession } from '@/lib/utils/auth.utils';
55
import { assertRecruiterOrAbove } from '@/lib/utils/permissions.utils';
6-
import { prisma } from '@/lib/prisma';
7-
import sesConnector from '@/lib/connectors/ses.connector';
8-
import { generateAssessmentInvitationHTML } from '@/lib/templates/assessment-invitation-email';
6+
import { sendAssessmentInvitationEmail } from '@/lib/services/assessment-invitation-email.service';
97

108
const sendAssessmentInvitationSchema = z.object({
119
candidateId: z.string().cuid(),
@@ -19,82 +17,14 @@ export async function POST(request: NextRequest) {
1917
const body = await request.json();
2018
const { candidateId } = sendAssessmentInvitationSchema.parse(body);
2119

22-
const candidate = await prisma.candidate.findUnique({
23-
where: { id: candidateId },
24-
include: {
25-
applications: {
26-
include: {
27-
assessment: true,
28-
position: true,
29-
},
30-
},
31-
organization: true,
32-
},
33-
});
34-
35-
//validation
36-
if (!candidate) {
37-
return Response.json({ error: 'Candidate not found' }, { status: 404 });
38-
}
39-
40-
if (candidate.orgId !== session.activeOrganizationId) {
41-
return Response.json({ error: 'Unauthorized' }, { status: 403 });
42-
}
43-
44-
const applicationWithAssessment = candidate.applications.find(
45-
(app) => app.assessment !== null
20+
const result = await sendAssessmentInvitationEmail(
21+
candidateId,
22+
session.activeOrganizationId
4623
);
4724

48-
if (!applicationWithAssessment?.assessment) {
49-
return Response.json(
50-
{ error: 'Candidate does not have an assigned assessment' },
51-
{ status: 400 }
52-
);
53-
}
54-
55-
const assessment = applicationWithAssessment.assessment;
56-
const position = applicationWithAssessment.position;
57-
const organization = candidate.organization;
58-
59-
//create url
60-
const baseUrl =
61-
process.env.BETTER_AUTH_URL ??
62-
process.env.NEXT_PUBLIC_APP_URL ??
63-
'http://localhost:3000';
64-
const assessmentUrl = `${baseUrl}/assessment/${assessment.uniqueLink}`;
65-
const logoUrl = `${baseUrl}/Sarge_logo.svg`;
66-
67-
//email html
68-
const htmlContent = generateAssessmentInvitationHTML({
69-
candidateName: candidate.name,
70-
positionTitle: position.title,
71-
organizationName: organization.name,
72-
assessmentId: assessment.id,
73-
assessmentUrl,
74-
logoUrl,
75-
});
76-
77-
//send email
78-
const emailSent = await sesConnector.sendEmail(
79-
candidate.email,
80-
`${organization.name} Software Engineering Role: Online Assessment Invitation`,
81-
`Hello ${candidate.name}, you have been invited to complete an online assessment for the ${position.title} position at ${organization.name}. Visit ${assessmentUrl} to begin.`,
82-
{ html: htmlContent }
83-
);
84-
85-
if (!emailSent) {
86-
return Response.json({ error: 'Failed to send invitation email' }, { status: 500 });
87-
}
88-
8925
return Response.json(
9026
{
91-
data: {
92-
success: true,
93-
message: `Assessment invitation sent to ${candidate.email}`,
94-
candidateName: candidate.name,
95-
positionTitle: position.title,
96-
assessmentId: assessment.id,
97-
},
27+
data: result,
9828
},
9929
{ status: 200 }
10030
);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { prisma } from '@/lib/prisma';
2+
import sesConnector from '@/lib/connectors/ses.connector';
3+
import { generateAssessmentInvitationHTML } from '@/lib/templates/assessment-invitation-email';
4+
5+
interface SendAssessmentInvitationResult {
6+
success: boolean;
7+
message: string;
8+
candidateName: string;
9+
positionTitle: string;
10+
assessmentId: string;
11+
}
12+
13+
export async function sendAssessmentInvitationEmail(
14+
candidateId: string,
15+
activeOrganizationId: string
16+
): Promise<SendAssessmentInvitationResult> {
17+
const candidate = await prisma.candidate.findUnique({
18+
where: { id: candidateId },
19+
include: {
20+
applications: {
21+
include: {
22+
assessment: true,
23+
position: true,
24+
},
25+
},
26+
organization: true,
27+
},
28+
});
29+
30+
if (!candidate) {
31+
throw new Error('Candidate not found');
32+
}
33+
34+
if (candidate.orgId !== activeOrganizationId) {
35+
throw new Error('Unauthorized');
36+
}
37+
38+
const applicationWithAssessment = candidate.applications.find((app) => app.assessment !== null);
39+
40+
if (!applicationWithAssessment?.assessment) {
41+
throw new Error('Candidate does not have an assigned assessment');
42+
}
43+
44+
const assessment = applicationWithAssessment.assessment;
45+
const position = applicationWithAssessment.position;
46+
const organization = candidate.organization;
47+
48+
const baseUrl =
49+
process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
50+
const assessmentUrl = `${baseUrl}/assessment/${assessment.uniqueLink}`;
51+
const logoUrl = `${baseUrl}/Sarge_logo.svg`;
52+
53+
//placeholder duration and expiration
54+
const durationMinutes = 120;
55+
const expirationDate = 'March 16, 2026 11:59PM EST';
56+
const htmlContent = generateAssessmentInvitationHTML({
57+
candidateName: candidate.name,
58+
positionTitle: position.title,
59+
organizationName: organization.name,
60+
assessmentId: assessment.id,
61+
assessmentUrl,
62+
logoUrl,
63+
durationMinutes,
64+
expirationDate,
65+
});
66+
67+
// Send email
68+
const emailSent = await sesConnector.sendEmail(
69+
candidate.email,
70+
`${organization.name} Software Engineering Role: Online Assessment Invitation`,
71+
`Hello ${candidate.name}, you have been invited to complete an online assessment for the ${position.title} position at ${organization.name}. Visit ${assessmentUrl} to begin.`,
72+
{ html: htmlContent }
73+
);
74+
75+
if (!emailSent) {
76+
throw new Error('Failed to send invitation email');
77+
}
78+
79+
return {
80+
success: true,
81+
message: `Assessment invitation sent to ${candidate.email}`,
82+
candidateName: candidate.name,
83+
positionTitle: position.title,
84+
assessmentId: assessment.id,
85+
};
86+
}

src/lib/templates/assessment-invitation-email.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export interface AssessmentInvitationEmailData {
55
assessmentId: string;
66
assessmentUrl: string;
77
logoUrl: string;
8+
durationMinutes: number;
9+
expirationDate: string;
810
}
911

1012
export function generateAssessmentInvitationHTML(data: AssessmentInvitationEmailData): string {
@@ -49,10 +51,10 @@ export function generateAssessmentInvitationHTML(data: AssessmentInvitationEmail
4951
5052
<div style="text-align: center; margin: 0 0 32px 0; padding: 24px 0;">
5153
<div style="margin-bottom: 16px; font-size: 14px; color: #333333;">
52-
<strong>Duration:</strong> 120 minutes
54+
<strong>Duration:</strong> ${data.durationMinutes} minutes
5355
</div>
5456
<div style="font-size: 14px; color: #333333;">
55-
<strong>Test Expiration Date:</strong> March 16, 2026 11:59PM EST
57+
<strong>Test Expiration Date:</strong> ${data.expirationDate}
5658
</div>
5759
</div>
5860

0 commit comments

Comments
 (0)