diff --git a/api/src/models/project-create.ts b/api/src/models/project-create.ts index 0c8aea4e7f..266535426a 100644 --- a/api/src/models/project-create.ts +++ b/api/src/models/project-create.ts @@ -238,13 +238,13 @@ export class PostFundingSource { * @class PostFundingData */ export class PostFundingData { - funding_agencies: PostFundingSource[]; + funding_sources: PostFundingSource[]; constructor(obj?: any) { defaultLog.debug({ label: 'PostFundingData', message: 'params', obj }); - this.funding_agencies = - (obj?.funding_agencies.length && obj.funding_agencies.map((item: any) => new PostFundingSource(item))) || []; + this.funding_sources = + (obj?.funding_sources.length && obj.funding_sources.map((item: any) => new PostFundingSource(item))) || []; } } diff --git a/api/src/models/project-update.ts b/api/src/models/project-update.ts index 64f0fb9d08..5192901507 100644 --- a/api/src/models/project-update.ts +++ b/api/src/models/project-update.ts @@ -273,3 +273,27 @@ export class GetProjectData { this.revision_count = projectData?.revision_count ?? null; } } + +export class PutFundingSource { + id: number; + investment_action_category: number; + agency_project_id: string; + funding_amount: number; + start_date: string; + end_date: string; + revision_count: number; + + constructor(obj?: any) { + defaultLog.debug({ label: 'PutFundingSource', message: 'params', obj }); + + const fundingSource = obj?.fundingSources?.length && obj.fundingSources[0]; + + this.id = fundingSource?.id || null; + this.investment_action_category = fundingSource?.investment_action_category || null; + this.agency_project_id = fundingSource?.agency_project_id || null; + this.funding_amount = fundingSource?.funding_amount || null; + this.start_date = fundingSource?.start_date || null; + this.end_date = fundingSource?.end_date || null; + this.revision_count = fundingSource?.revision_count || null; + } +} diff --git a/api/src/models/project-view-update.test.ts b/api/src/models/project-view-update.test.ts index 39fc153f6b..5298e0f718 100644 --- a/api/src/models/project-view-update.test.ts +++ b/api/src/models/project-view-update.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { GetSpeciesData } from './project-view-update'; +import { GetSpeciesData, GetFundingData } from './project-view-update'; describe('GetSpeciesData', () => { describe('No values provided', () => { @@ -75,4 +75,45 @@ describe('GetSpeciesData', () => { expect(data.ancillary_species).to.eql(['species 3']); }); }); + + describe('GetFundingData', () => { + describe('No values provided', () => { + let fundingData: GetFundingData; + + before(() => { + fundingData = new GetFundingData([]); + }); + + it('sets project funding sources', function () { + expect(fundingData.fundingSources).to.eql([]); + }); + }); + + describe('All values provided', () => { + let fundingData: GetFundingData; + + const fundingDataObj = [ + { + id: 1, + agency_id: '123', + agency_name: 'Agency name', + agency_project_id: 'Agency123', + investment_action_category: 'Investment', + investment_action_category_name: 'Investment name', + start_date: '01/01/2020', + end_date: '01/01/2021', + funding_amount: 123, + revision_count: 0 + } + ]; + + before(() => { + fundingData = new GetFundingData(fundingDataObj); + }); + + it('sets project funding sources', function () { + expect(fundingData.fundingSources).to.eql(fundingDataObj); + }); + }); + }); }); diff --git a/api/src/models/project-view-update.ts b/api/src/models/project-view-update.ts index 0aff35cd3c..acb0a13b40 100644 --- a/api/src/models/project-view-update.ts +++ b/api/src/models/project-view-update.ts @@ -19,3 +19,46 @@ export class GetSpeciesData { this.ancillary_species = (ancillary_species?.length && ancillary_species.map((item: any) => item.name)) || []; } } + +interface IGetFundingSource { + id: number; + agency_id: number; + investment_action_category: number; + investment_action_category_name: string; + agency_name: string; + funding_amount: number; + start_date: string; + end_date: string; + agency_project_id: string; + revision_count: number; +} + +export class GetFundingData { + fundingSources: IGetFundingSource[]; + + constructor(fundingData?: any[]) { + defaultLog.debug({ + label: 'GetFundingData', + message: 'params', + fundingData: fundingData + }); + + this.fundingSources = + (fundingData && + fundingData.map((item: any) => { + return { + id: item.id, + agency_id: item.agency_id, + investment_action_category: item.investment_action_category, + investment_action_category_name: item.investment_action_category_name, + agency_name: item.agency_name, + funding_amount: item.funding_amount, + start_date: item.start_date, + end_date: item.end_date, + agency_project_id: item.agency_project_id, + revision_count: item.revision_count + }; + })) || + []; + } +} diff --git a/api/src/models/project-view.test.ts b/api/src/models/project-view.test.ts index 05abfdc3ea..12d0984010 100644 --- a/api/src/models/project-view.test.ts +++ b/api/src/models/project-view.test.ts @@ -2,7 +2,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { GetCoordinatorData, - GetFundingData, GetIUCNClassificationData, GetLocationData, GetObjectivesData, @@ -201,43 +200,6 @@ describe('GetIUCNClassificationData', () => { }); }); -describe('GetFundingData', () => { - describe('No values provided', () => { - let fundingData: GetFundingData; - - before(() => { - fundingData = new GetFundingData([]); - }); - - it('sets funding agencies', function () { - expect(fundingData.fundingAgencies).to.eql([]); - }); - }); - - describe('All values provided', () => { - let fundingData: GetFundingData; - - const fundingDataObj = [ - { - agency_id: '123', - agency_name: 'Agency name', - investment_action_category: 'investment', - start_date: '01/01/2020', - end_date: '01/01/2021', - funding_amount: 123 - } - ]; - - before(() => { - fundingData = new GetFundingData(fundingDataObj); - }); - - it('sets funding agencies', function () { - expect(fundingData.fundingAgencies).to.eql(fundingDataObj); - }); - }); -}); - describe('GetObjectivesData', () => { describe('No values provided', () => { let projectObjectivesData: GetObjectivesData; diff --git a/api/src/models/project-view.ts b/api/src/models/project-view.ts index 6f3c0d4082..3c7f45cad2 100644 --- a/api/src/models/project-view.ts +++ b/api/src/models/project-view.ts @@ -140,47 +140,6 @@ export class GetIUCNClassificationData { } } -interface IGetFundingSource { - agency_id: string; - investment_action_category: string; - agency_name: string; - funding_amount: number; - start_date: string; - end_date: string; -} - -/** - * Pre-processes GET /projects/{id} funding data - * - * @export - * @class GetFundingData - */ -export class GetFundingData { - fundingAgencies: IGetFundingSource[]; - - constructor(fundingData?: any[]) { - defaultLog.debug({ - label: 'GetFundingData', - message: 'params', - fundingData: fundingData - }); - - this.fundingAgencies = - (fundingData && - fundingData.map((item: any) => { - return { - agency_id: item.agency_id, - investment_action_category: item.investment_action_category, - agency_name: item.agency_name, - funding_amount: item.funding_amount, - start_date: item.start_date, - end_date: item.end_date - }; - })) || - []; - } -} - /** * Pre-processes GET /projects/{id} partnerships data * diff --git a/api/src/openapi/schemas/project.ts b/api/src/openapi/schemas/project.ts index 91f15531e2..c42e57a41b 100644 --- a/api/src/openapi/schemas/project.ts +++ b/api/src/openapi/schemas/project.ts @@ -149,7 +149,7 @@ export const projectCreatePostRequestObject = { title: 'Project funding sources', type: 'object', properties: { - funding_agencies: { + funding_sources: { type: 'array', items: { title: 'Project funding agency', diff --git a/api/src/paths/project.ts b/api/src/paths/project.ts index 4c0fb68676..83f9d9a239 100644 --- a/api/src/paths/project.ts +++ b/api/src/paths/project.ts @@ -144,10 +144,10 @@ function createProject(): RequestHandler { ) ); - // Handle funding agencies + // Handle funding sources promises.push( Promise.all( - sanitizedProjectPostData.funding.funding_agencies.map((fundingSource: PostFundingSource) => + sanitizedProjectPostData.funding.funding_sources.map((fundingSource: PostFundingSource) => insertFundingSource(fundingSource, projectId, connection) ) ) diff --git a/api/src/paths/project/{projectId}/update.ts b/api/src/paths/project/{projectId}/update.ts index db897e1a9a..56f861bcb5 100644 --- a/api/src/paths/project/{projectId}/update.ts +++ b/api/src/paths/project/{projectId}/update.ts @@ -17,9 +17,10 @@ import { PutIUCNData, PutSpeciesData, IGetPutIUCN, - GetLocationData + GetLocationData, + PutFundingSource } from '../../../models/project-update'; -import { GetSpeciesData } from '../../../models/project-view-update'; +import { GetSpeciesData, GetFundingData } from '../../../models/project-view-update'; import { projectIdResponseObject, projectUpdateGetResponseObject, @@ -31,6 +32,7 @@ import { getIUCNActionClassificationByProjectSQL, getObjectivesByProjectSQL, getProjectByProjectSQL, + putProjectFundingSourceSQL, putProjectSQL } from '../../../queries/project/project-update-queries'; import { @@ -41,7 +43,8 @@ import { deleteAncillarySpeciesSQL, deleteIndigenousPartnershipsSQL, deleteStakeholderPartnershipsSQL, - deleteRegionsSQL + deleteRegionsSQL, + deleteFundingSQL } from '../../../queries/project/project-delete-queries'; import { getStakeholderPartnershipsByProjectSQL, @@ -149,7 +152,7 @@ export interface IGetProjectForUpdate { location: any; species: any; iucn: GetIUCNClassificationData | null; - funding: any; + funding: GetFundingData | null; partnerships: GetPartnershipsData | null; } @@ -242,6 +245,13 @@ function getProjectForUpdate(): RequestHandler { }) ); } + if (entities.includes(GET_ENTITIES.funding)) { + promises.push( + getProjectData(projectId, connection).then((value) => { + results.project = value; + }) + ); + } await Promise.all(promises); @@ -529,6 +539,10 @@ function updateProject(): RequestHandler { promises.push(updateProjectSpeciesData(projectId, entities, connection)); } + if (entities?.funding) { + promises.push(updateProjectFundingData(projectId, entities, connection)); + } + await Promise.all(promises); await connection.commit(); @@ -776,3 +790,35 @@ export const updateProjectData = async ( await Promise.all([...insertActivityPromises, ...insertClimateInitiativesPromises]); }; + +export const updateProjectFundingData = async ( + projectId: number, + entities: IUpdateProject, + connection: IDBConnection +): Promise => { + const putFundingSource = entities?.funding && new PutFundingSource(entities.funding); + + const sqlDeleteStatement = deleteFundingSQL(putFundingSource?.id); + + if (!sqlDeleteStatement) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + const deleteResult = await connection.query(sqlDeleteStatement.text, sqlDeleteStatement.values); + + if (!deleteResult) { + throw new HTTP409('Failed to delete project funding source'); + } + + const sqlInsertStatement = putProjectFundingSourceSQL(putFundingSource, projectId); + + if (!sqlInsertStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const insertResult = await connection.query(sqlInsertStatement.text, sqlInsertStatement.values); + + if (!insertResult) { + throw new HTTP409('Failed to put (insert) project funding source with incremented revision count'); + } +}; diff --git a/api/src/paths/project/{projectId}/view.ts b/api/src/paths/project/{projectId}/view.ts index 196cdb082e..23a56f5037 100644 --- a/api/src/paths/project/{projectId}/view.ts +++ b/api/src/paths/project/{projectId}/view.ts @@ -5,17 +5,15 @@ import { getDBConnection } from '../../../database/db'; import { HTTP400 } from '../../../errors/CustomError'; import { GetCoordinatorData, - GetFundingData, GetIUCNClassificationData, GetObjectivesData, GetPartnershipsData, GetProjectData, GetLocationData } from '../../../models/project-view'; -import { GetSpeciesData } from '../../../models/project-view-update'; +import { GetSpeciesData, GetFundingData } from '../../../models/project-view-update'; import { projectViewGetResponseObject } from '../../../openapi/schemas/project'; import { - getFundingSourceByProjectSQL, getIndigenousPartnershipsByProjectSQL, getIUCNActionClassificationByProjectSQL, getProjectSQL @@ -26,7 +24,8 @@ import { getAncillarySpeciesByProjectSQL, getLocationByProjectSQL, getClimateInitiativesByProjectSQL, - getActivitiesByProjectSQL + getActivitiesByProjectSQL, + getFundingSourceByProjectSQL } from '../../../queries/project/project-view-update-queries'; import { getLogger } from '../../../utils/logger'; import { logRequest } from '../../../utils/path-utils'; diff --git a/api/src/queries/project/project-create-queries.test.ts b/api/src/queries/project/project-create-queries.test.ts index 0b89f99136..b6d59936a0 100644 --- a/api/src/queries/project/project-create-queries.test.ts +++ b/api/src/queries/project/project-create-queries.test.ts @@ -225,6 +225,8 @@ describe('postProjectFundingSourceSQL', () => { 333 ); + console.log(response?.values); + expect(response).to.not.be.null; expect(response?.values).to.deep.include(333); expect(response?.values).to.deep.include(222); diff --git a/api/src/queries/project/project-create-queries.ts b/api/src/queries/project/project-create-queries.ts index b2870797ab..ffa15fa7aa 100644 --- a/api/src/queries/project/project-create-queries.ts +++ b/api/src/queries/project/project-create-queries.ts @@ -216,7 +216,6 @@ export const postProjectFundingSourceSQL = ( return null; } - // TODO model is missing agency name const sqlStatement: SQLStatement = SQL` INSERT INTO project_funding_source ( p_id, diff --git a/api/src/queries/project/project-delete-queries.test.ts b/api/src/queries/project/project-delete-queries.test.ts index bf465418b6..0778ac7d4c 100644 --- a/api/src/queries/project/project-delete-queries.test.ts +++ b/api/src/queries/project/project-delete-queries.test.ts @@ -8,7 +8,8 @@ import { deleteIndigenousPartnershipsSQL, deleteIUCNSQL, deleteRegionsSQL, - deleteStakeholderPartnershipsSQL + deleteStakeholderPartnershipsSQL, + deleteFundingSQL } from './project-delete-queries'; describe('deleteIUCNSQL', () => { @@ -122,3 +123,17 @@ describe('deleteClimateInitiativesSQL', () => { expect(response).to.not.be.null; }); }); + +describe('deleteFundingSQL', () => { + it('returns null response when null pfsId (project funding source) provided', () => { + const response = deleteFundingSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectId provided', () => { + const response = deleteFundingSQL(1); + + expect(response).to.not.be.null; + }); +}); diff --git a/api/src/queries/project/project-delete-queries.ts b/api/src/queries/project/project-delete-queries.ts index 1f5a594375..6457db9fbd 100644 --- a/api/src/queries/project/project-delete-queries.ts +++ b/api/src/queries/project/project-delete-queries.ts @@ -274,3 +274,37 @@ export const deleteClimateInitiativesSQL = (projectId: number): SQLStatement | n return sqlStatement; }; + +/** + * SQL query to delete the specific project funding source record. + * + * @param {pfsId} pfsId + * @returns {SQLStatement} sql query object + */ +export const deleteFundingSQL = (pfsId: number | undefined): SQLStatement | null => { + defaultLog.debug({ + label: 'deleteFundingSQL', + message: 'params', + pfsId + }); + + if (!pfsId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + DELETE + from project_funding_source + WHERE + id = ${pfsId}; + `; + + defaultLog.debug({ + label: 'deleteFundingSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; diff --git a/api/src/queries/project/project-update-queries.test.ts b/api/src/queries/project/project-update-queries.test.ts index 709f7ad7c5..d3a10e9393 100644 --- a/api/src/queries/project/project-update-queries.test.ts +++ b/api/src/queries/project/project-update-queries.test.ts @@ -1,13 +1,20 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { PutCoordinatorData, PutLocationData, PutObjectivesData, PutProjectData } from '../../models/project-update'; +import { + PutCoordinatorData, + PutLocationData, + PutObjectivesData, + PutProjectData, + PutFundingSource +} from '../../models/project-update'; import { getIndigenousPartnershipsByProjectSQL, getCoordinatorByProjectSQL, getIUCNActionClassificationByProjectSQL, getObjectivesByProjectSQL, getProjectByProjectSQL, - putProjectSQL + putProjectSQL, + putProjectFundingSourceSQL } from './project-update-queries'; describe('getIndigenousPartnershipsByProjectSQL', () => { @@ -207,3 +214,47 @@ describe('getObjectivesByProjectSQL', () => { expect(response).to.not.be.null; }); }); + +describe('putProjectFundingSourceSQL', () => { + describe('with invalid parameters', () => { + it('returns null when funding source is null', () => { + const response = putProjectFundingSourceSQL((null as unknown) as PutFundingSource, 1); + + expect(response).to.be.null; + }); + + it('returns null when project id is null', () => { + const response = putProjectFundingSourceSQL(new PutFundingSource({}), (null as unknown) as number); + + expect(response).to.be.null; + }); + }); + + describe('with valid parameters', () => { + it('returns a SQLStatement when all fields are passed in as expected', () => { + const response = putProjectFundingSourceSQL( + new PutFundingSource({ + fundingSources: [ + { + investment_action_category: 222, + agency_project_id: 'funding source name', + funding_amount: 10000, + start_date: '2020-02-02', + end_date: '2020-03-02', + revision_count: 11 + } + ] + }), + 1 + ); + + expect(response).to.not.be.null; + expect(response?.values).to.deep.include(222); + expect(response?.values).to.deep.include('funding source name'); + expect(response?.values).to.deep.include(10000); + expect(response?.values).to.deep.include('2020-02-02'); + expect(response?.values).to.deep.include('2020-03-02'); + expect(response?.values).to.deep.include(12); + }); + }); +}); diff --git a/api/src/queries/project/project-update-queries.ts b/api/src/queries/project/project-update-queries.ts index 6ab867d967..d4f0709d75 100644 --- a/api/src/queries/project/project-update-queries.ts +++ b/api/src/queries/project/project-update-queries.ts @@ -1,5 +1,11 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { PutCoordinatorData, PutLocationData, PutObjectivesData, PutProjectData } from '../../models/project-update'; +import { + PutCoordinatorData, + PutLocationData, + PutObjectivesData, + PutProjectData, + PutFundingSource +} from '../../models/project-update'; import { getLogger } from '../../utils/logger'; import { generateGeometryCollectionSQL } from '../generate-geometry-collection'; @@ -305,3 +311,51 @@ export const getObjectivesByProjectSQL = (projectId: number): SQLStatement | nul return sqlStatement; }; + +/** + * SQL query to put (insert) a project funding source row with incremented revision count. + * + * @param {PutFundingSource} fundingSource + * @returns {SQLStatement} sql query object + */ +export const putProjectFundingSourceSQL = ( + fundingSource: PutFundingSource | null, + projectId: number +): SQLStatement | null => { + defaultLog.debug({ label: 'putProjectFundingSourceSQL', message: 'params', fundingSource, projectId }); + + if (!fundingSource || !projectId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + INSERT INTO project_funding_source ( + p_id, + iac_id, + funding_source_project_id, + funding_amount, + funding_start_date, + funding_end_date, + revision_count + ) VALUES ( + ${projectId}, + ${fundingSource.investment_action_category}, + ${fundingSource.agency_project_id}, + ${fundingSource.funding_amount}, + ${fundingSource.start_date}, + ${fundingSource.end_date}, + ${fundingSource.revision_count + 1} + ) + RETURNING + id; + `; + + defaultLog.debug({ + label: 'putProjectFundingSourceSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; diff --git a/api/src/queries/project/project-view-queries.test.ts b/api/src/queries/project/project-view-queries.test.ts index 066a2af8da..fe830eed58 100644 --- a/api/src/queries/project/project-view-queries.test.ts +++ b/api/src/queries/project/project-view-queries.test.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { - getFundingSourceByProjectSQL, getIndigenousPartnershipsByProjectSQL, getIUCNActionClassificationByProjectSQL, getProjectListSQL, @@ -49,20 +48,6 @@ describe('getIUCNActionClassificationByProjectSQL', () => { }); }); -describe('getFundingSourceByProjectSQL', () => { - it('returns null response when null projectId provided', () => { - const response = getFundingSourceByProjectSQL((null as unknown) as number); - - expect(response).to.be.null; - }); - - it('returns non null response when valid projectId provided', () => { - const response = getFundingSourceByProjectSQL(1); - - expect(response).to.not.be.null; - }); -}); - describe('getIndigenousPartnershipsByProjectSQL', () => { it('Null projectId', () => { const response = getIndigenousPartnershipsByProjectSQL((null as unknown) as number); diff --git a/api/src/queries/project/project-view-queries.ts b/api/src/queries/project/project-view-queries.ts index 6c37ab3858..8048b35b4d 100644 --- a/api/src/queries/project/project-view-queries.ts +++ b/api/src/queries/project/project-view-queries.ts @@ -150,58 +150,6 @@ export const getIUCNActionClassificationByProjectSQL = (projectId: number): SQLS return sqlStatement; }; -/** - * SQL query to get funding source data - * - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getFundingSourceByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getFundingSourceByProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - pfs.funding_source_project_id as agency_id, - pfs.funding_amount::numeric::int, - pfs.funding_start_date as start_date, - pfs.funding_end_date as end_date, - iac.name as investment_action_category, - fs.name as agency_name - FROM - project_funding_source as pfs - LEFT OUTER JOIN - investment_action_category as iac - ON - pfs.iac_id = iac.id - LEFT OUTER JOIN - funding_source as fs - ON - iac.fs_id = fs.id - WHERE - pfs.p_id = ${projectId} - GROUP BY - pfs.funding_source_project_id, - pfs.funding_amount, - pfs.funding_start_date, - pfs.funding_end_date, - iac.name, - fs.name - `; - - defaultLog.debug({ - label: 'getFundingSourceByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - /** * SQL query to get project indigenous partnerships. * @param {number} projectId diff --git a/api/src/queries/project/project-view-update-queries.test.ts b/api/src/queries/project/project-view-update-queries.test.ts index 7cd438687d..cdbe7995cc 100644 --- a/api/src/queries/project/project-view-update-queries.test.ts +++ b/api/src/queries/project/project-view-update-queries.test.ts @@ -6,7 +6,8 @@ import { getAncillarySpeciesByProjectSQL, getActivitiesByProjectSQL, getClimateInitiativesByProjectSQL, - getLocationByProjectSQL + getLocationByProjectSQL, + getFundingSourceByProjectSQL } from './project-view-update-queries'; describe('getLocationByProjectSQL', () => { @@ -92,3 +93,17 @@ describe('getClimateInitiativesByProjectSQL', () => { expect(response).to.not.be.null; }); }); + +describe('getFundingSourceByProjectSQL', () => { + it('returns null response when null projectId provided', () => { + const response = getFundingSourceByProjectSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectId provided', () => { + const response = getFundingSourceByProjectSQL(1); + + expect(response).to.not.be.null; + }); +}); diff --git a/api/src/queries/project/project-view-update-queries.ts b/api/src/queries/project/project-view-update-queries.ts index ce9eb765dd..45f0aca9b4 100644 --- a/api/src/queries/project/project-view-update-queries.ts +++ b/api/src/queries/project/project-view-update-queries.ts @@ -203,3 +203,63 @@ export const getClimateInitiativesByProjectSQL = (projectId: number): SQLStateme return sqlStatement; }; + +/** + * SQL query to get funding source data + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getFundingSourceByProjectSQL = (projectId: number): SQLStatement | null => { + defaultLog.debug({ label: 'getFundingSourceByProjectSQL', message: 'params', projectId }); + + if (!projectId) { + return null; + } + + const sqlStatement = SQL` + SELECT + pfs.id as id, + fs.id as agency_id, + pfs.funding_amount::numeric::int, + pfs.funding_start_date as start_date, + pfs.funding_end_date as end_date, + iac.id as investment_action_category, + iac.name as investment_action_category_name, + fs.name as agency_name, + pfs.funding_source_project_id as agency_project_id, + pfs.revision_count as revision_count + FROM + project_funding_source as pfs + LEFT OUTER JOIN + investment_action_category as iac + ON + pfs.iac_id = iac.id + LEFT OUTER JOIN + funding_source as fs + ON + iac.fs_id = fs.id + WHERE + pfs.p_id = ${projectId} + GROUP BY + pfs.id, + fs.id, + pfs.funding_source_project_id, + pfs.funding_amount, + pfs.funding_start_date, + pfs.funding_end_date, + iac.id, + iac.name, + fs.name, + pfs.revision_count + `; + + defaultLog.debug({ + label: 'getFundingSourceByProjectSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; diff --git a/app/src/constants/i18n.ts b/app/src/constants/i18n.ts index 3bac6dbf3d..bf9d470c4a 100644 --- a/app/src/constants/i18n.ts +++ b/app/src/constants/i18n.ts @@ -62,3 +62,17 @@ export const EditSpeciesI18N = { editErrorText: 'An error has occurred while attempting to edit your species, please try again. If the error persists, please contact your system administrator.' }; + +export const AddFundingI18N = { + addTitle: 'Add Funding Source', + addErrorTitle: 'Error Adding Funding Source', + addErrorText: + 'An error has occurred while attempting to add your funding source details, please try again. If the error persists, please contact your system administrator.' +}; + +export const EditFundingI18N = { + editTitle: 'Edit Funding Source', + editErrorTitle: 'Error Editing Funding Source', + editErrorText: + 'An error has occurred while attempting to edit your funding source details, please try again. If the error persists, please contact your system administrator.' +}; diff --git a/app/src/features/projects/components/ProjectDetailsForm.test.tsx b/app/src/features/projects/components/ProjectDetailsForm.test.tsx index 763729db66..1038f2f2bb 100644 --- a/app/src/features/projects/components/ProjectDetailsForm.test.tsx +++ b/app/src/features/projects/components/ProjectDetailsForm.test.tsx @@ -10,15 +10,15 @@ import ProjectDetailsForm, { const project_type: IMultiAutocompleteFieldOption[] = [ { - value: 'type_1', + value: 1, label: 'type 1' }, { - value: 'type_2', + value: 2, label: 'type 2' }, { - value: 'type_3', + value: 3, label: 'type 3' } ]; @@ -78,7 +78,7 @@ describe('ProjectDetailsForm', () => { it('renders correctly with existing details values', () => { const existingFormValues: IProjectDetailsForm = { project_name: 'name 1', - project_type: 'type_1', + project_type: 2, project_activities: [2, 3], climate_change_initiatives: [1, 2], start_date: '2021-03-14', diff --git a/app/src/features/projects/components/ProjectFundingForm.test.tsx b/app/src/features/projects/components/ProjectFundingForm.test.tsx index 05d2eb794d..f2fd5b5618 100644 --- a/app/src/features/projects/components/ProjectFundingForm.test.tsx +++ b/app/src/features/projects/components/ProjectFundingForm.test.tsx @@ -8,6 +8,8 @@ import ProjectFundingForm, { ProjectFundingFormInitialValues, ProjectFundingFormYupSchema } from './ProjectFundingForm'; +import { codes } from 'test-helpers/code-helpers'; +import ProjectStepComponents from 'utils/ProjectStepComponents'; const funding_sources: IMultiAutocompleteFieldOption[] = [ { @@ -65,14 +67,17 @@ describe('ProjectFundingForm', () => { it('renders correctly with existing funding values', () => { const existingFormValues: IProjectFundingForm = { - funding_agencies: [ + funding_sources: [ { + id: 11, agency_id: 1, investment_action_category: 1, + investment_action_category_name: 'Action 23', agency_project_id: '111', funding_amount: 222, start_date: '2021-03-14', - end_date: '2021-04-14' + end_date: '2021-04-14', + revision_count: 23 } ] }; @@ -84,12 +89,7 @@ describe('ProjectFundingForm', () => { validateOnBlur={true} validateOnChange={false} onSubmit={async () => {}}> - {() => ( - - )} + {() => } ); diff --git a/app/src/features/projects/components/ProjectFundingForm.tsx b/app/src/features/projects/components/ProjectFundingForm.tsx index 3c806a2257..650f641996 100644 --- a/app/src/features/projects/components/ProjectFundingForm.tsx +++ b/app/src/features/projects/components/ProjectFundingForm.tsx @@ -7,18 +7,21 @@ import Icon from '@mdi/react'; import { mdiPlus, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import React, { useState } from 'react'; import { getFormattedDateRangeString, getFormattedAmount } from 'utils/Utils'; +import EditDialog from 'components/dialog/EditDialog'; import yup from 'utils/YupSchema'; import ProjectFundingItemForm, { IProjectFundingFormArrayItem, - ProjectFundingFormArrayItemInitialValues + ProjectFundingFormArrayItemInitialValues, + ProjectFundingFormArrayItemYupSchema } from './ProjectFundingItemForm'; +import { AddFundingI18N } from 'constants/i18n'; export interface IProjectFundingForm { - funding_agencies: IProjectFundingFormArrayItem[]; + funding_sources: IProjectFundingFormArrayItem[]; } export const ProjectFundingFormInitialValues: IProjectFundingForm = { - funding_agencies: [] + funding_sources: [] }; export const ProjectFundingFormYupSchema = yup.object().shape({}); @@ -68,10 +71,11 @@ const useStyles = makeStyles((theme) => ({ */ const ProjectFundingForm: React.FC = (props) => { const classes = useStyles(); + const formikProps = useFormikContext(); const { values } = formikProps; - // Tracks information about the current funding source item that is being added/edited + //Tracks information about the current funding source item that is being added/edited const [currentProjectFundingFormArrayItem, setCurrentProjectFundingFormArrayItem] = useState({ index: 0, values: ProjectFundingFormArrayItemInitialValues @@ -83,7 +87,7 @@ const ProjectFundingForm: React.FC = (props) => {
- Funding Sources ({values.funding_agencies.length}) + Funding Sources ({values.funding_sources.length}) - - - - - ); - }} - + // If an agency_id with a `Not Applicable` investment_action_category is chosen, auto select + // it for the user. + if (event.target.value !== 1 && event.target.value !== 2) { + setFieldValue( + 'investment_action_category', + props.investment_action_category.find((item) => item.fs_id === event.target.value)?.value || 0 + ); + } + }} + error={touched.agency_id && Boolean(errors.agency_id)} + displayEmpty + inputProps={{ 'aria-label': 'Agency Name' }}> + {props.funding_sources.map((item) => ( + + {item.label} + + ))} + + {errors.agency_id} + + + {investment_action_category_label && ( + + + {investment_action_category_label} + + {errors.investment_action_category} + + + )} + + + + + + + Funding Details + + + + + + + + + + + ); }; diff --git a/app/src/features/projects/components/__snapshots__/ProjectDetailsForm.test.tsx.snap b/app/src/features/projects/components/__snapshots__/ProjectDetailsForm.test.tsx.snap index c3fde93747..cb72e5a6da 100644 --- a/app/src/features/projects/components/__snapshots__/ProjectDetailsForm.test.tsx.snap +++ b/app/src/features/projects/components/__snapshots__/ProjectDetailsForm.test.tsx.snap @@ -516,7 +516,7 @@ exports[`ProjectDetailsForm renders correctly with existing details values 1`] = role="button" tabindex="0" > - type 1 + type 2 - (action 1) + (Investment action category)

- + + + Agency Project ID + + + + + - -
-
- -`; - -exports[`ProjectFundingItemForm renders correctly with existing funding item values with agency_id 1 1`] = ` - -
- -
+

- + + +

+
+ +
+ + +
+
- -
-
- -`; - -exports[`ProjectFundingItemForm renders correctly with existing funding item values with agency_id 2 1`] = ` - -
- - + + + Agency Project ID + + + +
+ - -
-
- -`; - -exports[`ProjectFundingItemForm renders correctly with existing funding item values with agency_id other than 1 or 2 1`] = ` - -
-
+ + + +
+ + Funding Details + +
+
+
+ +
+ + +
+
+
+
+
+
+ +
+ - - - -
+ + + +
- - Cancel - - - + +
+ + +
+
+ - +
-
- + class="MuiBox-root MuiBox-root-7" + > +
+ + + `; diff --git a/app/src/features/projects/view/ProjectDetails.tsx b/app/src/features/projects/view/ProjectDetails.tsx index 2369b6d00a..82e25a222f 100644 --- a/app/src/features/projects/view/ProjectDetails.tsx +++ b/app/src/features/projects/view/ProjectDetails.tsx @@ -61,7 +61,7 @@ const ProjectDetails: React.FC = (props) => {
- + diff --git a/app/src/features/projects/view/__snapshots__/ProjectDetails.test.tsx.snap b/app/src/features/projects/view/__snapshots__/ProjectDetails.test.tsx.snap index de8ccfd4e4..d57e970111 100644 --- a/app/src/features/projects/view/__snapshots__/ProjectDetails.test.tsx.snap +++ b/app/src/features/projects/view/__snapshots__/ProjectDetails.test.tsx.snap @@ -947,7 +947,7 @@ exports[`ProjectDetails renders correctly 1`] = ` />
diff --git a/app/src/features/projects/view/components/FundingSource.test.tsx b/app/src/features/projects/view/components/FundingSource.test.tsx index 70b2de502a..9ef6a82f91 100644 --- a/app/src/features/projects/view/components/FundingSource.test.tsx +++ b/app/src/features/projects/view/components/FundingSource.test.tsx @@ -1,12 +1,80 @@ -import { render } from '@testing-library/react'; -import { getProjectForViewResponse } from 'test-helpers/project-helpers'; +import { fireEvent, render, waitFor, cleanup } from '@testing-library/react'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import React from 'react'; +import { codes } from 'test-helpers/code-helpers'; +import { getProjectForViewResponse } from 'test-helpers/project-helpers'; import FundingSource from './FundingSource'; +jest.mock('../../../../hooks/useBioHubApi'); +const mockRefresh = jest.fn(); + +jest.mock('../../../../hooks/useBioHubApi'); +const mockUseBiohubApi = { + project: { + getProjectForUpdate: jest.fn, []>(), + updateProject: jest.fn() + } +}; + +const mockBiohubApi = ((useBiohubApi as unknown) as jest.Mock).mockReturnValue( + mockUseBiohubApi +); describe('FundingSource', () => { + beforeEach(() => { + // clear mocks before each test + mockBiohubApi().project.getProjectForUpdate.mockClear(); + mockBiohubApi().project.updateProject.mockClear(); + }); + + afterEach(() => { + cleanup(); + }); + it('renders correctly', () => { - const { asFragment } = render(); + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + it('opens the edit funding source dialog box when edit button is clicked, cancel and save buttons are pressed', async () => { + const { asFragment, getByText, queryByText } = render( + + ); + + await waitFor(() => { + expect(getByText('Funding Sources')).toBeInTheDocument(); + }); + + fireEvent.click(getByText('EDIT')); expect(asFragment()).toMatchSnapshot(); + + await waitFor(() => { + expect(getByText('Edit Funding Source')).toBeVisible(); + }); + + fireEvent.click(getByText('Cancel')); + + await waitFor(() => { + expect(queryByText('Cancel')).not.toBeInTheDocument(); + }); + + fireEvent.click(getByText('EDIT')); + + await waitFor(() => { + expect(getByText('Save Changes')).toBeVisible(); + }); + + fireEvent.click(getByText('Save Changes')); + + await waitFor(() => { + expect(queryByText('Save Changes')).not.toBeInTheDocument(); + }); + + await waitFor(() => { + expect(getByText('Funding Sources')).toBeInTheDocument(); + }); }); }); diff --git a/app/src/features/projects/view/components/FundingSource.tsx b/app/src/features/projects/view/components/FundingSource.tsx index e1913501e3..467a2e85d4 100644 --- a/app/src/features/projects/view/components/FundingSource.tsx +++ b/app/src/features/projects/view/components/FundingSource.tsx @@ -1,10 +1,22 @@ -import { Box, Grid, Button, Typography, Divider, IconButton } from '@material-ui/core'; -import React, { Fragment } from 'react'; -import { IGetProjectForViewResponse } from 'interfaces/useProjectApi.interface'; +import { Box, Grid, Button, Typography, Divider } from '@material-ui/core'; +import React, { Fragment, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { DATE_FORMAT } from 'constants/dateFormats'; -import { getFormattedDateRangeString, getFormattedAmount } from 'utils/Utils'; -import { Edit } from '@material-ui/icons'; +import { getFormattedDateRangeString, getFormattedAmount, getFormattedDate } from 'utils/Utils'; +import EditDialog from 'components/dialog/EditDialog'; +import { ErrorDialog, IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { EditFundingI18N } from 'constants/i18n'; +import ProjectFundingItemForm, { + IProjectFundingFormArrayItem, + ProjectFundingFormArrayItemInitialValues, + ProjectFundingFormArrayItemYupSchema +} from 'features/projects/components/ProjectFundingItemForm'; +import { APIError } from 'hooks/api/useAxios'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import { IGetProjectForViewResponse } from 'interfaces/useProjectApi.interface'; +import Icon from '@mdi/react'; +import { mdiPencilOutline } from '@mdi/js'; const useStyles = makeStyles({ heading: { @@ -19,8 +31,10 @@ const useStyles = makeStyles({ } }); -export interface IProjectDetailsProps { +export interface IProjectFundingProps { projectForViewData: IGetProjectForViewResponse; + codes: IGetAllCodeSetsResponse; + refresh: () => void; } /** @@ -28,15 +42,100 @@ export interface IProjectDetailsProps { * * @return {*} */ -const FundingSource: React.FC = (props) => { +const FundingSource: React.FC = (props) => { const { - projectForViewData: { funding } + projectForViewData: { funding, id }, + codes } = props; const classes = useStyles(); + const biohubApi = useBiohubApi(); + + const [errorDialogProps, setErrorDialogProps] = useState({ + dialogTitle: EditFundingI18N.editErrorTitle, + dialogText: EditFundingI18N.editErrorText, + open: false, + onClose: () => { + setErrorDialogProps({ ...errorDialogProps, open: false }); + }, + onOk: () => { + setErrorDialogProps({ ...errorDialogProps, open: false }); + } + }); + + const [fundingFormData, setFundingFormData] = useState( + ProjectFundingFormArrayItemInitialValues + ); + + const showErrorDialog = (textDialogProps?: Partial) => { + setErrorDialogProps({ ...errorDialogProps, ...textDialogProps, open: true }); + }; + const [openEditDialog, setOpenEditDialog] = useState(false); + + const handleDialogEditOpen = async (itemIndex: number) => { + const fundingSource = funding.fundingSources[itemIndex]; + + setFundingFormData({ + id: fundingSource.id, + agency_id: fundingSource.agency_id, + investment_action_category: fundingSource.investment_action_category, + investment_action_category_name: fundingSource.investment_action_category_name, + agency_project_id: fundingSource.agency_project_id, + funding_amount: fundingSource.funding_amount, + start_date: getFormattedDate(DATE_FORMAT.ShortDateFormat, fundingSource.start_date), + end_date: getFormattedDate(DATE_FORMAT.ShortDateFormat, fundingSource.end_date), + revision_count: fundingSource.revision_count + }); + + setOpenEditDialog(true); + }; + const handleDialogEditSave = async (values: IProjectFundingFormArrayItem) => { + const projectData = { + funding: { + fundingSources: [{ ...values }] + } + }; + + try { + await biohubApi.project.updateProject(id, projectData); + } catch (error) { + const apiError = error as APIError; + showErrorDialog({ dialogText: apiError.message, open: true }); + return; + } finally { + setOpenEditDialog(false); + } + + props.refresh(); + }; return ( <> + { + return { value: item.id, label: item.name }; + }) || [] + } + investment_action_category={ + codes?.investment_action_category?.map((item) => { + return { value: item.id, fs_id: item.fs_id, label: item.name }; + }) || [] + } + /> + ), + initialValues: fundingFormData, + validationSchema: ProjectFundingFormArrayItemYupSchema + }} + onCancel={() => setOpenEditDialog(false)} + onSave={handleDialogEditSave} + /> + @@ -53,23 +152,26 @@ const FundingSource: React.FC = (props) => { - {funding.fundingAgencies.map((item: any) => ( - + {funding.fundingSources.map((item: any, index: number) => ( + - + {item.agency_name} - - - EDIT - - + @@ -79,7 +181,7 @@ const FundingSource: React.FC = (props) => { - {item.agency_id} + {item.agency_project_id} @@ -101,14 +203,14 @@ const FundingSource: React.FC = (props) => { - {item.investment_action_category !== 'Not Applicable' && ( + {item.investment_action_category_name !== 'Not Applicable' && ( Investment Category - {item.investment_action_category} + {item.investment_action_category_name} diff --git a/app/src/features/projects/view/components/__snapshots__/FundingSource.test.tsx.snap b/app/src/features/projects/view/components/__snapshots__/FundingSource.test.tsx.snap index 2e96d5a32e..4c3abeb7c5 100644 --- a/app/src/features/projects/view/components/__snapshots__/FundingSource.test.tsx.snap +++ b/app/src/features/projects/view/components/__snapshots__/FundingSource.test.tsx.snap @@ -1,5 +1,202 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`FundingSource opens the edit funding source dialog box when edit button is clicked, cancel and save buttons are pressed 1`] = ` + +
+
+
+
+

+ Funding Sources +

+
+
+ +
+
+
+
+
+
+
+
+

+ agency name +

+
+
+
+ +
+
+
+
+
+ + Agency Project ID + +
+
+
+ ABC123 +
+
+
+
+
+ + Funding Amount + +
+
+
+ $ 333 +
+
+
+
+
+ + Funding Dates + +
+
+
+ 04/14/2000 - 04/13/2021 +
+
+
+
+
+ + Investment Category + +
+
+
+ investment action +
+
+
+
+
+ +`; + exports[`FundingSource renders correctly 1`] = `
diff --git a/app/src/interfaces/useProjectApi.interface.ts b/app/src/interfaces/useProjectApi.interface.ts index 97a714c68f..fa4492ab55 100644 --- a/app/src/interfaces/useProjectApi.interface.ts +++ b/app/src/interfaces/useProjectApi.interface.ts @@ -106,7 +106,7 @@ export interface IGetProjectForUpdateResponse { coordinator?: IGetProjectForUpdateResponseCoordinator; species?: IGetProjectForUpdateResponseSpecies; iucn?: IGetProjectForUpdateResponseIUCN; - funding?: IGetProjectForUpdateResponseFundingSource; + funding?: IGetProjectForUpdateResponseFundingData; partnerships?: IGetProjectForUpdateResponsePartnerships; } @@ -157,17 +157,20 @@ export interface IGetProjectForUpdateResponseIUCN { classificationDetails: IGetProjectForUpdateResponseIUCNArrayItem[]; } -interface IGetProjectForUpdateResponseFundingSourceArrayItem { +interface IGetProjectForUpdateResponseFundingSource { + id: number; agency_id: number; investment_action_category: number; + investment_action_category_name: string; agency_project_id: string; funding_amount: number; start_date: string; end_date: string; + revision_count: number; } -export interface IGetProjectForUpdateResponseFundingSource { - fundingAgencies: IGetProjectForUpdateResponseFundingSourceArrayItem[]; +export interface IGetProjectForUpdateResponseFundingData { + fundingSources: IGetProjectForUpdateResponseFundingSource[]; } export interface IGetProjectForUpdateResponsePartnerships { @@ -198,7 +201,7 @@ export interface IGetProjectForViewResponse { coordinator: IGetProjectForViewResponseCoordinator; species: IGetProjectForViewResponseSpecies; iucn: IGetProjectForViewResponseIUCN; - funding: IGetProjectForViewResponseFundingSource; + funding: IGetProjectForViewResponseFundingData; partnerships: IGetProjectForViewResponsePartnerships; } @@ -240,17 +243,21 @@ export interface IGetProjectForViewResponseIUCN { classificationDetails: IGetProjectForViewResponseIUCNArrayItem[]; } -interface IGetProjectForViewResponseFundingSourceArrayItem { - agency_id: string; +interface IGetProjectForViewResponseFundingSource { + id: number; + agency_id: number; agency_name: string; - investment_action_category: string; + investment_action_category: number; + investment_action_category_name: string; funding_amount: number; start_date: string; end_date: string; + agency_project_id: string; + revision_count: number; } -export interface IGetProjectForViewResponseFundingSource { - fundingAgencies: IGetProjectForViewResponseFundingSourceArrayItem[]; +export interface IGetProjectForViewResponseFundingData { + fundingSources: IGetProjectForViewResponseFundingSource[]; } export interface IGetProjectForViewResponseSpecies { diff --git a/app/src/test-helpers/project-helpers.ts b/app/src/test-helpers/project-helpers.ts index 544ba98dae..483d8ee6a8 100644 --- a/app/src/test-helpers/project-helpers.ts +++ b/app/src/test-helpers/project-helpers.ts @@ -45,14 +45,18 @@ export const getProjectForViewResponse: IGetProjectForViewResponse = { ] }, funding: { - fundingAgencies: [ + fundingSources: [ { - agency_id: '123', + id: 0, + agency_id: 123, agency_name: 'agency name', - investment_action_category: 'investment action', + agency_project_id: 'ABC123', + investment_action_category: 222, + investment_action_category_name: 'investment action', funding_amount: 333, start_date: '2000-04-14', - end_date: '2021-04-13' + end_date: '2021-04-13', + revision_count: 1 } ] }, diff --git a/testing/integration/postman/BioHubBC-API-DEV.postman_collection.json b/testing/integration/postman/BioHubBC-API-DEV.postman_collection.json index e5b3a3ebc1..7832fec15b 100644 --- a/testing/integration/postman/BioHubBC-API-DEV.postman_collection.json +++ b/testing/integration/postman/BioHubBC-API-DEV.postman_collection.json @@ -483,7 +483,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"coordinator\": {\r\n \"first_name\": \"first name\",\r\n \"last_name\": \"last name\",\r\n \"email_address\": \"email@email.com\",\r\n \"coordinator_agency\": \"coordinator agency\",\r\n \"share_contact_details\": \"false\"\r\n },\r\n \"permit\": {\r\n \"permits\": [\r\n {\r\n \"permit_number\": \"{{randomNumber}}\",\r\n \"sampling_conducted\": \"true\"\r\n }\r\n ]\r\n },\r\n \"project\": {\r\n \"project_name\": \"project name\",\r\n \"project_type\": 3,\r\n \"project_activities\": [\r\n 2,\r\n 4\r\n ],\r\n \"climate_change_initiatives\": [\r\n 2,\r\n 3\r\n ],\r\n \"start_date\": \"2021-03-08\",\r\n \"end_date\": \"2021-03-26\"\r\n },\r\n \"location\": {\r\n \"regions\": [\r\n \"West Coast\",\r\n \"Kootenays\"\r\n ],\r\n \"location_description\": \"A location description\",\r\n \"geometry\": [\r\n {\r\n \"type\": \"Feature\",\r\n \"id\": \"myGeo1\",\r\n \"geometry\": {\r\n \"type\": \"Polygon\",\r\n \"coordinates\": [[\r\n [-128, 55],\r\n [-128, 55.5],\r\n [-128, 56],\r\n [-126, 58],\r\n [-128, 55]\r\n ]]\r\n },\r\n \"properties\": {\r\n \"name\": \"Biohub Islands 1\"\r\n }\r\n },\r\n {\r\n \"type\": \"Feature\",\r\n \"id\": \"myGeo2\",\r\n \"geometry\": {\r\n \"type\": \"Polygon\",\r\n \"coordinates\": [[\r\n [-128.5, 55.5],\r\n [-128.5, 56.0],\r\n [-128.5, 56.5],\r\n [-126.5, 58.5],\r\n [-128.5, 55.5]\r\n ]]\r\n },\r\n \"properties\": {\r\n \"name\": \"Biohub Islands 2\"\r\n }\r\n }\r\n ]\r\n },\r\n \"species\": {\r\n \"focal_species\": [\r\n \"Acuteleaf Small Limestone Moss [Seligeria acutifolia]\",\r\n \"American Badger [Taxidea taxus]\"\r\n ],\r\n \"ancillary_species\": [\r\n \"Black Tern [Chlidonias niger]\",\r\n \"Blue Shark [Prionace glauca]\"\r\n ]\r\n },\r\n \"funding\": {\r\n \"funding_agencies\": [\r\n {\r\n \"agency_id\": 1,\r\n \"investment_action_category\": 23,\r\n \"agency_project_id\": \"11111\",\r\n \"funding_amount\": 222,\r\n \"start_date\": \"2021-03-09\",\r\n \"end_date\": \"2021-03-26\"\r\n }\r\n ],\r\n \"indigenous_partnerships\": [\r\n 1,\r\n 3\r\n ],\r\n \"stakeholder_partnerships\": [\r\n \"Fish and Wildlife Compensation Program\",\r\n \"Habitat Conservation Trust Fund\"\r\n ]\r\n }\r\n}", + "raw": "{\r\n \"coordinator\": {\r\n \"first_name\": \"first name\",\r\n \"last_name\": \"last name\",\r\n \"email_address\": \"email@email.com\",\r\n \"coordinator_agency\": \"coordinator agency\",\r\n \"share_contact_details\": \"false\"\r\n },\r\n \"permit\": {\r\n \"permits\": [\r\n {\r\n \"permit_number\": \"{{randomNumber}}\",\r\n \"sampling_conducted\": \"true\"\r\n }\r\n ]\r\n },\r\n \"project\": {\r\n \"project_name\": \"project name\",\r\n \"project_type\": 3,\r\n \"project_activities\": [\r\n 2,\r\n 4\r\n ],\r\n \"climate_change_initiatives\": [\r\n 2,\r\n 3\r\n ],\r\n \"start_date\": \"2021-03-08\",\r\n \"end_date\": \"2021-03-26\"\r\n },\r\n \"location\": {\r\n \"regions\": [\r\n \"West Coast\",\r\n \"Kootenays\"\r\n ],\r\n \"location_description\": \"A location description\",\r\n \"geometry\": [\r\n {\r\n \"type\": \"Feature\",\r\n \"id\": \"myGeo1\",\r\n \"geometry\": {\r\n \"type\": \"Polygon\",\r\n \"coordinates\": [[\r\n [-128, 55],\r\n [-128, 55.5],\r\n [-128, 56],\r\n [-126, 58],\r\n [-128, 55]\r\n ]]\r\n },\r\n \"properties\": {\r\n \"name\": \"Biohub Islands 1\"\r\n }\r\n },\r\n {\r\n \"type\": \"Feature\",\r\n \"id\": \"myGeo2\",\r\n \"geometry\": {\r\n \"type\": \"Polygon\",\r\n \"coordinates\": [[\r\n [-128.5, 55.5],\r\n [-128.5, 56.0],\r\n [-128.5, 56.5],\r\n [-126.5, 58.5],\r\n [-128.5, 55.5]\r\n ]]\r\n },\r\n \"properties\": {\r\n \"name\": \"Biohub Islands 2\"\r\n }\r\n }\r\n ]\r\n },\r\n \"species\": {\r\n \"focal_species\": [\r\n \"Acuteleaf Small Limestone Moss [Seligeria acutifolia]\",\r\n \"American Badger [Taxidea taxus]\"\r\n ],\r\n \"ancillary_species\": [\r\n \"Black Tern [Chlidonias niger]\",\r\n \"Blue Shark [Prionace glauca]\"\r\n ]\r\n },\r\n \"funding\": {\r\n \"funding_sources\": [\r\n {\r\n \"agency_id\": 1,\r\n \"investment_action_category\": 23,\r\n \"agency_project_id\": \"11111\",\r\n \"funding_amount\": 222,\r\n \"start_date\": \"2021-03-09\",\r\n \"end_date\": \"2021-03-26\"\r\n }\r\n ],\r\n \"indigenous_partnerships\": [\r\n 1,\r\n 3\r\n ],\r\n \"stakeholder_partnerships\": [\r\n \"Fish and Wildlife Compensation Program\",\r\n \"Habitat Conservation Trust Fund\"\r\n ]\r\n }\r\n}", "options": { "raw": { "language": "json" @@ -744,4 +744,4 @@ } } ] -} \ No newline at end of file +}