Skip to content
Merged
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
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,
};
49 changes: 30 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 Expand Up @@ -149,9 +149,20 @@ const getApplicationById = async (req: CustomRequest, res: CustomResponse) => {
}
};

const addIsNewFieldMigration = async (req: CustomRequest, res: CustomResponse) => {
try {
const responseData = await ApplicationModel.addIsNewField();
return res.json({ message: "Applications migration successful", ...responseData });
} catch (err) {
logger.error("Error in migration scripts", err);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
};

module.exports = {
getAllOrUserApplication,
addApplication,
updateApplication,
getApplicationById,
addIsNewFieldMigration,
};
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
67 changes: 63 additions & 4 deletions models/applications.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { application } from "../types/application";
const firestore = require("../utils/firestore");
const logger = require("../utils/logger");
const ApplicationsModel = firestore.collection("applicants");
const { DOCUMENT_WRITE_SIZE } = require("../constants/constants");

const getAllApplications = async (limit: number, lastDocId?: string) => {
try {
Expand Down Expand Up @@ -64,7 +66,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 +99,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 Expand Up @@ -134,11 +136,68 @@ const updateApplication = async (dataToUpdate: object, applicationId: string) =>
}
};

const addIsNewField = async () => {
const batchSize = DOCUMENT_WRITE_SIZE;
let lastDoc = null;
let isCompleted = false;

const summary = {
totalApplicationsProcessed: 0,
totalApplicationsUpdated: 0,
totalOperationsFailed: 0,
failedApplicationDetails: [],
};

try {
while (!isCompleted) {
let query = ApplicationsModel.orderBy("createdAt", "desc").limit(batchSize);
if (lastDoc) {
query = query.startAfter(lastDoc);
}
const snapshot = await query.get();

if (snapshot.empty) {
isCompleted = true;
break;
}

const batch = firestore.batch();
snapshot.docs.forEach((doc) => {
batch.update(doc.ref, { isNew: false });
});
summary.totalApplicationsProcessed += snapshot.docs.length;

try {
await batch.commit();
summary.totalApplicationsUpdated += snapshot.docs.length;
} catch (err) {
logger.error("Batch update failed for applications collection:", err);
summary.totalOperationsFailed += snapshot.docs.length;
summary.failedApplicationDetails.push(...snapshot.docs.map((doc) => doc.id));
}

lastDoc = snapshot.docs[snapshot.docs.length - 1];
isCompleted = snapshot.docs.length < batchSize;
}

logger.info("Applications migration completed:", summary);
return {
documentsModified: summary.totalApplicationsUpdated,
totalDocuments: summary.totalApplicationsProcessed,
...summary,
};
} catch (error) {
logger.error("Error during applications migration:", error);
throw error;
}
};

module.exports = {
getAllApplications,
getUserApplications,
addApplication,
updateApplication,
getApplicationsBasedOnStatus,
getApplicationById,
addIsNewField,
};
6 changes: 6 additions & 0 deletions routes/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,11 @@ router.patch(
applicationValidator.validateApplicationUpdateData,
applications.updateApplication
);
router.post(
"/migrations/add-is-new-field",
authenticate,
authorizeRoles([SUPERUSER]),
applications.addIsNewFieldMigration
);

module.exports = router;
Loading