Skip to content

Commit 9fe0443

Browse files
authored
release: create-application flow and migration (#2545)
* feat: add migration functionality for applications (#2537) * feat: add migration functionality for applications - Implemented `migrateApplications` controller to handle application migrations based on specified actions. - Added `addIsNewField` method in the model to update applications with a new `isNew` field. - Updated routes to include a new endpoint for triggering migrations. * fix: improve batch update logic in addIsNewField method * refactor: rename migration function and update route for adding 'isNew' field - Renamed `migrateApplications` to `addIsNewFieldMigration` for clarity. - Updated the route to directly call the new migration function without action parameters. * style: format route definition for addIsNewFieldMigration - Reformatted the route definition for better readability by aligning parameters in a multi-line format. * refactor: update addIsNewField logic and improve readability * refactor: streamline addIsNewField logic and enhance batch update process - Removed unnecessary tracking of skipped applications. - Simplified document processing by directly updating all documents in the snapshot. - Improved error handling by mapping document IDs for failed updates. * feat: Implement Create Application Flow for User Onboarding (#2534) * refactor: restructure application constants and enhance application creation logic - Updated APPLICATION_STATUS_TYPES to use an object for better clarity and added CHANGES_REQUESTED status. - Introduced APPLICATION_ROLES for role management. - Added new API response messages for application creation and updates. - Refactored addApplication controller to utilize createApplicationService for improved application handling. - Implemented validation for application roles in the application validator. - Added getApplicationByUserId method in the applications model to retrieve applications by user ID. - Created applicationService to encapsulate application creation logic and handle conflicts. - Updated application types to include role and social link structures. * test: add imageUrl to application data in integration and validation tests - Updated integration test to include imageUrl in application creation request. - Modified unit tests for application validator to include imageUrl in rawData for validation scenarios. * refactor: enhance application constants and update application retrieval logic * feat: add new API response message for successful application retrieval * test: add test for the application create flow (#2536) * refactor: restructure application constants and enhance application creation logic - Updated APPLICATION_STATUS_TYPES to use an object for better clarity and added CHANGES_REQUESTED status. - Introduced APPLICATION_ROLES for role management. - Added new API response messages for application creation and updates. - Refactored addApplication controller to utilize createApplicationService for improved application handling. - Implemented validation for application roles in the application validator. - Added getApplicationByUserId method in the applications model to retrieve applications by user ID. - Created applicationService to encapsulate application creation logic and handle conflicts. - Updated application types to include role and social link structures. * test: add imageUrl to application data in integration and validation tests - Updated integration test to include imageUrl in application creation request. - Modified unit tests for application validator to include imageUrl in rawData for validation scenarios. * refactor: enhance application constants and update application retrieval logic * feat: add new API response message for successful application retrieval * test: add unit tests for createApplicationService to validate application creation logic - Implemented tests for various scenarios including successful application creation, conflict errors, and boundary cases based on application creation dates. - Verified correct transformation of payload fields and handling of optional fields. - Ensured error handling and logging for different error scenarios. * refactor: simplify imageUrl assignment in application transformation logic - Removed conditional check for imageUrl and directly assigned it to the transformed object. - This change streamlines the transformation process for application payloads. * test: update createApplicationService test to focus on socialLink handling - Modified the test to specifically check the handling of the optional field socialLink when not provided. - Ensured that imageUrl is correctly assigned from the mock payload during application creation.
1 parent b84002f commit 9fe0443

File tree

11 files changed

+643
-62
lines changed

11 files changed

+643
-62
lines changed

constants/application.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,38 @@
1-
const APPLICATION_STATUS_TYPES = ["accepted", "rejected", "pending"];
1+
const APPLICATION_STATUS_TYPES = {
2+
ACCEPTED: "accepted",
3+
REJECTED: "rejected",
4+
PENDING: "pending",
5+
CHANGES_REQUESTED: "changes_requested",
6+
};
7+
8+
const APPLICATION_ROLES = {
9+
DEVELOPER: "developer",
10+
DESIGNER: "designer",
11+
PRODUCT_MANAGER: "product_manager",
12+
PROJECT_MANAGER: "project_manager",
13+
QA: "qa",
14+
SOCIAL_MEDIA: "social_media",
15+
};
216

317
const API_RESPONSE_MESSAGES = {
18+
APPLICATION_CREATED_SUCCESS: "Application created successfully",
419
APPLICATION_RETURN_SUCCESS: "Applications returned successfully",
520
};
621

7-
module.exports = { APPLICATION_STATUS_TYPES, API_RESPONSE_MESSAGES };
22+
const APPLICATION_ERROR_MESSAGES = {
23+
APPLICATION_ALREADY_REVIEWED: "Application has already been reviewed",
24+
};
25+
26+
/**
27+
* Business requirement: Applications created after this date are considered reviewed
28+
* and cannot be resubmitted. This date marks the start of the new application review cycle.
29+
*/
30+
const APPLICATION_REVIEW_CYCLE_START_DATE = new Date("2026-01-01T00:00:00.000Z");
31+
32+
module.exports = {
33+
APPLICATION_STATUS_TYPES,
34+
APPLICATION_ROLES,
35+
API_RESPONSE_MESSAGES,
36+
APPLICATION_ERROR_MESSAGES,
37+
APPLICATION_REVIEW_CYCLE_START_DATE,
38+
};

controllers/applications.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { CustomRequest, CustomResponse } from "../types/global";
44
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
55
const ApplicationModel = require("../models/applications");
66
const { API_RESPONSE_MESSAGES } = require("../constants/application");
7-
const { getUserApplicationObject } = require("../utils/application");
8-
const admin = require("firebase-admin");
7+
const { createApplicationService } = require("../services/applicationService");
8+
const { Conflict } = require("http-errors");
9+
const logger = require("../utils/logger");
910

1011
const getAllOrUserApplication = async (req: CustomRequest, res: CustomResponse): Promise<any> => {
1112
try {
@@ -66,35 +67,34 @@ const getAllOrUserApplication = async (req: CustomRequest, res: CustomResponse):
6667
const addApplication = async (req: CustomRequest, res: CustomResponse) => {
6768
try {
6869
const rawData = req.body;
69-
const { applications } = await ApplicationModel.getApplicationsBasedOnStatus("pending", 1, "", req.userData.id);
70-
if (applications.length) {
71-
return res.status(409).json({
72-
message: "User application is already present!",
73-
});
74-
}
75-
const createdAt = new Date().toISOString();
76-
const data = getUserApplicationObject(rawData, req.userData.id, createdAt);
70+
const userId = req.userData.id;
71+
72+
const result = await createApplicationService({
73+
userId,
74+
payload: rawData,
75+
});
7776

7877
const applicationLog = {
7978
type: logType.APPLICATION_ADDED,
8079
meta: {
8180
username: req.userData.username,
82-
userId: req.userData.id,
81+
userId: userId,
82+
applicationId: result.applicationId,
83+
isNew: result.isNew,
8384
},
84-
body: data,
85+
body: rawData,
8586
};
8687

87-
const promises = [
88-
ApplicationModel.addApplication(data),
89-
addLog(applicationLog.type, applicationLog.meta, applicationLog.body),
90-
];
91-
92-
await Promise.all(promises);
88+
await addLog(applicationLog.type, applicationLog.meta, applicationLog.body);
9389

9490
return res.status(201).json({
95-
message: "User application added.",
91+
message: API_RESPONSE_MESSAGES.APPLICATION_CREATED_SUCCESS,
92+
applicationId: result.applicationId,
9693
});
9794
} catch (err) {
95+
if (err instanceof Conflict) {
96+
return res.boom.conflict(err.message);
97+
}
9898
logger.error(`Error while adding application: ${err}`);
9999
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
100100
}
@@ -149,9 +149,20 @@ const getApplicationById = async (req: CustomRequest, res: CustomResponse) => {
149149
}
150150
};
151151

152+
const addIsNewFieldMigration = async (req: CustomRequest, res: CustomResponse) => {
153+
try {
154+
const responseData = await ApplicationModel.addIsNewField();
155+
return res.json({ message: "Applications migration successful", ...responseData });
156+
} catch (err) {
157+
logger.error("Error in migration scripts", err);
158+
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
159+
}
160+
};
161+
152162
module.exports = {
153163
getAllOrUserApplication,
154164
addApplication,
155165
updateApplication,
156166
getApplicationById,
167+
addIsNewFieldMigration,
157168
};

middlewares/validators/application.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,28 @@ import { NextFunction } from "express";
22
import { CustomRequest, CustomResponse } from "../../types/global";
33
import { customWordCountValidator } from "../../utils/customWordCountValidator";
44
const joi = require("joi");
5-
const { APPLICATION_STATUS_TYPES } = require("../../constants/application");
5+
const { APPLICATION_STATUS_TYPES, APPLICATION_ROLES } = require("../../constants/application");
6+
const { phoneNumberRegex } = require("../../constants/subscription-validator");
67
const logger = require("../../utils/logger");
78

89
const validateApplicationData = async (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
10+
if (req.body.socialLink?.phoneNo) {
11+
req.body.socialLink.phoneNo = req.body.socialLink.phoneNo.trim();
12+
}
13+
14+
const socialLinkSchema = joi
15+
.object({
16+
phoneNo: joi.string().optional().regex(phoneNumberRegex).message('"phoneNo" must be in a valid format'),
17+
github: joi.string().min(1).optional(),
18+
instagram: joi.string().min(1).optional(),
19+
linkedin: joi.string().min(1).optional(),
20+
twitter: joi.string().min(1).optional(),
21+
peerlist: joi.string().min(1).optional(),
22+
behance: joi.string().min(1).optional(),
23+
dribbble: joi.string().min(1).optional(),
24+
})
25+
.optional();
26+
927
const schema = joi
1028
.object()
1129
.strict()
@@ -34,13 +52,19 @@ const validateApplicationData = async (req: CustomRequest, res: CustomResponse,
3452
.required(),
3553
flowState: joi.string().optional(),
3654
numberOfHours: joi.number().min(1).max(100).required(),
55+
role: joi
56+
.string()
57+
.valid(...Object.values(APPLICATION_ROLES))
58+
.required(),
59+
imageUrl: joi.string().uri().required(),
60+
socialLink: socialLinkSchema,
3761
});
3862

3963
try {
4064
await schema.validateAsync(req.body);
4165
next();
4266
} catch (error) {
43-
logger.error(`Error in validating recruiter data: ${error}`);
67+
logger.error(`Error in validating application data: ${error}`);
4468
res.boom.badRequest(error.details[0].message);
4569
}
4670
};
@@ -55,7 +79,7 @@ const validateApplicationUpdateData = async (req: CustomRequest, res: CustomResp
5579
.min(1)
5680
.optional()
5781
.custom((value, helper) => {
58-
if (!APPLICATION_STATUS_TYPES.includes(value)) {
82+
if (!Object.values(APPLICATION_STATUS_TYPES).includes(value)) {
5983
return helper.message("Status is not valid");
6084
}
6185
return value;

models/applications.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { application } from "../types/application";
22
const firestore = require("../utils/firestore");
3+
const logger = require("../utils/logger");
34
const ApplicationsModel = firestore.collection("applicants");
5+
const { DOCUMENT_WRITE_SIZE } = require("../constants/constants");
46

57
const getAllApplications = async (limit: number, lastDocId?: string) => {
68
try {
@@ -64,7 +66,7 @@ const getApplicationsBasedOnStatus = async (status: string, limit: number, lastD
6466
lastDoc = await ApplicationsModel.doc(lastDocId).get();
6567
}
6668

67-
dbQuery = dbQuery.orderBy("createdAt", "desc");
69+
dbQuery = dbQuery.orderBy("createdAt", "desc");
6870

6971
if (lastDoc) {
7072
dbQuery = dbQuery.startAfter(lastDoc);
@@ -97,9 +99,9 @@ const getUserApplications = async (userId: string) => {
9799
try {
98100
const applicationsResult = [];
99101
const applications = await ApplicationsModel.where("userId", "==", userId)
100-
.orderBy("createdAt", "desc")
101-
.limit(1)
102-
.get();
102+
.orderBy("createdAt", "desc")
103+
.limit(1)
104+
.get();
103105

104106
applications.forEach((application) => {
105107
applicationsResult.push({
@@ -134,11 +136,68 @@ const updateApplication = async (dataToUpdate: object, applicationId: string) =>
134136
}
135137
};
136138

139+
const addIsNewField = async () => {
140+
const batchSize = DOCUMENT_WRITE_SIZE;
141+
let lastDoc = null;
142+
let isCompleted = false;
143+
144+
const summary = {
145+
totalApplicationsProcessed: 0,
146+
totalApplicationsUpdated: 0,
147+
totalOperationsFailed: 0,
148+
failedApplicationDetails: [],
149+
};
150+
151+
try {
152+
while (!isCompleted) {
153+
let query = ApplicationsModel.orderBy("createdAt", "desc").limit(batchSize);
154+
if (lastDoc) {
155+
query = query.startAfter(lastDoc);
156+
}
157+
const snapshot = await query.get();
158+
159+
if (snapshot.empty) {
160+
isCompleted = true;
161+
break;
162+
}
163+
164+
const batch = firestore.batch();
165+
snapshot.docs.forEach((doc) => {
166+
batch.update(doc.ref, { isNew: false });
167+
});
168+
summary.totalApplicationsProcessed += snapshot.docs.length;
169+
170+
try {
171+
await batch.commit();
172+
summary.totalApplicationsUpdated += snapshot.docs.length;
173+
} catch (err) {
174+
logger.error("Batch update failed for applications collection:", err);
175+
summary.totalOperationsFailed += snapshot.docs.length;
176+
summary.failedApplicationDetails.push(...snapshot.docs.map((doc) => doc.id));
177+
}
178+
179+
lastDoc = snapshot.docs[snapshot.docs.length - 1];
180+
isCompleted = snapshot.docs.length < batchSize;
181+
}
182+
183+
logger.info("Applications migration completed:", summary);
184+
return {
185+
documentsModified: summary.totalApplicationsUpdated,
186+
totalDocuments: summary.totalApplicationsProcessed,
187+
...summary,
188+
};
189+
} catch (error) {
190+
logger.error("Error during applications migration:", error);
191+
throw error;
192+
}
193+
};
194+
137195
module.exports = {
138196
getAllApplications,
139197
getUserApplications,
140198
addApplication,
141199
updateApplication,
142200
getApplicationsBasedOnStatus,
143201
getApplicationById,
202+
addIsNewField,
144203
};

routes/applications.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,11 @@ router.patch(
2424
applicationValidator.validateApplicationUpdateData,
2525
applications.updateApplication
2626
);
27+
router.post(
28+
"/migrations/add-is-new-field",
29+
authenticate,
30+
authorizeRoles([SUPERUSER]),
31+
applications.addIsNewFieldMigration
32+
);
2733

2834
module.exports = router;

0 commit comments

Comments
 (0)