diff --git a/packages/server/.env.example b/packages/server/.env.example index 2e59ee13f..23dcc8cd8 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -10,6 +10,7 @@ API_DOMAIN=http://localhost:8080 ENABLE_GRANTS_SCRAPER=true SHARE_TERMINOLOGY_ENABLED=true +SHOW_FORECASTED_GRANTS=true GRANTS_SCRAPER_DATE_RANGE=7 GRANTS_SCRAPER_DELAY=1000 NODE_OPTIONS=--max_old_space_size=1024 diff --git a/packages/server/__tests__/db/db.test.js b/packages/server/__tests__/db/db.test.js index fedb7b15b..390d0a673 100644 --- a/packages/server/__tests__/db/db.test.js +++ b/packages/server/__tests__/db/db.test.js @@ -368,6 +368,16 @@ describe('db', () => { const result = await db.getSingleGrantDetails({ grantId, tenantId: fixtures.users.staffUser.tenant_id }); expect(result).to.be.null; }); + it('shows forecasted grants', async () => { + const grantId = '444819'; + const result = await db.getSingleGrantDetails({ grantId, tenantId: fixtures.users.staffUser.tenant_id, showForecastedGrants: true }); + expect(result.grant_id).to.eq('444819'); + }); + it('hides forecasted grants', async () => { + const grantId = '444819'; + const result = await db.getSingleGrantDetails({ grantId, tenantId: fixtures.users.staffUser.tenant_id, showForecastedGrants: false }); + expect(result).to.be.null; + }); }); context('getGrantsAssignedAgency', () => { @@ -386,6 +396,49 @@ describe('db', () => { }); }); + context('getGrant', () => { + it('gets forecasted grant', async () => { + const result = await db.getGrant({ grantId: fixtures.grants.forecastedGrant.grant_id, showForecastedGrants: true }); + expect(result.grant_id).to.equal(fixtures.grants.forecastedGrant.grant_id); + }); + + it('hides forecasted grant', async () => { + const result = await db.getGrant({ grantId: fixtures.grants.forecastedGrant.grant_id, showForecastedGrants: false }); + console.log('marissasss'); + console.log(result); + expect(result).to.be.null; + }); + }); + + context('getGrantsNew', () => { + it('gets forecasted grants', async () => { + const result = await db.getGrantsNew( + {}, + { currentPage: 1, perPage: 10, isLengthAware: true }, + { orderBy: 'open_date', orderDesc: 'true' }, + fixtures.tenants.SBA.id, + fixtures.agencies.accountancy.id, + false, + true, + ); + const forecastedGrant = result.data.filter((grant) => grant.opportunity_status === 'forecasted'); + expect(forecastedGrant.length).to.equal(1); + }); + it('hides forecasted grants', async () => { + const result = await db.getGrantsNew( + { bill: 'Infrastructure Investment and Jobs Act' }, + { currentPage: 1, perPage: 10, isLengthAware: true }, + { orderBy: 'open_date', orderDesc: 'true' }, + fixtures.tenants.SBA.id, + fixtures.agencies.accountancy.id, + false, + false, + ); + const forecastedGrant = result.data.filter((grant) => grant.opportunity_status === 'forecasted'); + expect(forecastedGrant.length).to.equal(0); + }); + }); + context('getGrants with various filters', () => { /* filters: { diff --git a/packages/server/__tests__/db/seeds/fixtures.js b/packages/server/__tests__/db/seeds/fixtures.js index ed2a65fee..2c755f9eb 100644 --- a/packages/server/__tests__/db/seeds/fixtures.js +++ b/packages/server/__tests__/db/seeds/fixtures.js @@ -122,6 +122,48 @@ const agencyEligibilityCodes = { }; const grants = { + forecastedGrant: { + status: 'inbox', + grant_id: '444819', + grant_number: '29-468', + agency_code: 'NSF', + award_ceiling: '6500', + cost_sharing: 'No', + title: 'Forecasted Grant', + cfda_list: '47.050', + open_date: '2100-06-21', + close_date: '2200-11-03', + notes: 'auto-inserted by script', + search_terms: '[in title/desc]+', + reviewer_name: 'none', + opportunity_category: 'Discretionary', + description: 'Grant that is forecasted', + eligibility_codes: '25', + opportunity_status: 'forecasted', + raw_body_json: { + opportunity: { + id: '444819', + number: '29-468', + title: 'Forecasted Grant', + description: 'Grant that is forecasted', + category: { name: 'Discretionary' }, + milestones: { post_date: '2100-06-21', close: { date: '2200-11-03' } }, + }, + agency: { code: 'NSF' }, + cost_sharing_or_matching_requirement: false, + cfda_numbers: ['47.050'], + eligible_applicants: [{ code: '25' }], + funding_activity: { categories: [{ code: 'ISS', name: 'Income Security and Social Services' }] }, + award: { ceiling: '6500' }, + bill: 'Infrastructure Investment and Jobs Act (IIJA)', + funding_instrument_types: [{ code: 'CA' }, { code: 'G' }, { code: 'PC' }], + }, + funding_instrument_codes: 'CA G PC', + bill: 'Infrastructure Investment and Jobs Act (IIJA)', + funding_activity_category_codes: 'ISS', + created_at: '2024-08-11 11:30:38.89828-07', + updated_at: '2024-08-11 12:30:39.531-07', + }, earFellowship: { status: 'inbox', grant_id: '335255', diff --git a/packages/server/src/db/index.js b/packages/server/src/db/index.js index b0cff8338..df1af67c8 100755 --- a/packages/server/src/db/index.js +++ b/packages/server/src/db/index.js @@ -713,7 +713,7 @@ function addCsvData(qb) { tenantId: number agencyId: number */ -async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, agencyId, toCsv) { +async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, agencyId, toCsv, showForecastedGrants = false) { const errors = validateSearchFilters(filters); if (errors.length > 0) { throw new Error(`Invalid filters: ${errors.join(', ')}`); @@ -751,6 +751,7 @@ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, CASE WHEN grants.archive_date <= now() THEN 'archived' WHEN grants.close_date <= now() THEN 'closed' + WHEN grants.open_date > now() THEN 'forecasted' ELSE 'posted' END as opportunity_status `)) @@ -758,6 +759,11 @@ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, NULLIF(grants.award_ceiling, 0) as award_ceiling `)) .modify((qb) => grantsQuery(qb, filters, agencyId, orderingParams, paginationParams)) + .modify((qb) => { + if (!showForecastedGrants) { + qb.whereNot({ opportunity_status: 'forecasted' }); + } + }) .select(knex.raw(` count(*) OVER() AS full_count `)) @@ -841,7 +847,7 @@ async function enhanceGrantData(tenantId, data) { } async function getGrants({ - currentPage, perPage, tenantId, filters, orderBy, searchTerm, orderDesc, + currentPage, perPage, tenantId, filters, orderBy, searchTerm, orderDesc, showForecastedGrants, } = {}) { const data = await knex(TABLES.grants) .modify((queryBuilder) => { @@ -852,6 +858,9 @@ async function getGrants({ .orWhere(`${TABLES.grants}.title`, '~*', searchTerm), ); } + if (!showForecastedGrants) { + queryBuilder.andWhereNot(`${TABLES.grants}.opportunity_status`, 'forecasted'); + } if (filters) { if (filters.interestedByUser || filters.positiveInterest || filters.result || filters.rejected || filters.interestedByAgency) { queryBuilder.join(TABLES.grants_interested, `${TABLES.grants}.grant_id`, `${TABLES.grants_interested}.grant_id`) @@ -1002,17 +1011,27 @@ async function getGrants({ return { data: dataWithAgency, pagination }; } -async function getGrant({ grantId }) { +async function getGrant({ grantId, showForecastedGrants }) { const results = await knex.table(TABLES.grants) .select('*') - .where({ grant_id: grantId }); - return results[0]; + .where({ grant_id: grantId }) + .modify((queryBuilder) => { + if (!showForecastedGrants) { + queryBuilder.whereNot({ opportunity_status: 'forecasted' }); + } + }); + return results.length ? results[0] : null; } -async function getSingleGrantDetails({ grantId, tenantId }) { +async function getSingleGrantDetails({ grantId, tenantId, showForecastedGrants }) { const results = await knex.table(TABLES.grants) .select('*') - .where({ grant_id: grantId }); + .where({ grant_id: grantId }) + .modify((queryBuilder) => { + if (!showForecastedGrants) { + queryBuilder.whereNot({ opportunity_status: 'forecasted' }); + } + }); const enhancedResults = await enhanceGrantData(tenantId, results); return enhancedResults.length ? enhancedResults[0] : null; } diff --git a/packages/server/src/lib/email.js b/packages/server/src/lib/email.js index 88331428c..6776c2d83 100644 --- a/packages/server/src/lib/email.js +++ b/packages/server/src/lib/email.js @@ -423,7 +423,7 @@ async function sendGrantAssignedEmails({ grantId, agencyIds, userId }) { i. Send email */ try { - const grant = await db.getGrant({ grantId }); + const grant = await db.getGrant({ grantId, showForecastedGrants: process.env.SHOW_FORECASTED_GRANTS === 'true' }); const grantDetail = await buildGrantDetail(grant, notificationType.grantAssignment); const agencies = await db.getAgenciesByIds(agencyIds); await asyncBatch( @@ -517,6 +517,7 @@ async function getAndSendGrantForSavedSearch({ {}, userSavedSearch.tenantId, false, + process.env.SHOW_FORECASTED_GRANTS === 'true', ); return sendGrantDigestEmail({ diff --git a/packages/server/src/routes/grants.js b/packages/server/src/routes/grants.js index d7577e00e..97456340e 100755 --- a/packages/server/src/routes/grants.js +++ b/packages/server/src/routes/grants.js @@ -42,6 +42,7 @@ router.get('/', requireUser, async (req, res) => { }, orderBy: req.query.orderBy, orderDesc: req.query.orderDesc, + showForecastedGrants: process.env.SHOW_FORECASTED_GRANTS === 'true', }); res.json(grants); }); @@ -92,6 +93,7 @@ router.get('/next', requireUser, async (req, res) => { user.tenant_id, user.agency_id, false, + process.env.SHOW_FORECASTED_GRANTS === 'true', ); return res.json(grants); @@ -101,7 +103,7 @@ router.get('/next', requireUser, async (req, res) => { router.get('/:grantId/grantDetails', requireUser, async (req, res) => { const { grantId } = req.params; const { user } = req.session; - const response = await db.getSingleGrantDetails({ grantId, tenantId: user.tenant_id }); + const response = await db.getSingleGrantDetails({ grantId, tenantId: user.tenant_id, showForecastedGrants: process.env.SHOW_FORECASTED_GRANTS === 'true' }); res.json(response); }); @@ -128,6 +130,7 @@ router.get('/exportCSVNew', requireUser, async (req, res) => { user.tenant_id, user.agency_id, true, + process.env.SHOW_FORECASTED_GRANTS === 'true', ); // Generate CSV @@ -214,6 +217,7 @@ router.get('/exportCSV', requireUser, async (req, res) => { opportunityStatuses: parseCollectionQueryParam(req, 'opportunityStatuses'), opportunityCategories: parseCollectionQueryParam(req, 'opportunityCategories'), }, + showForecastedGrants: process.env.SHOW_FORECASTED_GRANTS === 'true', }); // Generate CSV diff --git a/terraform/prod.tfvars b/terraform/prod.tfvars index c6255dc75..f7f8b0ecf 100644 --- a/terraform/prod.tfvars +++ b/terraform/prod.tfvars @@ -77,6 +77,7 @@ api_log_retention_in_days = 30 api_container_environment = { NEW_GRANT_DETAILS_PAGE_ENABLED = true SHARE_TERMINOLOGY_ENABLED = true + SHOW_FORECASTED_GRANTS = false } // Postgres diff --git a/terraform/staging.tfvars b/terraform/staging.tfvars index 69284a8de..592d61c22 100644 --- a/terraform/staging.tfvars +++ b/terraform/staging.tfvars @@ -74,8 +74,9 @@ api_enable_saved_search_grants_digest = true api_enable_grant_digest_scheduled_task = true api_log_retention_in_days = 14 api_container_environment = { - NEW_GRANT_DETAILS_PAGE_ENABLED = true, - SHARE_TERMINOLOGY_ENABLED = true, + NEW_GRANT_DETAILS_PAGE_ENABLED = true + SHARE_TERMINOLOGY_ENABLED = true + SHOW_FORECASTED_GRANTS = true } // Postgres