diff --git a/backend/__tests__/controllers/accountsGamificationController.test.js b/backend/__tests__/controllers/accountsGamificationController.test.js index baa52696..e9fb0a9a 100644 --- a/backend/__tests__/controllers/accountsGamificationController.test.js +++ b/backend/__tests__/controllers/accountsGamificationController.test.js @@ -179,3 +179,109 @@ describe("PATCH /accounts/updatepoints", () => { ); }); }); + +describe("PATCH /accounts/updatestreaksfromlogin", () => { + const mockAccounts = { + userID: "2", + points: 0, + streaks: 0, + lastUnitCompletionDate: expect.anything(), + }; + + it("should update an account and return 204 on success", async () => { + const mockResponse = { status: 204, statusText: "OK" }; + + accountsGamificationService.updateStreaksFromLogin.mockResolvedValue(mockResponse); + + const response = await request(app) + .patch(`/accounts/updatestreaksfromlogin/${mockAccounts.userID}`) + .send(mockAccount); + + expect(response.status).toBe(200); + expect(response.body.statusText).toBe("Streaks Updated Successfully"); + expect(accountsGamificationService.updateStreaksFromLogin).toHaveBeenCalledTimes(1); + expect(accountsGamificationService.updateStreaksFromLogin).toHaveBeenCalledWith( + mockAccount.userID, + mockAccount.lastUnitCompletionDate + ); + }); + + it("should return 500 and an error message on failure", async () => { + const mockError = new Error("Database error"); + + accountsGamificationService.updateStreaksFromLogin.mockRejectedValue(mockError); + + const response = await request(app) + .patch(`/accounts/updatestreaksfromlogin/${mockAccounts.userID}`) + .send(mockAccount); + + expect(response.status).toBe(500); + expect(accountsGamificationService.updateStreaksFromLogin).toHaveBeenCalledTimes(1); + expect(accountsGamificationService.updateStreaksFromLogin).toHaveBeenCalledWith( + mockAccount.userID, + mockAccount.lastUnitCompletionDate + ); + }); +}); + +describe("PATCH /accounts/updatestreaksfromunit", () => { + const mockAccounts = { + userID: "2", + points: 0, + streaks: 0, + lastUnitCompletionDate: expect.anything(), + }; + + it("should update if it is a quiz and return 204 on success", async () => { + const mockResponse = { status: 204, statusText: "OK" }; + + accountsGamificationService.updateStreaksFromUnit.mockResolvedValue(mockResponse); + + const response = await request(app) + .patch(`/accounts/updatestreaksfromunit/${mockAccounts.userID}/quiz}`) + .send(mockAccount); + + expect(response.status).toBe(200); + expect(response.body.statusText).toBe("Streaks Updated Successfully"); + expect(accountsGamificationService.updateStreaksFromUnit).toHaveBeenCalledTimes(1); + expect(accountsGamificationService.updateStreaksFromUnit).toHaveBeenCalledWith( + mockAccount.userID, + mockAccount.lastUnitCompletionDate + ); + }); + + it("should not update if it is not a quiz and return 204 on success", async () => { + const mockResponse = { status: 204, statusText: "OK" }; + + accountsGamificationService.updateStreaksFromUnit.mockResolvedValue(mockResponse); + + const response = await request(app) + .patch(`/accounts/updatestreaksfromunit/${mockAccounts.userID}/lesson}`) + .send(mockAccount); + + expect(response.status).toBe(200); + expect(response.body.statusText).toBe("Streaks Updated Successfully"); + expect(accountsGamificationService.updateStreaksFromUnit).toHaveBeenCalledTimes(1); + expect(accountsGamificationService.updateStreaksFromUnit).toHaveBeenCalledWith( + mockAccount.userID, + mockAccount.lastUnitCompletionDate + ); + }); + + it("should return 500 and an error message on failure", async () => { + const mockError = new Error("Database error"); + + accountsGamificationService.updateStreaksFromUnit.mockRejectedValue(mockError); + + const response = await request(app) + .patch("/accounts/updatestreaksfromunit") + .send(mockAccount); + + expect(response.status).toBe(500); + expect(accountsGamificationService.updateStreaksFromUnit).toHaveBeenCalledTimes(1); + expect(accountsGamificationService.updateStreaksFromUnit).toHaveBeenCalledWith( + mockAccount.userID, + mockAccount.lastUnitCompletionDate + ); + }); +}); \ No newline at end of file diff --git a/backend/__tests__/services/accountsGamificationService.test.js b/backend/__tests__/services/accountsGamificationService.test.js index 99fdf288..3d2dedb7 100644 --- a/backend/__tests__/services/accountsGamificationService.test.js +++ b/backend/__tests__/services/accountsGamificationService.test.js @@ -18,7 +18,7 @@ let consoleErrorSpy; beforeEach(() => { jest.resetAllMocks(); - consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { }); }); afterEach(() => { @@ -461,3 +461,290 @@ describe("updatePoints", () => { }); }); + +describe("updateStreaksFromLogin", () => { + + + it('should return the same streak when login is successful and streak stays as login is consecutive', async () => { + + const mockData = { + userID: "123", + points: 10, + streaks: 5, + lastUnitCompletionDate: new Date(new Date().setDate(new Date().getDate() - 1)) // 1 day difference + + }; + + const mockSingle = jest + .fn() + .mockResolvedValue({ data: mockData, error: null }); + const mockEq = jest.fn().mockReturnValue({ single: mockSingle }); + const mockSelect = jest.fn().mockReturnValue({ eq: mockEq }); + supabase.from.mockReturnValue({ select: mockSelect }); + + const result = await accountsGamificationService.getGamificationData( + mockData.userID + ); + const mockUpdate = jest.fn().mockResolvedValue({ status: 204, statusText: "Streak Updated Successfully" }); + + expect(result).toBeInstanceOf(accountsGamificationModel.AccountsGamification); + expect(result).toEqual(mockData); + + expect(supabase.from).toHaveBeenCalledWith('accountsgamification'); + expect(mockEq).toHaveBeenCalledWith('userID', mockData.userID); + + }); + + it('should reset the streak if the user has not logged in for more than 1 day', async () => { + const mockData = { + userID: "123", + points: 10, + streaks: 5, + lastUnitCompletionDate: new Date(new Date().setDate(new Date().getDate() - 5)) // 5 days difference + }; + + // Mock for getGamificationData + const mockSingle = jest.fn().mockResolvedValue({ data: mockData, error: null }); + const mockEqSelect = jest.fn().mockReturnValue({ single: mockSingle }); + const mockSelect = jest.fn().mockReturnValue({ eq: mockEqSelect }); + + // Mock for updateStreaksFromLogin + const mockEqUpdate = jest.fn().mockResolvedValue({ status: 204, statusText: "Streak Updated Successfully" }); + const mockUpdate = jest.fn().mockReturnValue({ eq: mockEqUpdate }); + + // Setup the mock to return different values based on the method chain + supabase.from.mockReturnValue({ + select: mockSelect, + update: mockUpdate + }); + + // Call updateStreaksFromLogin + await accountsGamificationService.updateStreaksFromLogin(mockData.userID); + + // Verify getGamificationData was called correctly + expect(supabase.from).toHaveBeenCalledWith('accountsgamification'); + expect(mockSelect).toHaveBeenCalledWith('*'); + expect(mockEqSelect).toHaveBeenCalledWith('userID', mockData.userID); + + // Verify updateStreaksFromLogin updated the streak to 0 + expect(mockUpdate).toHaveBeenCalledWith({ streaks: 0 }); + expect(mockEqUpdate).toHaveBeenCalledWith('userID', mockData.userID); + }); + +}); + +describe("updateStreaksFromUnit", () => { + afterEach(() => { + // Reset the mocked dependency after each test + __RewireAPI__.__ResetDependency__("getGamificationData"); + }); + + // it('should create a result and update streak if unit is completed', async () => { + // const mockGamificationData = new accountsGamificationModel.AccountsGamification("123", 10, 5, new Date(new Date().setDate(new Date().getDate() - 1))); // Last completion 1 day ago + // const mockResult = { userID: "123", quizID: 1, dateCreated: new Date() }; + + // // Mock the getGamificationData response + // const getGamificationDataSpy = sinon.stub().returns(mockGamificationData); + // __RewireAPI__.__Rewire__("getGamificationData", getGamificationDataSpy); + + // // Mock the Supabase update query for streaks + // const mockEq = jest.fn().mockResolvedValue({ error: null }); + // const mockUpdate = jest.fn().mockReturnValue({ eq: mockEq }); + // supabase.from.mockReturnValue({ update: mockUpdate }); + + // // Mock the createResult function + // const createResultSpy = jest.fn().mockResolvedValue({ error: null }); + // __RewireAPI__.__Rewire__("createResult", createResultSpy); + + // // Call the service to test + // const result = await accountsGamificationService.updateStreaksFromUnit("123", 1); + + // // Check if the getGamificationData was called + // sinon.assert.calledWith(getGamificationDataSpy, "123"); + + // // Check if the update query for streaks was called with the correct data + // expect(mockUpdate).toHaveBeenCalledWith({ + // streaks: 6, // The streak should be incremented by 1 + // }); + + // // Check if the createResult was called with the correct result instance + // expect(createResultSpy).toHaveBeenCalledWith(mockResult); + // }); + + // it('should increase streak by 1 when unit is completed and previous unit is done the previous day', async () => { + + // // const mockData = { + // // userID: "123", + // // points: 10, + // // streaks: 5, + // // lastUnitCompletionDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // 1 day difference + // // getStreaks: jest.fn().mockReturnValue(5), + // // }; + // // const quizID = 1; + + // const mockGamificationData = new accountsGamificationModel.AccountsGamification("123", 10, 5, new Date(new Date().setDate(new Date().getDate() - 1)).toISOString()); // 5 is the current streak + + // // Mock the getGamificationData response using sinon + // const getGamificationDataSpy = sinon.stub().returns(mockGamificationData); + // __RewireAPI__.__Rewire__("getGamificationData", getGamificationDataSpy); + + // // Mock the Supabase update query + // const mockEq = jest.fn().mockResolvedValue({ + // error: null, + // }); + + // const mockUpdate = jest.fn().mockReturnValue({ eq: mockEq }); + // supabase.from.mockReturnValue({ update: mockUpdate }); + + // // Call the function to test + // const result = await accountsGamificationService.updateStreaksFromUnit("1", 1); + + // // Check if the flow hits the update function + // expect(mockUpdate).toHaveBeenCalled(); // This verifies the mockUpdate was called + // sinon.assert.calledWith(getGamificationDataSpy, "1"); + + // // Validate that the update was called with the correct data + // expect(mockUpdate).toHaveBeenCalledWith({ + // streaks: 5, // This should be 5 + // }); + + // }) + + // pass + // it('streak remains the same if unit is completed and previous unit is done the same day', async () => { + // const mockGamificationData = new accountsGamificationModel.AccountsGamification("123", 10, 5, new Date()); // 5 is the current streak + + // // Mock the getGamificationData response using sinon + // const getGamificationDataSpy = sinon.stub().returns(mockGamificationData); + // __RewireAPI__.__Rewire__("getGamificationData", getGamificationDataSpy); + + // // Mock the Supabase update query + // const mockEq = jest.fn().mockResolvedValue({ + // error: null, + // }); + + // const mockUpdate = jest.fn().mockReturnValue({ eq: mockEq }); + // supabase.from.mockReturnValue({ update: mockUpdate }); + + // // Call the function to test + // const result = await accountsGamificationService.updateStreaksFromUnit("1", 1); + + // // Check if the flow hits the update function + // expect(mockUpdate).toHaveBeenCalled(); // This verifies the mockUpdate was called + // sinon.assert.calledWith(getGamificationDataSpy, "1"); + + // // Validate that the update was called with the correct data + // expect(mockUpdate).toHaveBeenCalledWith({ + // streaks: 5, // This should be 5 + // }); + + // }); + + // it('should return the same streak if the last unit completion is today', async () => { + // const mockGamificationData = new accountsGamificationModel.AccountsGamification("123", 10, 5, new Date()); // Last completion is today + // const mockResult = new Result("123", 1, new Date()); + + // // Mock the getGamificationData response + // const getGamificationDataSpy = sinon.stub().returns(mockGamificationData); + // __RewireAPI__.__Rewire__("getGamificationData", getGamificationDataSpy); + + // // Mock the Supabase update query for streaks + // const mockEq = jest.fn().mockResolvedValue({ error: null }); + // const mockUpdate = jest.fn().mockReturnValue({ eq: mockEq }); + // supabase.from.mockReturnValue({ update: mockUpdate }); + + // // Mock the createResult function + // const createResultSpy = jest.fn().mockResolvedValue({ error: null }); + // __RewireAPI__.__Rewire__("createResult", createResultSpy); + + // // Call the service to test + // const result = await accountsGamificationService.updateStreaksFromUnit("123", 1); + + // // Check if the getGamificationData was called + // sinon.assert.calledWith(getGamificationDataSpy, "123"); + + // // Verify that update wasn't called because the streak doesn't change + // expect(mockUpdate).not.toHaveBeenCalled(); + + // // Check if the createResult was called with the correct result instance + // expect(createResultSpy).toHaveBeenCalledWith(mockResult); + // }); + + // it("should throw an error when getGamificationData throws the error, supabase function to update should not be called", async () => { + // const errorMessage = "Failed to fetch account"; + + // // Mock the getGamificationData response using sinon + // const getGamificationDataSpy = sinon + // .stub() + // .throws(new Error(errorMessage)); + // __RewireAPI__.__Rewire__("getGamificationData", getGamificationDataSpy); + + // // Mock the Supabase update query + // const mockEq = jest.fn().mockResolvedValue({ + // error: new Error("another"), + // }); + + // const mockUpdate = jest.fn().mockReturnValue({ eq: mockEq }); + // supabase.from.mockReturnValue({ update: mockUpdate }); + + // await expect( + // accountsGamificationService.updateStreaksFromUnit("123", 5) + // ).rejects.toThrow(errorMessage); + + // // Verify that the Supabase update function was never called + // expect(supabase.from).not.toHaveBeenCalled(); + // expect(mockUpdate).not.toHaveBeenCalled(); + // expect(mockEq).not.toHaveBeenCalled(); + + // }); + + + // it('streak remains the same if unit is completed and previous unit is done the same day', async () => { + + // const mockData = { + // userID: "123", + // points: 10, + // streaks: 5, + // lastUnitCompletionDate: new Date(new Date().setDate(new Date().getDate())) // 1 day difference + + // }; + // const mockSingle = jest + // .fn() + // .mockResolvedValue({ data: mockData, error: null }); + // const mockEq = jest.fn().mockReturnValue({ single: mockSingle }); + // const mockSelect = jest.fn().mockReturnValue({ eq: mockEq }); + // supabase.from.mockReturnValue({ select: mockSelect }); + + // const result = await accountsGamificationService.updateStreaksFromUnit('user1'); + + // expect(result.streaks).toBe(5); + // expect(result.userID).toBe(mockData.userID); + + // }) + + // it('streak is set to 1 if its the first unit completed in many days', async () => { + + // const mockData = { + // userID: "123", + // points: 10, + // streaks: 5, + // lastUnitCompletionDate: new Date(new Date().setDate(new Date().getDate() - 5)) // 5 day difference + + // }; + + // const mockSingle = jest + // .fn() + // .mockResolvedValue({ data: mockData, error: null }); + // const mockEq = jest.fn().mockReturnValue({ single: mockSingle }); + // const mockSelect = jest.fn().mockReturnValue({ eq: mockEq }); + // supabase.from.mockReturnValue({ select: mockSelect }); + + // const result = await updateStreaksFromUnit('user1'); + + // expect(result.streaks).toBe(1); + // expect(result.userID).toBe('user1'); + + // }) + + +}); diff --git a/backend/dist/controllers/accountsGamificationController.js b/backend/dist/controllers/accountsGamificationController.js index fa1d1d34..5fb9cacc 100644 --- a/backend/dist/controllers/accountsGamificationController.js +++ b/backend/dist/controllers/accountsGamificationController.js @@ -1,18 +1,18 @@ "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { +var __createBinding = (this && this.__createBinding) || (Object.create ? (function (o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; + desc = { enumerable: true, get: function () { return m[k]; } }; } Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { +}) : (function (o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function (o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { +}) : function (o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { @@ -35,6 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.updateStreaksFromUnit = exports.updateStreaksFromLogin = exports.updatePoints = exports.getGamificationData = exports.getTop5Accounts = void 0; exports.updatePoints = exports.getBadges = exports.getGamificationData = exports.getTop5Accounts = void 0; const accountsGamificationService = __importStar(require("../services/accountsGamificationService")); const errorHandling_1 = __importDefault(require("../errors/errorHandling")); @@ -96,3 +97,39 @@ const updatePoints = (req, res) => __awaiter(void 0, void 0, void 0, function* ( } }); exports.updatePoints = updatePoints; +const updateStreaksFromLogin = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + console.log("in this login"); + const userID = req.params.userid; + try { + const updatedStreak = yield accountsGamificationService.updateStreaksFromLogin(userID); + res.status(200).json({ + status: 200, + statusText: "Streak Updated Successfully", + }); + } + catch (error) { + const errorResponse = (0, errorHandling_1.default)(error); + if (errorResponse) { + res.status(errorResponse.status).json(errorResponse); + } + } +}); +exports.updateStreaksFromLogin = updateStreaksFromLogin; +const updateStreaksFromUnit = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + console.log("in this unit"); + console.log(req.params.userid, req.params.quizid); + try { + const updatedStreak = yield accountsGamificationService.updateStreaksFromUnit(req.params.userid, parseInt(req.params.quizid)); + res.status(200).json({ + status: 200, + statusText: "Streak Updated Successfully", + }); + } + catch (error) { + const errorResponse = (0, errorHandling_1.default)(error); + if (errorResponse) { + res.status(errorResponse.status).json(errorResponse); + } + } +}); +exports.updateStreaksFromUnit = updateStreaksFromUnit; diff --git a/backend/dist/models/accountsGamificationModel.js b/backend/dist/models/accountsGamificationModel.js index 45b566f2..5c02dbef 100644 --- a/backend/dist/models/accountsGamificationModel.js +++ b/backend/dist/models/accountsGamificationModel.js @@ -15,7 +15,10 @@ class AccountsGamification { return this.streaks; } getLastUnitCompletionDate() { - return this.lastUnitCompletionDate; + if (this.lastUnitCompletionDate === null) { + return null; + } + return new Date(this.lastUnitCompletionDate); } } exports.AccountsGamification = AccountsGamification; diff --git a/backend/dist/routes/accountsGamificationRouter.js b/backend/dist/routes/accountsGamificationRouter.js index e1b25372..507243ee 100644 --- a/backend/dist/routes/accountsGamificationRouter.js +++ b/backend/dist/routes/accountsGamificationRouter.js @@ -31,9 +31,11 @@ const express_1 = require("express"); const authMiddleware_1 = __importDefault(require("../middleware/authMiddleware")); const router = (0, express_1.Router)(); /* READ */ -router.get('/gamificationdata/:userid', authMiddleware_1.default, accountsGamificationController.getGamificationData); +router.get("/gamificationdata/:userid", authMiddleware_1.default, accountsGamificationController.getGamificationData); router.get("/leaderboard/:userid", authMiddleware_1.default, accountsGamificationController.getTop5Accounts); router.get("/badges/:userid", authMiddleware_1.default, accountsGamificationController.getBadges); /* UPDATE */ -router.patch('/updatepoints', authMiddleware_1.default, accountsGamificationController.updatePoints); +router.patch("/updatepoints", authMiddleware_1.default, accountsGamificationController.updatePoints); +router.patch("/updateloginstreaks/:userid", authMiddleware_1.default, accountsGamificationController.updateStreaksFromLogin); +router.patch("/updateunitstreaks/:userid/:quizid", authMiddleware_1.default, accountsGamificationController.updateStreaksFromUnit); exports.default = router; diff --git a/backend/dist/services/accountsGamificationService.js b/backend/dist/services/accountsGamificationService.js index 80116045..56fb80ae 100644 --- a/backend/dist/services/accountsGamificationService.js +++ b/backend/dist/services/accountsGamificationService.js @@ -1,18 +1,18 @@ "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { +var __createBinding = (this && this.__createBinding) || (Object.create ? (function (o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; + desc = { enumerable: true, get: function () { return m[k]; } }; } Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { +}) : (function (o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function (o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { +}) : function (o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { @@ -50,8 +50,12 @@ exports.getTop5Accounts = getTop5Accounts; exports.getGamificationData = getGamificationData; exports.getBadges = getBadges; exports.updatePoints = updatePoints; -const supabaseConfig_1 = __importDefault(require("../config/supabaseConfig")); +exports.updateStreaksFromUnit = updateStreaksFromUnit; +exports.updateStreaksFromLogin = updateStreaksFromLogin; const accountsGamificationModel_1 = require("../models/accountsGamificationModel"); +const resultModel_1 = require("../models/resultModel"); +const resultService_1 = require("./resultService"); +const supabaseConfig_1 = __importDefault(require("../config/supabaseConfig")); const resultService = __importStar(require("../services/resultService")); /* READ */ function getTop5Accounts(userID) { @@ -83,9 +87,9 @@ function getTop5Accounts(userID) { const filteredData = rankedData .filter((record) => record.rank <= 5 || record.userID === userID) .map((_a) => { - var { userID } = _a, rest = __rest(_a, ["userID"]); - return rest; - }); + var { userID } = _a, rest = __rest(_a, ["userID"]); + return rest; + }); return filteredData; } }); @@ -97,6 +101,7 @@ function getGamificationData(userID) { .select("*") .eq("userID", userID) .single(); + console.log("calling get gamification data"); if (error) { console.error(error); throw error; @@ -154,8 +159,8 @@ function updatePoints(userID, points) { const { status, statusText, error } = yield supabaseConfig_1.default .from("accountsgamification") .update({ - points: accountGamificationData.getPoints() + points, - }) + points: accountGamificationData.getPoints() + points, + }) .eq("userID", userID); if (error) { console.error(error); @@ -166,3 +171,106 @@ function updatePoints(userID, points) { } }); } +// Ensure that the GET request fetches accurate streak data for the specified user. +// for both login and normal streak calculation +// export async function getStreaks(userID: string) { +// try { +// const data = await getGamificationData(userID); +// return data.streaks; +// } catch (error) { +// throw error; +// } +// } +// Helper function to calculate streak based on last completion date +function calculateStreak(lastDate, today) { + if (!lastDate) + return 1; // No last date, start a new streak + const differenceInDays = Math.round((today.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24)); + console.log("today", today); + console.log(differenceInDays); + if (differenceInDays == 1) + return 1; // Increment streak by 1 if difference is 1 day + if (differenceInDays > 1) + return 2; // Reset streak if difference is greater than 1 day + return 0; // Default case, no streak update +} +// Ensure that the POST request correctly updates the user's streak when they complete a new unit. +function updateStreaksFromUnit(userID, quizID) { + return __awaiter(this, void 0, void 0, function* () { + const resultInstance = new resultModel_1.Result(userID, quizID, new Date()); + const createResultResponse = yield (0, resultService_1.createResult)(resultInstance); + const data = yield getGamificationData(userID); + console.log("from unit la"); + console.log("quiz is", quizID); + console.log(data); + try { + if (data.lastUnitCompletionDate != null) { + const lastUnitDate = new Date(data.lastUnitCompletionDate); + const today = new Date(); + const daysSegment = calculateStreak(lastUnitDate, today); + console.log("days segment is", daysSegment); + let currentStreak = data.getStreaks(); + // Check the difference in days to update the streak + if (daysSegment == 1) { + // If the difference is 1 day, increment the streak + console.log("diff 1 day, so + 1"); + currentStreak += 1; + } + else if (daysSegment > 1) { + // If the difference is greater than 1 day, reset the streak to 1 + console.log("diff > 1 day, so reset to 1"); + currentStreak = 1; + } + const { status, statusText, error } = yield supabaseConfig_1.default + .from("accountsgamification") + .update({ + streaks: currentStreak, + }) + .eq("userID", userID); + } + } + catch (error) { + console.log(error); + throw error; + } + }); +} +// Update user streak for homepage display +function updateStreaksFromLogin(userID) { + return __awaiter(this, void 0, void 0, function* () { + const data = yield getGamificationData(userID); + try { + const today = new Date(); + // Check if the user has logged in today + if (data.lastUnitCompletionDate != null) { + const lastUnitDate = new Date(data.lastUnitCompletionDate); + // Calculate the difference in days between today and the last completion date + const daysSegment = calculateStreak(lastUnitDate, today); + let currentStreak = data.getStreaks(); + // If the user has logged in today, do not update the streak + if (daysSegment === 0) { + console.log("last unit completion date is today. streak unchanged"); + } + else if (daysSegment > 1) { + // If the difference is greater than 1 day, reset the streak to 0 + console.log("last unit completion date v long ago. streak reset"); + currentStreak = 0; + } + else { + console.log("diff is 1 means streak + 1"); + currentStreak += 1; + } + const { status, statusText, error } = yield supabaseConfig_1.default + .from("accountsgamification") + .update({ + streaks: currentStreak, + }) + .eq("userID", userID); + } + } + catch (error) { + console.log(error); + throw error; + } + }); +} diff --git a/backend/src/controllers/accountsGamificationController.ts b/backend/src/controllers/accountsGamificationController.ts index 60dfc2ea..86e683e9 100644 --- a/backend/src/controllers/accountsGamificationController.ts +++ b/backend/src/controllers/accountsGamificationController.ts @@ -1,37 +1,38 @@ -import { Request, Response } from "express"; import * as accountsGamificationService from "../services/accountsGamificationService"; + +import { Request, Response } from "express"; + import { errorMapping } from "../errors/errorMappings"; import handleError from "../errors/errorHandling"; /* READ */ export const getTop5Accounts = async (req: Request, res: Response) => { - try { - const accounts = await accountsGamificationService.getTop5Accounts( - req.params.userid - ); - res.status(200).json(accounts); - } catch (error: any) { - const errorResponse = handleError(error); - if(errorResponse){ - res.status(errorResponse.status).json(errorResponse); - } - } + try { + const accounts = await accountsGamificationService.getTop5Accounts( + req.params.userid + ); + res.status(200).json(accounts); + } catch (error: any) { + const errorResponse = handleError(error); + if (errorResponse) { + res.status(errorResponse.status).json(errorResponse); + } + } }; export const getGamificationData = async (req: Request, res: Response) => { - try { - const gamificationData = await accountsGamificationService.getGamificationData( - req.params.userid - ); - res.status(200).json(gamificationData); - } catch (error: any) { - const errorResponse = handleError(error); - if(errorResponse){ - res.status(errorResponse.status).json(errorResponse); - } - } -} + try { + const gamificationData = + await accountsGamificationService.getGamificationData(req.params.userid); + res.status(200).json(gamificationData); + } catch (error: any) { + const errorResponse = handleError(error); + if (errorResponse) { + res.status(errorResponse.status).json(errorResponse); + } + } +}; export const getBadges = async (req: Request, res: Response) => { try { @@ -49,19 +50,61 @@ export const getBadges = async (req: Request, res: Response) => { /* UPDATE */ export const updatePoints = async (req: Request, res: Response) => { + const { userID, points } = req.body; - const { userID, points } = req.body; + try { + const updatedPoints = await accountsGamificationService.updatePoints( + userID, + points + ); + res.status(200).json({ + status: 200, + statusText: "Points Updated Successfully", + }); + } catch (error: any) { + const errorResponse = handleError(error); + if (errorResponse) { + res.status(errorResponse.status).json(errorResponse); + } + } +}; - try { - const updatedPoints = await accountsGamificationService.updatePoints(userID, points); - res.status(200).json({ - status: 200, - statusText: "Points Updated Successfully", - }); - } catch (error: any) { - const errorResponse = handleError(error); - if(errorResponse){ - res.status(errorResponse.status).json(errorResponse); - } - } -}; \ No newline at end of file +export const updateStreaksFromLogin = async (req: Request, res: Response) => { + console.log("in this login"); + const userID = req.params.userid; + + try { + const updatedStreak = + await accountsGamificationService.updateStreaksFromLogin(userID); + res.status(200).json({ + status: 200, + statusText: "Streak Updated Successfully", + }); + } catch (error: any) { + const errorResponse = handleError(error); + if (errorResponse) { + res.status(errorResponse.status).json(errorResponse); + } + } +}; + +export const updateStreaksFromUnit = async (req: Request, res: Response) => { + console.log("in this unit"); + console.log(req.params.userid, req.params.quizid); + try { + const updatedStreak = + await accountsGamificationService.updateStreaksFromUnit( + req.params.userid, + parseInt(req.params.quizid) + ); + res.status(200).json({ + status: 200, + statusText: "Streak Updated Successfully", + }); + } catch (error: any) { + const errorResponse = handleError(error); + if (errorResponse) { + res.status(errorResponse.status).json(errorResponse); + } + } +}; diff --git a/backend/src/models/accountsGamificationModel.ts b/backend/src/models/accountsGamificationModel.ts index ab4f7f8e..b2987a2d 100644 --- a/backend/src/models/accountsGamificationModel.ts +++ b/backend/src/models/accountsGamificationModel.ts @@ -1,31 +1,33 @@ - export class AccountsGamification { - userID: string; - points: number; - streaks: number; - lastUnitCompletionDate: Date | null; + userID: string; + points: number; + streaks: number; + lastUnitCompletionDate: Date | null; - constructor( - userID: string, - points: number, - streaks: number, - lastUnitCompletionDate: Date | null - ) { - this.userID = userID; - this.points = points; - this.streaks = streaks; - this.lastUnitCompletionDate = lastUnitCompletionDate; - } + constructor( + userID: string, + points: number, + streaks: number, + lastUnitCompletionDate: Date | null + ) { + this.userID = userID; + this.points = points; + this.streaks = streaks; + this.lastUnitCompletionDate = lastUnitCompletionDate; + } - getPoints(): number { - return this.points; - } + getPoints(): number { + return this.points; + } - getStreaks(): number { - return this.streaks; - } + getStreaks(): number { + return this.streaks; + } - getLastUnitCompletionDate(): Date | null { - return this.lastUnitCompletionDate; - } + getLastUnitCompletionDate(): Date | null { + if (this.lastUnitCompletionDate === null) { + return null; + } + return new Date(this.lastUnitCompletionDate); + } } diff --git a/backend/src/routes/accountsGamificationRouter.ts b/backend/src/routes/accountsGamificationRouter.ts index 0290d9ac..99a2ff2f 100644 --- a/backend/src/routes/accountsGamificationRouter.ts +++ b/backend/src/routes/accountsGamificationRouter.ts @@ -1,16 +1,43 @@ -import * as accountsGamificationController from '../controllers/accountsGamificationController'; +import * as accountsGamificationController from "../controllers/accountsGamificationController"; -import { Router } from 'express'; -import verifyToken from '../middleware/authMiddleware'; +import { Router } from "express"; +import verifyToken from "../middleware/authMiddleware"; const router = Router(); /* READ */ -router.get('/gamificationdata/:userid', verifyToken, accountsGamificationController.getGamificationData); -router.get("/leaderboard/:userid", verifyToken, accountsGamificationController.getTop5Accounts); -router.get("/badges/:userid", verifyToken, accountsGamificationController.getBadges); +router.get( + "/gamificationdata/:userid", + verifyToken, + accountsGamificationController.getGamificationData +); +router.get( + "/leaderboard/:userid", + verifyToken, + accountsGamificationController.getTop5Accounts +); +router.get( + "/badges/:userid", + verifyToken, + accountsGamificationController.getBadges +); /* UPDATE */ -router.patch('/updatepoints', verifyToken, accountsGamificationController.updatePoints); +router.patch( + "/updatepoints", + verifyToken, + accountsGamificationController.updatePoints +); -export default router; \ No newline at end of file +router.patch( + "/updateloginstreaks/:userid", + verifyToken, + accountsGamificationController.updateStreaksFromLogin +); + +router.patch( + "/updateunitstreaks/:userid/:quizid", + verifyToken, + accountsGamificationController.updateStreaksFromUnit +); +export default router; diff --git a/backend/src/services/accountsGamificationService.ts b/backend/src/services/accountsGamificationService.ts index cfbae5f0..f8273996 100644 --- a/backend/src/services/accountsGamificationService.ts +++ b/backend/src/services/accountsGamificationService.ts @@ -1,143 +1,247 @@ -import supabase from "../config/supabaseConfig"; -import { - AccountsGamification -} from "../models/accountsGamificationModel"; import * as resultService from "../services/resultService"; +import { AccountsGamification } from "../models/accountsGamificationModel"; +import { Result } from "../models/resultModel"; +import { createResult } from "./resultService"; +import supabase from "../config/supabaseConfig"; + /* READ */ export async function getTop5Accounts(userID: string) { - const { data, error } = await supabase - .from("accountsgamification") - .select("points, accounts!inner(userID, firstName, lastName)") - .order("points", { ascending: false }) - - if (error) { - console.error(error); - throw error; - } else { - - let rank = 1; - let previousPoints = data[0].points; - - const rankedData = data.map((record, index: number) => { - if (index > 0 && record.points < previousPoints) { - rank = rank + 1; - } - previousPoints = record.points; - - return { - rank: rank, - name: record.accounts.firstName + " " + record.accounts.lastName, - points: record.points, - userID: record.accounts.userID, - }; - }); - - // Filter accounts with rank <= 5 or userID = userID - const filteredData = rankedData - .filter((record) => record.rank <= 5 || record.userID === userID) - .map(({ userID, ...rest }) => rest); - - return filteredData; - } + const { data, error } = await supabase + .from("accountsgamification") + .select("points, accounts!inner(userID, firstName, lastName)") + .order("points", { ascending: false }); + + if (error) { + console.error(error); + throw error; + } else { + let rank = 1; + let previousPoints = data[0].points; + + const rankedData = data.map((record, index: number) => { + if (index > 0 && record.points < previousPoints) { + rank = rank + 1; + } + previousPoints = record.points; + + return { + rank: rank, + name: record.accounts.firstName + " " + record.accounts.lastName, + points: record.points, + userID: record.accounts.userID, + }; + }); + + // Filter accounts with rank <= 5 or userID = userID + const filteredData = rankedData + .filter((record) => record.rank <= 5 || record.userID === userID) + .map(({ userID, ...rest }) => rest); + + return filteredData; + } } export async function getGamificationData(userID: string) { - const { data, error } = await supabase - .from("accountsgamification") - .select("*") - .eq("userID", userID) - .single(); - - if (error) { - console.error(error); - throw error; - } else { - - if (!data.lastUnitCompletionDate) { - return new AccountsGamification( - data.userID, - data.points, - data.streaks, - null - ); - } - - return new AccountsGamification( - data.userID, - data.points, - data.streaks, - new Date(data.lastUnitCompletionDate) - ); - } + const { data, error } = await supabase + .from("accountsgamification") + .select("*") + .eq("userID", userID) + .single(); + console.log("calling get gamification data"); + + if (error) { + console.error(error); + throw error; + } else { + if (!data.lastUnitCompletionDate) { + return new AccountsGamification( + data.userID, + data.points, + data.streaks, + null + ); + } + + return new AccountsGamification( + data.userID, + data.points, + data.streaks, + new Date(data.lastUnitCompletionDate) + ); + } } export async function getBadges(userID: string) { + const completedUnit = await resultService.getNoOfCompletedUnit(userID); + + let badges = []; + + const { data: storageBadges, error } = await supabase.storage + .from("badges") + .list(); + + if (error) { + console.error(error); + throw error; + } + + if (storageBadges.length === 0) { + throw new Error("Badge Not Found"); + } + + const designed = storageBadges + .map((badge) => badge.name) + .filter((badge) => badge.includes("badge")); + + const withoutBadge = Math.max(0, completedUnit - designed.length); + const minBadges = completedUnit - withoutBadge; - const completedUnit = await resultService.getNoOfCompletedUnit(userID); - - let badges = []; - - const { data: storageBadges, error } = await supabase.storage - .from("badges") - .list(); - - if (error) { - console.error(error); - throw error; - } - - if (storageBadges.length === 0) { - throw new Error("Badge Not Found"); - } - - const designed = storageBadges - .map((badge) => badge.name) - .filter((badge) => badge.includes("badge")); - - const withoutBadge = Math.max(0, completedUnit - designed.length); - const minBadges = completedUnit - withoutBadge; - - for (let i = 0; i < withoutBadge; i++) { - const { data: publicUrlData } = await supabase.storage - .from("badges") - .getPublicUrl(`placeholder.png`); - - if (publicUrlData) { - badges.push(publicUrlData.publicUrl); - } - } - - for (let i = minBadges; i > 0; i--) { - const { data: publicUrlData } = await supabase.storage - .from("badges") - .getPublicUrl(`badge${i}.png`); - - if (publicUrlData) { - badges.push(publicUrlData.publicUrl); - } - } - - return badges; + for (let i = 0; i < withoutBadge; i++) { + const { data: publicUrlData } = await supabase.storage + .from("badges") + .getPublicUrl(`placeholder.png`); + if (publicUrlData) { + badges.push(publicUrlData.publicUrl); + } + } + + for (let i = minBadges; i > 0; i--) { + const { data: publicUrlData } = await supabase.storage + .from("badges") + .getPublicUrl(`badge${i}.png`); + + if (publicUrlData) { + badges.push(publicUrlData.publicUrl); + } + } + + return badges; } /* UPDATE */ export async function updatePoints(userID: string, points: number) { + const accountGamificationData = await getGamificationData(userID); + const { status, statusText, error } = await supabase + .from("accountsgamification") + .update({ + points: accountGamificationData.getPoints() + points, + }) + .eq("userID", userID); + + if (error) { + console.error(error); + throw error; + } else { + return { status, statusText }; + } +} - const accountGamificationData = await getGamificationData(userID); - - const { status, statusText, error } = await supabase - .from("accountsgamification") - .update({ - points: accountGamificationData.getPoints() + points, - }) - .eq("userID", userID); - - if (error) { - console.error(error); - throw error; - } else { - return { status, statusText }; - } -} \ No newline at end of file +// Ensure that the GET request fetches accurate streak data for the specified user. +// for both login and normal streak calculation +// export async function getStreaks(userID: string) { +// try { +// const data = await getGamificationData(userID); +// return data.streaks; +// } catch (error) { +// throw error; +// } +// } + +// Helper function to calculate streak based on last completion date +function calculateStreak(lastDate: Date | null, today: Date): number { + if (!lastDate) return 1; // No last date, start a new streak + + const differenceInDays = Math.round( + (today.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24) + ); + console.log("today", today); + console.log(differenceInDays); + + if (differenceInDays == 1) return 1; // Increment streak by 1 if difference is 1 day + if (differenceInDays > 1) return 2; // Reset streak if difference is greater than 1 day + + return 0; // Default case, no streak update +} + +// Ensure that the POST request correctly updates the user's streak when they complete a new unit. +export async function updateStreaksFromUnit(userID: string, quizID: number) { + const resultInstance = new Result(userID, quizID, new Date()); + + const createResultResponse = await createResult(resultInstance); + const data = await getGamificationData(userID); + console.log("from unit la"); + console.log("quiz is", quizID); + console.log(data); + try { + if (data.lastUnitCompletionDate != null) { + const lastUnitDate = new Date(data.lastUnitCompletionDate); + const today = new Date(); + + const daysSegment = calculateStreak(lastUnitDate, today); + console.log("days segment is", daysSegment); + let currentStreak = data.getStreaks(); + + // Check the difference in days to update the streak + if (daysSegment == 1) { + // If the difference is 1 day, increment the streak + console.log("diff 1 day, so + 1"); + currentStreak += 1; + } else if (daysSegment > 1) { + // If the difference is greater than 1 day, reset the streak to 1 + console.log("diff > 1 day, so reset to 1"); + currentStreak = 1; + } + + const { status, statusText, error } = await supabase + .from("accountsgamification") + .update({ + streaks: currentStreak, + }) + .eq("userID", userID); + } + } catch (error) { + console.log(error); + throw error; + } +} + +// Update user streak for homepage display +export async function updateStreaksFromLogin(userID: string) { + const data = await getGamificationData(userID); + try { + const today = new Date(); + + // Check if the user has logged in today + if (data.lastUnitCompletionDate != null) { + const lastUnitDate = new Date(data.lastUnitCompletionDate); + + // Calculate the difference in days between today and the last completion date + const daysSegment = calculateStreak(lastUnitDate, today); + let currentStreak = data.getStreaks(); + + // If the user has logged in today, do not update the streak + if (daysSegment === 0) { + console.log("last unit completion date is today. streak unchanged"); + } else if (daysSegment > 1) { + // If the difference is greater than 1 day, reset the streak to 0 + console.log("last unit completion date v long ago. streak reset"); + currentStreak = 0; + } else { + console.log("diff is 1 means streak + 1"); + currentStreak += 1; + } + + const { status, statusText, error } = await supabase + .from("accountsgamification") + .update({ + streaks: currentStreak, + }) + .eq("userID", userID); + } + } catch (error) { + console.log(error); + throw error; + } +}