Skip to content

Commit

Permalink
Adds a script to create new organizations, agencies, and users (#279)
Browse files Browse the repository at this point in the history
* fix: ensures db initializes when script is run

* feat: add ability to onboard new organizations

* feat: add helper functions to create orgs, agencies, users

* chore: add comments

* feat: add test coverage to agency creation

* feat: adds test coverage to get or create org

* feat: adds test coverage for user get or create

* fix: ensure upload form is not reliant on organization foreign key to reporting period

* feat: add ability to create reporting periods and tie to orgs

* fix: linting

* chore: make reportingPeriod.organizationId optional in order to phase it out

* feat: adds support for creating reporting period and templates

* fix: linting

* chore: add logging
  • Loading branch information
as1729 authored May 22, 2024
1 parent 19d7028 commit b221aa8
Show file tree
Hide file tree
Showing 25 changed files with 737 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- DropForeignKey
ALTER TABLE "ReportingPeriod" DROP CONSTRAINT "ReportingPeriod_organizationId_fkey";

-- AlterTable
ALTER TABLE "ReportingPeriod" ALTER COLUMN "organizationId" DROP NOT NULL;

-- AddForeignKey
ALTER TABLE "ReportingPeriod" ADD CONSTRAINT "ReportingPeriod_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;
4 changes: 2 additions & 2 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ model ReportingPeriod {
name String
startDate DateTime @db.Date
endDate DateTime @db.Date
organizationId Int
organization Organization @relation(fields: [organizationId], references: [id])
organizationId Int?
organization Organization? @relation(fields: [organizationId], references: [id])
inputTemplateId Int
inputTemplate InputTemplate @relation(fields: [inputTemplateId], references: [id], onDelete: NoAction, onUpdate: NoAction)
outputTemplateId Int
Expand Down
6 changes: 6 additions & 0 deletions api/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ export const ROLES = {
ORGANIZATION_STAFF: 'ORGANIZATION_STAFF',
USDR_ADMIN: 'USDR_ADMIN',
}

export const EXPENDITURE_CATEGORIES = {
'1A': 'Broadband Infrastructure',
'1B': 'Digital Connectivity Technology',
'1C': 'Multi-Purpose Community Facility',
}
44 changes: 38 additions & 6 deletions api/src/services/agencies/agencies.scenarios.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
import type { Prisma, agency } from '@prisma/client'
import type { Prisma } from '@prisma/client'

import type { ScenarioData } from '@redwoodjs/testing/api'

export const standard = defineScenario<Prisma.agencyCreateArgs>({
export const standard = defineScenario<
Prisma.OrganizationCreateArgs | Prisma.AgencyCreateArgs
>({
organization: {
one: {
data: {
name: 'USDR',
},
},
two: {
data: {
name: 'Example Organization',
},
},
},
agency: {
one: { data: { name: 'String', code: 'String' } },
two: { data: { name: 'String', code: 'String' } },
one: (scenario) => ({
data: {
name: 'Agency1',
organizationId: scenario.organization.one.id,
code: 'A1',
},
include: {
organization: true,
},
}),
two: (scenario) => ({
data: {
name: 'Agency2',
organizationId: scenario.organization.two.id,
code: 'A2',
},
include: {
organization: true,
},
}),
},
})

export type StandardScenario = ScenarioData<agency, 'agency'>
export type StandardScenario = ScenarioData<Organization, 'organization'> &
ScenarioData<Agency, 'agency'>
17 changes: 16 additions & 1 deletion api/src/services/agencies/agencies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createAgency,
updateAgency,
deleteAgency,
getOrCreateAgencies,
} from './agencies'
import type { StandardScenario } from './agencies.scenarios'

Expand All @@ -16,6 +17,20 @@ import type { StandardScenario } from './agencies.scenarios'
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations

describe('agencies', () => {
scenario(
'get or creates new agencies',
async (scenario: StandardScenario) => {
const result = await getOrCreateAgencies(scenario.organization.one.name, [
{ name: 'Agency1', code: 'A1', abbreviation: 'A1' },
{ name: 'Agency3', code: 'A3', abbreviation: 'A3' },
])

expect(result.length).toEqual(2)
expect(result[0].name).toEqual('Agency1')
expect(result[0].id).toEqual(scenario.agency.one.id)
expect(result[1].name).toEqual('Agency3')
}
)
scenario('returns all agencies', async (scenario: StandardScenario) => {
const result = await agencies()

Expand All @@ -25,7 +40,7 @@ describe('agencies', () => {
scenario('returns a single agency', async (scenario: StandardScenario) => {
const result = await agency({ id: scenario.agency.one.id })

expect(result).toEqual(scenario.agency.one)
expect(result.id).toEqual(scenario.agency.one.id)
})

scenario('creates a agency', async () => {
Expand Down
57 changes: 57 additions & 0 deletions api/src/services/agencies/agencies.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { QueryResolvers, MutationResolvers } from 'types/graphql'

import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const agencies: QueryResolvers['agencies'] = () => {
return db.agency.findMany()
Expand Down Expand Up @@ -34,6 +35,62 @@ export const deleteAgency: MutationResolvers['deleteAgency'] = ({ id }) => {
})
}

export const getOrCreateAgencies = async (orgName, agencyData) => {
// This function is used to create initial expenditure categories
// It is intended to only be called via the `onboardOrganization` script
// Hence, we hard-return if we detect a non-empty context
if (context && Object.keys(context).length > 0) {
logger.error(
{ custom: context },
`This function is intended to be called via the onboardOrganization script and not via GraphQL API. Skipping...`
)
return
}

try {
const agencies = []
/*
{
name: 'Sample Name',
abbreviation: SAMPLEABBR,
code: SAMPLECODE,
}
*/
const organization = await db.organization.findFirst({
where: { name: orgName },
})
for (const agency of agencyData) {
try {
logger.info(`Processing agency ${agency.name}`)
const existingAgency = await db.agency.findFirst({
where: { name: agency.name, organizationId: organization.id },
})
if (existingAgency) {
logger.info(
`${agency.name} exists for organization ${organization.name}`
)
agencies.push(existingAgency)
} else {
logger.info(`Creating ${agency.name}`)
agency.organizationId = organization.id
const newAgency = await db.agency.create({ data: agency })
agencies.push(newAgency)
}
} catch (error) {
logger.error(error, `Error processing agency ${agency.name}`)
continue
}
}
logger.info(`Agencies processed: ${agencies.length}`)
return agencies
} catch (error) {
logger.error(
error,
`Error getting or creating agencies for organization ${orgName}`
)
}
}

export const agenciesByOrganization: QueryResolvers['agenciesByOrganization'] =
async ({ organizationId }) => {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createExpenditureCategory,
updateExpenditureCategory,
deleteExpenditureCategory,
createOrSkipInitialExpenditureCategories,
} from './expenditureCategories'
import type { StandardScenario } from './expenditureCategories.scenarios'

Expand All @@ -16,6 +17,16 @@ import type { StandardScenario } from './expenditureCategories.scenarios'
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations

describe('expenditureCategories', () => {
scenario('creates initial expenditure categories', async () => {
const categoriesBefore = await expenditureCategories()
const result = await createOrSkipInitialExpenditureCategories()
const categoriesAfter = await expenditureCategories()

expect(result).toEqual(undefined)
expect(categoriesBefore.length).toEqual(2)
expect(categoriesAfter.length).toEqual(5)
})

scenario(
'returns all expenditureCategories',
async (scenario: StandardScenario) => {
Expand Down
45 changes: 45 additions & 0 deletions api/src/services/expenditureCategories/expenditureCategories.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { Prisma } from '@prisma/client'
import type {
QueryResolvers,
MutationResolvers,
ExpenditureCategoryRelationResolvers,
} from 'types/graphql'

import { EXPENDITURE_CATEGORIES } from 'src/lib/constants'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const expenditureCategories: QueryResolvers['expenditureCategories'] =
() => {
Expand All @@ -19,6 +22,48 @@ export const expenditureCategory: QueryResolvers['expenditureCategory'] = ({
})
}

export const createOrSkipInitialExpenditureCategories = async () => {
// This function is used to create initial expenditure categories
// It is intended to only be called via the `onboardOrganization` script
// Hence, we hard-return if we detect a non-empty context
if (context && Object.keys(context).length > 0) {
logger.error(
{ custom: context },
`This function is intended to be called via the onboardOrganization script and not via GraphQL API. Skipping...`
)
return
}

try {
const expenditureCategoriesData = Object.entries(
EXPENDITURE_CATEGORIES
).map(([code, name]) => ({ code, name }))
await Promise.all(
expenditureCategoriesData.map(
async (data: Prisma.ExpenditureCategoryCreateArgs['data']) => {
const existingExpenditureCategory =
await db.expenditureCategory.findFirst({
where: { name: data.name },
})
if (existingExpenditureCategory) {
logger.info(
`Expenditure category ${data.name} already exists. Skipping...`
)
return
}
const record = await db.expenditureCategory.create({ data })
logger.info(`Created expenditure category ${record.name}`)
}
)
)
} catch (error) {
logger.error(
error,
'Error creating or skipping initial expenditure categories'
)
}
}

export const createExpenditureCategory: MutationResolvers['createExpenditureCategory'] =
({ input }) => {
return db.expenditureCategory.create({
Expand Down
14 changes: 14 additions & 0 deletions api/src/services/inputTemplates/inputTemplates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createInputTemplate,
updateInputTemplate,
deleteInputTemplate,
getOrCreateInputTemplate,
} from './inputTemplates'
import type { StandardScenario } from './inputTemplates.scenarios'

Expand All @@ -16,6 +17,19 @@ import type { StandardScenario } from './inputTemplates.scenarios'
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations

describe('inputTemplates', () => {
scenario('gets or creates input template', async () => {
const result = await getOrCreateInputTemplate({
name: 'NEW TEMPLATE',
version: '1.0.0',
effectiveDate: '2023-12-07T18:17:24.374Z',
})

expect(result.name).toEqual('NEW TEMPLATE')
expect(result.version).toEqual('1.0.0')
expect(result.effectiveDate).toEqual(new Date('2023-12-07T00:00:00.000Z'))
expect(result.updatedAt).toBeDefined()
})

scenario('returns all inputTemplates', async (scenario: StandardScenario) => {
const result = await inputTemplates()

Expand Down
38 changes: 38 additions & 0 deletions api/src/services/inputTemplates/inputTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
} from 'types/graphql'

import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const inputTemplates: QueryResolvers['inputTemplates'] = () => {
return db.inputTemplate.findMany()
Expand All @@ -16,6 +17,43 @@ export const inputTemplate: QueryResolvers['inputTemplate'] = ({ id }) => {
})
}

export const getOrCreateInputTemplate = async (inputTemplateInfo) => {
// This function is used to create initial expenditure categories
// It is intended to only be called via the `onboardOrganization` script
// Hence, we hard-return if we detect a non-empty context
if (context && Object.keys(context).length > 0) {
logger.error(
{ custom: context },
`This function is intended to be called via the onboardOrganization script and not via GraphQL API. Skipping...`
)
return
}

try {
let inputTemplateRecord

const existingInputTemplate = await db.inputTemplate.findFirst({
where: { name: inputTemplateInfo.name },
})
if (existingInputTemplate) {
logger.info(`Input template ${inputTemplateInfo.name} already exists`)
inputTemplateRecord = existingInputTemplate
} else {
logger.info(`Creating ${inputTemplateInfo.name}`)
const data = inputTemplateInfo
inputTemplateRecord = await db.inputTemplate.create({
data,
})
}
return inputTemplateRecord
} catch (error) {
logger.error(
error,
`Error getting or creating input template ${inputTemplateInfo.name}`
)
}
}

export const createInputTemplate: MutationResolvers['createInputTemplate'] = ({
input,
}) => {
Expand Down
Loading

0 comments on commit b221aa8

Please sign in to comment.