From 000c977993ad8dd8cf9a5bb9e9e23e5c86d1e79a Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Thu, 24 Oct 2024 07:44:18 -0500 Subject: [PATCH 1/4] feat: allow graphql lambda to upload subrecipient files --- terraform/functions.tf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/terraform/functions.tf b/terraform/functions.tf index 94026980..57cce4d3 100644 --- a/terraform/functions.tf +++ b/terraform/functions.tf @@ -327,6 +327,17 @@ module "lambda_function-graphql" { "${module.reporting_data_bucket.bucket_arn}/treasuryreports/output-templates/*/*.xlsx", ] } + AllowUploadSubrecipientsFile = { + effect = "Allow" + actions = [ + "s3:PutObject", + ] + resources = [ + # These are temporary files shared across services containing subrecipient data. + # Path: treasuryreports/{organization_id}/{reporting_period_id}/subrecipients.json + "${module.reporting_data_bucket.bucket_arn}/treasuryreports/*/*/subrecipients.json", + ] + } AllowStepFunctionInvocation = { effect = "Allow" From 9fcaabcd99062ce914e98036c84d17a72b1fd1e3 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Thu, 24 Oct 2024 08:49:42 -0500 Subject: [PATCH 2/4] feat: add backend service to upload valid subrecipients for the reporting period --- .../subrecipients/subrecipients.scenarios.ts | 114 +++++++++++++++--- .../subrecipients/subrecipients.test.ts | 19 ++- .../services/subrecipients/subrecipients.ts | 60 +++++++++ 3 files changed, 172 insertions(+), 21 deletions(-) diff --git a/api/src/services/subrecipients/subrecipients.scenarios.ts b/api/src/services/subrecipients/subrecipients.scenarios.ts index 3d35355a..5d233e84 100644 --- a/api/src/services/subrecipients/subrecipients.scenarios.ts +++ b/api/src/services/subrecipients/subrecipients.scenarios.ts @@ -18,6 +18,50 @@ export const standard = defineScenario< | Prisma.SubrecipientCreateArgs | Prisma.SubrecipientUploadCreateArgs >({ + reportingPeriod: { + one: () => ({ + data: { + name: 'String', + startDate: '2024-01-26T15:11:27.688Z', + endDate: '2024-01-26T15:11:27.688Z', + inputTemplate: { + create: { + name: 'String', + version: 'String', + effectiveDate: '2024-01-26T15:11:27.688Z', + }, + }, + outputTemplate: { + create: { + name: 'String', + version: 'String', + effectiveDate: '2024-01-26T15:11:27.688Z', + }, + }, + }, + }), + q3: () => ({ + data: { + name: 'Q3 2024 [July 1 - September 30]', + startDate: '2024-07-01T00:00:00.000Z', + endDate: '2024-09-30T00:00:00.000Z', + inputTemplate: { + create: { + name: 'String', + version: 'String', + effectiveDate: '2024-01-26T15:11:27.688Z', + }, + }, + outputTemplate: { + create: { + name: 'String', + version: 'String', + effectiveDate: '2024-01-26T15:11:27.688Z', + }, + }, + }, + }), + }, organization: { one: { data: { @@ -25,6 +69,14 @@ export const standard = defineScenario< preferences: {}, }, }, + two: (scenario) => ({ + data: { + name: 'Q3 Testing Org', + preferences: { + current_reporting_period_id: scenario.reportingPeriod.q3.id, + }, + }, + }), }, agency: { one: (scenario) => ({ @@ -37,6 +89,16 @@ export const standard = defineScenario< organization: true, }, }), + two: (scenario) => ({ + data: { + name: 'Q3Agency', + organizationId: scenario.organization.two.id, + code: 'AQ3', + }, + include: { + organization: true, + }, + }), }, user: { one: (scenario) => ({ @@ -50,27 +112,15 @@ export const standard = defineScenario< agency: true, }, }), - }, - reportingPeriod: { - one: () => ({ + two: (scenario) => ({ data: { - name: 'String', - startDate: '2024-01-26T15:11:27.688Z', - endDate: '2024-01-26T15:11:27.688Z', - inputTemplate: { - create: { - name: 'String', - version: 'String', - effectiveDate: '2024-01-26T15:11:27.688Z', - }, - }, - outputTemplate: { - create: { - name: 'String', - version: 'String', - effectiveDate: '2024-01-26T15:11:27.688Z', - }, - }, + email: 'q3@test.com', + name: 'Q3 User', + role: 'USDR_ADMIN', + agencyId: scenario.agency.two.id, + }, + include: { + agency: true, }, }), }, @@ -89,6 +139,30 @@ export const standard = defineScenario< ueiTinCombo: '12485_920485', }, }), + q3_createdOctober: (scenario) => ({ + data: { + name: 'October Subrecipient', + organization: { connect: { id: scenario.organization.two.id } }, + ueiTinCombo: '17290_172900', + createdAt: '2024-10-15T00:00:00.000Z', + }, + }), + q3_createdNovember: (scenario) => ({ + data: { + name: 'November Subrecipient', + organization: { connect: { id: scenario.organization.two.id } }, + ueiTinCombo: '17291_172911', + createdAt: '2024-11-26T00:00:00.000Z', + }, + }), + q3_createdSeptember: (scenario) => ({ + data: { + name: 'September Subrecipient', + organization: { connect: { id: scenario.organization.two.id } }, + ueiTinCombo: '17292_172922', + createdAt: '2024-09-26T00:00:00.000Z', + }, + }), }, subrecipientUpload: { one: (scenario) => ({ diff --git a/api/src/services/subrecipients/subrecipients.test.ts b/api/src/services/subrecipients/subrecipients.test.ts index 96ccc4f4..f2afe07c 100644 --- a/api/src/services/subrecipients/subrecipients.test.ts +++ b/api/src/services/subrecipients/subrecipients.test.ts @@ -10,6 +10,7 @@ import { updateSubrecipient, deleteSubrecipient, Subrecipient as SubrecipientResolver, + uploadSubrecipients, } from './subrecipients' import type { StandardScenario } from './subrecipients.scenarios' @@ -24,7 +25,7 @@ describe('subrecipients', () => { mockCurrentUser(scenario.user.one) const result = await subrecipients() - expect(result.length).toEqual(Object.keys(scenario.subrecipient).length) + expect(result.length).toEqual(2) }) scenario( @@ -122,4 +123,20 @@ describe('subrecipients', () => { ) } ) + + scenario( + 'uploads all valid newly created subrecipients', + async (scenario: StandardScenario) => { + // uploadSubrecipients + const result = await uploadSubrecipients({ + input: { + organizationId: scenario.organization.two.id, + reportingPeriodId: scenario.reportingPeriod.q3.id, + }, + }) + expect(result.message).toEqual('Subrecipients uploaded successfully') + expect(result.success).toBe(true) + expect(result.countSubrecipients).toBe(1) + } + ) }) diff --git a/api/src/services/subrecipients/subrecipients.ts b/api/src/services/subrecipients/subrecipients.ts index e8ef33e5..10c03501 100644 --- a/api/src/services/subrecipients/subrecipients.ts +++ b/api/src/services/subrecipients/subrecipients.ts @@ -4,7 +4,9 @@ import type { SubrecipientRelationResolvers, } from 'types/graphql' +import { sendPutObjectToS3Bucket } from 'src/lib/aws' import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' export const subrecipients: QueryResolvers['subrecipients'] = () => { const currentUser = context.currentUser @@ -48,6 +50,64 @@ export const deleteSubrecipient: MutationResolvers['deleteSubrecipient'] = ({ }) } +export const uploadSubrecipients: MutationResolvers['uploadSubrecipients'] = + async ({ input }) => { + const { organizationId, reportingPeriodId } = input + const organization = await db.organization.findUnique({ + where: { id: organizationId }, + }) + const reportingPeriod = await db.reportingPeriod.findFirst({ + where: { id: reportingPeriodId }, + }) + if (!organization || !reportingPeriod) { + throw new Error('Organization or reporting period not found') + } + if ( + organization.preferences?.current_reporting_period_id !== + reportingPeriod.id + ) { + throw new Error( + 'Reporting period does not match current reporting period' + ) + } + + try { + const subrecipientKey = `treasuryreports/${organizationId}/${reportingPeriodId}/subrecipients.json` + + const startDate = new Date( + reportingPeriod.endDate.getFullYear(), + reportingPeriod.endDate.getMonth() + 1, + 1 + ) + const endDate = new Date( + reportingPeriod.endDate.getFullYear(), + reportingPeriod.endDate.getMonth() + 2, + 0 + ) + + const subrecipientsWithUploads = await db.subrecipient.findMany({ + where: { createdAt: { lte: endDate, gte: startDate }, organizationId }, + include: { subrecipientUploads: true }, + }) + const subrecipients = { + subrecipients: subrecipientsWithUploads, + } + await sendPutObjectToS3Bucket( + `${process.env.REPORTING_DATA_BUCKET_NAME}`, + subrecipientKey, + JSON.stringify(subrecipients) + ) + return { + message: 'Subrecipients uploaded successfully', + success: true, + countSubrecipients: subrecipientsWithUploads.length, + } + } catch (err) { + logger.error(`Error saving subrecipients JSON file to S3: ${err}`) + throw new Error('Error saving subrecipient info to S3') + } + } + export const Subrecipient: SubrecipientRelationResolvers = { organization: (_obj, { root }) => { return db.subrecipient From 09fa8a7817e8480a625d50d8ba5361a12ce30609 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Thu, 24 Oct 2024 08:52:21 -0500 Subject: [PATCH 3/4] fix: ensure subrecipients are chosen based on closing-month. ensure subrecipients are scoped to organization --- .../processValidationJson.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/api/src/functions/processValidationJson/processValidationJson.ts b/api/src/functions/processValidationJson/processValidationJson.ts index 4709bf1b..7f1adcee 100644 --- a/api/src/functions/processValidationJson/processValidationJson.ts +++ b/api/src/functions/processValidationJson/processValidationJson.ts @@ -224,9 +224,21 @@ export const processRecord = async ( try { const subrecipientKey = `treasuryreports/${organizationId}/${reportingPeriod.id}/subrecipients.json` - const { startDate, endDate } = reportingPeriod + const startDate = new Date( + reportingPeriod.endDate.getFullYear(), + reportingPeriod.endDate.getMonth() + 1, + 1 + ) + const endDate = new Date( + reportingPeriod.endDate.getFullYear(), + reportingPeriod.endDate.getMonth() + 2, + 0 + ) const subrecipientsWithUploads = await db.subrecipient.findMany({ - where: { createdAt: { lte: endDate, gte: startDate } }, + where: { + createdAt: { lte: endDate, gte: startDate }, + organizationId, + }, include: { subrecipientUploads: true }, }) const subrecipients = { From 77b815106a34d928b3b289a8c08d3f9ef691c158 Mon Sep 17 00:00:00 2001 From: Aditya Sridhar Date: Thu, 24 Oct 2024 09:09:14 -0500 Subject: [PATCH 4/4] fix: mock aws --- api/src/services/subrecipients/subrecipients.scenarios.ts | 8 ++++---- api/src/services/subrecipients/subrecipients.test.ts | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/api/src/services/subrecipients/subrecipients.scenarios.ts b/api/src/services/subrecipients/subrecipients.scenarios.ts index 5d233e84..0dc79b98 100644 --- a/api/src/services/subrecipients/subrecipients.scenarios.ts +++ b/api/src/services/subrecipients/subrecipients.scenarios.ts @@ -47,15 +47,15 @@ export const standard = defineScenario< endDate: '2024-09-30T00:00:00.000Z', inputTemplate: { create: { - name: 'String', - version: 'String', + name: 'Q3 - input', + version: '2024_q3', effectiveDate: '2024-01-26T15:11:27.688Z', }, }, outputTemplate: { create: { - name: 'String', - version: 'String', + name: 'Q3 - output', + version: '2024_q3', effectiveDate: '2024-01-26T15:11:27.688Z', }, }, diff --git a/api/src/services/subrecipients/subrecipients.test.ts b/api/src/services/subrecipients/subrecipients.test.ts index f2afe07c..f619c3ff 100644 --- a/api/src/services/subrecipients/subrecipients.test.ts +++ b/api/src/services/subrecipients/subrecipients.test.ts @@ -3,6 +3,8 @@ import type { GraphQLResolveInfo } from 'graphql' import type { RedwoodGraphQLContext } from '@redwoodjs/graphql-server' +import { sendPutObjectToS3Bucket } from 'src/lib/aws' + import { subrecipients, subrecipient, @@ -19,7 +21,10 @@ import type { StandardScenario } from './subrecipients.scenarios' // Please refer to the RedwoodJS Testing Docs: // https://redwoodjs.com/docs/testing#testing-services // https://redwoodjs.com/docs/testing#jest-expect-type-considerations - +jest.mock('src/lib/aws', () => ({ + ...jest.requireActual('src/lib/aws'), + sendPutObjectToS3Bucket: jest.fn(), +})) describe('subrecipients', () => { scenario('returns all subrecipients', async (scenario: StandardScenario) => { mockCurrentUser(scenario.user.one) @@ -134,6 +139,7 @@ describe('subrecipients', () => { reportingPeriodId: scenario.reportingPeriod.q3.id, }, }) + expect(sendPutObjectToS3Bucket).toHaveBeenCalled() expect(result.message).toEqual('Subrecipients uploaded successfully') expect(result.success).toBe(true) expect(result.countSubrecipients).toBe(1)