diff --git a/apps/api/package.json b/apps/api/package.json index e77af9a..ca1942b 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,6 +10,7 @@ "start": "node dist/index.js", "db:push": "npx drizzle-kit push", "dev:worker": "nodemon --watch src --ext ts,json --exec \"tsx ./src/queues/worker.ts\"", + "index:qdrant": "nodemon src --ext ts,json --exec \"tsx ./src/db/qdrant-index.ts\"", "test": "vitest", "test:unit": "vitest run --reporter=verbose src/__tests__/unit/*.test.ts", "test:integration": "vitest run --reporter=verbose src/__tests__/integration/*.test.ts", diff --git a/apps/api/src/__tests__/unit/interview.test.ts b/apps/api/src/__tests__/unit/interview.test.ts new file mode 100644 index 0000000..938bf97 --- /dev/null +++ b/apps/api/src/__tests__/unit/interview.test.ts @@ -0,0 +1,727 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { db } from "../../db/drizzle.js"; +import { getVectorStore } from "../../db/qdrant.js"; +import type { IInterview, IQuestion } from "../../db/types.js"; +import { InterviewService } from "../../app/modules/interview/interview.service.js"; + +vi.mock("../../db/drizzle.js", () => ({ + db: { + query: { + candidate: { + findFirst: vi.fn(), + }, + interview: { + findMany: vi.fn(), + findFirst: vi.fn(), + }, + question: { + findMany: vi.fn(), + }, + }, + insert: vi.fn(), + update: vi.fn(), + }, +})); + +vi.mock("../../queues/producer.js", () => ({ + addResumeToQueue: vi.fn(), +})); + +vi.mock("../../db/qdrant.js", () => ({ + getVectorStore: vi.fn(), +})); + +const mockInvoke = vi.fn(); +vi.mock("@langchain/groq", () => { + return { + ChatGroq: class MockChatGroq { + constructor() {} + invoke = mockInvoke; + }, + }; +}); + +// Create a reusable mock vector store +const createMockVectorStore = (resumeChunks: any[] = []) => ({ + similaritySearchWithScore: vi.fn().mockResolvedValue(resumeChunks), +}); + +// Helpers +const createInsertMock = (returnValue: any) => ({ + values: vi.fn().mockReturnValue({ + returning: vi.fn().mockResolvedValue([returnValue]), + }), +}); + +const createUpdateMock = (returnValue: any) => ({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + returning: vi.fn().mockResolvedValue([returnValue]), + }), + }), +}); + +describe("Interview Service - Unit Tests", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("Create Interview", () => { + it("Should create interview successfully when there is no active interview and resume exists", async () => { + const mockCandidate = { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }; + const allInterviews: IInterview[] = [ + { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, + score: 100, + feedback: "Great", + attempt: 1, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "interview-1234", + candidateId: "candidate-123", + isActive: false, + score: 10, + feedback: "Needs Improvement", + attempt: 2, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + const mockResumeChunks = [ + [ + { + pageContent: "Resume content...", + metadata: { candidateId: "candidate-123", chunkIndex: 0 }, + }, + 0.95, + ], + ]; + const mockNewInterview: IInterview = { + id: "interview-new", + candidateId: "candidate-123", + isActive: true, + score: null, + feedback: null, + attempt: 3, // allInterviews.length + 1 = 2 + 1 + isCompleted: false, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + + vi.mocked(db.query.candidate.findFirst).mockResolvedValue(mockCandidate); + vi.mocked(db.query.interview.findMany).mockResolvedValue(allInterviews); + vi.mocked(getVectorStore).mockResolvedValue( + createMockVectorStore(mockResumeChunks) as any, + ); + vi.mocked(db.insert).mockReturnValue( + createInsertMock(mockNewInterview) as any, + ); + + // Act - call the service + const result = await InterviewService.createInterview("user-123"); + + // Assert + expect(result.newInterview).toBeDefined(); + expect(result.newInterview?.id).toBe("interview-new"); + expect(result.newInterview?.isActive).toBe(true); + expect(result.newInterview?.attempt).toBe(3); + expect(result.newInterview?.candidateId).toBe("candidate-123"); + + // Verify mocks were called correctly + expect(db.query.candidate.findFirst).toHaveBeenCalledOnce(); + expect(db.query.interview.findMany).toHaveBeenCalledOnce(); + expect(getVectorStore).toHaveBeenCalledOnce(); + expect(db.insert).toHaveBeenCalledOnce(); + }); + it("Should throw ApiError when candidate profile not found", async () => { + const mockCandidate = undefined; + vi.mocked(db.query.candidate.findFirst).mockResolvedValue(mockCandidate); + await expect( + InterviewService.createInterview("user-123"), + ).rejects.toThrow("Candidate profile not found"); + }); + it("Should throw ApiError when an active interview already exists", async () => { + const mockCandidate = { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }; + const allInterviews: IInterview[] = [ + { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, + score: 100, + feedback: "Great", + attempt: 1, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "interview-1234", + candidateId: "candidate-123", + isActive: true, + score: null, + feedback: "Needs Improvement", + attempt: 2, + isCompleted: false, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + vi.mocked(db.query.candidate.findFirst).mockResolvedValue(mockCandidate); + vi.mocked(db.query.interview.findMany).mockResolvedValue(allInterviews); + await expect( + InterviewService.createInterview("user-123"), + ).rejects.toThrow("An active interview already exists"); + }); + it("Should throw ApiError when resume is not uploaded", async () => { + const mockCandidate = { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }; + const allInterviews: IInterview[] = [ + { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, + score: 100, + feedback: "Great", + attempt: 1, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "interview-1234", + candidateId: "candidate-123", + isActive: false, + score: 10, + feedback: "Needs Improvement", + attempt: 2, + isCompleted: false, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + const mockResumeChunks: [ + { + pageContent: string; + metadata: { candidateId: string; chunkIndex: number }; + }, + number, + ][] = []; + + vi.mocked(db.query.candidate.findFirst).mockResolvedValue(mockCandidate); + vi.mocked(db.query.interview.findMany).mockResolvedValue(allInterviews); + vi.mocked(getVectorStore).mockResolvedValue( + createMockVectorStore(mockResumeChunks) as any, + ); + + await expect( + InterviewService.createInterview("user-123"), + ).rejects.toThrow("No resume found for the candidate"); + }); + }); + + describe("Finish interview", () => { + it("should finish interview successfully when interview exists", async () => { + const mockInterviewWithCandidate = { + id: "interview-123", + candidateId: "candidate-123", + isActive: true, + score: null, + feedback: null, + attempt: 1, + isCompleted: false, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + candidate: { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }, + }; + const mockedQuestions: IQuestion[] = [ + { + id: "question-123", + interviewId: "interview-123", + questionText: "What is your greatest strength?", + answerText: "My greatest strength is problem-solving.", + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + const mockUpdatedInterview: IInterview = { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, + score: 85, + feedback: "Good problem-solving skills demonstrated.", + attempt: 1, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + + // Mock LLM response + mockInvoke.mockResolvedValue({ + content: + '{"score": 85, "feedback": "Good problem-solving skills demonstrated."}', + }); + + vi.mocked(db.query.interview.findFirst).mockResolvedValue( + mockInterviewWithCandidate, + ); + vi.mocked(db.query.question.findMany).mockResolvedValue(mockedQuestions); + vi.mocked(db.update).mockReturnValue( + createUpdateMock(mockUpdatedInterview) as any, + ); + + // Act + const result = await InterviewService.finishInterview( + "user-123", + "interview-123", + ); + + // Assert + expect(result).toBeDefined(); + expect(result?.isActive).toBe(false); + expect(result?.isCompleted).toBe(true); + expect(result?.score).toBe(85); + expect(result?.feedback).toBe( + "Good problem-solving skills demonstrated.", + ); + expect(mockInvoke).toHaveBeenCalledOnce(); + expect(db.update).toHaveBeenCalledOnce(); + }); + it("should throw ApiError when interview not found", async () => { + vi.mocked(db.query.interview.findFirst).mockResolvedValue(undefined); + + await expect( + InterviewService.finishInterview("user-123", "interview-123"), + ).rejects.toThrow("Interview not found"); + }); + it("should throw ApiError when user is not the owner of the interview", async () => { + const mockInterviewWithCandidate = { + id: "interview-123", + candidateId: "candidate-123", + isActive: true, + score: null, + feedback: null, + attempt: 1, + isCompleted: false, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + candidate: { + id: "candidate-123", + userId: "different-user", // Different user + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }, + }; + + vi.mocked(db.query.interview.findFirst).mockResolvedValue( + mockInterviewWithCandidate, + ); + + await expect( + InterviewService.finishInterview("user-123", "interview-123"), + ).rejects.toThrow("Unauthorized access to this interview"); + }); + }); + + describe("Get Candidate Resume", () => { + it("should return resume text when candidate and resume exist", async () => { + const mockCandidate = { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }; + + const mockResumeChunks = [ + [ + { + pageContent: "Part 2 of resume", + metadata: { candidateId: "candidate-123", chunkIndex: 1 }, + }, + 0.9, + ], + [ + { + pageContent: "Part 1 of resume", + metadata: { candidateId: "candidate-123", chunkIndex: 0 }, + }, + 0.95, + ], + ]; + + vi.mocked(db.query.candidate.findFirst).mockResolvedValue(mockCandidate); + vi.mocked(getVectorStore).mockResolvedValue( + createMockVectorStore(mockResumeChunks) as any, + ); + + const result = await InterviewService.getCandidateResume("user-123"); + + expect(result.resume).toBeDefined(); + expect(result.resume).toBe("Part 1 of resume\nPart 2 of resume"); + expect(db.query.candidate.findFirst).toHaveBeenCalledOnce(); + expect(getVectorStore).toHaveBeenCalledOnce(); + }); + it("should throw ApiError when candidate not found", async () => { + vi.mocked(db.query.candidate.findFirst).mockResolvedValue(undefined); + + await expect( + InterviewService.getCandidateResume("user-123"), + ).rejects.toThrow("Interview not found"); + }); + it("should throw ApiError when no resume found", async () => { + const mockCandidate = { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }; + + vi.mocked(db.query.candidate.findFirst).mockResolvedValue(mockCandidate); + vi.mocked(getVectorStore).mockResolvedValue( + createMockVectorStore([]) as any, + ); + + await expect( + InterviewService.getCandidateResume("user-123"), + ).rejects.toThrow("No resume found for the candidate"); + }); + }); + + describe("Evaluate Interview", () => { + it("should evaluate interview successfully", async () => { + const mockInterviewWithCandidate = { + id: "interview-123", + candidateId: "candidate-123", + isActive: true, + score: null, + feedback: null, + attempt: 1, + isCompleted: false, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + candidate: { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }, + }; + + const mockUpdatedInterview: IInterview = { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, + score: 75, + feedback: "Candidate showed good communication skills.", + attempt: 1, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + + mockInvoke.mockResolvedValue({ + content: + '{"score": 75, "feedback": "Candidate showed good communication skills."}', + }); + + vi.mocked(db.query.interview.findFirst).mockResolvedValue( + mockInterviewWithCandidate, + ); + vi.mocked(db.update).mockReturnValue( + createUpdateMock(mockUpdatedInterview) as any, + ); + + const result = await InterviewService.evaluateInterview( + { + transcript: + "Interviewer: Tell me about yourself.\nCandidate: I am a software developer...", + interviewId: "interview-123", + }, + "user-123", + ); + + expect(result).toBeDefined(); + expect(result?.isActive).toBe(false); + expect(result?.isCompleted).toBe(true); + expect(result?.score).toBe(75); + expect(result?.feedback).toBe( + "Candidate showed good communication skills.", + ); + expect(mockInvoke).toHaveBeenCalledOnce(); + expect(db.update).toHaveBeenCalledOnce(); + }); + it("should throw ApiError when interview not found", async () => { + vi.mocked(db.query.interview.findFirst).mockResolvedValue(undefined); + + await expect( + InterviewService.evaluateInterview( + { transcript: "test", interviewId: "interview-123" }, + "user-123", + ), + ).rejects.toThrow("Interview not found"); + }); + it("should throw ApiError when interview is not active", async () => { + const mockInterviewWithCandidate = { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, // Not active + score: 50, + feedback: "Previous feedback", + attempt: 1, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + candidate: { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }, + }; + + vi.mocked(db.query.interview.findFirst).mockResolvedValue( + mockInterviewWithCandidate, + ); + + await expect( + InterviewService.evaluateInterview( + { transcript: "test", interviewId: "interview-123" }, + "user-123", + ), + ).rejects.toThrow("Cannot save question to an inactive interview"); + }); + it("should throw ApiError when user is not the owner", async () => { + const mockInterviewWithCandidate = { + id: "interview-123", + candidateId: "candidate-123", + isActive: true, + score: null, + feedback: null, + attempt: 1, + isCompleted: false, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + candidate: { + id: "candidate-123", + userId: "different-user", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }, + }; + + vi.mocked(db.query.interview.findFirst).mockResolvedValue( + mockInterviewWithCandidate, + ); + + await expect( + InterviewService.evaluateInterview( + { transcript: "test", interviewId: "interview-123" }, + "user-123", + ), + ).rejects.toThrow("Unauthorized access to this interview"); + }); + }); + + describe("Save Recording URL", () => { + it("should save recording URL successfully", async () => { + const mockInterviewWithCandidate = { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, + score: 85, + feedback: "Great interview", + attempt: 1, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + candidate: { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }, + }; + + const mockUpdatedInterview: IInterview = { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, + score: 85, + feedback: "Great interview", + attempt: 1, + isCompleted: true, + recordingUrl: "https://example.com/recording.mp4", + createdAt: new Date(), + updatedAt: new Date(), + }; + + vi.mocked(db.query.interview.findFirst).mockResolvedValue( + mockInterviewWithCandidate, + ); + vi.mocked(db.update).mockReturnValue( + createUpdateMock(mockUpdatedInterview) as any, + ); + + await InterviewService.saveRecordingUrl( + "interview-123", + "https://example.com/recording.mp4", + "user-123", + ); + + expect(db.query.interview.findFirst).toHaveBeenCalledOnce(); + expect(db.update).toHaveBeenCalledOnce(); + }); + it("should throw ApiError when interview not found", async () => { + vi.mocked(db.query.interview.findFirst).mockResolvedValue(undefined); + + await expect( + InterviewService.saveRecordingUrl( + "interview-123", + "https://example.com/recording.mp4", + "user-123", + ), + ).rejects.toThrow("Interview not found"); + }); + it("should throw ApiError when user is not the owner", async () => { + const mockInterviewWithCandidate = { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, + score: 85, + feedback: "Great interview", + attempt: 1, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + candidate: { + id: "candidate-123", + userId: "different-user", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }, + }; + + vi.mocked(db.query.interview.findFirst).mockResolvedValue( + mockInterviewWithCandidate, + ); + + await expect( + InterviewService.saveRecordingUrl( + "interview-123", + "https://example.com/recording.mp4", + "user-123", + ), + ).rejects.toThrow("Unauthorized access to this interview"); + }); + it("should throw ApiError when update fails", async () => { + const mockInterviewWithCandidate = { + id: "interview-123", + candidateId: "candidate-123", + isActive: false, + score: 85, + feedback: "Great interview", + attempt: 1, + isCompleted: true, + recordingUrl: null, + createdAt: new Date(), + updatedAt: new Date(), + candidate: { + id: "candidate-123", + userId: "user-123", + fullName: "John Doe", + createdAt: new Date(), + updatedAt: new Date(), + address: null, + }, + }; + + // Mock update returning empty array (no result) + const failedUpdateMock = { + set: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + returning: vi.fn().mockResolvedValue([]), + }), + }), + }; + + vi.mocked(db.query.interview.findFirst).mockResolvedValue( + mockInterviewWithCandidate, + ); + vi.mocked(db.update).mockReturnValue(failedUpdateMock as any); + + await expect( + InterviewService.saveRecordingUrl( + "interview-123", + "https://example.com/recording.mp4", + "user-123", + ), + ).rejects.toThrow("Failed to save recording URL"); + }); + }); +}); diff --git a/apps/api/src/app/modules/job/job.service.ts b/apps/api/src/app/modules/job/job.service.ts index 56c866f..2b8e18c 100644 --- a/apps/api/src/app/modules/job/job.service.ts +++ b/apps/api/src/app/modules/job/job.service.ts @@ -59,7 +59,7 @@ const findEmployeesForJob = async (userId: string, jobId: string) => { if (isJobExists.recruiter.userId !== userId) { throw new ApiError( 403, - "You are not authorized to view candidates for this job." + "You are not authorized to view candidates for this job.", ); } @@ -72,7 +72,7 @@ const findEmployeesForJob = async (userId: string, jobId: string) => { } if (isJobExists.additionalSkills?.length) { queryTextParts.push( - `Additional skills: ${isJobExists.additionalSkills.join(", ")}` + `Additional skills: ${isJobExists.additionalSkills.join(", ")}`, ); } @@ -83,7 +83,7 @@ const findEmployeesForJob = async (userId: string, jobId: string) => { const vectorStor = await getVectorStore(); const matchedChunks = await vectorStor.similaritySearchWithScore( queryText, - k + k, ); type Aggregate = { @@ -134,7 +134,7 @@ const findEmployeesForJob = async (userId: string, jobId: string) => { } } const ranked = Array.from(perCandidate.values()).sort( - (a, b) => b.avgScore - a.avgScore + (a, b) => b.avgScore - a.avgScore, ); const finalResult = await Promise.all( ranked.map(async (r) => { @@ -156,7 +156,7 @@ const findEmployeesForJob = async (userId: string, jobId: string) => { candidate: cand?.user || null, interview: isInterviewed, }; - }) + }), ); const sorterInterviewedFirst = finalResult diff --git a/apps/api/src/db/qdrant-index.ts b/apps/api/src/db/qdrant-index.ts index 9814085..de4b65d 100644 --- a/apps/api/src/db/qdrant-index.ts +++ b/apps/api/src/db/qdrant-index.ts @@ -6,16 +6,41 @@ const client = new QdrantClient({ apiKey: config.qdrant.key!, }); -async function createIndex() { +async function setupCollection() { + const collectionName = "resume"; + try { - await client.createPayloadIndex("resume", { + // Check if collection exists + const collections = await client.getCollections(); + const exists = collections.collections.some( + (c) => c.name === collectionName, + ); + + if (exists) { + console.log(`Collection "${collectionName}" already exists. Deleting...`); + await client.deleteCollection(collectionName); + } + + // Create collection with proper vector config for all-MiniLM-L6-v2 (384 dimensions) + await client.createCollection(collectionName, { + vectors: { + size: 384, + distance: "Cosine", + }, + }); + console.log( + `✅ Collection "${collectionName}" created with 384-dim vectors`, + ); + + // Create payload index for candidateId + await client.createPayloadIndex(collectionName, { field_name: "metadata.candidateId", field_schema: "keyword", }); console.log("✅ Index created for candidateId"); } catch (error) { - console.error("❌ Error creating index:", error); + console.error("❌ Error setting up collection:", error); } } -createIndex(); +setupCollection(); diff --git a/apps/api/src/db/qdrant.ts b/apps/api/src/db/qdrant.ts index f462973..c1e3c01 100644 --- a/apps/api/src/db/qdrant.ts +++ b/apps/api/src/db/qdrant.ts @@ -15,7 +15,8 @@ export const connectQdrant = async () => { url: config.qdrant.url!, collectionName: "resume", apiKey: config.qdrant.key!, - } + contentPayloadKey: "content", + }, ); console.log("✅ Qdrant vector store connected"); }; @@ -31,7 +32,8 @@ export const getVectorStore = async (): Promise => { url: config.qdrant.url!, collectionName: "resume", apiKey: config.qdrant.key!, - } + contentPayloadKey: "content", + }, ); console.log("✅ Qdrant vector store connected"); return vectorStoreInstance; diff --git a/apps/api/src/db/types.ts b/apps/api/src/db/types.ts index bafdc02..7250b0d 100644 --- a/apps/api/src/db/types.ts +++ b/apps/api/src/db/types.ts @@ -1,6 +1,8 @@ import type { InferSelectModel } from "drizzle-orm"; -import type { job, session, user } from "./schema.js"; +import type { interview, job, question, session, user } from "./schema.js"; export type IJob = InferSelectModel; export type IUser = InferSelectModel; export type ISession = InferSelectModel; +export type IInterview = InferSelectModel; +export type IQuestion = InferSelectModel; diff --git a/apps/api/src/lib/multer.ts b/apps/api/src/lib/multer.ts index 805bfd8..2f39b21 100644 --- a/apps/api/src/lib/multer.ts +++ b/apps/api/src/lib/multer.ts @@ -1,8 +1,23 @@ import multer from "multer"; +import path from "path"; +import fs from "fs"; +import { fileURLToPath } from "url"; + +// Get directory path for ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Create uploads directory path (relative to project root, not src) +const uploadsDir = path.join(__dirname, "../../uploads"); + +// Ensure uploads directory exists +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} const storage = multer.diskStorage({ destination: function (req, file, cb) { - cb(null, "uploads/"); + cb(null, uploadsDir); }, filename: function (req, file, cb) { cb(null, Date.now() + "-" + file.originalname); diff --git a/apps/api/src/queues/worker.ts b/apps/api/src/queues/worker.ts index 927dfef..fee5969 100644 --- a/apps/api/src/queues/worker.ts +++ b/apps/api/src/queues/worker.ts @@ -13,7 +13,9 @@ import { getVectorStore } from "../db/qdrant.js"; const worker = new Worker( "resume-upload-queue", async (job: Job<{ queueData: string }>) => { + console.log("s"); const { filePath, candidateId } = JSON.parse(job.data.queueData); + console.log("worker is processing", filePath); const pdfParser = new PDFParse({ url: filePath }); const parsedData = (await pdfParser.getText()).text; @@ -30,12 +32,18 @@ const worker = new Worker( candidateId, chunkIndex: idx, }, - }) + }), ); const vectorStore = await getVectorStore(); - await vectorStore.addDocuments(docs); - console.log("all docs are added to vector database"); + console.log("Vector store obtained, adding", docs.length, "documents..."); + try { + await vectorStore.addDocuments(docs); + console.log("all docs are added to vector database"); + } catch (err) { + console.error("Error adding documents to Qdrant:", err); + throw err; + } const absolutePath = path.resolve(filePath); fs.unlink(absolutePath, (err) => { if (err) { @@ -47,5 +55,28 @@ const worker = new Worker( { connection: bullmqConnection, concurrency: 5, - } + }, ); + +// Worker event handlers +worker.on("ready", () => { + console.log("✅ Worker is ready and listening for jobs"); +}); + +worker.on("active", (job) => { + console.log(`🔄 Job ${job.id} has started processing`); +}); + +worker.on("completed", (job) => { + console.log(`✅ Job ${job.id} completed successfully`); +}); + +worker.on("failed", (job, err) => { + console.error(`❌ Job ${job?.id} failed:`, err.message); +}); + +worker.on("error", (err) => { + console.error("Worker error:", err); +}); + +console.log("Worker script loaded, connecting to Redis..."); diff --git a/apps/web/components/auth/signup-form.tsx b/apps/web/components/auth/signup-form.tsx index 796f5e8..29c8fcc 100644 --- a/apps/web/components/auth/signup-form.tsx +++ b/apps/web/components/auth/signup-form.tsx @@ -70,6 +70,7 @@ export function SignupForm({ organizationRole: role === "RECRUITER" ? organizationRole : undefined, resume: role === "CANDIDATE" ? resumeFile : undefined, }); + console.log(result); // Check if signup was successful if (result && result.user) { diff --git a/apps/web/components/job/JobDetails.tsx b/apps/web/components/job/JobDetails.tsx index 01101f6..82f46ff 100644 --- a/apps/web/components/job/JobDetails.tsx +++ b/apps/web/components/job/JobDetails.tsx @@ -51,6 +51,7 @@ export const JobDetails = ({ job }: JobDetailsProps) => { setShowCandidates(true); try { const result = await getCandidateSuggestions(job.id); + console.log(result); if (result?.data) { setCandidates(result.data); } @@ -103,13 +104,17 @@ export const JobDetails = ({ job }: JobDetailsProps) => {
-
-

Salary Range

-

- ${formatSalary(`${job.salaryRange.min}`)} - $ - {formatSalary(`${job.salaryRange.max}`)} -

-
+ {job?.salaryRange?.min && job?.salaryRange?.max ? ( +
+

Salary Range

+

+ ${formatSalary(`${job?.salaryRange?.min}`)} - $ + {formatSalary(`${job?.salaryRange?.max}`)} +

+
+ ) : ( +

Not specified

+ )} {/* Posted Date */} diff --git a/apps/web/config/index.ts b/apps/web/config/index.ts index c6f3873..bcc3f6a 100644 --- a/apps/web/config/index.ts +++ b/apps/web/config/index.ts @@ -8,10 +8,7 @@ export const config = { process.env.NODE_ENV === "development" ? process.env.NEXT_PUBLIC_SERVER_URL_DEV : process.env.NEXT_PUBLIC_SERVER_URL_PROD, - better_auth_key: - process.env.NODE_ENV === "development" - ? process.env.BETTER_AUTH_TOKEN_KEY_DEV - : process.env.BETTER_AUTH_TOKEN_KEY_PROD, + better_auth_key: process.env.BETTER_AUTH_TOKEN_KEY_PROD, vapi_workflow_id: process.env.NEXT_PUBLIC_VAPI_WORKFLOW_ID, vapi_public_key: process.env.NEXT_PUBLIC_VAPI_PUBLIC_KEY, cloudinary: { diff --git a/apps/web/services/job.ts b/apps/web/services/job.ts index 3d08406..f461344 100644 --- a/apps/web/services/job.ts +++ b/apps/web/services/job.ts @@ -5,7 +5,7 @@ import { IJob } from "@/types/job"; import { cookies } from "next/headers"; export const createJob = async (payload: Partial) => { - const token = (await cookies()).get("better-auth.session_token")?.value; + const token = (await cookies()).get(config.better_auth_key!)?.value; const response = await fetch(`${config.server_url}/job`, { method: "POST", headers: { @@ -18,7 +18,7 @@ export const createJob = async (payload: Partial) => { }; export const getMyUploadedJobs = async () => { - const token = (await cookies()).get("better-auth.session_token")?.value; + const token = (await cookies()).get(config.better_auth_key!)?.value; const response = await fetch(`${config.server_url}/job/my-uploaded-jobs`, { method: "GET", headers: { @@ -29,7 +29,7 @@ export const getMyUploadedJobs = async () => { }; export const getASingleJob = async (jobId: string) => { - const token = (await cookies()).get("better-auth.session_token")?.value; + const token = (await cookies()).get(config.better_auth_key!)?.value; const response = await fetch(`${config.server_url}/job/single-job/${jobId}`, { method: "GET", headers: { @@ -40,7 +40,7 @@ export const getASingleJob = async (jobId: string) => { }; export const getCandidateSuggestions = async (jobId: string) => { - const token = (await cookies()).get("better-auth.session_token")?.value; + const token = (await cookies()).get(config.better_auth_key!)?.value; const response = await fetch( `${config.server_url}/job/find-employees-for-job/${jobId}`, { @@ -48,7 +48,7 @@ export const getCandidateSuggestions = async (jobId: string) => { headers: { authorization: token!, }, - } + }, ); return response.json(); }; diff --git a/apps/web/services/users.ts b/apps/web/services/users.ts index 78f8b90..63dded9 100644 --- a/apps/web/services/users.ts +++ b/apps/web/services/users.ts @@ -26,18 +26,13 @@ export const signUp = async (payload: { } const formData = new FormData(); formData.append("resume", payload.resume); - const res = await fetch(`${config.server_url}/user/upload-resume`, { + await fetch(`${config.server_url}/user/upload-resume`, { method: "POST", headers: { authorization: token, }, body: formData, }); - if (!res.ok) { - const data = await res.json(); - console.log(data); - throw new Error(data.error || "Failed to upload resume"); - } } return result; }; @@ -123,7 +118,7 @@ export const getProfile = async () => { }; export const getCandidateProfile = async ( - candidateId: string + candidateId: string, ): Promise> => { const token = (await cookies()).get(config.better_auth_key!)?.value; if (!token) { @@ -140,7 +135,7 @@ export const getCandidateProfile = async ( headers: { authorization: token, }, - } + }, ); return response.json(); };