From 6b0bafeaa2952dc2922106404d76140af80dd205 Mon Sep 17 00:00:00 2001 From: "Yumiko (Yumi) Chow" <75456756+yumi520@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:52:03 -0400 Subject: [PATCH 1/3] mapped grants to revenue row --- .../__test__/cashflow-revenue.service.spec.ts | 61 ++++++++++++++++++- .../src/revenue/cashflow-revenue.service.ts | 56 ++++++++++++++++- 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/backend/src/revenue/__test__/cashflow-revenue.service.spec.ts b/backend/src/revenue/__test__/cashflow-revenue.service.spec.ts index e50b32b5..0692b4fc 100644 --- a/backend/src/revenue/__test__/cashflow-revenue.service.spec.ts +++ b/backend/src/revenue/__test__/cashflow-revenue.service.spec.ts @@ -3,6 +3,8 @@ import { BadRequestException, InternalServerErrorException } from '@nestjs/commo import { RevenueService } from '../cashflow-revenue.service'; import { RevenueType } from '../../../../middle-layer/types/RevenueType'; import { CashflowRevenue } from '../../../../middle-layer/types/CashflowRevenue'; +import { Grant } from '../../../../middle-layer/types/Grant'; +import { Status } from '../../../../middle-layer/types/Status'; import { describe, it, expect, beforeEach, afterEach, beforeAll, vi } from 'vitest'; // ─── Mock function declarations ─────────────────────────────────────────────── @@ -43,23 +45,63 @@ const mockDatabase: CashflowRevenue[] = [ { name: 'Revenue Three', amount: 3000, type: RevenueType.Sponsorship, installments: [{ amount: 3000, date: '2024-03-01' as any }] }, ]; +const activeGrant: Grant = { + grantId: 1, + organization: 'Active Grant Org', + does_bcan_qualify: true, + status: Status.Active, + amount: 4000, + grant_start_date: '2024-07-01' as any, + application_deadline: '2024-06-01' as any, + timeline: 1, + estimated_completion_time: 8, + bcan_poc: { POC_name: 'bcan', POC_email: 'bcan@gmail.com' } as any, + attachments: [] as any, + isRestricted: false, +} as Grant; + +const inactiveGrant: Grant = { + ...activeGrant, + grantId: 2, + organization: 'Inactive Grant Org', + status: Status.Inactive, + grant_start_date: '2024-08-01' as any, +} as Grant; + // ─── Test suite ─────────────────────────────────────────────────────────────── describe('RevenueService', () => { let service: RevenueService; + let revenueItems: CashflowRevenue[]; + let grantItems: Grant[]; beforeAll(() => { process.env.CASHFLOW_REVENUE_TABLE_NAME = 'test-revenue-table'; + process.env.DYNAMODB_GRANT_TABLE_NAME = 'test-grant-table'; }); // Guarantee env var is restored after every test, even if the test throws afterEach(() => { process.env.CASHFLOW_REVENUE_TABLE_NAME = 'test-revenue-table'; + process.env.DYNAMODB_GRANT_TABLE_NAME = 'test-grant-table'; }); beforeEach(async () => { vi.clearAllMocks(); - mockScan.mockReturnValue(resolved({})); + revenueItems = mockDatabase; + grantItems = []; + + mockScan.mockImplementation((params) => { + if (params.TableName === 'test-revenue-table') { + return resolved({ Items: revenueItems }); + } + + if (params.TableName === 'test-grant-table') { + return resolved({ Items: grantItems }); + } + + return resolved({}); + }); mockGet.mockReturnValue(resolved({})); mockPut.mockReturnValue(resolved({})); mockDelete.mockReturnValue(resolved({})); @@ -79,6 +121,23 @@ describe('RevenueService', () => { const result = await service.getAllRevenue(); expect(result).toHaveLength(3); expect(mockScan).toHaveBeenCalledWith({ TableName: 'test-revenue-table' }); + expect(mockScan).toHaveBeenCalledWith({ TableName: 'test-grant-table' }); + }); + + it('should include the active grant in the revenue list', async () => { + grantItems = [activeGrant, inactiveGrant]; + + const result = await service.getAllRevenue(); + + expect(result).toEqual(expect.arrayContaining([ + expect.objectContaining({ + name: 'Active Grant Org', + amount: 4000, + type: RevenueType.Grants, + installments: [{ amount: 4000, date: '2024-07-01' }], + }), + ])); + expect(result.some((item) => item.name === 'Inactive Grant Org')).toBe(false); }); it('should return an empty array when no items exist', async () => { diff --git a/backend/src/revenue/cashflow-revenue.service.ts b/backend/src/revenue/cashflow-revenue.service.ts index f9b5f0bc..bce1d46f 100644 --- a/backend/src/revenue/cashflow-revenue.service.ts +++ b/backend/src/revenue/cashflow-revenue.service.ts @@ -7,14 +7,17 @@ import { import * as AWS from "aws-sdk"; import { CashflowRevenue } from "../types/CashflowRevenue"; import { AWSError } from "aws-sdk"; +import { Grant } from "../../../middle-layer/types/Grant"; import { RevenueType } from "../../../middle-layer/types/RevenueType"; import { Installment } from "../../../middle-layer/types/Installment"; +import { Status } from "../../../middle-layer/types/Status"; @Injectable() export class RevenueService { private readonly logger = new Logger(RevenueService.name); private dynamoDb = new AWS.DynamoDB.DocumentClient(); - private revenueTableName : string = process.env.CASHFLOW_REVENUE_TABLE_NAME || "" + private revenueTableName : string = process.env.CASHFLOW_REVENUE_TABLE_NAME || ""; + private grantTableName : string = process.env.DYNAMODB_GRANT_TABLE_NAME || ""; /** * Helper method to check if an error is an AWS error and extract relevant information */ @@ -206,6 +209,53 @@ private validateTableName(tableName : string){ } } + +/** + * Retrieves all active grants and maps them into cashflow revenue items. + */ +private async getActiveGrantRevenues(): Promise { + this.validateTableName(this.grantTableName); + + const params = { TableName: this.grantTableName }; + + this.logger.debug(`Scanning Grant DynamoDB table: ${params.TableName}`); + const data = await this.dynamoDb.scan(params).promise(); + + if (!data || !data.Items) { + this.logger.error("There has been an error retrieving grants for revenue mapping"); + throw new InternalServerErrorException("Internal Server Error"); + } + + const grants = (data.Items as Grant[]) || []; + const activeGrantRevenues = grants + .filter((grant) => grant.status === Status.Active) + .map((grant) => this.mapGrantToRevenue(grant)); + + this.logger.log( + `Mapped ${activeGrantRevenues.length} active grants into cashflow revenue items`, + ); + + return activeGrantRevenues; +} + +/** + * Maps an active grant to a cashflow revenue item. + * Only the fields needed by the cashflow UI are preserved. + */ +private mapGrantToRevenue(grant: Grant): CashflowRevenue { + return { + amount: grant.amount, + type: RevenueType.Grants, + name: grant.organization.trim(), + installments: [ + { + amount: grant.amount, + date: grant.grant_start_date as unknown as Date, + }, + ], + }; +} + /** * Method to retrieve all of the revenue data * @returns All the revenue objects in the data base @@ -215,6 +265,7 @@ private validateTableName(tableName : string){ this.validateTableName(this.revenueTableName) + this.validateTableName(this.grantTableName) const params = { TableName: this.revenueTableName }; @@ -231,10 +282,11 @@ private validateTableName(tableName : string){ return [] as CashflowRevenue[]; } const revenue = (data.Items as CashflowRevenue[]) || []; + const grantRevenues = await this.getActiveGrantRevenues(); this.logger.log( `Retrived ${revenue.length} revenue items from the backend`, ); - return revenue; + return [...revenue, ...grantRevenues]; } catch (error) { if (error instanceof InternalServerErrorException) { throw error; From 93a0f9c08a314ea7e412f71b4715e9a51d0e1f76 Mon Sep 17 00:00:00 2001 From: "Yumiko (Yumi) Chow" <75456756+yumi520@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:06:00 -0400 Subject: [PATCH 2/3] moved logic to frontend --- .../src/revenue/cashflow-revenue.service.ts | 53 +------------------ .../cash-flow/processCashflowData.ts | 38 +++++++++++-- 2 files changed, 34 insertions(+), 57 deletions(-) diff --git a/backend/src/revenue/cashflow-revenue.service.ts b/backend/src/revenue/cashflow-revenue.service.ts index bce1d46f..c8fe2621 100644 --- a/backend/src/revenue/cashflow-revenue.service.ts +++ b/backend/src/revenue/cashflow-revenue.service.ts @@ -7,17 +7,14 @@ import { import * as AWS from "aws-sdk"; import { CashflowRevenue } from "../types/CashflowRevenue"; import { AWSError } from "aws-sdk"; -import { Grant } from "../../../middle-layer/types/Grant"; import { RevenueType } from "../../../middle-layer/types/RevenueType"; import { Installment } from "../../../middle-layer/types/Installment"; -import { Status } from "../../../middle-layer/types/Status"; @Injectable() export class RevenueService { private readonly logger = new Logger(RevenueService.name); private dynamoDb = new AWS.DynamoDB.DocumentClient(); private revenueTableName : string = process.env.CASHFLOW_REVENUE_TABLE_NAME || ""; - private grantTableName : string = process.env.DYNAMODB_GRANT_TABLE_NAME || ""; /** * Helper method to check if an error is an AWS error and extract relevant information */ @@ -210,52 +207,6 @@ private validateTableName(tableName : string){ } -/** - * Retrieves all active grants and maps them into cashflow revenue items. - */ -private async getActiveGrantRevenues(): Promise { - this.validateTableName(this.grantTableName); - - const params = { TableName: this.grantTableName }; - - this.logger.debug(`Scanning Grant DynamoDB table: ${params.TableName}`); - const data = await this.dynamoDb.scan(params).promise(); - - if (!data || !data.Items) { - this.logger.error("There has been an error retrieving grants for revenue mapping"); - throw new InternalServerErrorException("Internal Server Error"); - } - - const grants = (data.Items as Grant[]) || []; - const activeGrantRevenues = grants - .filter((grant) => grant.status === Status.Active) - .map((grant) => this.mapGrantToRevenue(grant)); - - this.logger.log( - `Mapped ${activeGrantRevenues.length} active grants into cashflow revenue items`, - ); - - return activeGrantRevenues; -} - -/** - * Maps an active grant to a cashflow revenue item. - * Only the fields needed by the cashflow UI are preserved. - */ -private mapGrantToRevenue(grant: Grant): CashflowRevenue { - return { - amount: grant.amount, - type: RevenueType.Grants, - name: grant.organization.trim(), - installments: [ - { - amount: grant.amount, - date: grant.grant_start_date as unknown as Date, - }, - ], - }; -} - /** * Method to retrieve all of the revenue data * @returns All the revenue objects in the data base @@ -265,7 +216,6 @@ private mapGrantToRevenue(grant: Grant): CashflowRevenue { this.validateTableName(this.revenueTableName) - this.validateTableName(this.grantTableName) const params = { TableName: this.revenueTableName }; @@ -282,11 +232,10 @@ private mapGrantToRevenue(grant: Grant): CashflowRevenue { return [] as CashflowRevenue[]; } const revenue = (data.Items as CashflowRevenue[]) || []; - const grantRevenues = await this.getActiveGrantRevenues(); this.logger.log( `Retrived ${revenue.length} revenue items from the backend`, ); - return [...revenue, ...grantRevenues]; + return revenue; } catch (error) { if (error instanceof InternalServerErrorException) { throw error; diff --git a/frontend/src/main-page/cash-flow/processCashflowData.ts b/frontend/src/main-page/cash-flow/processCashflowData.ts index 0fc2dd47..019da77e 100644 --- a/frontend/src/main-page/cash-flow/processCashflowData.ts +++ b/frontend/src/main-page/cash-flow/processCashflowData.ts @@ -4,6 +4,9 @@ import { fetchCashflowCosts, fetchCashflowRevenues, setCashflowSettings } from " import {CashflowRevenue} from "../../../../middle-layer/types/CashflowRevenue.ts"; import {CashflowCost} from "../../../../middle-layer/types/CashflowCost.ts"; import {CashflowSettings} from "../../../../middle-layer/types/CashflowSettings.ts"; +import { Grant } from "../../../../middle-layer/types/Grant.ts"; +import { RevenueType } from "../../../../middle-layer/types/RevenueType.ts"; +import { Status } from "../../../../middle-layer/types/Status.ts"; import { api } from "../../api.ts"; // This has not been tested yet but the basic structure when implemented should be the same @@ -26,12 +29,37 @@ export const fetchCosts = async () => { export const fetchRevenues = async () => { try { - const response = await api("/cashflow-revenue"); - if (!response.ok) { - throw new Error(`HTTP Error, Status: ${response.status}`); + const [revenueResponse, grantResponse] = await Promise.all([ + api("/cashflow-revenue"), + api("/grant"), + ]); + + if (!revenueResponse.ok) { + throw new Error(`HTTP Error, Status: ${revenueResponse.status}`); + } + + if (!grantResponse.ok) { + throw new Error(`HTTP Error, Status: ${grantResponse.status}`); } - const updatedRevenues: CashflowRevenue[] = await response.json(); - fetchCashflowRevenues(updatedRevenues); + + const updatedRevenues: CashflowRevenue[] = await revenueResponse.json(); + const grants: Grant[] = await grantResponse.json(); + + const mappedActiveGrantRevenues: CashflowRevenue[] = grants + .filter((grant) => grant.status === Status.Active) + .map((grant) => ({ + amount: grant.amount, + type: RevenueType.Grants, + name: grant.organization.trim(), + installments: [ + { + amount: grant.amount, + date: new Date(grant.grant_start_date), + }, + ], + })); + + fetchCashflowRevenues([...updatedRevenues, ...mappedActiveGrantRevenues]); } catch (error) { console.error("Error fetching revenues:", error); } From bb4fe01f4d75e56b695b3d2c70494032ff29ca22 Mon Sep 17 00:00:00 2001 From: "Yumiko (Yumi) Chow" <75456756+yumi520@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:19:22 -0400 Subject: [PATCH 3/3] fixed test to only scan revenue --- .../__test__/cashflow-revenue.service.spec.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/backend/src/revenue/__test__/cashflow-revenue.service.spec.ts b/backend/src/revenue/__test__/cashflow-revenue.service.spec.ts index 0692b4fc..177a6fc2 100644 --- a/backend/src/revenue/__test__/cashflow-revenue.service.spec.ts +++ b/backend/src/revenue/__test__/cashflow-revenue.service.spec.ts @@ -121,23 +121,15 @@ describe('RevenueService', () => { const result = await service.getAllRevenue(); expect(result).toHaveLength(3); expect(mockScan).toHaveBeenCalledWith({ TableName: 'test-revenue-table' }); - expect(mockScan).toHaveBeenCalledWith({ TableName: 'test-grant-table' }); }); - it('should include the active grant in the revenue list', async () => { + it('should only scan the revenue table', async () => { grantItems = [activeGrant, inactiveGrant]; - const result = await service.getAllRevenue(); + await service.getAllRevenue(); - expect(result).toEqual(expect.arrayContaining([ - expect.objectContaining({ - name: 'Active Grant Org', - amount: 4000, - type: RevenueType.Grants, - installments: [{ amount: 4000, date: '2024-07-01' }], - }), - ])); - expect(result.some((item) => item.name === 'Inactive Grant Org')).toBe(false); + expect(mockScan).toHaveBeenCalledTimes(1); + expect(mockScan).toHaveBeenCalledWith({ TableName: 'test-revenue-table' }); }); it('should return an empty array when no items exist', async () => {