Skip to content

Commit 4a071dc

Browse files
authored
feat: add nudge application functionality (#2542) (#2553)
* feat: add nudge application functionality - Introduced a new endpoint to nudge applications, allowing users to send reminders. - Implemented logic to prevent nudging if the last nudge was less than 24 hours ago, with appropriate error messages. - Updated application constants to include new API response and error messages related to the nudge feature. - Enhanced the applications controller to handle nudge requests and update application nudge counts accordingly. * refactor: enhance nudge application logic * feat: add error handling for nudge application when status is not pending * fix: correct last nudge timestamp logic in nudgeApplication function * refactor: improve nudge application logic and update response messages - Enhanced the nudgeApplication function to streamline error handling and improve readability. - Updated API response and error messages for nudging applications to provide clearer feedback. - Removed redundant checks and utilized a transaction for better performance and consistency in the nudge process. * refactor: add NUDGE_APPLICATION_STATUS constants * test: add comprehensive tests for nudge application functionality (#2543) * test: add comprehensive tests for nudge application functionality * chore: add logger utility to discordService and logService for improved logging * test: enhance nudge application tests to cover pending status validation * refactor: remove duplicate logger import and unused config in discordService * nit: remove unused logger import * refactor: update nudge application logic and messages - Changed the success message for nudging an application to "Nudge sent successfully". - Updated error messages for nudging to be more user-friendly. - Refactored the nudgeApplication function to streamline logic and improve readability. - Adjusted integration and unit tests to reflect the updated messages and logic. * refactor: nudge model try and catch block --------- Co-authored-by: Amit Prakash <[email protected]>
2 parents c0b997d + c0328e5 commit 4a071dc

File tree

7 files changed

+456
-6
lines changed

7 files changed

+456
-6
lines changed

constants/application.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,23 @@ const APPLICATION_ROLES = {
1717
const API_RESPONSE_MESSAGES = {
1818
APPLICATION_CREATED_SUCCESS: "Application created successfully",
1919
APPLICATION_RETURN_SUCCESS: "Applications returned successfully",
20+
NUDGE_SUCCESS: "Nudge sent successfully",
2021
};
2122

2223
const APPLICATION_ERROR_MESSAGES = {
2324
APPLICATION_ALREADY_REVIEWED: "Application has already been reviewed",
25+
NUDGE_TOO_SOON: "Nudge unavailable. You'll be able to nudge again after 24 hours.",
26+
NUDGE_ONLY_PENDING_ALLOWED: "Nudge unavailable. Only pending applications can be nudged.",
2427
};
2528

29+
const NUDGE_APPLICATION_STATUS = {
30+
notFound: "notFound",
31+
unauthorized: "unauthorized",
32+
notPending: "notPending",
33+
tooSoon: "tooSoon",
34+
success: "success",
35+
} as const;
36+
2637
/**
2738
* Business requirement: Applications created after this date are considered reviewed
2839
* and cannot be resubmitted. This date marks the start of the new application review cycle.
@@ -35,4 +46,5 @@ module.exports = {
3546
API_RESPONSE_MESSAGES,
3647
APPLICATION_ERROR_MESSAGES,
3748
APPLICATION_REVIEW_CYCLE_START_DATE,
49+
NUDGE_APPLICATION_STATUS,
3850
};

controllers/applications.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ const { logType } = require("../constants/logs");
33
import { CustomRequest, CustomResponse } from "../types/global";
44
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
55
const ApplicationModel = require("../models/applications");
6-
const { API_RESPONSE_MESSAGES } = require("../constants/application");
6+
const { API_RESPONSE_MESSAGES, APPLICATION_ERROR_MESSAGES, NUDGE_APPLICATION_STATUS } = require("../constants/application");
77
const { createApplicationService } = require("../services/applicationService");
88
const { Conflict } = require("http-errors");
99
const logger = require("../utils/logger");
10+
const { APPLICATION_STATUS_TYPES } = require("../constants/application");
1011

1112
const getAllOrUserApplication = async (req: CustomRequest, res: CustomResponse): Promise<any> => {
1213
try {
@@ -149,9 +150,43 @@ const getApplicationById = async (req: CustomRequest, res: CustomResponse) => {
149150
}
150151
};
151152

153+
const nudgeApplication = async (req: CustomRequest, res: CustomResponse) => {
154+
try {
155+
const { applicationId } = req.params;
156+
157+
const result = await ApplicationModel.nudgeApplication({
158+
applicationId,
159+
userId: req.userData.id,
160+
});
161+
162+
switch (result.status) {
163+
case NUDGE_APPLICATION_STATUS.notFound:
164+
return res.boom.notFound("Application not found");
165+
case NUDGE_APPLICATION_STATUS.unauthorized:
166+
return res.boom.unauthorized("You are not authorized to nudge this application");
167+
case NUDGE_APPLICATION_STATUS.notPending:
168+
return res.boom.badRequest(APPLICATION_ERROR_MESSAGES.NUDGE_ONLY_PENDING_ALLOWED);
169+
case NUDGE_APPLICATION_STATUS.tooSoon:
170+
return res.boom.tooManyRequests(APPLICATION_ERROR_MESSAGES.NUDGE_TOO_SOON);
171+
case NUDGE_APPLICATION_STATUS.success:
172+
return res.json({
173+
message: API_RESPONSE_MESSAGES.NUDGE_SUCCESS,
174+
nudgeCount: result.nudgeCount,
175+
lastNudgeAt: result.lastNudgeAt,
176+
});
177+
default:
178+
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
179+
}
180+
} catch (err) {
181+
logger.error(`Error while nudging application: ${err}`);
182+
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
183+
}
184+
};
185+
152186
module.exports = {
153187
getAllOrUserApplication,
154188
addApplication,
155189
updateApplication,
156190
getApplicationById,
191+
nudgeApplication,
157192
};

models/applications.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { application } from "../types/application";
22
const firestore = require("../utils/firestore");
33
const logger = require("../utils/logger");
44
const ApplicationsModel = firestore.collection("applicants");
5-
const { DOCUMENT_WRITE_SIZE } = require("../constants/constants");
5+
const { APPLICATION_STATUS_TYPES, NUDGE_APPLICATION_STATUS } = require("../constants/application");
6+
const { convertDaysToMilliseconds } = require("../utils/time");
67

78
const getAllApplications = async (limit: number, lastDocId?: string) => {
89
try {
@@ -136,11 +137,63 @@ const updateApplication = async (dataToUpdate: object, applicationId: string) =>
136137
}
137138
};
138139

140+
const nudgeApplication = async ({ applicationId, userId }: { applicationId: string; userId: string }) => {
141+
const currentTime = Date.now();
142+
const twentyFourHoursInMilliseconds = convertDaysToMilliseconds(1);
143+
144+
const result = await firestore.runTransaction(async (transaction) => {
145+
const applicationRef = ApplicationsModel.doc(applicationId);
146+
const applicationDoc = await transaction.get(applicationRef);
147+
148+
if (!applicationDoc.exists) {
149+
return { status: NUDGE_APPLICATION_STATUS.notFound };
150+
}
151+
152+
const application = applicationDoc.data();
153+
154+
if (application.userId !== userId) {
155+
return { status: NUDGE_APPLICATION_STATUS.unauthorized };
156+
}
157+
158+
if (application.status !== APPLICATION_STATUS_TYPES.PENDING) {
159+
return { status: NUDGE_APPLICATION_STATUS.notPending };
160+
}
161+
162+
const lastNudgeAt = application.lastNudgeAt;
163+
if (lastNudgeAt) {
164+
const lastNudgeTimestamp = new Date(lastNudgeAt).getTime();
165+
const timeDifference = currentTime - lastNudgeTimestamp;
166+
167+
if (timeDifference <= twentyFourHoursInMilliseconds) {
168+
return { status: NUDGE_APPLICATION_STATUS.tooSoon };
169+
}
170+
}
171+
172+
const currentNudgeCount = application.nudgeCount || 0;
173+
const updatedNudgeCount = currentNudgeCount + 1;
174+
const newLastNudgeAt = new Date(currentTime).toISOString();
175+
176+
transaction.update(applicationRef, {
177+
nudgeCount: updatedNudgeCount,
178+
lastNudgeAt: newLastNudgeAt,
179+
});
180+
181+
return {
182+
status: NUDGE_APPLICATION_STATUS.success,
183+
nudgeCount: updatedNudgeCount,
184+
lastNudgeAt: newLastNudgeAt,
185+
};
186+
});
187+
188+
return result;
189+
};
190+
139191
module.exports = {
140192
getAllApplications,
141193
getUserApplications,
142194
addApplication,
143195
updateApplication,
144196
getApplicationsBasedOnStatus,
145197
getApplicationById,
198+
nudgeApplication,
146199
};

routes/applications.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ router.patch(
2424
applicationValidator.validateApplicationUpdateData,
2525
applications.updateApplication
2626
);
27+
router.patch("/:applicationId/nudge", authenticate, applications.nudgeApplication);
2728

2829
module.exports = router;

services/logService.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ interface LogBody {
1919
* @param meta { LogMeta }: Meta data of the log
2020
* @param body { LogBody }: Body of the log
2121
*/
22-
export const addLog = async (type: string, meta: LogMeta, body: LogBody): Promise<FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>> => {
22+
export const addLog = async (
23+
type: string,
24+
meta: LogMeta,
25+
body: LogBody
26+
): Promise<FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>> => {
2327
try {
2428
const log = {
2529
type,
@@ -32,4 +36,4 @@ export const addLog = async (type: string, meta: LogMeta, body: LogBody): Promis
3236
logger.error("Error in adding log", err);
3337
throw new Error(INTERNAL_SERVER_ERROR);
3438
}
35-
};
39+
};

test/integration/application.test.ts

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ import chai from "chai";
22
import chaiHttp from "chai-http";
33
const { expect } = chai;
44
import config from "config";
5+
import sinon from "sinon";
56
const app = require("../../server");
67
const addUser = require("../utils/addUser");
78
const cleanDb = require("../utils/cleanDb");
89
const authService = require("../../services/authService");
910
const userData = require("../fixtures/user/user")();
1011
const applicationModel = require("../../models/applications");
11-
const { requestRoleData } = require("../fixtures/discordactions/discordactions");
1212

1313
const applicationsData = require("../fixtures/applications/applications")();
1414
const cookieName = config.get("userToken.cookieName");
15-
const { getUserApplicationObject } = require("../../utils/application");
15+
const { APPLICATION_ERROR_MESSAGES, API_RESPONSE_MESSAGES } = require("../../constants/application");
1616

1717
const appOwner = userData[3];
1818
const superUser = userData[4];
@@ -65,6 +65,7 @@ describe("Application", function () {
6565

6666
after(async function () {
6767
await cleanDb();
68+
sinon.restore();
6869
});
6970

7071
describe("GET /applications", function () {
@@ -488,4 +489,150 @@ describe("Application", function () {
488489
});
489490
});
490491
});
492+
493+
describe("PATCH /applications/:applicationId/nudge", function () {
494+
let nudgeApplicationId: string;
495+
496+
beforeEach(async function () {
497+
const applicationData = { ...applicationsData[0], userId };
498+
nudgeApplicationId = await applicationModel.addApplication(applicationData);
499+
});
500+
501+
afterEach(async function () {
502+
sinon.restore();
503+
});
504+
505+
it("should successfully nudge a pending application when user owns it and no previous nudge exists", function (done) {
506+
chai
507+
.request(app)
508+
.patch(`/applications/${nudgeApplicationId}/nudge`)
509+
.set("cookie", `${cookieName}=${jwt}`)
510+
.end(function (err, res) {
511+
if (err) return done(err);
512+
513+
expect(res).to.have.status(200);
514+
expect(res.body.message).to.be.equal(API_RESPONSE_MESSAGES.NUDGE_SUCCESS);
515+
expect(res.body.nudgeCount).to.be.equal(1);
516+
expect(res.body.lastNudgeAt).to.be.a("string");
517+
done();
518+
});
519+
});
520+
521+
it("should successfully nudge an application when 24 hours have passed since last nudge", function (done) {
522+
chai
523+
.request(app)
524+
.patch(`/applications/${nudgeApplicationId}/nudge`)
525+
.set("cookie", `${cookieName}=${jwt}`)
526+
.end(function (err, res) {
527+
if (err) return done(err);
528+
529+
expect(res).to.have.status(200);
530+
expect(res.body.nudgeCount).to.be.equal(1);
531+
532+
const twentyFiveHoursAgo = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
533+
applicationModel.updateApplication({ lastNudgeAt: twentyFiveHoursAgo }, nudgeApplicationId).then(() => {
534+
chai
535+
.request(app)
536+
.patch(`/applications/${nudgeApplicationId}/nudge`)
537+
.set("cookie", `${cookieName}=${jwt}`)
538+
.end(function (err, res) {
539+
if (err) return done(err);
540+
541+
expect(res).to.have.status(200);
542+
expect(res.body.message).to.be.equal(API_RESPONSE_MESSAGES.NUDGE_SUCCESS);
543+
expect(res.body.nudgeCount).to.be.equal(2);
544+
expect(res.body.lastNudgeAt).to.be.a("string");
545+
done();
546+
});
547+
});
548+
});
549+
});
550+
551+
it("should return 404 if the application doesn't exist", function (done) {
552+
chai
553+
.request(app)
554+
.patch(`/applications/non-existent-id/nudge`)
555+
.set("cookie", `${cookieName}=${jwt}`)
556+
.end(function (err, res) {
557+
if (err) return done(err);
558+
559+
expect(res).to.have.status(404);
560+
expect(res.body.error).to.be.equal("Not Found");
561+
expect(res.body.message).to.be.equal("Application not found");
562+
done();
563+
});
564+
});
565+
566+
it("should return 401 if user is not authenticated", function (done) {
567+
chai
568+
.request(app)
569+
.patch(`/applications/${nudgeApplicationId}/nudge`)
570+
.end(function (err, res) {
571+
if (err) return done(err);
572+
573+
expect(res).to.have.status(401);
574+
expect(res.body.error).to.be.equal("Unauthorized");
575+
expect(res.body.message).to.be.equal("Unauthenticated User");
576+
done();
577+
});
578+
});
579+
580+
it("should return 401 if user does not own the application", function (done) {
581+
chai
582+
.request(app)
583+
.patch(`/applications/${nudgeApplicationId}/nudge`)
584+
.set("cookie", `${cookieName}=${secondUserJwt}`)
585+
.end(function (err, res) {
586+
if (err) return done(err);
587+
588+
expect(res).to.have.status(401);
589+
expect(res.body.error).to.be.equal("Unauthorized");
590+
expect(res.body.message).to.be.equal("You are not authorized to nudge this application");
591+
done();
592+
});
593+
});
594+
595+
it("should return 429 when trying to nudge within 24 hours", function (done) {
596+
chai
597+
.request(app)
598+
.patch(`/applications/${nudgeApplicationId}/nudge`)
599+
.set("cookie", `${cookieName}=${jwt}`)
600+
.end(function (err, res) {
601+
if (err) return done(err);
602+
603+
expect(res).to.have.status(200);
604+
605+
chai
606+
.request(app)
607+
.patch(`/applications/${nudgeApplicationId}/nudge`)
608+
.set("cookie", `${cookieName}=${jwt}`)
609+
.end(function (err, res) {
610+
if (err) return done(err);
611+
612+
expect(res).to.have.status(429);
613+
expect(res.body.error).to.be.equal("Too Many Requests");
614+
expect(res.body.message).to.be.equal(APPLICATION_ERROR_MESSAGES.NUDGE_TOO_SOON);
615+
done();
616+
});
617+
});
618+
});
619+
620+
it("should return 400 when trying to nudge an application that is not in pending status", function (done) {
621+
const nonPendingApplicationData = { ...applicationsData[1], userId };
622+
applicationModel.addApplication(nonPendingApplicationData).then((nonPendingApplicationId: string) => {
623+
chai
624+
.request(app)
625+
.patch(`/applications/${nonPendingApplicationId}/nudge`)
626+
.set("cookie", `${cookieName}=${jwt}`)
627+
.end(function (err, res) {
628+
if (err) return done(err);
629+
630+
expect(res).to.have.status(400);
631+
expect(res.body.error).to.be.equal("Bad Request");
632+
expect(res.body.message).to.be.equal(APPLICATION_ERROR_MESSAGES.NUDGE_ONLY_PENDING_ALLOWED);
633+
done();
634+
});
635+
});
636+
});
637+
});
491638
});

0 commit comments

Comments
 (0)