diff --git a/constants/application.ts b/constants/application.ts index 60f5d0a15..e82836904 100644 --- a/constants/application.ts +++ b/constants/application.ts @@ -48,6 +48,11 @@ const APPLICATION_STATUS = { * Business requirement: Applications created after this date are considered reviewed * and cannot be resubmitted. This date marks the start of the new application review cycle. */ +const APPLICATION_SCORE = { + INITIAL_SCORE: 50, + NUDGE_BONUS: 10, +}; + const APPLICATION_REVIEW_CYCLE_START_DATE = new Date("2026-01-01T00:00:00.000Z"); module.exports = { @@ -57,5 +62,6 @@ module.exports = { APPLICATION_ERROR_MESSAGES, APPLICATION_LOG_MESSAGES, APPLICATION_REVIEW_CYCLE_START_DATE, - APPLICATION_STATUS + APPLICATION_STATUS, + APPLICATION_SCORE }; diff --git a/controllers/applications.ts b/controllers/applications.ts index 0809348fa..ba344d643 100644 --- a/controllers/applications.ts +++ b/controllers/applications.ts @@ -202,6 +202,7 @@ const nudgeApplication = async (req: CustomRequest, res: CustomResponse) => { message: API_RESPONSE_MESSAGES.NUDGE_SUCCESS, nudgeCount: result.nudgeCount, lastNudgeAt: result.lastNudgeAt, + score: result.score, }); default: return res.boom.badImplementation(INTERNAL_SERVER_ERROR); diff --git a/middlewares/validators/application.ts b/middlewares/validators/application.ts index 6f4293ff3..7fbde2c7f 100644 --- a/middlewares/validators/application.ts +++ b/middlewares/validators/application.ts @@ -31,7 +31,7 @@ const validateApplicationData = async (req: CustomRequest, res: CustomResponse, userId: joi.string().optional(), firstName: joi.string().min(1).required(), lastName: joi.string().min(1).required(), - college: joi.string().min(1).required(), + institution: joi.string().min(1).required(), skills: joi.string().min(5).required(), city: joi.string().min(1).required(), state: joi.string().min(1).required(), @@ -130,6 +130,15 @@ const validateApplicationUpdateData = async (req: CustomRequest, res: CustomResp .strict() .min(1) .keys({ + institution: joi.string().min(1).optional(), + skills: joi.string().min(5).optional(), + city: joi.string().min(1).optional(), + state: joi.string().min(1).optional(), + country: joi.string().min(1).optional(), + role: joi + .string() + .valid(...Object.values(APPLICATION_ROLES)) + .optional(), imageUrl: joi.string().uri().optional(), foundFrom: joi.string().min(1).optional(), introduction: joi.string().min(1).optional(), diff --git a/models/applications.ts b/models/applications.ts index 9f59b84f3..08e1edc62 100644 --- a/models/applications.ts +++ b/models/applications.ts @@ -4,7 +4,7 @@ const { logType } = require("../constants/logs"); const firestore = require("../utils/firestore"); const logger = require("../utils/logger"); const ApplicationsModel = firestore.collection("applicants"); -const { APPLICATION_STATUS_TYPES, APPLICATION_STATUS } = require("../constants/application"); +const { APPLICATION_STATUS_TYPES, APPLICATION_STATUS, APPLICATION_SCORE } = require("../constants/application"); const { convertDaysToMilliseconds } = require("../utils/time"); const getAllApplications = async (limit: number, lastDocId?: string) => { @@ -16,7 +16,7 @@ const getAllApplications = async (limit: number, lastDocId?: string) => { lastDoc = await ApplicationsModel.doc(lastDocId).get(); } - let dbQuery = ApplicationsModel.orderBy("createdAt", "desc"); + let dbQuery = ApplicationsModel.where("isNew", "==", true).orderBy("createdAt", "desc"); if (lastDoc) { dbQuery = dbQuery.startAfter(lastDoc); @@ -59,7 +59,7 @@ const getApplicationsBasedOnStatus = async (status: string, limit: number, lastD try { let lastDoc = null; const applications = []; - let dbQuery = ApplicationsModel.where("status", "==", status); + let dbQuery = ApplicationsModel.where("isNew", "==", true).where("status", "==", status); if (userId) { dbQuery = dbQuery.where("userId", "==", userId); @@ -86,7 +86,7 @@ const getApplicationsBasedOnStatus = async (status: string, limit: number, lastD }); }); - let countQuery = ApplicationsModel.where("status", "==", status); + let countQuery = ApplicationsModel.where("isNew", "==", true).where("status", "==", status); const totalApplications = await countQuery.get(); const totalCount = totalApplications.size; @@ -219,16 +219,19 @@ const nudgeApplication = async ({ applicationId, userId }: { applicationId: stri const currentNudgeCount = application.nudgeCount || 0; const updatedNudgeCount = currentNudgeCount + 1; const newLastNudgeAt = new Date(currentTime).toISOString(); + const updatedScore = (application.score || 0) + APPLICATION_SCORE.NUDGE_BONUS; transaction.update(applicationRef, { nudgeCount: updatedNudgeCount, lastNudgeAt: newLastNudgeAt, + score: updatedScore, }); return { status: APPLICATION_STATUS.success, nudgeCount: updatedNudgeCount, lastNudgeAt: newLastNudgeAt, + score: updatedScore, }; }); diff --git a/services/applicationService.ts b/services/applicationService.ts index b1e81a4a1..664480257 100644 --- a/services/applicationService.ts +++ b/services/applicationService.ts @@ -5,6 +5,7 @@ const { APPLICATION_STATUS_TYPES, APPLICATION_ERROR_MESSAGES, APPLICATION_REVIEW_CYCLE_START_DATE, + APPLICATION_SCORE, } = require("../constants/application"); const logger = require("../utils/logger"); @@ -31,7 +32,7 @@ const transformPayloadToApplication = (payload: applicationPayload, userId: stri country: payload.country, }, professional: { - institution: payload.college, + institution: payload.institution, skills: payload.skills, }, intro: { @@ -86,7 +87,7 @@ export const createApplicationService = async ( const applicationData: application = { ...transformPayloadToApplication(payload, userId), - score: 0, + score: APPLICATION_SCORE.INITIAL_SCORE, status: APPLICATION_STATUS_TYPES.PENDING, createdAt, isNew: true, diff --git a/test/fixtures/applications/applications.ts b/test/fixtures/applications/applications.ts index ea50ab3fc..4e2f0c314 100644 --- a/test/fixtures/applications/applications.ts +++ b/test/fixtures/applications/applications.ts @@ -3,7 +3,7 @@ module.exports = () => { { firstName: "vinayak", lastName: "triveid", - college: "Christ Church college", + institution: "Christ Church institution", skills: "React, Ember, Node js", city: "Kanpur", state: "Uttar Pradesh", @@ -21,12 +21,13 @@ module.exports = () => { }, createdAt: null, role: "developer", + isNew: true, }, { userId: "xyajkdfsfsd", firstName: "Ritik", lastName: "Jaiwal", - college: "Tata Consultancy services", + institution: "Tata Consultancy services", skills: "React, Ember, Node js", city: "Bangalore", state: "Karnataka", @@ -44,9 +45,10 @@ module.exports = () => { }, createdAt: null, role: "developer", + isNew: true, }, { - college: "Groww", + institution: "Groww", state: "Karnataka", firstName: "Vaibhav", lastName: "Desai", @@ -68,9 +70,10 @@ module.exports = () => { status: "rejected", createdAt: null, role: "developer", + isNew: true, }, { - college: "Groww", + institution: "Groww", state: "Karnataka", firstName: "Vaibhav", lastName: "Desai", @@ -92,9 +95,10 @@ module.exports = () => { status: "rejected", createdAt: null, role: "developer", + isNew: true, }, { - college: "Groww", + institution: "Groww", state: "Karnataka", firstName: "Vaibhav", lastName: "Desai", @@ -116,9 +120,10 @@ module.exports = () => { status: "rejected", createdAt: null, role: "developer", + isNew: true, }, { - college: "Groww", + institution: "Groww", state: "Karnataka", firstName: "Vaibhav", lastName: "Desai", @@ -145,7 +150,7 @@ module.exports = () => { city: "Kanpur", state: "UP", country: "India", - college: "Christ Church College", + institution: "Christ Church College", skills: "React, NodeJs, Ember", introduction: "mattis aliquam faucibus purus in massa tempor nec feugiat nisl pretium fusce id velit ut tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed viverra tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at augue eget arcu dictum varius duis at consectetur lorem donec massa sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare massa eget egestas purus viverra accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu augue ut lectus arcu bibendum at", diff --git a/test/integration/application.test.ts b/test/integration/application.test.ts index 8b48dcd78..8773ed9ea 100644 --- a/test/integration/application.test.ts +++ b/test/integration/application.test.ts @@ -12,7 +12,7 @@ const applicationModel = require("../../models/applications"); const applicationsData = require("../fixtures/applications/applications")(); const cookieName = config.get("userToken.cookieName"); -const { APPLICATION_ERROR_MESSAGES, API_RESPONSE_MESSAGES } = require("../../constants/application"); +const { APPLICATION_ERROR_MESSAGES, API_RESPONSE_MESSAGES, APPLICATION_SCORE } = require("../../constants/application"); const appOwner = userData[3]; const superUser = userData[4]; @@ -334,25 +334,27 @@ describe("Application", function () { }); describe("POST /applications", function () { - it("should create a application and return 201 if the user has not yet submitted the application", function (done) { - chai + it("should create a application and return 201 if the user has not yet submitted the application", async function () { + const res = await chai .request(app) .post(`/applications`) .set("cookie", `${cookieName}=${secondUserJwt}`) .send({ ...applicationsData[5], imageUrl: "https://example.com/image.jpg", - }) - .end((err, res) => { - if (err) { - return done(err); - } - - expect(res).to.have.status(201); - expect(res.body.message).to.be.equal("Application created successfully"); - expect(res.body).to.have.property("applicationId"); - return done(); }); + + expect(res).to.have.status(201); + expect(res.body.message).to.be.equal("Application created successfully"); + expect(res.body).to.have.property("applicationId"); + + const getRes = await chai + .request(app) + .get(`/applications/${res.body.applicationId}`) + .set("cookie", `${cookieName}=${superUserJwt}`); + + expect(getRes).to.have.status(200); + expect(getRes.body.application.score).to.be.equal(50); }); }); @@ -501,6 +503,48 @@ describe("Application", function () { expect(secondRes.body.error).to.be.equal("Conflict"); expect(secondRes.body.message).to.be.equal(APPLICATION_ERROR_MESSAGES.EDIT_TOO_SOON); }); + + it("should return 200 when updating city, state, and country", async function () { + const applicationData = { ...applicationsData[0], userId }; + const testApplicationId = await applicationModel.addApplication(applicationData); + + const res = await chai + .request(app) + .patch(`/applications/${testApplicationId}`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ city: "New Delhi", state: "Delhi", country: "India" }); + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Application updated successfully"); + }); + + it("should return 200 when updating role with a valid role", async function () { + const applicationData = { ...applicationsData[0], userId }; + const testApplicationId = await applicationModel.addApplication(applicationData); + + const res = await chai + .request(app) + .patch(`/applications/${testApplicationId}`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ role: "designer" }); + + expect(res).to.have.status(200); + expect(res.body.message).to.be.equal("Application updated successfully"); + }); + + it("should return 400 when updating role with an invalid role", async function () { + const applicationData = { ...applicationsData[0], userId }; + const testApplicationId = await applicationModel.addApplication(applicationData); + + const res = await chai + .request(app) + .patch(`/applications/${testApplicationId}`) + .set("cookie", `${cookieName}=${jwt}`) + .send({ role: "invalid_role" }); + + expect(res).to.have.status(400); + expect(res.body.error).to.be.equal("Bad Request"); + }); }); describe("PATCH /applications/:applicationId/feedback", function () { @@ -800,7 +844,7 @@ describe("Application", function () { let nudgeApplicationId: string; beforeEach(async function () { - const applicationData = { ...applicationsData[0], userId }; + const applicationData = { ...applicationsData[0], userId, score: APPLICATION_SCORE.INITIAL_SCORE }; nudgeApplicationId = await applicationModel.addApplication(applicationData); }); @@ -820,6 +864,7 @@ describe("Application", function () { expect(res.body.message).to.be.equal(API_RESPONSE_MESSAGES.NUDGE_SUCCESS); expect(res.body.nudgeCount).to.be.equal(1); expect(res.body.lastNudgeAt).to.be.a("string"); + expect(res.body.score).to.be.equal(APPLICATION_SCORE.INITIAL_SCORE + APPLICATION_SCORE.NUDGE_BONUS); done(); }); }); @@ -834,6 +879,7 @@ describe("Application", function () { expect(res).to.have.status(200); expect(res.body.nudgeCount).to.be.equal(1); + expect(res.body.score).to.be.equal(APPLICATION_SCORE.INITIAL_SCORE + APPLICATION_SCORE.NUDGE_BONUS); const twentyFiveHoursAgo = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(); applicationModel @@ -856,6 +902,7 @@ describe("Application", function () { expect(res.body.message).to.be.equal(API_RESPONSE_MESSAGES.NUDGE_SUCCESS); expect(res.body.nudgeCount).to.be.equal(2); expect(res.body.lastNudgeAt).to.be.a("string"); + expect(res.body.score).to.be.equal(APPLICATION_SCORE.INITIAL_SCORE + 2 * APPLICATION_SCORE.NUDGE_BONUS); done(); }); }) diff --git a/test/unit/services/applicationService.test.ts b/test/unit/services/applicationService.test.ts index 8f4f4bc12..2e6639d11 100644 --- a/test/unit/services/applicationService.test.ts +++ b/test/unit/services/applicationService.test.ts @@ -140,7 +140,7 @@ describe("createApplicationService", () => { const applicationData = addApplicationStub.getCall(0).args[0]; expect(applicationData.isNew).to.equal(true); - expect(applicationData.score).to.equal(0); + expect(applicationData.score).to.equal(50); expect(applicationData.status).to.equal(APPLICATION_STATUS_TYPES.PENDING); expect(applicationData.nudgeCount).to.equal(0); }); @@ -186,7 +186,7 @@ describe("createApplicationService", () => { expect(applicationData.location.city).to.equal(mockPayload.city); expect(applicationData.location.state).to.equal(mockPayload.state); expect(applicationData.location.country).to.equal(mockPayload.country); - expect(applicationData.professional.institution).to.equal(mockPayload.college); + expect(applicationData.professional.institution).to.equal(mockPayload.institution); expect(applicationData.professional.skills).to.equal(mockPayload.skills); expect(applicationData.intro.introduction).to.equal(mockPayload.introduction); expect(applicationData.intro.funFact).to.equal(mockPayload.funFact); diff --git a/test/unit/utils/application.test.ts b/test/unit/utils/application.test.ts index 63af7921d..4bcdf8745 100644 --- a/test/unit/utils/application.test.ts +++ b/test/unit/utils/application.test.ts @@ -10,7 +10,7 @@ describe("getUserApplicationObject", async function () { city: "Kanpur", state: "UP", country: "India", - college: "Christ Church College", + institution: "Christ Church College", skills: "React, NodeJs, Ember", introduction: "not needed", funFact: "kdfkasdjfkdk", @@ -33,7 +33,7 @@ describe("getUserApplicationObject", async function () { country: rawData.country, }, professional: { - institution: rawData.college, + institution: rawData.institution, skills: rawData.skills, }, intro: { diff --git a/types/application.d.ts b/types/application.d.ts index b2b0f54db..ebff3f1bc 100644 --- a/types/application.d.ts +++ b/types/application.d.ts @@ -60,7 +60,7 @@ export type applicationPayload = { city: string; state: string; country: string; - college: string; + institution: string; skills: string; introduction: string; funFact: string; @@ -74,6 +74,12 @@ export type applicationPayload = { }; export type applicationUpdatePayload = { + city?: string; + state?: string; + country?: string; + institution?: string; + skills?: string; + role?: ApplicationRole; imageUrl?: string; foundFrom?: string; introduction?: string; diff --git a/utils/application.ts b/utils/application.ts index b1a999612..a7055609d 100644 --- a/utils/application.ts +++ b/utils/application.ts @@ -13,7 +13,7 @@ const getUserApplicationObject = (rawData: applicationPayload, userId: string, c country: rawData.country, }, professional: { - institution: rawData.college, + institution: rawData.institution, skills: rawData.skills, }, intro: { @@ -31,6 +31,12 @@ const getUserApplicationObject = (rawData: applicationPayload, userId: string, c }; const FLAT_FIELD_MAP: Record, string> = { + city: "location.city", + state: "location.state", + country: "location.country", + institution: "professional.institution", + skills: "professional.skills", + role: "role", imageUrl: "imageUrl", foundFrom: "foundFrom", introduction: "intro.introduction",