Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session 6: New endpoints by Juan Arroyes #1

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions backend/src/application/services/candidateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { validateCandidateData } from '../validator';
import { Education } from '../../domain/models/Education';
import { WorkExperience } from '../../domain/models/WorkExperience';
import { Resume } from '../../domain/models/Resume';
import { Application } from '../../domain/models/Application';
import { InterviewStep } from '../../domain/models/InterviewStep';

export const addCandidate = async (candidateData: any) => {
try {
Expand Down Expand Up @@ -63,3 +65,22 @@ export const findCandidateById = async (id: number): Promise<Candidate | null> =
throw new Error('Error al recuperar el candidato');
}
};

export const updateCandidateStageService = async (candidateId: number, currentInterviewStepName: string) => {
const application = await Application.findByCandidateId(candidateId);

if (!application) {
return null;
}

const interviewStep = await InterviewStep.findOneBy({ name: currentInterviewStepName });

if (!interviewStep || interviewStep.id === undefined) {
throw new Error('Interview step not found or ID is undefined');
}

application.currentInterviewStep = interviewStep.id;
await application.save();

return application;
};
25 changes: 25 additions & 0 deletions backend/src/application/services/positionService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Application } from '../../domain/models/Application';

export const getCandidatesForPositionService = async (positionId: number) => {
const applications = await Application.findByPositionId(positionId);

if (applications.length === 0) {
return null;
}

return applications.map(application => {
if (!application.candidate) {
throw new Error(`Candidate not found for application ID ${application.id}`);
}

const averageScore =
application.interviews.reduce((acc, interview) => acc + (interview.score || 0), 0) /
application.interviews.length;

return {
fullName: `${application.candidate.firstName} ${application.candidate.lastName}`,
currentInterviewStep: application.currentInterviewStep,
averageScore: averageScore || 0,
};
});
};
34 changes: 26 additions & 8 deletions backend/src/domain/models/Application.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PrismaClient } from '@prisma/client';
import { Interview } from './Interview';
import { Candidate } from './Candidate';

const prisma = new PrismaClient();

Expand All @@ -11,6 +12,7 @@ export class Application {
currentInterviewStep: number;
notes?: string;
interviews: Interview[]; // Added this line
candidate?: Candidate; // Added this line

constructor(data: any) {
this.id = data.id;
Expand All @@ -20,6 +22,7 @@ export class Application {
this.currentInterviewStep = data.currentInterviewStep;
this.notes = data.notes;
this.interviews = data.interviews || []; // Added this line
this.candidate = data.candidate; // Added this line
}

async save() {
Expand All @@ -32,22 +35,37 @@ export class Application {
};

if (this.id) {
return await prisma.application.update({
// Update existing application
await prisma.application.update({
where: { id: this.id },
data: applicationData,
});
} else {
return await prisma.application.create({
// Create new application
const newApplication = await prisma.application.create({
data: applicationData,
});
this.id = newApplication.id;
}
}

static async findOne(id: number): Promise<Application | null> {
const data = await prisma.application.findUnique({
where: { id: id },
static async findByPositionId(positionId: number): Promise<Application[]> {
const applications = await prisma.application.findMany({
where: { positionId },
include: {
candidate: true,
interviews: true,
},
});
if (!data) return null;
return new Application(data);

return applications.map(app => new Application(app));
}

static async findByCandidateId(candidateId: number): Promise<Application | null> {
const application = await prisma.application.findFirst({
where: { candidateId },
});

return application ? new Application(application) : null;
}
}
}
8 changes: 8 additions & 0 deletions backend/src/domain/models/InterviewStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,13 @@ export class InterviewStep {
if (!data) return null;
return new InterviewStep(data);
}

static async findOneBy(params: { name: string }): Promise<InterviewStep | null> {
const data = await prisma.interviewStep.findFirst({
where: { name: params.name },
});
if (!data) return null;
return new InterviewStep(data);
}
}

3 changes: 3 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PrismaClient } from '@prisma/client';
import dotenv from 'dotenv';
import candidateRoutes from './routes/candidateRoutes';
import { uploadFile } from './application/services/fileUploadService';
import positionRoutes from './routes/positionRoutes';
import cors from 'cors';

// Extender la interfaz Request para incluir prisma
Expand Down Expand Up @@ -42,6 +43,8 @@ app.use('/candidates', candidateRoutes);
// Route for file uploads
app.post('/upload', uploadFile);

app.use('/position', positionRoutes);

app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
Expand Down
25 changes: 25 additions & 0 deletions backend/src/presentation/controllers/candidateController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import { addCandidate, findCandidateById } from '../../application/services/candidateService';
import { updateCandidateStageService } from '../../application/services/candidateService';

export const addCandidateController = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -31,4 +32,28 @@ export const getCandidateById = async (req: Request, res: Response) => {
}
};

export const updateCandidateStage = async (req: Request, res: Response) => {
const candidateId = parseInt(req.params.id);
const { currentInterviewStep } = req.body;

if (isNaN(candidateId)) {
return res.status(400).json({ error: 'Invalid candidate ID format' });
}

if (!currentInterviewStep) {
return res.status(400).json({ error: 'currentInterviewStep is required' });
}

try {
const updatedCandidate = await updateCandidateStageService(candidateId, currentInterviewStep);
if (!updatedCandidate) {
return res.status(404).json({ error: 'Candidate not found' });
}
res.json(updatedCandidate);
} catch (error) {
console.error(`Error updating candidate stage for candidate ID ${candidateId}:`, error);
res.status(500).json({ error: 'Internal Server Error' });
}
};

export { addCandidate };
29 changes: 29 additions & 0 deletions backend/src/presentation/controllers/positionController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Request, Response } from 'express';
import { getCandidatesForPositionService } from '../../application/services/positionService';

export const getCandidatesForPosition = async (req: Request, res: Response) => {
const positionId = parseInt(req.params.id);
if (isNaN(positionId)) {
return res.status(400).json({ error: 'Invalid position ID format' });
}

try {
const candidates = await getCandidatesForPositionService(positionId);
if (!candidates) {
return res.status(404).json({ error: 'Position not found' });
}
res.json(candidates);
} catch (error) {
console.error(`Error fetching candidates for position ID ${positionId}:`, error);

if (error instanceof Error) {
if (error.message.includes('Candidate not found')) {
res.status(404).json({ error: error.message });
} else {
res.status(500).json({ error: 'Internal Server Error' });
}
} else {
res.status(500).json({ error: 'Unknown error occurred' });
}
}
};
3 changes: 3 additions & 0 deletions backend/src/routes/candidateRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Router } from 'express';
import { addCandidate, getCandidateById } from '../presentation/controllers/candidateController';
import { updateCandidateStage } from '../presentation/controllers/candidateController';

const router = Router();

Expand All @@ -17,6 +18,8 @@ router.post('/', async (req, res) => {
}
});

router.put('/:id', updateCandidateStage);

router.get('/:id', getCandidateById);

export default router;
8 changes: 8 additions & 0 deletions backend/src/routes/positionRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from 'express';
import { getCandidatesForPosition } from '../presentation/controllers/positionController';

const router = express.Router();

router.get('/:id/candidates', getCandidatesForPosition);

export default router;
91 changes: 91 additions & 0 deletions backend/src/tests/positionService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { getCandidatesForPositionService } from '../application/services/positionService';
import { Application } from '../domain/models/Application';

jest.mock('../domain/models/Application', () => ({
Application: {
findByPositionId: jest.fn(),
},
}));

describe('getCandidatesForPositionService', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should return null if no applications are found', async () => {
(Application.findByPositionId as jest.Mock).mockResolvedValue([]);

const result = await getCandidatesForPositionService(1);

expect(result).toBeNull();
});

it('should throw an error if an application has no candidate', async () => {
const applications = [
{
id: 1,
candidate: null,
interviews: [],
currentInterviewStep: 'Step 1',
},
];
(Application.findByPositionId as jest.Mock).mockResolvedValue(applications);

await expect(getCandidatesForPositionService(1)).rejects.toThrow('Candidate not found for application ID 1');
});

it('should return candidates with their full name, current interview step, and average score', async () => {
const applications = [
{
id: 1,
candidate: { firstName: 'John', lastName: 'Doe' },
interviews: [{ score: 4 }, { score: 5 }],
currentInterviewStep: 'Step 1',
},
{
id: 2,
candidate: { firstName: 'Jane', lastName: 'Smith' },
interviews: [{ score: 3 }, { score: 4 }],
currentInterviewStep: 'Step 2',
},
];
(Application.findByPositionId as jest.Mock).mockResolvedValue(applications);

const result = await getCandidatesForPositionService(1);

expect(result).toEqual([
{
fullName: 'John Doe',
currentInterviewStep: 'Step 1',
averageScore: 4.5,
},
{
fullName: 'Jane Smith',
currentInterviewStep: 'Step 2',
averageScore: 3.5,
},
]);
});

it('should handle applications with no interviews gracefully', async () => {
const applications = [
{
id: 1,
candidate: { firstName: 'John', lastName: 'Doe' },
interviews: [],
currentInterviewStep: 'Step 1',
},
];
(Application.findByPositionId as jest.Mock).mockResolvedValue(applications);

const result = await getCandidatesForPositionService(1);

expect(result).toEqual([
{
fullName: 'John Doe',
currentInterviewStep: 'Step 1',
averageScore: 0,
},
]);
});
});
Loading