diff --git a/backend/src/grant/__test__/grant.service.spec.ts b/backend/src/grant/__test__/grant.service.spec.ts index 30fe3da9..d006096d 100644 --- a/backend/src/grant/__test__/grant.service.spec.ts +++ b/backend/src/grant/__test__/grant.service.spec.ts @@ -49,6 +49,40 @@ const mockGrants: Grant[] = [ attachments: [], isRestricted: true }, + { + grantId: 3, + organization: "Test Organization", + does_bcan_qualify: true, + status: Status.Active, + amount: 1000, + grant_start_date: "2024-01-01", + application_deadline: "2025-01-01", + report_deadlines: ["2025-01-01"], + description: "Test Description", + timeline: 1, + estimated_completion_time: 100, + grantmaker_poc: { POC_name: "name", POC_email: "test@test.com" }, + bcan_poc: { POC_name: "name", POC_email: ""}, + attachments: [], + isRestricted: false + }, + { + grantId: 4, + organization: "Test Organization 2", + does_bcan_qualify: false, + status: Status.Active, + amount: 1000, + grant_start_date: "2025-02-15", + application_deadline: "2025-02-01", + report_deadlines: ["2025-03-01", "2025-04-01"], + description: "Test Description 2", + timeline: 2, + estimated_completion_time: 300, + bcan_poc: { POC_name: "Allie", POC_email: "allie@gmail.com" }, + grantmaker_poc: { POC_name: "Benjamin", POC_email: "benpetrillo@yahoo.com" }, + attachments: [], + isRestricted: true + }, ]; // Create mock functions that we can reference @@ -58,6 +92,7 @@ const mockGet = vi.fn().mockReturnThis(); const mockDelete = vi.fn().mockReturnThis(); const mockUpdate = vi.fn().mockReturnThis(); const mockPut = vi.fn().mockReturnThis(); +// const mockGetGrantById = vi.fn(); const mockDocumentClient = { scan: mockScan, @@ -99,6 +134,8 @@ describe("GrantService", () => { updateNotification: vi.fn() } }); + + controller = module.get(GrantController); grantService = module.get(GrantService); @@ -167,53 +204,44 @@ describe("GrantService", () => { }); }); - describe("unarchiveGrants()", () => { - it("should unarchive multiple grants and return their ids", async () => { - mockPromise - .mockResolvedValueOnce({ Attributes: { isArchived: false } }) - .mockResolvedValueOnce({ Attributes: { isArchived: false } }); - - const data = await grantService.unarchiveGrants([1, 2]); - - expect(data).toEqual([1, 2]); - expect(mockUpdate).toHaveBeenCalledTimes(2); - - const firstCallArgs = mockUpdate.mock.calls[0][0]; - const secondCallArgs = mockUpdate.mock.calls[1][0]; + describe("makeGrantsInactive()", () => { + it("should inactivate multiple grants and return the updated grant objects", async () => { + const inactiveGrant3 = { + grantId: 3, + organization: "Test Organization", + does_bcan_qualify: true, + status: Status.Inactive, + amount: 1000, + grant_start_date: "2024-01-01", + application_deadline: "2025-01-01", + report_deadlines: ["2025-01-01"], + description: "Test Description", + timeline: 1, + estimated_completion_time: 100, + grantmaker_poc: { POC_name: "name", POC_email: "test@test.com" }, + bcan_poc: { POC_name: "name", POC_email: ""}, + attachments: [], + isRestricted: false + }; - expect(firstCallArgs).toMatchObject({ + mockPromise.mockResolvedValueOnce({ Attributes: inactiveGrant3 }); + + const data = await grantService.makeGrantsInactive(3); + + expect(data).toEqual(inactiveGrant3); + + expect(mockUpdate).toHaveBeenCalledTimes(1); + + const callArgs = mockUpdate.mock.calls[0][0]; + + expect(callArgs).toMatchObject({ TableName: "Grants", - Key: { grantId: 1 }, - UpdateExpression: "set isArchived = :archived", - ExpressionAttributeValues: { ":archived": false }, - ReturnValues: "UPDATED_NEW", + Key: { grantId: 3 }, + UpdateExpression: "SET #status = :inactiveStatus", + ExpressionAttributeNames: { "#status": "status" }, + ExpressionAttributeValues: { ":inactiveStatus": Status.Inactive }, + ReturnValues: "ALL_NEW", }); - expect(secondCallArgs).toMatchObject({ - TableName: "Grants", - Key: { grantId: 2 }, - UpdateExpression: "set isArchived = :archived", - ExpressionAttributeValues: { ":archived": false }, - ReturnValues: "UPDATED_NEW", - }); - }); - - it("should skip over grants that are already ", async () => { - mockPromise - .mockResolvedValueOnce({ Attributes: { isArchived: true } }) - .mockResolvedValueOnce({ Attributes: { isArchived: false } }); - - const data = await grantService.unarchiveGrants([1, 2]); - - expect(data).toEqual([2]); - expect(mockUpdate).toHaveBeenCalledTimes(2); - }); - - it("should throw an error if any update call fails", async () => { - mockPromise.mockRejectedValueOnce(new Error("DB Error")); - - await expect(grantService.unarchiveGrants([90])).rejects.toThrow( - "Failed to update Grant 90 status." - ); }); }); @@ -364,7 +392,9 @@ describe("GrantService", () => { // Tests for deleteGrantById method describe('deleteGrantById', () => { it('should call DynamoDB delete with the correct params and return success message', async () => { - mockPromise.mockResolvedValueOnce({}); + mockDelete.mockReturnValue({ + promise: vi.fn().mockResolvedValue({}) + }); const result = await grantService.deleteGrantById('123'); @@ -387,14 +417,18 @@ describe('deleteGrantById', () => { const conditionalError = new Error('Conditional check failed'); (conditionalError as any).code = 'ConditionalCheckFailedException'; - mockPromise.mockRejectedValueOnce(conditionalError); + mockDelete.mockReturnValue({ + promise: vi.fn().mockRejectedValue(conditionalError) + }); await expect(grantService.deleteGrantById('999')) .rejects.toThrow(/does not exist/); }); it('should throw a generic failure when DynamoDB fails for other reasons', async () => { - mockPromise.mockRejectedValueOnce(new Error('Some other DynamoDB error')); + mockDelete.mockReturnValue({ + promise: vi.fn().mockRejectedValue(new Error('Some other DynamoDB error')) + }); await expect(grantService.deleteGrantById('123')) .rejects.toThrow(/Failed to delete/); diff --git a/backend/src/grant/grant.controller.ts b/backend/src/grant/grant.controller.ts index 49352be6..dff64af5 100644 --- a/backend/src/grant/grant.controller.ts +++ b/backend/src/grant/grant.controller.ts @@ -11,23 +11,19 @@ export class GrantController { return await this.grantService.getAllGrants(); } - @Get(':id') - async getGrantById(@Param('id') GrantId: string) { - return await this.grantService.getGrantById(parseInt(GrantId, 10)); - } - - @Put('archive') - async archive( - @Body('grantIds') grantIds: number[] - ): Promise { - return await this.grantService.unarchiveGrants(grantIds) - } + - @Put('unarchive') - async unarchive( + @Put('inactivate') + async inactivate( @Body('grantIds') grantIds: number[] - ): Promise { - return await this.grantService.unarchiveGrants(grantIds) + ): Promise { + let grants: Grant[] = []; + for(const id of grantIds){ + Logger.log(`Inactivating grant with ID: ${id}`); + let newGrant = await this.grantService.makeGrantsInactive(id) + grants.push(newGrant); + } + return grants; } @Post('new-grant') @@ -47,5 +43,8 @@ export class GrantController { async deleteGrant(@Param('grantId') grantId: string) { return await this.grantService.deleteGrantById(grantId); } - + @Get(':id') + async getGrantById(@Param('id') GrantId: string) { + return await this.grantService.getGrantById(parseInt(GrantId, 10)); + } } \ No newline at end of file diff --git a/backend/src/grant/grant.service.ts b/backend/src/grant/grant.service.ts index 7ccc4492..08ab72cb 100644 --- a/backend/src/grant/grant.service.ts +++ b/backend/src/grant/grant.service.ts @@ -4,6 +4,7 @@ import { Grant } from '../../../middle-layer/types/Grant'; import { NotificationService } from '.././notifications/notifcation.service'; import { Notification } from '../../../middle-layer/types/Notification'; import { TDateISO } from '../utils/date'; +import { Status } from '../../../middle-layer/types/Status'; @Injectable() export class GrantService { private readonly logger = new Logger(GrantService.name); @@ -19,9 +20,31 @@ export class GrantService { }; try { - const data = await this.dynamoDb.scan(params).promise(); + const data = await this.dynamoDb.scan(params).promise(); + const grants = (data.Items as Grant[]) || []; + const inactiveGrantIds: number[] = []; + const now = new Date(); + + for (const grant of grants) { + if (grant.status === "Active") { + const startDate = new Date(grant.grant_start_date); + + // add timeline years to start date + const endDate = new Date(startDate); + endDate.setFullYear( + endDate.getFullYear() + grant.timeline + ); + + if (now >= endDate) { + inactiveGrantIds.push(grant.grantId); + let newGrant = this.makeGrantsInactive(grant.grantId) + grants.filter(g => g.grantId !== grant.grantId); + grants.push(await newGrant); - return data.Items as Grant[] || []; + } + } + } + return grants; } catch (error) { console.log(error) throw new Error('Could not retrieve grants.'); @@ -54,38 +77,43 @@ export class GrantService { } } - // Method to unarchive grants takes in array - async unarchiveGrants(grantIds :number[]) : Promise { - let successfulUpdates: number[] = []; - for (const grantId of grantIds) { - const params = { - TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', - Key: { - grantId: grantId, - }, - UpdateExpression: "set isArchived = :archived", - ExpressionAttributeValues: { ":archived": false }, - ReturnValues: "UPDATED_NEW", - }; + // Method to make grants inactive +async makeGrantsInactive(grantId: number): Promise { + let updatedGrant: Grant = {} as Grant; - try{ - const res = await this.dynamoDb.update(params).promise(); - console.log(res) + const params = { + TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || "TABLE_FAILURE", + Key: { grantId }, + UpdateExpression: "SET #status = :inactiveStatus", + ExpressionAttributeNames: { + "#status": "status", + }, + ExpressionAttributeValues: { + ":inactiveStatus": Status.Inactive as String, + }, + ReturnValues: "ALL_NEW", + }; + + try { + const res = await this.dynamoDb.update(params).promise(); + + if (res.Attributes?.status === Status.Inactive) { + console.log(`Grant ${grantId} successfully marked as inactive.`); + + const currentGrant = res.Attributes as Grant; + console.log(currentGrant); + updatedGrant = currentGrant + } else { + console.log(`Grant ${grantId} update failed or no change in status.`); + } + } catch (err) { + console.log(err); + throw new Error(`Failed to update Grant ${grantId} status.`); + } + + return updatedGrant; +} - if (res.Attributes && res.Attributes.isArchived === false) { - console.log(`Grant ${grantId} successfully un-archived.`); - successfulUpdates.push(grantId); - } else { - console.log(`Grant ${grantId} update failed or no change in status.`); - } - } - catch(err){ - console.log(err); - throw new Error(`Failed to update Grant ${grantId} status.`); - } - }; - return successfulUpdates; - } /** * Will push or overwrite new grant data to database @@ -175,7 +203,7 @@ export class GrantService { try { await this.dynamoDb.delete(params).promise(); this.logger.log(`Grant ${grantId} deleted successfully`); - return 'Grant ${grantId} deleted successfully'; + return `Grant ${grantId} deleted successfully`; } catch (error: any) { if (error.code === "ConditionalCheckFailedException") { throw new Error(`Grant ${grantId} does not exist`); @@ -183,7 +211,6 @@ export class GrantService { this.logger.error(`Failed to delete Grant ${grantId}`, error.stack); throw new Error(`Failed to delete Grant ${grantId}`); } - } /*