Skip to content
Merged
35 changes: 33 additions & 2 deletions constants/application.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
const APPLICATION_STATUS_TYPES = ["accepted", "rejected", "pending"];
const APPLICATION_STATUS_TYPES = {
ACCEPTED: "accepted",
REJECTED: "rejected",
PENDING: "pending",
CHANGES_REQUESTED: "changes_requested",
};

const APPLICATION_ROLES = {
DEVELOPER: "developer",
DESIGNER: "designer",
PRODUCT_MANAGER: "product_manager",
PROJECT_MANAGER: "project_manager",
QA: "qa",
SOCIAL_MEDIA: "social_media",
};

const API_RESPONSE_MESSAGES = {
APPLICATION_CREATED_SUCCESS: "Application created successfully",
APPLICATION_RETURN_SUCCESS: "Applications returned successfully",
};

module.exports = { APPLICATION_STATUS_TYPES, API_RESPONSE_MESSAGES };
const APPLICATION_ERROR_MESSAGES = {
APPLICATION_ALREADY_REVIEWED: "Application has already been reviewed",
};

/**
* 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_REVIEW_CYCLE_START_DATE = new Date("2026-01-01T00:00:00.000Z");

module.exports = {
APPLICATION_STATUS_TYPES,
APPLICATION_ROLES,
API_RESPONSE_MESSAGES,
APPLICATION_ERROR_MESSAGES,
APPLICATION_REVIEW_CYCLE_START_DATE,
};
38 changes: 19 additions & 19 deletions controllers/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { CustomRequest, CustomResponse } from "../types/global";
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
const ApplicationModel = require("../models/applications");
const { API_RESPONSE_MESSAGES } = require("../constants/application");
const { getUserApplicationObject } = require("../utils/application");
const admin = require("firebase-admin");
const { createApplicationService } = require("../services/applicationService");
const { Conflict } = require("http-errors");
const logger = require("../utils/logger");

const getAllOrUserApplication = async (req: CustomRequest, res: CustomResponse): Promise<any> => {
try {
Expand Down Expand Up @@ -66,35 +67,34 @@ const getAllOrUserApplication = async (req: CustomRequest, res: CustomResponse):
const addApplication = async (req: CustomRequest, res: CustomResponse) => {
try {
const rawData = req.body;
const { applications } = await ApplicationModel.getApplicationsBasedOnStatus("pending", 1, "", req.userData.id);
if (applications.length) {
return res.status(409).json({
message: "User application is already present!",
});
}
const createdAt = new Date().toISOString();
const data = getUserApplicationObject(rawData, req.userData.id, createdAt);
const userId = req.userData.id;

const result = await createApplicationService({
userId,
payload: rawData,
});

const applicationLog = {
type: logType.APPLICATION_ADDED,
meta: {
username: req.userData.username,
userId: req.userData.id,
userId: userId,
applicationId: result.applicationId,
isNew: result.isNew,
},
body: data,
body: rawData,
};

const promises = [
ApplicationModel.addApplication(data),
addLog(applicationLog.type, applicationLog.meta, applicationLog.body),
];

await Promise.all(promises);
await addLog(applicationLog.type, applicationLog.meta, applicationLog.body);

return res.status(201).json({
message: "User application added.",
message: API_RESPONSE_MESSAGES.APPLICATION_CREATED_SUCCESS,
applicationId: result.applicationId,
});
} catch (err) {
if (err instanceof Conflict) {
return res.boom.conflict(err.message);
}
logger.error(`Error while adding application: ${err}`);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
Expand Down
30 changes: 27 additions & 3 deletions middlewares/validators/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,28 @@ import { NextFunction } from "express";
import { CustomRequest, CustomResponse } from "../../types/global";
import { customWordCountValidator } from "../../utils/customWordCountValidator";
const joi = require("joi");
const { APPLICATION_STATUS_TYPES } = require("../../constants/application");
const { APPLICATION_STATUS_TYPES, APPLICATION_ROLES } = require("../../constants/application");
const { phoneNumberRegex } = require("../../constants/subscription-validator");
const logger = require("../../utils/logger");

const validateApplicationData = async (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
if (req.body.socialLink?.phoneNo) {
req.body.socialLink.phoneNo = req.body.socialLink.phoneNo.trim();
}

const socialLinkSchema = joi
.object({
phoneNo: joi.string().optional().regex(phoneNumberRegex).message('"phoneNo" must be in a valid format'),
github: joi.string().min(1).optional(),
instagram: joi.string().min(1).optional(),
linkedin: joi.string().min(1).optional(),
twitter: joi.string().min(1).optional(),
peerlist: joi.string().min(1).optional(),
behance: joi.string().min(1).optional(),
dribbble: joi.string().min(1).optional(),
})
.optional();

const schema = joi
.object()
.strict()
Expand Down Expand Up @@ -34,13 +52,19 @@ const validateApplicationData = async (req: CustomRequest, res: CustomResponse,
.required(),
flowState: joi.string().optional(),
numberOfHours: joi.number().min(1).max(100).required(),
role: joi
.string()
.valid(...Object.values(APPLICATION_ROLES))
.required(),
imageUrl: joi.string().uri().required(),
socialLink: socialLinkSchema,
});

try {
await schema.validateAsync(req.body);
next();
} catch (error) {
logger.error(`Error in validating recruiter data: ${error}`);
logger.error(`Error in validating application data: ${error}`);
res.boom.badRequest(error.details[0].message);
}
};
Expand All @@ -55,7 +79,7 @@ const validateApplicationUpdateData = async (req: CustomRequest, res: CustomResp
.min(1)
.optional()
.custom((value, helper) => {
if (!APPLICATION_STATUS_TYPES.includes(value)) {
if (!Object.values(APPLICATION_STATUS_TYPES).includes(value)) {
return helper.message("Status is not valid");
}
return value;
Expand Down
9 changes: 5 additions & 4 deletions models/applications.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { application } from "../types/application";
const firestore = require("../utils/firestore");
const logger = require("../utils/logger");
const ApplicationsModel = firestore.collection("applicants");

const getAllApplications = async (limit: number, lastDocId?: string) => {
Expand Down Expand Up @@ -64,7 +65,7 @@ const getApplicationsBasedOnStatus = async (status: string, limit: number, lastD
lastDoc = await ApplicationsModel.doc(lastDocId).get();
}

dbQuery = dbQuery.orderBy("createdAt", "desc");
dbQuery = dbQuery.orderBy("createdAt", "desc");

if (lastDoc) {
dbQuery = dbQuery.startAfter(lastDoc);
Expand Down Expand Up @@ -97,9 +98,9 @@ const getUserApplications = async (userId: string) => {
try {
const applicationsResult = [];
const applications = await ApplicationsModel.where("userId", "==", userId)
.orderBy("createdAt", "desc")
.limit(1)
.get();
.orderBy("createdAt", "desc")
.limit(1)
.get();

applications.forEach((application) => {
applicationsResult.push({
Expand Down
111 changes: 111 additions & 0 deletions services/applicationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { application, applicationPayload } from "../types/application";
import { Conflict } from "http-errors";
const ApplicationModel = require("../models/applications");
const {
APPLICATION_STATUS_TYPES,
APPLICATION_ERROR_MESSAGES,
APPLICATION_REVIEW_CYCLE_START_DATE,
} = require("../constants/application");
const logger = require("../utils/logger");

interface CreateApplicationServiceParams {
userId: string;
payload: applicationPayload;
}

interface CreateApplicationServiceResponse {
applicationId: string;
isNew: boolean;
}

const transformPayloadToApplication = (payload: applicationPayload, userId: string): application => {
const transformed: application = {
userId,
biodata: {
firstName: payload.firstName,
lastName: payload.lastName,
},
location: {
city: payload.city,
state: payload.state,
country: payload.country,
},
professional: {
institution: payload.college,
skills: payload.skills,
},
intro: {
introduction: payload.introduction,
funFact: payload.funFact,
forFun: payload.forFun,
whyRds: payload.whyRds,
numberOfHours: payload.numberOfHours,
},
foundFrom: payload.foundFrom,
role: payload.role,
};

if (payload.imageUrl) {
transformed.imageUrl = payload.imageUrl;
}

if (payload.socialLink) {
transformed.socialLink = payload.socialLink;
}

return transformed;
};

/**
* Service to create application
* Handles the logic for:
* - Checking existing applications created after the review cycle start date (business requirement)
* - Creating new applications if no application found after the review cycle start date
* - Always creates a new application (no update flow)
*
* @param params - Object containing userId and payload
* @returns Promise resolving to application creation response
* @throws Conflict if application already exists after the review cycle start date
*/
export const createApplicationService = async (
params: CreateApplicationServiceParams
): Promise<CreateApplicationServiceResponse> => {
try {
const { userId, payload } = params;

const userApplications = await ApplicationModel.getUserApplications(userId);
const existingApplication = userApplications.length > 0 ? userApplications[0] : null;

if (existingApplication) {
const existingCreatedAt = new Date(existingApplication.createdAt);

if (existingCreatedAt > APPLICATION_REVIEW_CYCLE_START_DATE) {
throw new Conflict(APPLICATION_ERROR_MESSAGES.APPLICATION_ALREADY_REVIEWED);
}
}

const createdAt = new Date().toISOString();

const applicationData: application = {
...transformPayloadToApplication(payload, userId),
score: 0,
status: APPLICATION_STATUS_TYPES.PENDING,
createdAt,
isNew: true,
nudgeCount: 0,
};

const applicationId = await ApplicationModel.addApplication(applicationData);

return {
applicationId,
isNew: true,
};
} catch (err) {
if (err instanceof Conflict) {
throw err;
}
logger.error("Error in createApplicationService", err);
throw err;
}
};
7 changes: 7 additions & 0 deletions test/fixtures/applications/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = () => {
numberOfHours: 20,
},
createdAt: null,
role: "developer",
},
{
userId: "xyajkdfsfsd",
Expand All @@ -42,6 +43,7 @@ module.exports = () => {
numberOfHours: 20,
},
createdAt: null,
role: "developer",
},
{
college: "Groww",
Expand All @@ -65,6 +67,7 @@ module.exports = () => {
numberOfHours: 20,
status: "rejected",
createdAt: null,
role: "developer",
},
{
college: "Groww",
Expand All @@ -88,6 +91,7 @@ module.exports = () => {
numberOfHours: 20,
status: "rejected",
createdAt: null,
role: "developer",
},
{
college: "Groww",
Expand All @@ -111,6 +115,7 @@ module.exports = () => {
numberOfHours: 20,
status: "rejected",
createdAt: null,
role: "developer",
},
{
college: "Groww",
Expand All @@ -132,6 +137,7 @@ module.exports = () => {
"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",
country: "India",
numberOfHours: 20,
role: "developer",
},
{
firstName: "vinayak",
Expand All @@ -151,6 +157,7 @@ module.exports = () => {
whyRds:
"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",
foundFrom: "twitter",
role: "developer",
},
];
};
Loading
Loading