diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3fcddbfc82..bafa213cfc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -28,11 +28,11 @@ jobs: uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/deploy-app-to-env.yml b/.github/workflows/deploy-app-to-env.yml index 9f16e54e47..4d3f867520 100644 --- a/.github/workflows/deploy-app-to-env.yml +++ b/.github/workflows/deploy-app-to-env.yml @@ -82,22 +82,19 @@ jobs: - name: Generate Code run: npx lerna run generate - - name: Compile proto migration files - run: npx lerna run build --scope=app-proto - - name: upload app web gen directory - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: app-web-gen + name: app-web-gen deploy ${{ inputs.stage_name }} path: ./services/app-web/src/gen - name: upload cypress gen directory - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: cypress-gen + name: cypress-gen deploy ${{ inputs.stage_name }} path: ./services/cypress/gen - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: lambda-layers-prisma-client-migration path: ./services/app-api/lambda-layers-prisma-client-migration @@ -107,7 +104,7 @@ jobs: tar -C ./services/app-api/lambda-layers-prisma-client-migration -xf ./services/app-api/lambda-layers-prisma-client-migration/nodejs.tar.gz rm -rf ./services/app-api/lambda-layers-prisma-client-migration/nodejs.tar.gz - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: lambda-layers-prisma-client-engine path: ./services/app-api/lambda-layers-prisma-client-engine diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c07871b1d0..91164a872a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -109,19 +109,19 @@ jobs: run: yarn test:once --coverage - name: upload app web gen directory - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: app-web-gen path: ./services/app-web/src/gen - name: upload cypress gen directory - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: cypress-gen path: ./services/cypress/gen - name: upload unit test coverage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: unit-test-coverage path: ./services/app-web/coverage/coverage-final.json @@ -189,7 +189,7 @@ jobs: yarn test:once --coverage - name: upload api test coverage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: api-test-coverage path: ./services/app-api/coverage/coverage-final.json @@ -239,12 +239,12 @@ jobs: working-directory: services/app-api run: ./scripts/prepare-prisma-layer.sh - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: lambda-layers-prisma-client-migration path: ./services/app-api/lambda-layers-prisma-client-migration - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: lambda-layers-prisma-client-engine path: ./services/app-api/lambda-layers-prisma-client-engine @@ -458,12 +458,12 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: app-web-gen path: ./services/app-web/src/gen - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: cypress-gen path: ./services/cypress/gen @@ -508,26 +508,26 @@ jobs: CYPRESS_VIDEOS_FOLDER: services/cypress/videos - name: Upload cypress screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() && steps.cypress.outcome == 'failure' with: - name: cypress-screenshots + name: cypress-screenshots-${{ matrix.containers}} path: services/cypress/screenshots - name: Upload cypress video - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() && steps.cypress.outcome != 'skipped' with: - name: cypress-videos + name: cypress-videos-${{ matrix.containers}} path: services/cypress/videos - name: upload partial cypress coverage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: partial-cypress-coverage-${{ matrix.containers}} path: ./coverage-cypress/lcov.info - name: upload partial cypress coverage json - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: cypress-json-coverage-${{ matrix.containers}} path: ./coverage-cypress/coverage-final.json @@ -560,7 +560,7 @@ jobs: echo $(pwd) ls - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 - name: Create combined test coverage report run: | @@ -597,7 +597,7 @@ jobs: cd coverage-all echo "Coverage reports merged" - name: Upload combined test coverage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: combined-test-coverage path: ./coverage-all/ diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index aef65d8ffa..7be91378a8 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -96,7 +96,7 @@ jobs: run: PRISMA_CLI_BINARY_TARGETS=rhel-openssl-1.0.x yarn install --prefer-offline --frozen-lockfile --cache-folder ${{ steps.yarn-cache-dir-path.outputs.dir }} - name: Generate protos - run: npx lerna run generate --scope=app-proto && npx lerna run build --scope=app-proto + run: npx lerna run generate --scope=app-proto # Generate Prisma Client and binary that can run in a lambda environment - name: Prepare prisma client @@ -107,12 +107,12 @@ jobs: working-directory: services/app-api run: ./scripts/prepare-prisma-layer.sh - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: lambda-layers-prisma-client-migration path: ./services/app-api/lambda-layers-prisma-client-migration - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: lambda-layers-prisma-client-engine path: ./services/app-api/lambda-layers-prisma-client-engine @@ -223,14 +223,14 @@ jobs: - name: Setup env uses: ./.github/actions/setup_env - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: app-web-gen + name: app-web-gen deploy prod path: ./services/app-web/src/gen - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: cypress-gen + name: cypress-gen deploy prod path: ./services/cypress/gen - name: Cypress chrome fix @@ -264,7 +264,7 @@ jobs: CYPRESS_VIDEOS_FOLDER: services/cypress/videos - name: Upload cypress video - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() && steps.cypress.outcome == 'failure' with: name: cypress-videos diff --git a/.gitignore b/.gitignore index 86696f5511..92deb6291a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ gen/ todo.txt notes/ .tool-versions +dbdump-* # testing diff --git a/.images/postgres-handler-diagram.png b/.images/postgres-handler-diagram.png new file mode 100644 index 0000000000..92a4a48c6a Binary files /dev/null and b/.images/postgres-handler-diagram.png differ diff --git a/.images/resolver-design-diagram.png b/.images/resolver-design-diagram.png new file mode 100644 index 0000000000..8daf0b3e8b Binary files /dev/null and b/.images/resolver-design-diagram.png differ diff --git a/.images/session-expired-banner.png b/.images/session-expired-banner.png new file mode 100644 index 0000000000..bb37c21fe7 Binary files /dev/null and b/.images/session-expired-banner.png differ diff --git a/.images/session-expiring.png b/.images/session-expiring.png new file mode 100644 index 0000000000..5dedea9539 Binary files /dev/null and b/.images/session-expiring.png differ diff --git a/dev_tool/package.json b/dev_tool/package.json index 0376fa8df4..147c69009c 100644 --- a/dev_tool/package.json +++ b/dev_tool/package.json @@ -20,9 +20,14 @@ ] }, "dependencies": { + "@aws-sdk/client-cloudfront": "^3.450.0", + "@aws-sdk/client-ec2": "^3.441.0", + "@aws-sdk/client-secrets-manager": "^3.441.0", + "node-ssh": "^13.1.0", "yargs": "^17.2.1" }, "devDependencies": { + "@types/ssh2": "^1.11.15", "typescript": "^4.4.4" } } diff --git a/dev_tool/src/aws.ts b/dev_tool/src/aws.ts new file mode 100644 index 0000000000..4b2a0490d8 --- /dev/null +++ b/dev_tool/src/aws.ts @@ -0,0 +1,243 @@ +import { + CloudFrontClient, + ListDistributionsCommand, +} from '@aws-sdk/client-cloudfront' +import { + EC2Client, + DescribeInstancesCommand, + DescribeInstancesCommandInput, + StopInstancesCommand, + StartInstancesCommand, + DescribeSecurityGroupsCommand, + SecurityGroup, + AuthorizeSecurityGroupIngressCommand, + Instance, +} from '@aws-sdk/client-ec2' +import { + SecretsManagerClient, + ListSecretsCommand, + GetSecretValueCommand, +} from '@aws-sdk/client-secrets-manager' + +// deployment environments -> url mapping +const accountURLs: { [key: string]: string } = { + dev: 'mc-review-dev.onemac.cms.gov', // DO NOT CHECK THIS IN YET + val: 'mc-review-val.onemac.cms.gov', + prod: 'mc-review.onemac.cms.gov', +} + +// checkAWSAccess checks that the current creds are valid and for the requested environment +async function checkAWSAccess(envName: string): Promise { + // Our environments will always have CloudFront distributions for the UI of the app + // Let's fetch those and make sure we're in the right place. + const client = new CloudFrontClient({ region: 'us-east-1' }) + const command = new ListDistributionsCommand({}) + + try { + const response = await client.send(command) + + // One of our cloudfront distributions will have an alias for this environment + const thisDistributionAlias = accountURLs[envName] + const found = response.DistributionList?.Items?.find((dist) => + dist.Aliases?.Items?.find( + (alias) => alias === thisDistributionAlias + ) + ) + + if (!found) { + console.info( + `You are logged into the wrong account. Load credentials for ${envName} from CloudTamer` + ) + return new Error('Wrong account') + } + return undefined + } catch (e) { + if (e.name === 'CredentialsProviderError') { + console.info( + 'No AWS credentials found. Load current ones into your environment from CloudTamer' + ) + } else if (e.Code === 'ExpiredToken') { + console.info( + 'Your AWS credentials have expired. Load current ones into your environment from CloudTamer' + ) + } else if (e.Code === 'InvalidClientTokenId') { + console.info( + 'Your AWS credentials are wrong. Load current ones into your environment from CloudTamer' + ) + } else { + console.error('Unknown error returned by AWS call, update ./dev?') + throw e + } + return e + } +} + +// describeSecurityGroup returns the single security group with this ID +async function describeSecurityGroup( + groupID: string +): Promise { + const ec2 = new EC2Client({ region: 'us-east-1' }) + const describeSGs = new DescribeSecurityGroupsCommand({ + Filters: [{ Name: 'group-id', Values: [groupID] }], + }) + + try { + const instances = await ec2.send(describeSGs) + + if (instances.SecurityGroups?.length != 1) { + return new Error( + `didnt get back the expected secuirty group. found: ${instances.SecurityGroups?.length}` + ) + } + + const securityGroup = instances.SecurityGroups[0] + + return securityGroup + } catch (err) { + return err + } +} + +// addAllowlistRuleToGroup adds an SSH Allowlist rule to the given groupID +async function addSSHAllowlistRuleToGroup( + groupID: string, + ipAddress: string +): Promise { + const ec2 = new EC2Client({ region: 'us-east-1' }) + const addAllowlist = new AuthorizeSecurityGroupIngressCommand({ + CidrIp: ipAddress + '/32', + FromPort: 22, + IpProtocol: 'TCP', + ToPort: 22, + GroupId: groupID, + }) + + try { + await ec2.send(addAllowlist) + return undefined + } catch (err) { + return err + } +} + +// describeInstance returns a single instance that matches the query +async function describeInstance( + input: DescribeInstancesCommandInput = {} +): Promise { + const ec2 = new EC2Client({ region: 'us-east-1' }) + const describeInstances = new DescribeInstancesCommand(input) + + try { + const instances = await ec2.send(describeInstances) + + if ( + instances.Reservations?.length !== 1 || + instances.Reservations[0].Instances?.length !== 1 + ) { + return new Error('Did not find one and only one instance') + } + + const instance = instances.Reservations[0].Instances[0] + return instance + } catch (err) { + return err + } +} + +async function stopInstance(id: string) { + const ec2 = new EC2Client({ region: 'us-east-1' }) + const cmd = new StopInstancesCommand({ + InstanceIds: [id], + }) + + try { + await ec2.send(cmd) + } catch (err) { + return err + } + return undefined +} + +async function startInstance(id: string) { + const ec2 = new EC2Client({ region: 'us-east-1' }) + + const cmd = new StartInstancesCommand({ + InstanceIds: [id], + }) + + try { + await ec2.send(cmd) + } catch (err) { + console.error('Error starting instance', err) + return err + } +} + +interface DBConnection { + host: string + user: string + port: number + dbname: string + password: string +} + +async function getSecretsForRDS(stage: string): Promise { + const client = new SecretsManagerClient({ region: 'us-east-1' }) + const SMName = `aurora_postgres_${stage}` + + const list = new ListSecretsCommand({ + Filters: [ + { + Key: 'name', + Values: [SMName], + }, + ], + }) + const secrets = await client.send(list) + + if (secrets.SecretList?.length !== 1) { + throw new Error('Did not find one and only one secret on ' + stage) + } + + const rdsSecret = secrets.SecretList[0] + + const getValue = new GetSecretValueCommand({ + SecretId: rdsSecret.ARN, + }) + + try { + const secretValues = await client.send(getValue) + + if (!secretValues.SecretString) { + return new Error('AWS didnt return a value for this secret') + } + + const parsedSecrets = JSON.parse(secretValues.SecretString) + + return { + host: parsedSecrets.host, + user: parsedSecrets.username, + port: parsedSecrets.port, + dbname: parsedSecrets.dbname, + password: parsedSecrets.password, + } + } catch (err) { + if (err.__type === 'AccessDeniedException') { + console.error( + 'These creds dont have the ability to get the RDS credentials. Log in with better creds.' + ) + return err + } + return err + } +} + +export { + checkAWSAccess, + describeInstance, + describeSecurityGroup, + addSSHAllowlistRuleToGroup, + getSecretsForRDS, + stopInstance, + startInstance, +} diff --git a/dev_tool/src/dev.ts b/dev_tool/src/dev.ts index 0642af40e7..12eea1cac0 100644 --- a/dev_tool/src/dev.ts +++ b/dev_tool/src/dev.ts @@ -1,6 +1,7 @@ import { spawn } from 'child_process' import yargs from 'yargs' import { parseRunFlags } from './flags.js' +import { cloneDBLocally } from './jumpbox.js' import { compileGraphQLTypesOnce, compileProto, @@ -155,7 +156,7 @@ function runPrisma(args: string[]) { }) } -function main() { +async function main() { // check to see if local direnv vars have loaded if (!process.env.REACT_APP_AUTH_MODE) { console.info( @@ -174,7 +175,7 @@ function main() { All valid arguments to dev should be enumerated here, this is the entrypoint to the script */ - yargs(process.argv.slice(2)) + await yargs(process.argv.slice(2)) .scriptName('dev') .command( 'local', @@ -493,6 +494,45 @@ function main() { ) } ) + .command('jumpbox', 'run commands on a jumpbox', (yargs) => { + return yargs + .command( + 'clone ', + 'copy the database in the given aws environment locally', + (yargs) => { + return yargs + .positional('env', { + describe: + 'the environment to clone from. You must have AWS credentials configured for this environment.', + demandOption: true, + type: 'string', + choices: ['dev', 'val', 'prod'], + }) + .option('stop-after', { + type: 'boolean', + default: true, + }) + .option('ssh-key', { + type: 'string', + default: '~/.ssh/id_rsa', + }) + .example([ + [ + '$0 jumpbox clone dev', + 'clone the db from the dev AWS environment to your local machine', + ], + ]) + }, + async (args) => { + await cloneDBLocally( + args.env, + args.sshKey, + args.stopAfter + ) + } + ) + .demandCommand(1, 'you must pick a subcommand for jumpbox') + }) .command( 'prisma', 'run the prisma command in app-api. all arguments after -- will be passed directly into the prisma command.', @@ -547,20 +587,6 @@ function main() { runAllGenerate() } ) - .command( - 'hybrid', - '[deprecated use ./dev local web --hybrid instead] run app-web locally connected to the review app deployed for this branch', - (yargs) => { - return yargs.option('stage', { - type: 'string', - describe: - 'an alternative Serverless stage in your AWS account to run against', - }) - }, - (args) => { - runWebAgainstAWS(args.stage) - } - ) .demandCommand(1, '') .help() .strict().argv // this prints out the help if you don't call a subcommand diff --git a/dev_tool/src/flags.ts b/dev_tool/src/flags.ts index 764c9388c0..90ab064163 100644 --- a/dev_tool/src/flags.ts +++ b/dev_tool/src/flags.ts @@ -53,7 +53,7 @@ export function parseRunFlags( // Sadly, `any` lives in here. The types in parseRunFlags's signature are right, but it's not clear to me how // to make typescript happy with actually mapping one type to another. // so we do the unsafe thing. Having the types in the signature is still a big win. - let parsedFlags = Object.fromEntries( + const parsedFlags = Object.fromEntries( Object.entries(inputFlags).map( ([key, value]: [string, boolean | undefined]): [ string, diff --git a/dev_tool/src/jumpbox.ts b/dev_tool/src/jumpbox.ts new file mode 100644 index 0000000000..7f898c9321 --- /dev/null +++ b/dev_tool/src/jumpbox.ts @@ -0,0 +1,349 @@ +import { + addSSHAllowlistRuleToGroup, + checkAWSAccess, + describeInstance, + describeSecurityGroup, + getSecretsForRDS, + startInstance, + stopInstance, +} from './aws.js' +import { NodeSSH } from 'node-ssh' +import os from 'node:os' +import { retry } from './retry.js' +import { fileExists, httpRequest } from './nodeWrappers.js' +import { Instance } from '@aws-sdk/client-ec2' + +function stageForEnv(env: string): string { + if (env === 'dev') { + return 'main' + } + return env +} + +// waitForJumpboxToReachState repeatedly checks the ec2 instance to be in the given state or times out +async function waitForJumpboxToReachState( + instanceID: string, + instanceStateCode: number +): Promise { + const stoppedResult = await retry(async () => { + try { + const instance = await describeInstance({ + Filters: [ + { + Name: 'instance-id', + Values: [instanceID], + }, + ], + }) + if (instance instanceof Error) { + console.error('Error describing instance', instance) + return instance + } + + const state = instance.State?.Code + + if (state === undefined) { + return new Error('didnt get state back for the instance') + } + + process.stdout.write('.') + + if (state === instanceStateCode) { + return true + } + } catch (err) { + return err + } + + return false + }, 60 * 1000) // 60 second timeout + if (stoppedResult instanceof Error) { + return stoppedResult + } + return undefined +} + +// ensureJumpboxIsRunning gets the jumpbox Instance and if it's not running it starts it +async function ensureJumpboxIsRunning(): Promise { + const instance = await describeInstance({ + Filters: [ + { + Name: 'tag:mcr-vmuse', + Values: ['jumpbox'], + }, + ], + }) + if (instance instanceof Error) { + return instance + } + + const jumpboxStartInstance = instance + + const jumpboxStartID = jumpboxStartInstance.InstanceId + const jumpboxStartState = jumpboxStartInstance.State?.Code + + if (!jumpboxStartID || jumpboxStartState === undefined) { + return new Error( + `AWS didn't return info we needed. id: ${jumpboxStartID} state: ${jumpboxStartState}` + ) + } + + if (jumpboxStartState === 16) { + // running state code + console.info('Jumpbox is running') + return jumpboxStartInstance + } + + console.info('Jumpbox is not running', jumpboxStartInstance.State?.Name) + + if (jumpboxStartState !== 80) { + console.info('Jumpbox is not stopped yet. waiting to start it') + // wait for it to be stopped + const stopped = await waitForJumpboxToReachState(jumpboxStartID, 80) // 80 is stopped + if (stopped instanceof Error) { + return stopped + } + console.info('Jumpbox Stopped') + } + + console.info('Starting Jumpbox') + // issue the start command + const startResult = await startInstance(jumpboxStartID) + if (startResult instanceof Error) { + return startResult + } + + const started = await waitForJumpboxToReachState(jumpboxStartID, 16) // 16 is running + if (started instanceof Error) { + return started + } + + console.info('Jumpbox Started') + + const startedInstance = await describeInstance({ + Filters: [ + { + Name: 'instance-id', + Values: [jumpboxStartID], + }, + ], + }) + if (startedInstance instanceof Error) { + console.error('error fetching restarted jumpbox', startedInstance) + return startedInstance + } + + return startedInstance +} + +async function ensureAllowlistIP( + instance: Instance +): Promise { + // get my IP address + const myIPAddress = await httpRequest('http://ifconfig.me/ip') + if (myIPAddress instanceof Error) { + return myIPAddress + } + + // find the security group we care about for this + const securityGroupID = instance.SecurityGroups?.find((sg) => + sg.GroupName?.includes('PostgresVm') + ) + if (!securityGroupID || securityGroupID.GroupId === undefined) { + return new Error('No security groups on the instance to update') + } + + // get the right rule, check if my IP is in it + const securityGroup = await describeSecurityGroup(securityGroupID.GroupId) + if (securityGroup instanceof Error) { + return securityGroup + } + + const port22Rules = securityGroup.IpPermissions?.find( + (perm) => perm.FromPort === 22 + ) + if (!port22Rules) { + return new Error( + 'Security Group does not have port 22 on it, this will probably need to be fixed by hand, contact @mcrd' + ) + } + + const myRule = port22Rules.IpRanges?.find((range) => + range.CidrIp?.startsWith(myIPAddress) + ) + if (myRule) { + // if our IP is already allowlisted, we're good. + console.info('Already Allowlisted') + return undefined + } + + // in this case, now we need to add our IP address. + console.info('Allowlisting', myIPAddress) + + const result = await addSSHAllowlistRuleToGroup( + securityGroupID.GroupId, + myIPAddress + ) + if (result instanceof Error) { + return result + } + + return undefined +} + +async function cloneDBLocally( + envName: string, + sshKeyPath: string, + stopAfter = true +) { + // check that the ssh key exists + // node doesn't support ~ expansion natively, so we do it here. + if (sshKeyPath.startsWith('~/')) { + sshKeyPath = sshKeyPath.replace(/^~(?=$|\/|\\)/, os.homedir()) + } + + const sshKeyExists = fileExists(sshKeyPath) + if (sshKeyExists instanceof Error) { + console.error('failed to check if the ssh key exists', sshKeyExists) + process.exit(2) + } + if (!sshKeyExists) { + console.error( + 'The provided SSH key does not appear to exist: ', + sshKeyPath + ) + console.error( + 'use the --ssh-key option to specify your ssh key for the jumpbox' + ) + process.exit(1) + } + + const check = await checkAWSAccess(envName) + if (check instanceof Error) { + process.exit(1) + } + + // Figure out if Jumpbox is running + const instance = await ensureJumpboxIsRunning() + if (instance instanceof Error) { + console.error('Error getting jumpbox running', instance) + process.exit(1) + } + + const allowlist = await ensureAllowlistIP(instance) + if (allowlist instanceof Error) { + console.error('Error setting IP allowlist', allowlist) + process.exit(1) + } + + const jumpboxInstance = instance + + const jumpboxIP = jumpboxInstance.PublicIpAddress + const jumpboxInstanceID = jumpboxInstance.InstanceId + + if (!jumpboxIP || !jumpboxInstanceID) { + console.error( + 'EC2 didnt return required information', + jumpboxIP, + jumpboxInstanceID + ) + process.exit(1) + } + + // Get the secrets for the DB. + const dbSecrets = await getSecretsForRDS(stageForEnv(envName)) + if (dbSecrets instanceof Error) { + console.error('error fetching secrets', dbSecrets) + process.exit(1) + } + + try { + // start an SSH connection + console.info('Connecting to ', jumpboxIP) + const ssh = new NodeSSH() + + // If the jumpbox just started we often have the connection refused for ~10 seconds until it's really up. + const connectionResult = await retry(async () => { + try { + await ssh.connect({ + host: jumpboxIP, + username: 'ubuntu', + privateKeyPath: sshKeyPath, + }) + console.info('Connected') + return true + } catch (err) { + if (err.code === 'ECONNREFUSED') { + process.stdout.write('.') + return false + } + return err + } + }, 60 * 1000) + if (connectionResult instanceof Error) { + console.error( + 'failed to connect to jumpbox over ssh', + connectionResult + ) + process.exit(1) + } + + // create the filename for this db dump + const now = new Date() + const timeStamp = `${now.getFullYear()}${(now.getMonth() + 1) + .toString() + .padStart( + 2, + '0' + )}${now.getDate()}${now.getHours()}${now.getMinutes()}${now.getSeconds()}` + const thereDumpFileName = `dbdump-${envName}-${timeStamp}.sqlfc` + + console.info('dumping db on the server') + // `pg_dump -Fc -h $hostname -p $port -U $username -d $dbname > [prod]-[date].sqlfc` + const pgDumpArgs = [ + '-Fc', + '-h', + dbSecrets.host, + '-p', + dbSecrets.port.toString(), + '-U', + dbSecrets.user, + '-d', + dbSecrets.dbname, + '-f', + thereDumpFileName, + ] + const result = await ssh.exec('pg_dump', pgDumpArgs, { + stdin: dbSecrets.password, // pg_dump asks for the password on stdin so we pass it here + stream: 'both', // This triggers a result response type, undocumented trash. + }) + if (result.code !== 0) { + console.error('PG Dump Failed on server.') + console.error(result.stderr) + process.exit(1) + } + + await ssh.getFile(thereDumpFileName, thereDumpFileName) + console.info('copied db locally: ', thereDumpFileName) + + // remove the file on the server + await ssh.exec('rm', [thereDumpFileName]) + } catch (err) { + console.error('ssh failed out', err) + process.exit(1) + } + + // stop jumpbox + if (stopAfter) { + const stopRes = await stopInstance(jumpboxInstanceID) + if (stopRes instanceof Error) { + console.error('Stopping instance failed', stopRes) + process.exit(1) + } + console.info('Stopped jumpbox') + } + + process.exit(0) +} + +export { cloneDBLocally } diff --git a/dev_tool/src/nodeWrappers.ts b/dev_tool/src/nodeWrappers.ts new file mode 100644 index 0000000000..392ee3f7cb --- /dev/null +++ b/dev_tool/src/nodeWrappers.ts @@ -0,0 +1,56 @@ +import http from 'node:http' +import fs from 'node:fs' + +// httpRequest wraps node's http request in a promise. +// Right now this only supports getting a URL, but could be extended to cover the whole api +async function httpRequest(url: string): Promise { + return new Promise(function (resolve, _reject) { + const req = http.request(url, function (res) { + // reject on bad status + if ( + !res.statusCode || + res.statusCode < 200 || + res.statusCode >= 300 + ) { + return resolve(new Error('statusCode=' + res.statusCode)) + } + // accumulate data + const body: Uint8Array[] = [] + res.on('data', function (chunk) { + body.push(chunk) + }) + // resolve on end + res.on('end', function () { + try { + const bodyString = Buffer.concat(body).toString() + resolve(bodyString) + } catch (e) { + console.error('body Error', e) + resolve(e) + } + }) + }) + // reject on request error + req.on('error', function (err) { + // This is not a "Second reject", just a different sort of failure + resolve(err) + }) + // IMPORTANT + req.end() + }) +} + +// fileExists returns whether or not a file exists +function fileExists(path: string): boolean | Error { + try { + fs.statSync(path) + return true + } catch (err) { + if (err.code === 'ENOENT') { + return false + } + return err + } +} + +export { httpRequest, fileExists } diff --git a/dev_tool/src/retry.ts b/dev_tool/src/retry.ts new file mode 100644 index 0000000000..0f26d9eb3e --- /dev/null +++ b/dev_tool/src/retry.ts @@ -0,0 +1,26 @@ +// retry takes in a function that must return bool or error. true means that the function succeeded, error means an error occured, false means keep trying +async function retry( + body: () => Promise, + duration: number +): Promise { + const timeout = new Date().getTime() + duration + while (new Date().getTime() < timeout) { + const result = await body() + if (result === true) { + return undefined + } else if (result instanceof Error) { + return result + } + + // wait a second before checking again + await new Promise((resolve) => { + setTimeout(() => { + resolve() + }, 1000) + }) + } + + return new Error('Retry timed out') +} + +export { retry } diff --git a/docs/Configuration.md b/docs/Configuration.md index caee859e83..0136eaf640 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -211,9 +211,14 @@ We plan to move this to the DB down the road. Until then, know that if these val *[same in prod/val]* This the help address displayed in state emails for contacting the analyst review team. -#### `/configuration/email/dmcp` +#### `/configuration/email/dmcpSubmission` + +*[environment specific]* This contains the DMCP primary inbox. The DMCP team is focused on policy issues related to managed care. They review all submissions, excluding CHIP and state of PR. This inbox is also primarily used for internal communication between DMCP, OACT, and DMCO. + +#### `/configuration/email/dmcpReview` + +*[environment specific]* This contains the DMCP inbox for external communication and Q&A notifications. -*[environment specific]* This contains the DMCP primary inbox. The DMCP team is focused on policy issues related to managed care. They review all submissions, excluding CHIP and state of PR. #### `/configuration/email/oact` @@ -223,6 +228,7 @@ We plan to move this to the DB down the road. Until then, know that if these val *[environment specific]* This contains the DMCO primary inbox. The DMCO team is focused on managed care contracts and they review all submissions. + #### `/configuration/email/reviewTeamAddresses` *[environment specific]* List of emails for dev teams/individuals that want to follow all emails. In prod, this is two addresses associated with MC-Review dev team. diff --git a/docs/README.md b/docs/README.md index 4a620a493c..2c6c63738b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,6 @@ ## Additional folders with technical details and history -- The [technical design folder](./technical-design/) contains technical design documents and how-tos for developer workflows. +- The [Technical Design folder](./technical-design/) contains technical design documents, feature briefs, and how-tos for developer workflows. - The [Architectural Decision Records(ADRs) folder](./architectural-decision-records/) summarizes major technical decisions in application development. - The [Templates folder](./templates/) contains markdown templates for creating new documentation. diff --git a/docs/architectural-decision-records/011-typescript-error-handling.md b/docs/architectural-decision-records/011-typescript-error-handling.md index f04f9af45d..3e61ab970e 100644 --- a/docs/architectural-decision-records/011-typescript-error-handling.md +++ b/docs/architectural-decision-records/011-typescript-error-handling.md @@ -8,7 +8,7 @@ Sometimes the code we call will error. There are a number of different ways to h ### Functions that can error return their success type unioned with the Error type -For example, our postgres store functions like insertDraftSubmission should return the DraftSubmissionType that was inserted when the insertion is successful, or a StoreError type if not. So the signature will look like: +For example, our postgres store functions like insertDraftSubmission should return the DraftSubmissionType that was inserted when the insertion is successful, or an Error type if not. So the signature will look like: ``` function insertDraftSubmission(args: {...}): DraftSubmissionType | Error diff --git a/docs/architectural-decision-records/025-use-clamavd-for-uploads-scanning.md b/docs/architectural-decision-records/025-use-clamavd-for-uploads-scanning.md new file mode 100644 index 0000000000..9a4e830066 --- /dev/null +++ b/docs/architectural-decision-records/025-use-clamavd-for-uploads-scanning.md @@ -0,0 +1,99 @@ +--- +title: Move AV scanning to persistant clamavd +--- + +## ADR 0025 — Move AV scanning to persistant clamavd + +- Status: Decided +- Date: 2023-12-05 + +## Decision Drivers + +CMS requires that all files uploaded to S3 go through a virus scanning process. When beginning this project, the MacPro “quickstart” template included a method of virus scanning that relies on S3 bucket events triggering an AWS Lambda. This lambda uses ClamAV’s clamscan tool in a layer, which scans the file and tags it with results. If a file is deemed infected, a policy is added to that file which prevents anyone from downloading it. + +This process takes around 30 seconds for a scan to complete, almost all of it (29s) being the time it takes for ClamAV to start up and load virus definitions from disk. To our knowledge, other CMS teams using this system don’t include virus scanning in their application’s UX – if a file is uploaded by a user and is later found to be infected, the user will not know until they go to download the file at a later date and receive an error. + +MC-Review found skipping scanning to be suboptimal UX, instead we scan the files as they are uploaded and won’t let a user proceed in the application flow until all files are deemed clean. + +Since this takes around 30 seconds to complete, a major pain point has arisen around Cypress tests. Cypress will often flake waiting for a file upload and scan to complete. This causes a lot of issues in CI, as engineers have to re-run the CI suites to get a passing test due to flakes. We also have no good way of knowing when a ClamAV scan is done, so our workaround has been to poll the S3 bucket until we get back a 200 (the assumption being that a file is not available for download until the ‘CLEAN’ tag and associated policy are added allowing downloads). + +Additionally, users have had flakes around document upload as well. We’ve decided that we need a better way to do scanning that is faster for our users (and Cypress)., + +## Constraints + +Include any decisions explicitly out of scope for this ADR. +​ + +## Considered Options + +### Option 1: Cloud Storage Security Tool (CSST – AWS Marketplace) + +This is a tool that CMS suggested and is approved in their environments. It deploys (via CloudFormation) a system that has always-on virus scanning instances. It deploys quite a lot of infrastructure: + +- 1-2 Services and Tasks (Scanning Agents) to existing region cluster. This is used to run the scanning agents that process the objects. + +- 1 ECS Cluster and 1-2 Services and Tasks in each additional region you scan buckets. This is used to run the scanning agents in new regions. + +- SNS Topic, SQS Queue, S3 Bucket events, CloudWatch Log Groups --> Streams. These are used to keep track of the object work. + +#### Pros and Cons + +- `+` We don’t have to write it ourselves. Third party is responsible for building it. +- `+` There’s a virus scanning UI that they provide to view usage/stats. +- `-` We can’t manage this with our own CI +- `-` CMS cloud support would have to either run the initial install or give us increased user permissions to create IAM roles and other things required. +- `-` Lots of infra resources are being created, which seems like overkill and is expensive. + +### Option 2: bucketAV (AWS Marketplace) + +Similar to the Cloud Storage Security Tool (CSST) above, a SaaS that deploys an always-on virus scanning infrastructure via CloudFormation. Uses less resources than the other tool. + +#### Pros and Cons + +- `+` Uses an ec2 instance instead of Docker + Fargate. Overall a smaller install footprint +- `+` Cheaper than CSST +- `-` A con of the option + +### Option 3: Change UX to async virus scanning + +Currently we wait on the virus scanning lambda to mark files CLEAN or INFECTED before letting a user step through the application. The other teams that use this virus scanning setup don’t do this – they let users upload files and don’t worry about the tags on the file. If a file is infected, the user is just not allowed to download that file again and they return an error message. + +We discussed changing the UX to accommodate this in DTBM with product & design. + +#### Pros and Cons + +- `+` We can continue using the serverless infra that other MACPro teams are using for av scanning +- `+` No change to CI +- `+` Would align with how other teams are doing av scanning +- `+` Better experience for users as they don’t have to wait on scanning at all +- `-` Would require changes to UX and frontend code +- `-` Users wouldn’t get alerted immediately when virus scanning errors occur. At best they would get alerted at review & submit and need to be rerouted to earlier stages of the form to re-upload files. + +### Option 4: Write a clamavd server ourselves that is always-on + +This option would introduce an always on clamavd process in our infrastructure that our lambda could call for scanning. This would largely mean the lambda would stay the same, but instead of calling the clamscan tool that is local to the lambda layer (incurring startup costs), we’d call out to an always on daemon. Two options were considered: + +#### A: Connecting over an API + +We’d need to put the file on an EFS mount shared between the lambda and the clamavd server. We’d then call a small API that we’d need to write that would take the file path of the file to scan and trigger a scan of the file, sending results back over to the lambda. + +#### B: Connecting over a TCP socket + +clamdscan can be configured to connect to a clamavd instance either locally or remotely. In this case we would configure a new lambda layer to have the clamdscan tool and the appropriate config files. We’d also be building the ec2 instance that runs and configures clamavd. + +The major difference here is that if we can rely on the clamavd server to accept connections over TCP we won’t have to write and maintain a special API to receive scan requests on the ClamAV server. + +#### Pros and Cons + +- `+` Less infra than other options – just an ec2 instance. +- `+` Would not need to change our av scan lambda much +- `+` We can deploy this with CI +- `-` We have to write and maintain the ec2 instance +- `-` We have to maintain the lambda layer +- `-` We have to write and maintain the API shim (for option a) + +## Chosen Solution + +Chosen option: Write our own clamavd server that is always on, connecting over a TCP socket. + +This seemed to give us the most flexibility while reducing the time users spend in the document upload phase waiting on AV scanning. We won't have to use a third party tool that is outside of our current Serverless deployment pipelines. There should be very little change to our lambda that handles our current scanning while adding a minimal amount of additional infrastructure. diff --git a/docs/architectural-decision-records/026-jwt-lambda-authorizer-for-api-access.md b/docs/architectural-decision-records/026-jwt-lambda-authorizer-for-api-access.md new file mode 100644 index 0000000000..f134b3e805 --- /dev/null +++ b/docs/architectural-decision-records/026-jwt-lambda-authorizer-for-api-access.md @@ -0,0 +1,55 @@ +--- +title: Use a JWT Lambda Authorizer to enable 3rd party access to our API +--- +## ADR 0026 — Use a JWT Lambda Authorizer to enable 3rd party access to our API + +- Status: Decided +- Date: 2023-12-12 + +## Decision Drivers + +We are granting another team access to our API which up until now has only been used by our very own app-web. This access pattern is very different from one of our end users. End users log in using IDM to acquire a 30 minute web session. This tool is going to be hitting our API on a regular basis indefinitely from within a CMS datacenter. We need to be able to furnish them with long lived credentials that grant them access to our API . + +## Considered Options + +### Lambda Authorizer for API Gateway + +Create a new API gateway endpoint that uses a Lambda Authorizer to protect access to our API. API clients would send a JWT along with all requests that the authorizer would validate. + +**Pro** +* Very simple for clients to implement, just add a token that we control to the header +* expiration is built in to JWT to enforce rotation +* No need to get CMS Cloud involved to manage users/keys + +**Con** +* opens up a new way to access our API not protected by AWS IAM roles +* requires changing how app-api works since there wont be cognito user information embedded in requests + +### Add API IAM user with long lived creds + +Ask CMS Cloud to create a user in our accounts that can have long lived AWS credentials. It's unclear if that's something they would allow, but we could ask. Then with those creds they could assume the CognitoUser role and sign requests to API gateway the same way that IDM users do. + +**Pro** +* no changes to our API internals +* no changes to our API security surface + +**Con** +* Clients have to figure out how to use AWS API Gateway tooling to correctly sign requests +* Unknown wether we can get what we need from CMS Cloud + +### Use STS to grant Role to external IAM User Accounts + +Have clients set up their own AWS IAM User in their own AWS account that they can get long lived credentials for. We can craft a trust policy that allows a user from an external AWS account to assume our CognitoUser role temporarily. Then they could sign requests the same way that IDM users do. + +**Pro** +* no changes to our API internals +* no changes to our API security surface +* no need for creating our own AWS accounts with long lived credentials + +**Con** +* Clients would need to set up/have their own AWS accounts & infra +* Clients have to figure out how to use AWS API Gateway tooling to correctly sign requests + +## Chosen Solution + +We're going to setup a Lambda Authorizer to grant access to our API. This plan provides thorough security and is simplest for our users to adopt. Plus, it gives us control over API access credentials without involving the complexity of IAM users and roles which are heavily managed. diff --git a/docs/technical-design/auth-context.md b/docs/technical-design/auth-context.md deleted file mode 100644 index 60fcf6b3c1..0000000000 --- a/docs/technical-design/auth-context.md +++ /dev/null @@ -1,13 +0,0 @@ -# AuthContext and the session expiration warning modal - -We use a third-party authentication provider which logs out sessions due to inactivity. We don't have direct access to their timekeeping, but we do know how long they allow a session to be inactive. We store that number in our `MINUTES_UNTIL_SESSION_EXPIRES` feature flag. We then show a warning modal some number of minutes prior to the end of the inactivity timer, with the precise number set by our `MODAL_COUNTDOWN_DURATION` feature flag. - -We use several variables in AuthContext to help us do this work. - -- sessionExpirationTime: The date and time at which the session will expire -- updateSessionExpirationTime: The method to extend the session when the user is active -- logoutCountdownDuration: This mirrors the value of `MODAL_COUNTDOWN_DURATION`, _**converted to seconds**_ -- setLogoutCountdownDuration: The method to decrement the countdown duration for display in the modal -- sessionIsExpiring: The boolean that tracks whether we're inside the countdown duration -- updateSessionExpirationState: The method to set `sessionIsExpiring` -- checkIfSessionsIsAboutToExpire: The method that checks the current time on an interval to determine if it's within `MODAL_COUNTDWON_DURATION` of the session expiration time diff --git a/docs/technical-design/database-diagram.md b/docs/technical-design/database-diagram.md index 5a94c1cf9a..ea0bc53b18 100644 --- a/docs/technical-design/database-diagram.md +++ b/docs/technical-design/database-diagram.md @@ -8,22 +8,7 @@ State { Int latestStateSubmissionNumber String stateCode } -HealthPlanPackageTable { - String stateCode - String id -} -HealthPlanRevisionTable { - DateTime createdAt - String pkgID - String formDataProto - DateTime submittedAt - DateTime unlockedAt - String unlockedBy - String unlockedReason - String submittedBy - String submittedReason - String id -} + Question { String pkgID DateTime createdAt @@ -66,24 +51,15 @@ QuestionResponseDocument { String id } -State ||--o{ HealthPlanPackageTable : stateCode -HealthPlanPackageTable ||--o{ HealthPlanRevisionTable : pkgID -HealthPlanPackageTable ||--o{ Question : pkgID +State ||--o{ ContractTable : stateCode +ContractTable ||--o{ Question : pkgID User ||--o{ Question : addedByUserID User ||--o{ QuestionReponse : addedByUserID Question ||--o{ QuestionDocument : questionID Question ||--o{ QuestionReponse: questionID QuestionReponse ||--o{ QuestionResponseDocument: questionResponseID User }o--o{ State : "" -``` - -## Contract & Rates DB - -This is the db diagram for the contracts and rates work, still disconnected from the rest of the db. - -```mermaid -erDiagram ContractTable { String id @@ -155,5 +131,4 @@ User { String id } - ``` diff --git a/docs/technical-design/email-notifications.md b/docs/technical-design/email-notifications.md new file mode 100644 index 0000000000..250a2a4375 --- /dev/null +++ b/docs/technical-design/email-notifications.md @@ -0,0 +1,99 @@ +# Email Notifications + +Certain contract actions in the MC-Review app will initiate email notifications sent to the State and CMS. These actions include: +- New contract submission +- Unlocking a submitted contract +- Resubmitting an unlocked contract +- New Q&A question submitted +- New Q&A response submitted + +The email recipients for each action vary depending on contract data, such as contract type, the state the contract applies to, and the designated state contacts for the contract. The CMS email recipients for contract submission, unlock, and resubmission are generated using the helper function `generateCMSReviewerEmails`. + +### New contract submission +- #### State email receivers: + - **stateContacts**: All contacts in the latest submitted contract revision's `formData.stateContacts`. + - **submitterEmails**: User emails that submitted/resubmitted the contract. In resolvers, `submitterEmails` is generated by `contractSubmitters` function that loops through revisions and adding submitInfo.updatedBy emails to a string array. + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. +- #### CMS email receivers: + - `CONTRACT_ONLY` submissions + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **dmcpEmails**: DMCP primary inbox from parameter store `/configuration/email/dmcp`. + - `CONTRACT_AND_RATES` submissions + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **dmcpEmails**: DMCP primary inbox from parameter store `/configuration/email/dmcp`. + - **oactEmails**: OACT primary inbox from parameter store `/configuration/email/oact`. + - `CHIP` and State of `PR` submissions + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. +### Unlocking a submitted contract +- #### State email receivers: + - **stateContacts**: All contacts in the latest submitted contract revision's `formData.stateContacts`. + - **submitterEmails**: User emails that submitted/resubmitted the contract. In resolvers, `submitterEmails` is generated by `contractSubmitters` function that loops through revisions and adding submitInfo.updatedBy emails to a string array. + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. +- #### CMS email receivers: + - `CONTRACT_ONLY` submissions + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **dmcpEmails**: DMCP primary inbox from parameter store `/configuration/email/dmcp`. + - `CONTRACT_AND_RATES` submissions + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **dmcpEmails**: DMCP primary inbox from parameter store `/configuration/email/dmcp`. + - **oactEmails**: OACT primary inbox from parameter store `/configuration/email/oact`. + - `CHIP` and State of `PR` submissions + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. +### Resubmitting an unlocked contract +- #### State email receivers: + - **stateContacts**: All contacts in the latest submitted contract revision's `formData.stateContacts`. + - **submitterEmails**: User emails that submitted/resubmitted the contract. In resolvers, `submitterEmails` is generated by `contractSubmitters` function that loops through revisions and adding submitInfo.updatedBy emails to a string array. + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. +- #### CMS email receivers: + - `CONTRACT_ONLY` submissions + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **dmcpEmails**: DMCP primary inbox from parameter store `/configuration/email/dmcp`. + - `CONTRACT_AND_RATES` submissions + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **dmcpEmails**: DMCP primary inbox from parameter store `/configuration/email/dmcp`. + - **oactEmails**: OACT primary inbox from parameter store `/configuration/email/oact`. + - `CHIP` and State of `PR` submissions + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. +### New Q&A question submitted +- #### State email receivers: + - **stateContacts**: All contacts in the latest submitted contract revision's `formData.stateContacts`. + - **submitterEmails**: User emails that submitted/resubmitted the contract. In resolvers, `submitterEmails` is generated by `contractSubmitters` function that loops through revisions and adding submitInfo.updatedBy emails to a string array. + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. +- #### CMS email receivers: + - Questions from `DMCO` + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - Questions from `DMCP` + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **dmcpReviewEmails**: DMCP inbox for external communication and Q&A notifications `/configuration/email/dmcpReview`. + - Questions from `OACT` + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **oactEmails**: OACT primary inbox from parameter store `/configuration/email/oact`. +### New Q&A response submitted +- #### State email receivers: + - **stateContacts**: All contacts in the latest submitted contract revision's `formData.stateContacts`. + - **submitterEmails**: User emails that submitted/resubmitted the contract. In resolvers, `submitterEmails` is generated by `contractSubmitters` function that loops through revisions and adding submitInfo.updatedBy emails to a string array. + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. +- #### CMS email receivers: + - Responses to Questions from `DMCO` + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - Responses to Questions from `DMCP` + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **dmcpReviewEmails**: DMCP inbox for external communication and Q&A notifications `/configuration/email/dmcpReview`. + - Responses to Questions from `OACT` + - **devReviewTeamEmails**: Dev team email address from parameter store `/configuration/email/reviewTeamAddresses`. + - **stateAnalystsEmails**: specific state analyst emails from parameter store `/configuration/[STATE]/stateanalysts/email`. + - **oactEmails**: OACT primary inbox from parameter store `/configuration/email/oact`. diff --git a/docs/technical-design/feature-brief-session-expiration.md b/docs/technical-design/feature-brief-session-expiration.md new file mode 100644 index 0000000000..f497fdaafc --- /dev/null +++ b/docs/technical-design/feature-brief-session-expiration.md @@ -0,0 +1,41 @@ +# Feature brief: Session Expiration + +## Introduction + +We use a third-party authentication provider ([IDM](https://confluenceent.cms.gov/display/IDM/IDM+Trainings+and+Guides)) which automatically logs out sessions due to inactivity after about 30 minutes. We don't have direct access to their timekeeping, but we know this is about how long they allow a session to be inactive. Thus, we manually track sessions internally for the same time period. + +Importantly this featured is permanently feature-flagged since we have different requirements between production/staging and lower environments. More details about feature flags [below](#implementation-details). + +## Expected behavior + Two minutes before the session will expire due to inactivity we show a warning modal. The modal displays a live countdown and has CTA buttons with 1. the ability to log out immediately 2. the ability to extend the session. This helps us fulfill accessibility requirements around [WCAG 2.2.1 Timing Adjustable](https://www.w3.org/WAI/WCAG21/Understanding/timing-adjustable.html). + +![session expiration modal](../../.images/session-expiring.png) + +### Possible outcomes after the session expiration modal is displayed: +1. If the user chooses to logout, we redirect to landing page. +2. If the user extends the session, we refresh their tokens and restart our counter for the session. +3. If the user takes no action and the browser is still active, the 2 minute countdown will complete and the user will be automatically logged out and a error banner will appear on the landing page notifying them what happened. + +![session expired banner - relevant for outcome 3](../../.images/session-expired-banner.png) + +This auto logout behavior will happen even if the browser tab is in the background. Either way we are keep track of time and the session expiration flow will work as expected. + +## Known edge cases +These are edge cases we decided not to address. Documenting for visibility. + +- The user puts computer to sleep while logged in (before session expiration modal is visible) and comes back after the session has expired. + - In this case, the session expiration modal will not display. The user will still appear to be logged in when they relaunch their computer. However, as soon as the user takes an action that hits the API and we get a 403, we will follow the code path for outcome #3 above - automatically log the user and show the session expired error banner. +- The user has MC-Review open in multiple tabs and then logs out of only one tab manually before session expiration. + - This is not an ideal user experience. This is why we recommend users navigate the application in one tab at time. If the user logs out then goes to another tab that is still open and starts using the application, they will be able to make some requests with the cached user but at a certain point the requests will error (this may or may not be auth errors, sometimes the API may fail first with 400s and thus the generic failed request banner will show) + +## Implementation details +- There are two evergreen feature flags (`SESSION_EXPIRING_MODAL` and `MINUTES_UNTIL_SESSION_EXPIRES`) associated with this feature. + - `SESSION_EXPIRING_MODAL` allows turning this feature off on dev and review apps since it can disrupt Cypress. Expect that VAL and PROD have this flag turned on permanently. + - `MINUTES_UNTIL_SESSION_EXPIRES` allows us shorten the session expiration time to turn on modal quickly and test functionality. Expect that VAL and PROD the default 30 min is used. +- Primary logic for the feature is found in `AuthContext.tsx` + - `MODAL_COUNTDOWN_DURATION` is the hard-coded constant that holds the amount of time the modal will be visible prior to logout for inactivity. It is set to 2 minutes. + - use of `session-timeout`query param to ensure error banner displays + - `sessionExpirationTime` is the date and time at which the session will expire and `updateSessionExpirationTime` is the method to extend the session when the user is active + - `setLogoutCountdownDuration` used to decrement the countdown duration for display in the modal + - `sessionIsExpiring` boolean tracks whether we're inside the countdown duration (if true, modal is visible) this is updated with `updateSessionExpirationState` + - `checkIfSessionsIsAboutToExpire` check current session time on an interval in the background. It determines if it is time to show modal diff --git a/docs/technical-design/future-designs/qa-db.md b/docs/technical-design/future-designs/qa-db.md deleted file mode 100644 index 558c5a904d..0000000000 --- a/docs/technical-design/future-designs/qa-db.md +++ /dev/null @@ -1,135 +0,0 @@ -# Q&A Database Design Proposal - -## Document Upload Design - -```mermaid -erDiagram - -HealthPlanPackageTable ||--|{ HealthPlanRevisionTable: "has many" -HealthPlanPackageTable }|--|| State: "has many" - -HealthPlanPackageTable { - string id - string stateCode -} - -HealthPlanRevisionTable { - string id - string pkgID - datetime createdAt - bytes formDataProto - datetime submittedAt - string submittedBy - string submittedReason - datetime unlockedAt - string unlockedBy - string unlockedReason -} - -State { - string stateCode - string name - int latestSubmissionNumber -} - -User { - int id - string givenName - string familyName - string email - string role -} - -User }|--|{ State: "many to many" - -QuestionRound { - string id - string pkgID - string name - string requestedBy - datetime createdAt - datetime requestedAt -} - -QuestionRound ||--|| User: "requestedBy" - -QuestionRound ||--|{ Document: "has many" -HealthPlanPackageTable ||--|{ QuestionRound: "has many" - -Document { - string id - string name - string s3URL - string uploadedBy - datetime createdAt - bool virusScan -} - -``` - -## Single Questions Upload Design - -```mermaid -erDiagram - -HealthPlanPackageTable ||--|{ HealthPlanRevisionTable: "has many" -HealthPlanPackageTable }|--|| State: "has many" - -HealthPlanPackageTable { - string id - string stateCode -} - -HealthPlanRevisionTable { - string id - string pkgID - datetime createdAt - bytes formDataProto - datetime submittedAt - string submittedBy - string submittedReason - datetime unlockedAt - string unlockedBy - string unlockedReason -} - -State { - string stateCode - string name - int latestSubmissionNumber -} - -User { - int id - string givenName - string familyName - string email - string role -} - -User }|--|{ State: "many to many" - -CMSQuestion { - string id - string pkgID - string text - string requestedBy - datetime createdAt - datetime requestedAt -} - -StateAnswer { - string id - string questionId - string text - string answeredBy - datetime createdAt -} - -CMSQuestion ||--|| User: "requestedBy" -StateAnswer ||--|| User: "answeredBy" - -CMSQuestion ||--|{ StateAnswer: "has many" -HealthPlanPackageTable ||--|{ CMSQuestion: "has many" - -``` diff --git a/docs/technical-design/howto-migrations.md b/docs/technical-design/howto-migrations.md index 12d910c0c4..30adbc740a 100644 --- a/docs/technical-design/howto-migrations.md +++ b/docs/technical-design/howto-migrations.md @@ -48,7 +48,7 @@ This type of migration is run as standalone lambda that developers must manually 1. Start the app. 1. When the app is running, connect to the local database so that you can inspect the changes you're making. Use the DATABASE_URL from your .envrc.local file to connect to the database interface of your choice. You can connect via the terminal, or an app like TablePlus, dBeaver, Postico, DataGrip, etc. 1. Run some Cypress tests to populate the database. The CMSWorkflow tests are a good choice, since they create two entries which contain rates, one of which has a revision. -1. Verify that in the local database you can see entries in the HealthPlanPackageTable and HealthPlanRevisionTable. +1. Verify that in the local database you can see entries in the ContractTable and ContractRevisionTable. 1. Now you're ready to run your lambda. In a different terminal instance, run `DIRENV ALLOW` again. 1. Navigate to `services/app-api`. 1. Run `npx serverless invoke local --function `name_of_your_lambda``. You should see it build with webpack, then run. Any log statements you've put in your code will display in this window. @@ -71,18 +71,7 @@ This type of migration is run as standalone lambda that developers must manually ## How to dump VAL data for local testing -Dependencies: -- [Jumpbox access exists for your IP](../../services/postgres/README.md#access-to-aurora-postgres-via-aws-jump-box) -- `postgresql` installed locally +1. use `./dev jumpbox clone val` to clone the val database to your local machine -1. SSH to instance of db you will copy and make sure you have DB secrets available for that environment db. - - See [SSH directions](../../services/postgres/README.md#ssh-to-the-instances) and [postgres credentials docs](../../services/postgres/README.md#accessing-postgres-authentication-credentials) - -1. Dump database. - - `pg_dump -Fc -h $hostname -p $port -U $username -d $dbname > [val]-[date].sqlfc`. You will have to enter the db password (also in the secrets). - -1. Exit SSH. Copy down from from remote to the current directory. - - `scp ubuntu@$host:[val]-[date].sqlfc .` - -1. From current directory, start up localhost db with the copied db - - `pg_restore -h localhost -p 5432 -U postgres -d postgres --clean val]-[date].sqlfc`. You will be promoted to enter in local db password `shhhsecret`. You will see print out errors but the database has spun up successfully. \ No newline at end of file +2. Load that db dump into your local running postgres instance + - `pg_restore -h localhost -p 5432 -U postgres -d postgres --clean val-[date].sqlfc`. You will be promoted to enter in local db password `shhhsecret`. You will see print out errors but the database has spun up successfully. diff --git a/docs/technical-design/howto-update-state-programs.md b/docs/technical-design/howto-update-state-programs.md index 5eb222c6b5..3da740b4bb 100644 --- a/docs/technical-design/howto-update-state-programs.md +++ b/docs/technical-design/howto-update-state-programs.md @@ -10,8 +10,7 @@ The source of truth for that file comes from a CSV maintained by product and des ## Steps 1. Download the latest version of csv from google docs when prompted by product/design. -2. Run the script following the command listed in the `import-programs.ts`. -3. Overwrite existing state programs JSON with the new output. Your usage of the script will likely look something like this: `cd scripts && yarn tsc && node import-programs.js path/to/data.csv > ../services/app-web/src/common-code/data/statePrograms.json` -4. Double check the diff. It's important not to delete any programs that have already been used for a submission because although programs are not in the database, we still store references to the program ID in postgres as if they are stable. Also, we want to be sure we are only changing programs expected to change. -5. For any newly created programs, manually populate the `id` field usings a UUID generator -6. Make a PR to update the statePrograms file in the codebase +2. Run the script following the command listed in the `import-programs.ts`. This will overwrite existing state programs JSON with the new output. Your usage of the script will likely look something like this: `cd scripts && yarn tsc && node import-programs.js path/to/data.csv > ../services/app-web/src/common-code/data/statePrograms.json` +3. Double check the diff. It's important not to delete any programs that have already been used for a submission because although programs are not in the database, we still store references to the program ID in postgres as if they are stable. Also, we want to be sure we are only changing programs expected to change. +4. For any newly created programs, manually populate the `id` field using a UUID generator +5. Make a PR to update the statePrograms file in the codebase diff --git a/docs/technical-design/resovler-design.md b/docs/technical-design/resovler-design.md new file mode 100644 index 0000000000..a50bb607a2 --- /dev/null +++ b/docs/technical-design/resovler-design.md @@ -0,0 +1,64 @@ +# Resolver design + +## Background +Resolver functions are responsible for handling requests made to the MC-Review GraphQL API and populating data for the response on the backend. Between receiving the request data and returning a response, the `app-api` resolvers perform various tasks for generating the response. + +These tasks include: +- Business logic, data transformation, and validations +- Database operations +- Third party API calls + - SES Email + - LaunchDarkly Feature Flags +- Error handling + +The resolvers often do not directly contain the code to perform these tasks, instead they call functions specific for each task. Importantly we use types defined in our [domain models](design-patterns.md#domain-models) as the internal communication interface on the backend for different services as an attempt to decrease the coupling of our code. + +Many of these functions are accessible from the resolver via [dependency injection](design-patterns.md#dependency-injection) and configured in [apollo_gql.ts](../../services/app-api/src/handlers). There are a few reasons to use dependency injection approach for the resolvers, but the two main points for the API are: +- Configuration of the dependencies for deployment environments. +- Configuration of the dependencies for unit tests. Configuration can be found in [gqlHelpers.ts](../../services/app-api/src/testHelpers/gqlHelpers.ts). + + +Errors from these functions propagate up to the resolver where it will be handled and a response sent to the client. See docs about [error handling](error-handling.md) for details. + +### Diagram +[Miro link](https://miro.com/app/board/o9J_lS5oLDk=/?moveToWidget=3458764573512051070&cot=14) +![resolver-design-diagram](../../.images/resolver-design-diagram.png) + +## General Guidance +### Postgres Module +[Postgres](https://www.postgresql.org/docs/) database operations are contained in functions that we call **postgres handlers**. You can find all the handlers in [`services/app-api/src/postgres/`](../../services/app-api/src/postgres). In these handler functions the database operations are performed using [Prisma ORM](https://www.prisma.io/docs/orm) which itself uses Prisma generated types and returns data from the database as these types. So in most of the handlers, data from the operation must be converted to a domain model type before returning to the resolver. Any errors occurring in the handler functions should be returned to the resolver to handle. + +It's important that data from the DB is converted to the domain model before returning to the resolver to adhere to our strategy of using the domain model as the internal communication protocol for different parts of our app. + +Resolvers are passed Postgres handlers via [dependency injection](design-patterns.md#dependency-injection). The configuration for the Postgres handler dependency and many others are done in [apollo_gql.ts](../../services/app-api/src/handlers). + +The diagram below is the data flow diagram for `createHealthPlanPackage` resolver. + +[Miro link](https://miro.com/app/board/o9J_lS5oLDk=/?moveToWidget=3458764573517610448&cot=14) +![postgres-handler-diagram](../../.images/postgres-handler-diagram.png) + +Form the diagram above, you can see that `createHealthPlanPackage` resolver calls `insertDraftContract` handler function in the Postgres Module to create a new draft contract. + +Notice the arrow coming from the resolver to the handler does not directly connect to the `Prisma ORM` this is because most handlers will do more data transformations and validations before passing them off to the `Prisma ORM` to perform the `Postgres` operation. + +After performing the operation the `Postgres` data returned to `Prisma ORM` is passed into `parseContractWithHistory`, which is a function that coverts our Prisma model to domain model before returning data to the resolver. + +Any errors that occurs in the handlers should be returned to the resolver to handle. + +### Feature Flag Module +### Parameter Store Module +### Email Notifications Module +### Proto Module +### Validations +### Error handling + +## Related Documentation +### Internal +- [Domain Models](design-patterns.md#domain-models) +- [Dependency Injection](design-patterns.md#dependency-injection) +- [Email Notifications](email-notifications.md) +- [Creating endpoints](creating-and-testing-endpoints.md) +- [Error Handling](error-handling.md) +### External +- [Postgres](https://www.postgresql.org/docs/) +- [Prisma ORM](https://www.prisma.io/docs/orm) diff --git a/docs/technical-design/submission-diagram.md b/docs/technical-design/submission-diagram.md index 293614cce6..79aa49c367 100644 --- a/docs/technical-design/submission-diagram.md +++ b/docs/technical-design/submission-diagram.md @@ -97,7 +97,7 @@ CapitationRatesAmendedInfo { Document { string name string S3URL - array documentCategories + string sha256 } diff --git a/docs/templates/feature-brief-template.md b/docs/templates/feature-brief-template.md new file mode 100644 index 0000000000..73b578fe07 --- /dev/null +++ b/docs/templates/feature-brief-template.md @@ -0,0 +1,23 @@ +--- +title: Feature Brief Template +--- + +# Feature brief: Name of feature +e.g. "File Upload" or "Session Expiration Modal". This type of documentation useful for cases where a product brief was not written but a written outline of design and functionality is needed for future developers. + +## Introduction + +Discussion of the feature high level and the engineering approach. Clearly outline any design choices. + +## Expected behavior +This is the happy path behavior for the feature. May include wireframes. + +1. +2. +3. + +### Error paths or known edge cases +- Discuss known error paths and/or edge cases + +## Implementation details +- Link to files in codebase and discuss any variables or methods that are not readily understood from code comments. \ No newline at end of file diff --git a/package.json b/package.json index 3150907256..27aa9d3f14 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "husky": "^8.0.1", "lerna": "^7.0.0", "lint-staged": "^14.0.1", - "prettier": "^2.4.1" + "prettier": "^3.1.0" }, "dependencies": { "cypress-file-upload": "^5.0.8", diff --git a/services/app-api/package.json b/services/app-api/package.json index 02544ba272..287f933d2a 100644 --- a/services/app-api/package.json +++ b/services/app-api/package.json @@ -50,7 +50,7 @@ "@opentelemetry/semantic-conventions": "^1.9.1", "apollo-server-core": "^3.11.1", "apollo-server-lambda": "^3.5.0", - "archiver": "^5.3.0", + "archiver": "^6.0.1", "axios": "^1.1.3", "eta": "^2.0.0", "graphql": "^16.2.0", @@ -66,7 +66,7 @@ "devDependencies": { "@graphql-tools/jest-transform": "^2.0.0", "@prisma/client": "^4.6", - "@types/archiver": "^5.1.1", + "@types/archiver": "^6.0.2", "@types/aws-lambda": "^8.10.83", "@types/glob": "^8.0.0", "@types/jest": "^29.5.6", @@ -77,13 +77,13 @@ "copy-webpack-plugin": "^11.0.0", "csv-parser": "^3.0.0", "eslint": "^8.3.0", - "eslint-config-prettier": "^8.3.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^27.0.1", "eslint-plugin-prettier": "^4.0.0", "graphql-tag": "^2.12.5", "jest": "^29.6.2", "lint-staged": "^14.0.1", - "prettier": "^2.4.1", + "prettier": "^3.1.0", "prisma": "^4.6", "serverless": "^3.27.0", "serverless-associate-waf": "^1.2.1", diff --git a/services/app-api/prisma/schema.prisma b/services/app-api/prisma/schema.prisma index 7481812ab7..62c7854c66 100644 --- a/services/app-api/prisma/schema.prisma +++ b/services/app-api/prisma/schema.prisma @@ -392,13 +392,6 @@ enum ContractType { AMENDMENT } -enum DocumentCategory { - CONTRACT - RATES - CONTRACT_RELATED - RATES_RELATED -} - enum ActuaryCommunication { OACT_TO_ACTUARY OACT_TO_STATE diff --git a/services/app-api/scripts/prepare-prisma-layer.sh b/services/app-api/scripts/prepare-prisma-layer.sh index 182667a72f..1e3e6ade82 100755 --- a/services/app-api/scripts/prepare-prisma-layer.sh +++ b/services/app-api/scripts/prepare-prisma-layer.sh @@ -10,7 +10,6 @@ function preparePrismaLayer() { mkdir -p lambda-layers-prisma-client-migration/nodejs/node_modules/@prisma/engines mkdir -p lambda-layers-prisma-client-migration/nodejs/node_modules/prisma mkdir -p lambda-layers-prisma-client-migration/nodejs/prisma - mkdir -p lambda-layers-prisma-client-migration/nodejs/protoMigrator mkdir -p lambda-layers-prisma-client-migration/nodejs/dataMigrations mkdir -p lambda-layers-prisma-client-migration/nodejs/gen @@ -43,7 +42,6 @@ function preparePrismaLayer() { rsync -av prisma/ lambda-layers-prisma-client-engine/nodejs/prisma echo "Copy proto migration files to layer..." - rsync -av ../app-proto/build/ lambda-layers-prisma-client-migration/nodejs/protoMigrator rsync -av ../app-proto/gen/ lambda-layers-prisma-client-migration/nodejs/gen rsync -av ../../node_modules/uuid/ lambda-layers-prisma-client-migration/nodejs/node_modules/uuid diff --git a/services/app-api/serverless.yml b/services/app-api/serverless.yml index b5fc62aaa1..a020028d12 100644 --- a/services/app-api/serverless.yml +++ b/services/app-api/serverless.yml @@ -152,39 +152,6 @@ functions: email_submit: handler: src/handlers/email_submit.main - add_sha: - handler: src/handlers/add_sha.main - layers: - - !Ref PrismaClientEngineLambdaLayer - - arn:aws:lambda:us-east-1:901920570463:layer:aws-otel-nodejs-amd64-ver-1-9-1:1 - timeout: 300 - vpc: - securityGroupIds: - - ${self:custom.sgId} - subnetIds: ${self:custom.privateSubnets} - - proto_to_db: - handler: src/handlers/proto_to_db.main - layers: - - !Ref PrismaClientEngineLambdaLayer - - arn:aws:lambda:us-east-1:901920570463:layer:aws-otel-nodejs-amd64-ver-1-9-1:1 - timeout: 300 - vpc: - securityGroupIds: - - ${self:custom.sgId} - subnetIds: ${self:custom.privateSubnets} - - migrate_rate_documents: - handler: src/handlers/migrate_rate_documents.main - layers: - - !Ref PrismaClientEngineLambdaLayer - - arn:aws:lambda:us-east-1:901920570463:layer:aws-otel-nodejs-amd64-ver-1-9-1:1 - timeout: 300 - vpc: - securityGroupIds: - - ${self:custom.sgId} - subnetIds: ${self:custom.privateSubnets} - health: handler: src/handlers/health_check.main events: @@ -242,6 +209,7 @@ functions: method: post cors: true authorizer: aws_iam + timeout: 60 # zipping large amount of files hits default time out of 6 seconds cleanup: handler: src/handlers/cleanup.main diff --git a/services/app-api/src/authn/cognitoAuthn.ts b/services/app-api/src/authn/cognitoAuthn.ts index ebf22e6695..2049179c61 100644 --- a/services/app-api/src/authn/cognitoAuthn.ts +++ b/services/app-api/src/authn/cognitoAuthn.ts @@ -9,7 +9,6 @@ import { import type { UserType } from '../domain-models' import { performance } from 'perf_hooks' import type { Store, InsertUserArgsType } from '../postgres' -import { isStoreError } from '../postgres' import { isValidCmsDivison } from '../domain-models' export function parseAuthProvider( @@ -238,7 +237,7 @@ export async function userFromCognitoAuthProvider( } const result = await store.insertUser(userToInsert) - if (isStoreError(result)) { + if (result instanceof Error) { console.error(`Could not insert user: ${JSON.stringify(result)}`) return cognitoUserResult } @@ -273,9 +272,9 @@ export async function lookupUserAurora( userID: string ): Promise { const userFromPG = await store.findUser(userID) - if (isStoreError(userFromPG)) { + if (userFromPG instanceof Error) { return new Error( - `Error looking up user in postgres: ${userFromPG.code}: ${userFromPG.message}` + `Error looking up user in postgres: ${userFromPG.message}` ) } diff --git a/services/app-api/src/authn/localAuthn.ts b/services/app-api/src/authn/localAuthn.ts index b0ccbc66fb..22317e7935 100644 --- a/services/app-api/src/authn/localAuthn.ts +++ b/services/app-api/src/authn/localAuthn.ts @@ -2,7 +2,6 @@ import type { Result } from 'neverthrow' import { ok, err } from 'neverthrow' import type { UserType } from '../domain-models/index' import type { Store, InsertUserArgsType } from '../postgres' -import { isStoreError } from '../postgres' import { lookupUserAurora } from './cognitoAuthn' export async function userFromLocalAuthProvider( @@ -55,7 +54,7 @@ export async function insertUserToLocalAurora( const result = await store.insertUser(userToInsert) - if (isStoreError(result)) { + if (result instanceof Error) { console.error(`Could not insert user: ${JSON.stringify(result)}`) return localUser } diff --git a/services/app-api/src/domain-models/contractAndRates/contractTypes.ts b/services/app-api/src/domain-models/contractAndRates/contractTypes.ts index e732224d69..eef03f44a5 100644 --- a/services/app-api/src/domain-models/contractAndRates/contractTypes.ts +++ b/services/app-api/src/domain-models/contractAndRates/contractTypes.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import { contractRevisionWithRatesSchema } from './revisionTypes' import { statusSchema } from './statusType' +import { pruneDuplicateEmails } from '../../emailer/formatters' // Contract represents the contract specific information in a submission package // All that data is contained in revisions, each revision represents the data in a single submission @@ -26,6 +27,22 @@ const draftContractSchema = contractSchema.extend({ type ContractType = z.infer type DraftContractType = z.infer -export { contractRevisionWithRatesSchema, draftContractSchema, contractSchema } +function contractSubmitters(contract: ContractType): string[] { + const submitters: string[] = [] + contract.revisions.forEach( + (revision) => + revision.submitInfo?.updatedBy && + submitters.push(revision.submitInfo?.updatedBy) + ) + + return pruneDuplicateEmails(submitters) +} + +export { + contractRevisionWithRatesSchema, + draftContractSchema, + contractSchema, + contractSubmitters, +} export type { ContractType, DraftContractType } diff --git a/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts b/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts index ae306635c1..2bf4aa672c 100644 --- a/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts +++ b/services/app-api/src/domain-models/contractAndRates/convertContractWithRatesToHPP.ts @@ -142,11 +142,9 @@ function convertContractWithRatesToFormData( rateCapitationType, rateDocuments: rateDocuments.map((doc) => ({ ...doc, - documentCategories: ['RATES'], })) as SubmissionDocument[], supportingDocuments: supportingDocuments.map((doc) => ({ ...doc, - documentCategories: ['RATES_RELATED'], })) as SubmissionDocument[], rateAmendmentInfo: rateAmendmentInfo, rateDateStart, @@ -178,14 +176,12 @@ function convertContractWithRatesToFormData( addtlActuaryContacts: [...pkgAdditionalCertifyingActuaries], documents: contractRev.formData.supportingDocuments.map((doc) => ({ ...doc, - documentCategories: ['CONTRACT_RELATED'], })) as SubmissionDocument[], contractType: contractRev.formData.contractType, contractExecutionStatus: contractRev.formData.contractExecutionStatus, contractDocuments: contractRev.formData.contractDocuments.map( (doc) => ({ ...doc, - documentCategories: ['CONTRACT'], }) ) as SubmissionDocument[], contractDateStart: contractRev.formData.contractDateStart, diff --git a/services/app-api/src/domain-models/contractAndRates/index.ts b/services/app-api/src/domain-models/contractAndRates/index.ts index 6c2a8baad1..5ffaed57cf 100644 --- a/services/app-api/src/domain-models/contractAndRates/index.ts +++ b/services/app-api/src/domain-models/contractAndRates/index.ts @@ -1,6 +1,10 @@ export { rateSchema, draftRateSchema } from './rateTypes' -export { contractSchema, draftContractSchema } from './contractTypes' +export { + contractSchema, + draftContractSchema, + contractSubmitters, +} from './contractTypes' export { contractFormDataSchema, rateFormDataSchema } from './formDataTypes' diff --git a/services/app-api/src/domain-models/index.ts b/services/app-api/src/domain-models/index.ts index 38f4ab6cff..6eda7c5662 100644 --- a/services/app-api/src/domain-models/index.ts +++ b/services/app-api/src/domain-models/index.ts @@ -31,6 +31,7 @@ export { export { convertContractWithRatesRevtoHPPRev, convertContractWithRatesToUnlockedHPP, + contractSubmitters, } from './contractAndRates' export type { diff --git a/services/app-api/src/emailer/emailer.ts b/services/app-api/src/emailer/emailer.ts index 031345e8ab..09c06464fb 100644 --- a/services/app-api/src/emailer/emailer.ts +++ b/services/app-api/src/emailer/emailer.ts @@ -8,12 +8,21 @@ import { unlockPackageStateEmail, resubmitPackageStateEmail, resubmitPackageCMSEmail, + sendQuestionStateEmail, + sendQuestionCMSEmail, + sendQuestionResponseCMSEmail, + sendQuestionResponseStateEmail, } from './' import type { LockedHealthPlanFormDataType, UnlockedHealthPlanFormDataType, } from '../../../app-web/src/common-code/healthPlanFormDataType' -import type { UpdateInfoType, ProgramType } from '../domain-models' +import type { + UpdateInfoType, + ProgramType, + ContractRevisionWithRatesType, + Question, +} from '../domain-models' import { SESServiceException } from '@aws-sdk/client-ses' // See more discussion of configuration in docs/Configuration.md @@ -30,7 +39,8 @@ type EmailConfiguration = { */ devReviewTeamEmails: string[] // added by default to all incoming submissions oactEmails: string[] // OACT division emails - dmcpEmails: string[] // DMCP division emails + dmcpReviewEmails: string[] // DMCP division emails for reviews + dmcpSubmissionEmails: string[] // DMCP division emails for submissions dmcoEmails: string[] // DMCO division emails /* Email addresses used in display text @@ -56,9 +66,11 @@ type EmailData = { bodyHTML?: string } +type SendEmailFunction = (emailData: EmailData) => Promise + type Emailer = { config: EmailConfiguration - sendEmail: (emailData: EmailData) => Promise + sendEmail: SendEmailFunction sendCMSNewPackage: ( formData: LockedHealthPlanFormDataType, stateAnalystsEmails: StateAnalystsEmails, @@ -93,27 +105,48 @@ type Emailer = { stateAnalystsEmails: StateAnalystsEmails, statePrograms: ProgramType[] ) => Promise + sendQuestionsStateEmail: ( + contract: ContractRevisionWithRatesType, + submitterEmails: string[], + statePrograms: ProgramType[], + question: Question + ) => Promise + sendQuestionsCMSEmail: ( + contract: ContractRevisionWithRatesType, + stateAnalystsEmails: StateAnalystsEmails, + statePrograms: ProgramType[], + questions: Question[] + ) => Promise + sendQuestionResponseCMSEmail: ( + contractRevision: ContractRevisionWithRatesType, + statePrograms: ProgramType[], + stateAnalystsEmails: StateAnalystsEmails, + currentQuestion: Question, + allContractQuestions: Question[] + ) => Promise + sendQuestionResponseStateEmail: ( + contractRevision: ContractRevisionWithRatesType, + statePrograms: ProgramType[], + submitterEmails: string[], + currentQuestion: Question, + allContractQuestions: Question[] + ) => Promise } +const localEmailerLogger = (emailData: EmailData) => + console.info(` + EMAIL SENT + ${'(¯`·.¸¸.·´¯`·.¸¸.·´¯·.¸¸.·´¯`·.¸¸.·´¯`·.¸¸.·´¯`·.¸¸.·´)'} + ${JSON.stringify(getSESEmailParams(emailData))} + ${'(¯`·.¸¸.·´¯`·.¸¸.·´¯·.¸¸.·´¯`·.¸¸.·´¯`·.¸¸.·´¯`·.¸¸.·´)'} + `) -function newSESEmailer(config: EmailConfiguration): Emailer { +function emailer( + config: EmailConfiguration, + sendEmail: SendEmailFunction +): Emailer { return { config, - sendEmail: async (emailData: EmailData): Promise => { - const emailRequestParams = getSESEmailParams(emailData) - - try { - await sendSESEmail(emailRequestParams) - return - } catch (err) { - if (err instanceof SESServiceException) { - return new Error( - 'SES email send failed. Error: ' + JSON.stringify(err) - ) - } - - return new Error('SES email send failed. Error: ' + err) - } - }, + sendEmail, sendCMSNewPackage: async function ( formData, stateAnalystsEmails, @@ -224,137 +257,117 @@ function newSESEmailer(config: EmailConfiguration): Emailer { return await this.sendEmail(emailData) } }, - } -} - -const localEmailerLogger = (emailData: EmailData) => - console.info(` - EMAIL SENT - ${'(¯`·.¸¸.·´¯`·.¸¸.·´¯·.¸¸.·´¯`·.¸¸.·´¯`·.¸¸.·´¯`·.¸¸.·´)'} - ${JSON.stringify(getSESEmailParams(emailData))} - ${'(¯`·.¸¸.·´¯`·.¸¸.·´¯·.¸¸.·´¯`·.¸¸.·´¯`·.¸¸.·´¯`·.¸¸.·´)'} - `) - -function newLocalEmailer(config: EmailConfiguration): Emailer { - return { - config, - sendEmail: async (emailData: EmailData): Promise => { - localEmailerLogger(emailData) - }, - sendCMSNewPackage: async ( - formData, - stateAnalystsEmails, - statePrograms - ) => { - const result = await newPackageCMSEmail( - formData, - config, - stateAnalystsEmails, - statePrograms - ) - if (result instanceof Error) { - console.error(result) - return result - } else { - localEmailerLogger(result) - } - }, - sendStateNewPackage: async ( - formData, + sendQuestionsStateEmail: async function ( + contract, submitterEmails, - statePrograms - ) => { - const result = await newPackageStateEmail( - formData, + statePrograms, + question + ) { + const emailData = await sendQuestionStateEmail( + contract, submitterEmails, config, - statePrograms + statePrograms, + question ) - if (result instanceof Error) { - console.error(result) - return result + if (emailData instanceof Error) { + return emailData } else { - localEmailerLogger(result) + return await this.sendEmail(emailData) } }, - sendUnlockPackageCMSEmail: async ( - formData, - updateInfo, + sendQuestionsCMSEmail: async function ( + contract, stateAnalystsEmails, - statePrograms - ) => { - const emailData = await unlockPackageCMSEmail( - formData, - updateInfo, - config, + statePrograms, + questions + ) { + const emailData = await sendQuestionCMSEmail( + contract, stateAnalystsEmails, - statePrograms + config, + statePrograms, + questions ) if (emailData instanceof Error) { return emailData } else { - localEmailerLogger(emailData) + return await this.sendEmail(emailData) } }, - sendUnlockPackageStateEmail: async ( - formData, - updateInfo, + sendQuestionResponseCMSEmail: async function ( + contractRevision, statePrograms, - submitterEmails - ) => { - const emailData = await unlockPackageStateEmail( - formData, - updateInfo, + stateAnalystsEmails, + currentQuestion, + allContractQuestions + ) { + const emailData = await sendQuestionResponseCMSEmail( + contractRevision, config, statePrograms, - submitterEmails + stateAnalystsEmails, + currentQuestion, + allContractQuestions ) if (emailData instanceof Error) { return emailData } else { - localEmailerLogger(emailData) + return await this.sendEmail(emailData) } }, - sendResubmittedStateEmail: async ( - formData, - updateInfo, + sendQuestionResponseStateEmail: async function ( + contractRevision, + statePrograms, submitterEmails, - statePrograms - ) => { - const emailData = await resubmitPackageStateEmail( - formData, - submitterEmails, - updateInfo, + currentQuestion, + allContractQuestions + ) { + const emailData = await sendQuestionResponseStateEmail( + contractRevision, config, - statePrograms + submitterEmails, + statePrograms, + allContractQuestions, + currentQuestion ) if (emailData instanceof Error) { return emailData } else { - localEmailerLogger(emailData) + return await this.sendEmail(emailData) } }, - sendResubmittedCMSEmail: async ( - formData, - updateInfo, - stateAnalystsEmails, - statePrograms - ) => { - const emailData = await resubmitPackageCMSEmail( - formData, - updateInfo, - config, - stateAnalystsEmails, - statePrograms + } +} + +const sendSESEmails = async (emailData: EmailData): Promise => { + const emailRequestParams = getSESEmailParams(emailData) + + try { + await sendSESEmail(emailRequestParams) + return + } catch (err) { + if (err instanceof SESServiceException) { + return new Error( + 'SES email send failed. Error: ' + JSON.stringify(err) ) - if (emailData instanceof Error) { - return emailData - } else { - localEmailerLogger(emailData) - } - }, + } + + return new Error('SES email send failed. Error: ' + err) } } -export { newLocalEmailer, newSESEmailer } +function newSESEmailer(config: EmailConfiguration): Emailer { + return emailer(config, sendSESEmails) +} + +const sendLocalEmails = async (emailData: EmailData): Promise => { + localEmailerLogger(emailData) +} + +function newLocalEmailer(config: EmailConfiguration): Emailer { + return emailer(config, sendLocalEmails) +} + +export { newLocalEmailer, newSESEmailer, emailer } export type { Emailer, EmailConfiguration, EmailData, StateAnalystsEmails } diff --git a/services/app-api/src/emailer/emails/__snapshots__/sendQuestionCMSEmail.test.ts.snap b/services/app-api/src/emailer/emails/__snapshots__/sendQuestionCMSEmail.test.ts.snap new file mode 100644 index 0000000000..82115559bd --- /dev/null +++ b/services/app-api/src/emailer/emails/__snapshots__/sendQuestionCMSEmail.test.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders overall email for a new question as expected 1`] = ` +"DMCO sent questions to the state for submission MCR-MN-0003-SNBC
+Sent by: Ronald McDonald (DMCO) cms@email.com +
+Round: 1
+Date: 01/01/2024
+
+View submission Q&A +" +`; diff --git a/services/app-api/src/emailer/emails/__snapshots__/sendQuestionResponseCMSEmail.test.ts.snap b/services/app-api/src/emailer/emails/__snapshots__/sendQuestionResponseCMSEmail.test.ts.snap new file mode 100644 index 0000000000..24c38425bb --- /dev/null +++ b/services/app-api/src/emailer/emails/__snapshots__/sendQuestionResponseCMSEmail.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders overall CMS email for a new state response as expected 1`] = ` +"The state submitted responses to OACT's questions about MCR-MN-0003-SNBC
+Submitted by: James Brown james@example.com
+Round: 2
+Questions sent on: 02/03/2024
+
+View submission Q&A +" +`; diff --git a/services/app-api/src/emailer/emails/__snapshots__/sendQuestionResponseStateEmail.test.ts.snap b/services/app-api/src/emailer/emails/__snapshots__/sendQuestionResponseStateEmail.test.ts.snap new file mode 100644 index 0000000000..eec78d8a93 --- /dev/null +++ b/services/app-api/src/emailer/emails/__snapshots__/sendQuestionResponseStateEmail.test.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders overall email for a new response as expected 1`] = ` +"DMCO round 1 response was successfully submitted
+
+Date: 01/01/2024
+
+View response +
+
+What comes next:
+
    +
  1. Questions: You may receive additional questions from CMS as they conduct their reviews.
  2. +
  3. Decision: Once all questions have been addressed, CMS will contact you with their final recommendation.
  4. +
+
+If you need assistance or to make changes to your submission:
+
    +
  • For assistance with programmatic, contractual, or operational issues, please reach out to MCOGDMCOActions@cms.hhs.gov and/or your CMS primary contact.
  • +
  • For assistance on policy and actuarial issues, please reach out to MMCratesetting@cms.hhs.gov.
  • +
  • For issues related to MC-Review or all other inquiries, please reach out to MC_Review_HelpDesk@cms.hhs.gov.
  • +
+ +" +`; diff --git a/services/app-api/src/emailer/emails/__snapshots__/sendQuestionStateEmail.test.ts.snap b/services/app-api/src/emailer/emails/__snapshots__/sendQuestionStateEmail.test.ts.snap new file mode 100644 index 0000000000..6960507fe4 --- /dev/null +++ b/services/app-api/src/emailer/emails/__snapshots__/sendQuestionStateEmail.test.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders overall email for a new question as expected 1`] = ` +"CMS asked questions about MCR-MN-0003-SNBC
+Sent by: Ronald McDonald (DMCO) cms@email.com +
+Date: 01/01/2024
+
+You must answer the question before CMS can continue reviewing it.
+
+Open the submission in MC-Review to answer questions + +" +`; diff --git a/services/app-api/src/emailer/emails/index.ts b/services/app-api/src/emailer/emails/index.ts index f47f5f840b..a4692ec0c8 100644 --- a/services/app-api/src/emailer/emails/index.ts +++ b/services/app-api/src/emailer/emails/index.ts @@ -4,3 +4,7 @@ export { unlockPackageCMSEmail } from './unlockPackageCMSEmail' export { unlockPackageStateEmail } from './unlockPackageStateEmail' export { resubmitPackageCMSEmail } from './resubmitPackageCMSEmail' export { resubmitPackageStateEmail } from './resubmitPackageStateEmail' +export { sendQuestionStateEmail } from './sendQuestionStateEmail' +export { sendQuestionCMSEmail } from './sendQuestionCMSEmail' +export { sendQuestionResponseCMSEmail } from './sendQuestionResponseCMSEmail' +export { sendQuestionResponseStateEmail } from './sendQuestionResponseStateEmail' diff --git a/services/app-api/src/emailer/emails/newPackageCMSEmail.test.ts b/services/app-api/src/emailer/emails/newPackageCMSEmail.test.ts index 030d202799..edf4b674c5 100644 --- a/services/app-api/src/emailer/emails/newPackageCMSEmail.test.ts +++ b/services/app-api/src/emailer/emails/newPackageCMSEmail.test.ts @@ -27,8 +27,7 @@ test('to addresses list includes review team email addresses', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } testEmailConfig().devReviewTeamEmails.forEach((emailAddress) => { @@ -51,8 +50,7 @@ test('to addresses list includes OACT and DMCP group emails for contract and rat ) if (template instanceof Error) { - console.error(template) - return + throw template } testEmailConfig().oactEmails.forEach((emailAddress) => { @@ -63,7 +61,7 @@ test('to addresses list includes OACT and DMCP group emails for contract and rat ) }) - testEmailConfig().dmcpEmails.forEach((emailAddress) => { + testEmailConfig().dmcpSubmissionEmails.forEach((emailAddress) => { expect(template).toEqual( expect.objectContaining({ toAddresses: expect.arrayContaining([emailAddress]), @@ -92,8 +90,7 @@ test('to addresses list does not include OACT and DMCP group emails for CHIP su ) if (template instanceof Error) { - console.error(template) - return + throw template } testEmailConfig().oactEmails.forEach((emailAddress) => { @@ -104,7 +101,7 @@ test('to addresses list does not include OACT and DMCP group emails for CHIP su ) }) - testEmailConfig().dmcpEmails.forEach((emailAddress) => { + testEmailConfig().dmcpSubmissionEmails.forEach((emailAddress) => { expect(template).not.toEqual( expect.objectContaining({ toAddresses: expect.arrayContaining([emailAddress]), @@ -124,8 +121,7 @@ test('to addresses list does not include help addresses', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -155,8 +151,7 @@ test('to addresses list does not include duplicate review email addresses', asyn ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template.toAddresses).toEqual(['duplicate@example.com']) @@ -180,8 +175,7 @@ test('subject line is correct', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -208,8 +202,7 @@ test('includes expected data summary for a contract only submission', async () = ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -247,7 +240,6 @@ test('includes expected data summary for a contract and rates submission CMS ema s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -281,8 +273,7 @@ test('includes expected data summary for a contract and rates submission CMS ema ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -335,7 +326,6 @@ test('includes expected data summary for a multi-rate contract and rates submiss s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -364,7 +354,6 @@ test('includes expected data summary for a multi-rate contract and rates submiss s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -393,7 +382,6 @@ test('includes expected data summary for a multi-rate contract and rates submiss s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -432,8 +420,7 @@ test('includes expected data summary for a multi-rate contract and rates submiss ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -517,7 +504,6 @@ test('includes expected data summary for a contract amendment submission', async s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -551,8 +537,7 @@ test('includes expected data summary for a contract amendment submission', async ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -599,7 +584,6 @@ test('includes expected data summary for a rate amendment submission CMS email', s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -636,8 +620,7 @@ test('includes expected data summary for a rate amendment submission CMS email', ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -674,8 +657,7 @@ test('includes link to submission', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -699,8 +681,7 @@ test('includes state specific analyst on contract only submission', async () => ) if (template instanceof Error) { - console.error(template) - return + throw template } const reviewerEmails = [ @@ -728,8 +709,7 @@ test('includes state specific analyst on contract and rate submission', async () ) if (template instanceof Error) { - console.error(template) - return + throw template } const reviewerEmails = [ @@ -758,8 +738,7 @@ test('does not include state specific analyst on contract and rate submission', ) if (template instanceof Error) { - console.error(template) - return + throw template } testStateAnalystEmails.forEach((emailAddress) => { @@ -782,8 +761,7 @@ test('includes oactEmails on contract and rate submission', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } const reviewerEmails = [ @@ -810,8 +788,7 @@ test('does not include oactEmails on contract only submission', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } const ratesReviewerEmails = [...testEmailConfig().oactEmails] @@ -839,8 +816,7 @@ test('CHIP contract only submission does include state specific analysts emails' ) if (template instanceof Error) { - console.error(template) - return + throw template } testStateAnalystEmails.forEach((emailAddress) => { @@ -864,7 +840,6 @@ test('CHIP contract and rate submission does include state specific analysts ema s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -896,8 +871,7 @@ test('CHIP contract and rate submission does include state specific analysts ema ) if (template instanceof Error) { - console.error(template) - return + throw template } testStateAnalystEmails.forEach((emailAddress) => { @@ -923,8 +897,7 @@ test('CHIP contract only submission does not include oactEmails', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } const excludedEmails = [...testEmailConfig().oactEmails] @@ -950,7 +923,6 @@ test('CHIP contract and rate submission does not include oactEmails', async () = s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -981,8 +953,7 @@ test('CHIP contract and rate submission does not include oactEmails', async () = ) if (template instanceof Error) { - console.error(template) - return + throw template } const excludedEmails = [...testEmailConfig().oactEmails] @@ -1006,8 +977,7 @@ test('does not include rate name on contract only submission', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -1030,7 +1000,6 @@ test('renders overall email as expected', async () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -1059,7 +1028,6 @@ test('renders overall email as expected', async () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], diff --git a/services/app-api/src/emailer/emails/newPackageStateEmail.test.ts b/services/app-api/src/emailer/emails/newPackageStateEmail.test.ts index c16657803e..28dee5c07e 100644 --- a/services/app-api/src/emailer/emails/newPackageStateEmail.test.ts +++ b/services/app-api/src/emailer/emails/newPackageStateEmail.test.ts @@ -26,8 +26,7 @@ test('to addresses list includes submitter emails', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -62,8 +61,7 @@ test('to addresses list includes all state contacts on submission', async () => ) if (template instanceof Error) { - console.error(template) - return + throw template } sub.stateContacts.forEach((contact) => { @@ -100,8 +98,7 @@ test('to addresses list does not include duplicate state receiver emails on subm ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template.toAddresses).toEqual([ @@ -129,8 +126,7 @@ test('subject line is correct and clearly states submission is complete', async ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -161,8 +157,7 @@ test('includes mcog, rate, and team email addresses', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -204,8 +199,7 @@ test('includes link to submission', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -235,8 +229,7 @@ test('includes information about what is next', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -259,7 +252,6 @@ test('includes expected data summary for a contract and rates submission State e s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -293,8 +285,7 @@ test('includes expected data summary for a contract and rates submission State e ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -347,7 +338,6 @@ test('includes expected data summary for a multi-rate contract and rates submiss s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -376,7 +366,6 @@ test('includes expected data summary for a multi-rate contract and rates submiss s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -405,7 +394,6 @@ test('includes expected data summary for a multi-rate contract and rates submiss s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -445,8 +433,7 @@ test('includes expected data summary for a multi-rate contract and rates submiss ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -517,7 +504,6 @@ test('includes expected data summary for a rate amendment submission State email s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -554,8 +540,7 @@ test('includes expected data summary for a rate amendment submission State email ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -600,7 +585,6 @@ test('renders overall email for a new package with a rate amendment as expected' s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], diff --git a/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.test.ts b/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.test.ts index ebb4ff2dc2..b2a97c9468 100644 --- a/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.test.ts +++ b/services/app-api/src/emailer/emails/resubmitPackageCMSEmail.test.ts @@ -39,8 +39,7 @@ describe('with rates', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -65,7 +64,6 @@ describe('with rates', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -98,8 +96,7 @@ describe('with rates', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -166,7 +163,6 @@ describe('with rates', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -195,7 +191,6 @@ describe('with rates', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -224,7 +219,6 @@ describe('with rates', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -263,8 +257,7 @@ describe('with rates', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } //Expect only have 3 rate names using regex to match name pattern specific to rate names. @@ -320,11 +313,10 @@ describe('with rates', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } - testEmailConfig().dmcpEmails.forEach((emailAddress) => { + testEmailConfig().dmcpSubmissionEmails.forEach((emailAddress) => { expect(template).toEqual( expect.objectContaining({ toAddresses: expect.arrayContaining([emailAddress]), @@ -360,8 +352,7 @@ describe('with rates', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -395,8 +386,7 @@ describe('with rates', () => { ] if (template instanceof Error) { - console.error(template) - return + throw template } reviewerEmails.forEach((emailAddress) => { @@ -426,7 +416,6 @@ describe('with rates', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -458,8 +447,7 @@ describe('with rates', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } testStateAnalystEmails.forEach((emailAddress) => { @@ -482,7 +470,6 @@ describe('with rates', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -515,8 +502,7 @@ describe('with rates', () => { const excludedEmails = [...testEmailConfig().oactEmails] if (template instanceof Error) { - console.error(template) - return + throw template } excludedEmails.forEach((emailAddress) => { @@ -721,7 +707,6 @@ test('renders overall email as expected', async () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -750,7 +735,6 @@ test('renders overall email as expected', async () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], diff --git a/services/app-api/src/emailer/emails/resubmitPackageStateEmail.test.ts b/services/app-api/src/emailer/emails/resubmitPackageStateEmail.test.ts index 4a703584a9..c29245e402 100644 --- a/services/app-api/src/emailer/emails/resubmitPackageStateEmail.test.ts +++ b/services/app-api/src/emailer/emails/resubmitPackageStateEmail.test.ts @@ -27,7 +27,6 @@ const submission: LockedHealthPlanFormDataType = { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -68,8 +67,7 @@ test('contains correct subject and clearly states successful resubmission', asyn ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -94,8 +92,7 @@ test('includes expected data summary for a contract and rates resubmission State ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -152,7 +149,6 @@ test('includes expected data summary for a multi-rate contract and rates resubmi s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -181,7 +177,6 @@ test('includes expected data summary for a multi-rate contract and rates resubmi s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -210,7 +205,6 @@ test('includes expected data summary for a multi-rate contract and rates resubmi s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -249,8 +243,7 @@ test('includes expected data summary for a multi-rate contract and rates resubmi ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -317,8 +310,7 @@ test('renders overall email as expected', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template.bodyHTML).toMatchSnapshot() diff --git a/services/app-api/src/emailer/emails/sendQuestionCMSEmail.test.ts b/services/app-api/src/emailer/emails/sendQuestionCMSEmail.test.ts new file mode 100644 index 0000000000..daefe7e903 --- /dev/null +++ b/services/app-api/src/emailer/emails/sendQuestionCMSEmail.test.ts @@ -0,0 +1,241 @@ +import { + testEmailConfig, + mockContractRev, + mockMNState, + mockQuestionAndResponses, +} from '../../testHelpers/emailerHelpers' +import type { CMSUserType, StateType, Question } from '../../domain-models' +import { packageName } from 'app-web/src/common-code/healthPlanFormDataType' +import { sendQuestionCMSEmail } from './index' +import { getTestStateAnalystsEmails } from '../../testHelpers/parameterStoreHelpers' + +const stateAnalysts = getTestStateAnalystsEmails('FL') + +const flState: StateType = { + stateCode: 'FL', + name: 'Florida', +} + +const cmsUser: CMSUserType = { + id: '1234', + role: 'CMS_USER', + divisionAssignment: 'DMCO', + familyName: 'McDonald', + givenName: 'Ronald', + email: 'cms@email.com', + stateAssignments: [flState], +} + +const questions: Question[] = [ + mockQuestionAndResponses({ + id: 'test-question-id-1', + addedBy: cmsUser, + division: 'DMCO', + }), +] + +test('to addresses list only includes state analyst when a DMCO user submits a question', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + + const template = await sendQuestionCMSEmail( + sub, + stateAnalysts, + testEmailConfig(), + defaultStatePrograms, + questions + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.not.objectContaining({ + toAddresses: expect.arrayContaining([ + ...testEmailConfig().oactEmails, + ...testEmailConfig().dmcpReviewEmails, + ]), + }) + ) + + expect(template).toEqual( + expect.objectContaining({ + toAddresses: expect.arrayContaining([...stateAnalysts]), + }) + ) +}) + +test('to addresses list includes state analyst and OACT group emails when an OACT user submits a question', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const oactUser: CMSUserType = { + ...cmsUser, + divisionAssignment: 'OACT', + } + const questionsFromOACT: Question[] = [ + { + ...questions[0], + addedBy: oactUser, + }, + ] + const template = await sendQuestionCMSEmail( + sub, + stateAnalysts, + testEmailConfig(), + defaultStatePrograms, + questionsFromOACT + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + toAddresses: expect.arrayContaining([ + ...stateAnalysts, + ...testEmailConfig().oactEmails, + ]), + }) + ) +}) + +test('to addresses list includes state analyst and DMCP group emails when a DMCP user submits a question', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const dmcpUser: CMSUserType = { + ...cmsUser, + divisionAssignment: 'DMCP', + } + const questionsFromDMCP: Question[] = [ + { + ...questions[0], + addedBy: dmcpUser, + }, + ] + const template = await sendQuestionCMSEmail( + sub, + stateAnalysts, + testEmailConfig(), + defaultStatePrograms, + questionsFromDMCP + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + toAddresses: expect.arrayContaining([ + ...stateAnalysts, + ...testEmailConfig().dmcpReviewEmails, + ]), + }) + ) +}) + +test('subject line is correct', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const name = packageName( + sub.contract.stateCode, + sub.contract.stateNumber, + sub.formData.programIDs, + defaultStatePrograms + ) + + const template = await sendQuestionCMSEmail( + sub, + stateAnalysts, + testEmailConfig(), + defaultStatePrograms, + questions + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + subject: expect.stringContaining(`Questions sent for ${name}`), + bodyText: expect.stringContaining(`${name}`), + }) + ) +}) + +test('includes link to the question response page', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionCMSEmail( + sub, + stateAnalysts, + testEmailConfig(), + defaultStatePrograms, + questions + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringMatching(/View submission Q&A/), + bodyHTML: expect.stringContaining( + `http://localhost/submissions/${sub.contract.id}/question-and-answer` + ), + }) + ) +}) + +test('includes expected data on the CMS analyst who sent the question', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + + const template = await sendQuestionCMSEmail( + sub, + stateAnalysts, + testEmailConfig(), + defaultStatePrograms, + questions + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining( + 'Sent by: Ronald McDonald (DMCO) cms@email.com (cms@email.com)' + ), + }) + ) + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining('Date: 01/01/2024'), + }) + ) +}) + +test('renders overall email for a new question as expected', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const result = await sendQuestionCMSEmail( + sub, + stateAnalysts, + testEmailConfig(), + defaultStatePrograms, + questions + ) + + if (result instanceof Error) { + console.error(result) + return + } + + expect(result.bodyHTML).toMatchSnapshot() +}) diff --git a/services/app-api/src/emailer/emails/sendQuestionCMSEmail.ts b/services/app-api/src/emailer/emails/sendQuestionCMSEmail.ts new file mode 100644 index 0000000000..c8ae547ec5 --- /dev/null +++ b/services/app-api/src/emailer/emails/sendQuestionCMSEmail.ts @@ -0,0 +1,83 @@ +import { packageName as generatePackageName } from '../../../../app-web/src/common-code/healthPlanFormDataType' +import { formatCalendarDate } from '../../../../app-web/src/common-code/dateHelpers' +import { pruneDuplicateEmails } from '../formatters' +import type { EmailConfiguration, EmailData, StateAnalystsEmails } from '..' +import type { ProgramType, Question } from '../../domain-models' +import { + stripHTMLFromTemplate, + renderTemplate, + findContractPrograms, + getQuestionRound, +} from '../templateHelpers' +import { submissionQuestionResponseURL } from '../generateURLs' +import type { ContractRevisionWithRatesType } from '../../domain-models/contractAndRates' + +export const sendQuestionCMSEmail = async ( + contractRev: ContractRevisionWithRatesType, + stateAnalystsEmails: StateAnalystsEmails, + config: EmailConfiguration, + statePrograms: ProgramType[], + questions: Question[] +): Promise => { + const newQuestion = questions[questions.length - 1] + let receiverEmails = [...stateAnalystsEmails, ...config.devReviewTeamEmails] + if (newQuestion.addedBy.divisionAssignment === 'DMCP') { + receiverEmails.push(...config.dmcpReviewEmails) + } else if (newQuestion.addedBy.divisionAssignment === 'OACT') { + receiverEmails.push(...config.oactEmails) + } + receiverEmails = pruneDuplicateEmails(receiverEmails) + + //This checks to make sure all programs contained in submission exists for the state. + const packagePrograms = findContractPrograms(contractRev, statePrograms) + if (packagePrograms instanceof Error) { + return packagePrograms + } + + const packageName = generatePackageName( + contractRev.contract.stateCode, + contractRev.contract.stateNumber, + contractRev.formData.programIDs, + packagePrograms + ) + + const questionResponseURL = submissionQuestionResponseURL( + contractRev.contract.id, + config.baseUrl + ) + const questionRound = getQuestionRound(questions, newQuestion) + + if (questionRound instanceof Error) { + return questionRound + } + + const data = { + packageName, + questionResponseURL, + cmsRequestorEmail: newQuestion.addedBy.email, + cmsRequestorName: `${newQuestion.addedBy.givenName} ${newQuestion.addedBy.familyName}`, + cmsRequestorDivision: newQuestion.addedBy.divisionAssignment, + dateAsked: formatCalendarDate(newQuestion.createdAt), + questionRound, + } + + const result = await renderTemplate( + 'sendQuestionCMSEmail', + data + ) + + if (result instanceof Error) { + return result + } else { + return { + toAddresses: receiverEmails, + sourceEmail: config.emailSource, + replyToAddresses: [config.helpDeskEmail], + subject: `${ + config.stage !== 'prod' ? `[${config.stage}] ` : '' + }Questions sent for ${packageName}`, + bodyText: stripHTMLFromTemplate(result), + bodyHTML: result, + } + } +} diff --git a/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.test.ts b/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.test.ts new file mode 100644 index 0000000000..f9b7b344ad --- /dev/null +++ b/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.test.ts @@ -0,0 +1,144 @@ +import { + mockContractRev, + mockMNState, + mockQuestionAndResponses, + testEmailConfig, +} from '../../testHelpers/emailerHelpers' +import { getTestStateAnalystsEmails } from '../../testHelpers/parameterStoreHelpers' +import { testCMSUser } from '../../testHelpers/userHelpers' +import { sendQuestionResponseCMSEmail } from './sendQuestionResponseCMSEmail' + +const stateAnalysts = getTestStateAnalystsEmails('FL') +const oactCMSUser = testCMSUser({ + givenName: 'Bob', + familyName: 'Smith', + divisionAssignment: 'OACT', +}) +const dmcpUser = testCMSUser({ + givenName: 'Bob', + familyName: 'Smith', + divisionAssignment: 'DMCP', +}) +const contractRev = mockContractRev() +const defaultMNStatePrograms = mockMNState().programs +const questions = [ + mockQuestionAndResponses({ + id: 'test-question-id-4', + createdAt: new Date('02/03/2024'), + contractID: contractRev.id, + addedBy: oactCMSUser, + division: 'OACT', + }), + mockQuestionAndResponses({ + id: 'test-question-id-3', + createdAt: new Date('02/03/2024'), + contractID: contractRev.id, + addedBy: dmcpUser, + division: 'DMCP', + }), + mockQuestionAndResponses({ + id: 'test-question-id-2', + createdAt: new Date('02/03/2024'), + contractID: contractRev.id, + addedBy: dmcpUser, + division: 'DMCO', + }), + mockQuestionAndResponses({ + id: 'test-question-id-1', + createdAt: new Date('01/03/2024'), + contractID: contractRev.id, + addedBy: oactCMSUser, + division: 'OACT', + }), +] + +test.each([ + { + questions, + currentQuestion: mockQuestionAndResponses({ + id: 'test-question-id-4', + createdAt: new Date('02/03/2024'), + contractID: contractRev.id, + addedBy: oactCMSUser, + division: 'OACT', + }), + expectedResult: [ + ...stateAnalysts, + ...testEmailConfig().devReviewTeamEmails, + ...testEmailConfig().oactEmails, + ], + testDescription: 'OACT Q&A response email contains correct recipients', + }, + { + questions, + currentQuestion: mockQuestionAndResponses({ + id: 'test-question-id-3', + createdAt: new Date('02/03/2024'), + contractID: contractRev.id, + addedBy: dmcpUser, + division: 'DMCP', + }), + expectedResult: [ + ...stateAnalysts, + ...testEmailConfig().devReviewTeamEmails, + ...testEmailConfig().dmcpReviewEmails, + ], + testDescription: 'DMCP Q&A response email contains correct recipients', + }, + { + questions, + currentQuestion: mockQuestionAndResponses({ + id: 'test-question-id-2', + createdAt: new Date('02/03/2024'), + contractID: contractRev.id, + addedBy: dmcpUser, + division: 'DMCO', + }), + expectedResult: [ + ...stateAnalysts, + ...testEmailConfig().devReviewTeamEmails, + ], + testDescription: 'DMCO Q&A response email contains correct recipients', + }, +])( + '$testDescription', + async ({ questions, currentQuestion, expectedResult }) => { + const result = await sendQuestionResponseCMSEmail( + contractRev, + testEmailConfig(), + defaultMNStatePrograms, + stateAnalysts, + currentQuestion, + questions + ) + + if (result instanceof Error) { + throw new Error(`Unexpect error: ${result.message}`) + } + + expect(result).toEqual( + expect.objectContaining({ + toAddresses: expect.arrayContaining(expectedResult), + }) + ) + } +) + +test('renders overall CMS email for a new state response as expected', async () => { + const currentQuestion = questions[0] + + const result = await sendQuestionResponseCMSEmail( + contractRev, + testEmailConfig(), + defaultMNStatePrograms, + stateAnalysts, + currentQuestion, + questions + ) + + if (result instanceof Error) { + throw new Error(`Unexpect error: ${result.message}`) + } + + expect(result.bodyHTML).toMatchSnapshot() +}) diff --git a/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.ts b/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.ts new file mode 100644 index 0000000000..e0fbab73a9 --- /dev/null +++ b/services/app-api/src/emailer/emails/sendQuestionResponseCMSEmail.ts @@ -0,0 +1,91 @@ +import { packageName as generatePackageName } from '../../../../app-web/src/common-code/healthPlanFormDataType' +import { formatCalendarDate } from '../../../../app-web/src/common-code/dateHelpers' +import { pruneDuplicateEmails } from '../formatters' +import type { EmailConfiguration, EmailData } from '..' +import type { ProgramType, Question } from '../../domain-models' +import { + stripHTMLFromTemplate, + renderTemplate, + findContractPrograms, + getQuestionRound, +} from '../templateHelpers' +import { submissionQuestionResponseURL } from '../generateURLs' +import type { ContractRevisionWithRatesType } from '../../domain-models/contractAndRates' +import type { StateAnalystsEmails } from '..' + +export const sendQuestionResponseCMSEmail = async ( + contractRev: ContractRevisionWithRatesType, + config: EmailConfiguration, + statePrograms: ProgramType[], + stateAnalystsEmails: StateAnalystsEmails, + currentQuestion: Question, + allContractQuestions: Question[] +): Promise => { + // currentQuestion is the question the new response belongs to. Responses can be uploaded to any question round. + const { responses, division } = currentQuestion + const latestResponse = responses[0] + const questionRound = getQuestionRound( + allContractQuestions, + currentQuestion + ) + + if (questionRound instanceof Error) { + return questionRound + } + + let receiverEmails = [...stateAnalystsEmails, ...config.devReviewTeamEmails] + if (division === 'DMCP') { + receiverEmails.push(...config.dmcpReviewEmails) + } else if (division === 'OACT') { + receiverEmails.push(...config.oactEmails) + } + receiverEmails = pruneDuplicateEmails(receiverEmails) + + //This checks to make sure all programs contained in submission exists for the state. + const packagePrograms = findContractPrograms(contractRev, statePrograms) + if (packagePrograms instanceof Error) { + return packagePrograms + } + + const packageName = generatePackageName( + contractRev.contract.stateCode, + contractRev.contract.stateNumber, + contractRev.formData.programIDs, + packagePrograms + ) + + const questionResponseURL = submissionQuestionResponseURL( + contractRev.contract.id, + config.baseUrl + ) + + const data = { + packageName, + questionResponseURL, + cmsRequestorDivision: division, + stateResponseSubmitterEmail: latestResponse.addedBy.email, + stateResponseSubmitterName: `${latestResponse.addedBy.givenName} ${latestResponse.addedBy.familyName}`, + questionRound, + dateAsked: formatCalendarDate(currentQuestion.createdAt), + } + + const result = await renderTemplate( + 'sendQuestionResponseCMSEmail', + data + ) + + if (result instanceof Error) { + return result + } else { + return { + toAddresses: receiverEmails, + sourceEmail: config.emailSource, + replyToAddresses: [config.helpDeskEmail], + subject: `${ + config.stage !== 'prod' ? `[${config.stage}] ` : '' + }New Responses for ${packageName}`, + bodyText: stripHTMLFromTemplate(result), + bodyHTML: result, + } + } +} diff --git a/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.test.ts b/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.test.ts new file mode 100644 index 0000000000..15b13f8255 --- /dev/null +++ b/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.test.ts @@ -0,0 +1,331 @@ +import { + testEmailConfig, + mockContractRev, + mockMNState, +} from '../../testHelpers/emailerHelpers' +import type { + CMSUserType, + ContractRevisionWithRatesType, + StateType, +} from '../../domain-models' +import type { ContractFormDataType, Question } from '../../domain-models' +import { packageName } from 'app-web/src/common-code/healthPlanFormDataType' +import { sendQuestionResponseStateEmail } from './index' + +const defaultSubmitters = ['submitter1@example.com', 'submitter2@example.com'] + +const flState: StateType = { + stateCode: 'FL', + name: 'Florida', +} + +const cmsUser: CMSUserType = { + id: '1234', + role: 'CMS_USER', + divisionAssignment: 'DMCO', + familyName: 'McDonald', + givenName: 'Ronald', + email: 'cms@email.com', + stateAssignments: [flState], +} + +const questions: Question[] = [ + { + id: '1234', + contractID: 'contract-id-test', + createdAt: new Date('01/01/2024'), + addedBy: cmsUser, + documents: [], + division: 'DMCO', + responses: [], + }, +] + +const currentQuestion = questions[0] + +const formData: ContractFormDataType = { + programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + populationCovered: 'CHIP', + submissionType: 'CONTRACT_AND_RATES', + riskBasedContract: false, + submissionDescription: 'A submitted submission', + supportingDocuments: [ + { + s3URL: 'bar', + name: 'foo', + sha256: 'fakesha', + }, + ], + contractType: 'BASE', + contractExecutionStatus: undefined, + contractDocuments: [ + { + s3URL: 'bar', + name: 'foo', + sha256: 'fakesha', + }, + ], + contractDateStart: new Date('01/01/2024'), + contractDateEnd: new Date('01/01/2025'), + managedCareEntities: ['MCO'], + federalAuthorities: ['VOLUNTARY', 'BENCHMARK'], + inLieuServicesAndSettings: undefined, + modifiedBenefitsProvided: undefined, + modifiedGeoAreaServed: undefined, + modifiedMedicaidBeneficiaries: undefined, + modifiedRiskSharingStrategy: undefined, + modifiedIncentiveArrangements: undefined, + modifiedWitholdAgreements: undefined, + modifiedStateDirectedPayments: undefined, + modifiedPassThroughPayments: undefined, + modifiedPaymentsForMentalDiseaseInstitutions: undefined, + modifiedMedicalLossRatioStandards: undefined, + modifiedOtherFinancialPaymentIncentive: undefined, + modifiedEnrollmentProcess: undefined, + modifiedGrevienceAndAppeal: undefined, + modifiedNetworkAdequacyStandards: undefined, + modifiedLengthOfContract: undefined, + modifiedNonRiskPaymentArrangements: undefined, + statutoryRegulatoryAttestation: undefined, + statutoryRegulatoryAttestationDescription: undefined, + stateContacts: [ + { + name: 'test1', + titleRole: 'Foo1', + email: 'test1@example.com', + }, + { + name: 'test2', + titleRole: 'Foo2', + email: 'test2@example.com', + }, + ], +} + +test('to addresses list includes submitter emails', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionResponseStateEmail( + sub, + testEmailConfig(), + defaultSubmitters, + defaultStatePrograms, + questions, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + toAddresses: expect.arrayContaining(defaultSubmitters), + }) + ) +}) + +test('to addresses list includes all state contacts on submission', async () => { + const sub: ContractRevisionWithRatesType = { + ...mockContractRev({ formData }), + } + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionResponseStateEmail( + sub, + testEmailConfig(), + defaultSubmitters, + defaultStatePrograms, + questions, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + sub.formData.stateContacts.forEach((contact) => { + expect(template).toEqual( + expect.objectContaining({ + toAddresses: expect.arrayContaining([contact.email]), + }) + ) + }) +}) + +test('to addresses list does not include duplicate state receiver emails on submission', async () => { + const formDataWithDuplicateStateContacts = { + ...formData, + stateContacts: [ + { + name: 'test1', + titleRole: 'Foo1', + email: 'test1@example.com', + }, + { + name: 'test1', + titleRole: 'Foo1', + email: 'test1@example.com', + }, + ], + } + + const sub: ContractRevisionWithRatesType = mockContractRev({ + formData: formDataWithDuplicateStateContacts, + }) + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionResponseStateEmail( + sub, + testEmailConfig(), + defaultSubmitters, + defaultStatePrograms, + questions, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template.toAddresses).toEqual([ + 'test1@example.com', + ...defaultSubmitters, + ...testEmailConfig().devReviewTeamEmails, + ]) +}) + +test('subject line is correct and clearly states submission was successful', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const name = packageName( + sub.contract.stateCode, + sub.contract.stateNumber, + sub.formData.programIDs, + defaultStatePrograms + ) + + const template = await sendQuestionResponseStateEmail( + sub, + testEmailConfig(), + defaultSubmitters, + defaultStatePrograms, + questions, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + subject: expect.stringContaining( + `Response submitted to CMS for ${name}` + ), + }) + ) +}) + +test('includes link to submission', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionResponseStateEmail( + sub, + testEmailConfig(), + defaultSubmitters, + defaultStatePrograms, + questions, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining('View response'), + bodyHTML: expect.stringContaining( + `http://localhost/submissions/${sub.contract.id}/question-and-answer` + ), + }) + ) +}) + +test('includes information about what to do next', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionResponseStateEmail( + sub, + testEmailConfig(), + defaultSubmitters, + defaultStatePrograms, + questions, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining( + 'Questions: You may receive additional questions from CMS as they conduct their reviews.' + ), + }) + ) +}) + +test('includes expected data', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + + const template = await sendQuestionResponseStateEmail( + sub, + testEmailConfig(), + defaultSubmitters, + defaultStatePrograms, + questions, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + // Includes correct division the response was sent to + // Includes the correct round number for the response + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining( + 'DMCO round 1 response was successfully submitted' + ), + }) + ) + // Includes correct date response was submitted + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining('Date: 01/01/2024'), + }) + ) +}) + +test('renders overall email for a new response as expected', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const result = await sendQuestionResponseStateEmail( + sub, + testEmailConfig(), + defaultSubmitters, + defaultStatePrograms, + questions, + currentQuestion + ) + + if (result instanceof Error) { + console.error(result) + return + } + + expect(result.bodyHTML).toMatchSnapshot() +}) diff --git a/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.ts b/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.ts new file mode 100644 index 0000000000..4b94fbe54f --- /dev/null +++ b/services/app-api/src/emailer/emails/sendQuestionResponseStateEmail.ts @@ -0,0 +1,89 @@ +import { packageName as generatePackageName } from '../../../../app-web/src/common-code/healthPlanFormDataType' +import { formatCalendarDate } from '../../../../app-web/src/common-code/dateHelpers' +import { pruneDuplicateEmails } from '../formatters' +import type { EmailConfiguration, EmailData } from '..' +import type { ProgramType, Question } from '../../domain-models' +import { + stripHTMLFromTemplate, + renderTemplate, + findContractPrograms, + getQuestionRound, +} from '../templateHelpers' +import { submissionQuestionResponseURL } from '../generateURLs' +import type { ContractRevisionWithRatesType } from '../../domain-models/contractAndRates' + +export const sendQuestionResponseStateEmail = async ( + contractRev: ContractRevisionWithRatesType, + config: EmailConfiguration, + submitterEmails: string[], + statePrograms: ProgramType[], + allContractQuestions: Question[], + currentQuestion: Question +): Promise => { + // currentQuestion is the question the new response belongs to. Responses can be uploaded to any question round. + const division = currentQuestion.division + const stateContactEmails: string[] = [] + + contractRev.formData.stateContacts.forEach((contact) => { + if (contact.email) stateContactEmails.push(contact.email) + }) + const receiverEmails = pruneDuplicateEmails([ + ...stateContactEmails, + ...submitterEmails, + ...config.devReviewTeamEmails, + ]) + + const questionRound = getQuestionRound( + allContractQuestions, + currentQuestion + ) + + if (questionRound instanceof Error) { + return questionRound + } + //This checks to make sure all programs contained in submission exists for the state. + const packagePrograms = findContractPrograms(contractRev, statePrograms) + if (packagePrograms instanceof Error) { + return packagePrograms + } + + const packageName = generatePackageName( + contractRev.contract.stateCode, + contractRev.contract.stateNumber, + contractRev.formData.programIDs, + packagePrograms + ) + + const questionResponseURL = submissionQuestionResponseURL( + contractRev.contract.id, + config.baseUrl + ) + + const data = { + packageName, + questionResponseURL, + cmsRequestorDivision: division, + dateAsked: formatCalendarDate(currentQuestion.createdAt), + questionRound, + } + + const result = await renderTemplate( + 'sendQuestionResponseStateEmail', + data + ) + + if (result instanceof Error) { + return result + } else { + return { + toAddresses: receiverEmails, + sourceEmail: config.emailSource, + replyToAddresses: [config.helpDeskEmail], + subject: `${ + config.stage !== 'prod' ? `[${config.stage}] ` : '' + }Response submitted to CMS for ${packageName}`, + bodyText: stripHTMLFromTemplate(result), + bodyHTML: result, + } + } +} diff --git a/services/app-api/src/emailer/emails/sendQuestionStateEmail.test.ts b/services/app-api/src/emailer/emails/sendQuestionStateEmail.test.ts new file mode 100644 index 0000000000..47c2a37102 --- /dev/null +++ b/services/app-api/src/emailer/emails/sendQuestionStateEmail.test.ts @@ -0,0 +1,317 @@ +import { + testEmailConfig, + mockContractRev, + mockMNState, +} from '../../testHelpers/emailerHelpers' +import type { + CMSUserType, + ContractRevisionWithRatesType, + StateType, +} from '../../domain-models' +import type { ContractFormDataType, Question } from '../../domain-models' +import { packageName } from 'app-web/src/common-code/healthPlanFormDataType' +import { sendQuestionStateEmail } from './index' + +const defaultSubmitters = ['submitter1@example.com', 'submitter2@example.com'] + +const flState: StateType = { + stateCode: 'FL', + name: 'Florida', +} + +const cmsUser: CMSUserType = { + id: '1234', + role: 'CMS_USER', + divisionAssignment: 'DMCO', + familyName: 'McDonald', + givenName: 'Ronald', + email: 'cms@email.com', + stateAssignments: [flState], +} + +const currentQuestion: Question = { + id: '1234', + contractID: 'contract-id-test', + createdAt: new Date('01/01/2024'), + addedBy: cmsUser, + documents: [], + division: 'DMCO', + responses: [], +} + +const formData: ContractFormDataType = { + programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + populationCovered: 'CHIP', + submissionType: 'CONTRACT_AND_RATES', + riskBasedContract: false, + submissionDescription: 'A submitted submission', + supportingDocuments: [ + { + s3URL: 'bar', + name: 'foo', + sha256: 'fakesha', + }, + ], + contractType: 'BASE', + contractExecutionStatus: undefined, + contractDocuments: [ + { + s3URL: 'bar', + name: 'foo', + sha256: 'fakesha', + }, + ], + contractDateStart: new Date('01/01/2024'), + contractDateEnd: new Date('01/01/2025'), + managedCareEntities: ['MCO'], + federalAuthorities: ['VOLUNTARY', 'BENCHMARK'], + inLieuServicesAndSettings: undefined, + modifiedBenefitsProvided: undefined, + modifiedGeoAreaServed: undefined, + modifiedMedicaidBeneficiaries: undefined, + modifiedRiskSharingStrategy: undefined, + modifiedIncentiveArrangements: undefined, + modifiedWitholdAgreements: undefined, + modifiedStateDirectedPayments: undefined, + modifiedPassThroughPayments: undefined, + modifiedPaymentsForMentalDiseaseInstitutions: undefined, + modifiedMedicalLossRatioStandards: undefined, + modifiedOtherFinancialPaymentIncentive: undefined, + modifiedEnrollmentProcess: undefined, + modifiedGrevienceAndAppeal: undefined, + modifiedNetworkAdequacyStandards: undefined, + modifiedLengthOfContract: undefined, + modifiedNonRiskPaymentArrangements: undefined, + statutoryRegulatoryAttestation: undefined, + statutoryRegulatoryAttestationDescription: undefined, + stateContacts: [ + { + name: 'test1', + titleRole: 'Foo1', + email: 'test1@example.com', + }, + { + name: 'test2', + titleRole: 'Foo2', + email: 'test2@example.com', + }, + ], +} + +test('to addresses list includes submitter emails', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionStateEmail( + sub, + defaultSubmitters, + testEmailConfig(), + defaultStatePrograms, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + toAddresses: expect.arrayContaining(defaultSubmitters), + }) + ) +}) + +test('to addresses list includes all state contacts on submission', async () => { + const sub: ContractRevisionWithRatesType = { + ...mockContractRev({ formData }), + } + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionStateEmail( + sub, + defaultSubmitters, + testEmailConfig(), + defaultStatePrograms, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + sub.formData.stateContacts.forEach((contact) => { + expect(template).toEqual( + expect.objectContaining({ + toAddresses: expect.arrayContaining([contact.email]), + }) + ) + }) +}) + +test('to addresses list does not include duplicate state receiver emails on submission', async () => { + const formDataWithDuplicateStateContacts = { + ...formData, + stateContacts: [ + { + name: 'test1', + titleRole: 'Foo1', + email: 'test1@example.com', + }, + { + name: 'test1', + titleRole: 'Foo1', + email: 'test1@example.com', + }, + ], + } + + const sub: ContractRevisionWithRatesType = mockContractRev({ + formData: formDataWithDuplicateStateContacts, + }) + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionStateEmail( + sub, + defaultSubmitters, + testEmailConfig(), + defaultStatePrograms, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template.toAddresses).toEqual([ + 'test1@example.com', + ...defaultSubmitters, + ...testEmailConfig().devReviewTeamEmails, + ]) +}) + +test('subject line is correct and clearly states submission is complete', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const name = packageName( + sub.contract.stateCode, + sub.contract.stateNumber, + sub.formData.programIDs, + defaultStatePrograms + ) + + const template = await sendQuestionStateEmail( + sub, + defaultSubmitters, + testEmailConfig(), + defaultStatePrograms, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + subject: expect.stringContaining(`New questions about ${name}`), + bodyText: expect.stringContaining(`${name}`), + }) + ) +}) + +test('includes link to submission', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionStateEmail( + sub, + defaultSubmitters, + testEmailConfig(), + defaultStatePrograms, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining( + 'Open the submission in MC-Review to answer question' + ), + bodyHTML: expect.stringContaining( + `http://localhost/submissions/${sub.contract.id}/question-and-answer` + ), + }) + ) +}) + +test('includes information about what to do next', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const template = await sendQuestionStateEmail( + sub, + defaultSubmitters, + testEmailConfig(), + defaultStatePrograms, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining( + 'You must answer the question before CMS can continue reviewing it' + ), + }) + ) +}) + +test('includes expected data on the CMS analyst who sent the question', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + + const template = await sendQuestionStateEmail( + sub, + defaultSubmitters, + testEmailConfig(), + defaultStatePrograms, + currentQuestion + ) + + if (template instanceof Error) { + throw template + } + + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining( + 'Sent by: Ronald McDonald (DMCO) cms@email.com (cms@email.com)' + ), + }) + ) + expect(template).toEqual( + expect.objectContaining({ + bodyText: expect.stringContaining('Date: 01/01/2024'), + }) + ) +}) + +test('renders overall email for a new question as expected', async () => { + const sub = mockContractRev() + const defaultStatePrograms = mockMNState().programs + const result = await sendQuestionStateEmail( + sub, + defaultSubmitters, + testEmailConfig(), + defaultStatePrograms, + currentQuestion + ) + + if (result instanceof Error) { + console.error(result) + return + } + + expect(result.bodyHTML).toMatchSnapshot() +}) diff --git a/services/app-api/src/emailer/emails/sendQuestionStateEmail.ts b/services/app-api/src/emailer/emails/sendQuestionStateEmail.ts new file mode 100644 index 0000000000..36f4c98e7b --- /dev/null +++ b/services/app-api/src/emailer/emails/sendQuestionStateEmail.ts @@ -0,0 +1,78 @@ +import { packageName as generatePackageName } from '../../../../app-web/src/common-code/healthPlanFormDataType' +import { formatCalendarDate } from '../../../../app-web/src/common-code/dateHelpers' +import { pruneDuplicateEmails } from '../formatters' +import type { EmailConfiguration, EmailData } from '..' +import type { ProgramType, Question } from '../../domain-models' +import { + stripHTMLFromTemplate, + renderTemplate, + findContractPrograms, +} from '../templateHelpers' +import { submissionQuestionResponseURL } from '../generateURLs' +import type { ContractRevisionWithRatesType } from '../../domain-models/contractAndRates' + +export const sendQuestionStateEmail = async ( + contractRev: ContractRevisionWithRatesType, + submitterEmails: string[], + config: EmailConfiguration, + statePrograms: ProgramType[], + currentQuestion: Question +): Promise => { + const stateContactEmails: string[] = [] + + contractRev.formData.stateContacts.forEach((contact) => { + if (contact.email) stateContactEmails.push(contact.email) + }) + const receiverEmails = pruneDuplicateEmails([ + ...stateContactEmails, + ...submitterEmails, + ...config.devReviewTeamEmails, + ]) + + //This checks to make sure all programs contained in submission exists for the state. + const packagePrograms = findContractPrograms(contractRev, statePrograms) + if (packagePrograms instanceof Error) { + return packagePrograms + } + + const packageName = generatePackageName( + contractRev.contract.stateCode, + contractRev.contract.stateNumber, + contractRev.formData.programIDs, + packagePrograms + ) + + const questionResponseURL = submissionQuestionResponseURL( + contractRev.contract.id, + config.baseUrl + ) + + const data = { + packageName, + questionResponseURL: questionResponseURL, + cmsRequestorEmail: currentQuestion.addedBy.email, + cmsRequestorName: `${currentQuestion.addedBy.givenName} ${currentQuestion.addedBy.familyName}`, + cmsRequestorDivision: currentQuestion.addedBy.divisionAssignment, + dateAsked: formatCalendarDate(currentQuestion.createdAt), + } + + const result = await renderTemplate( + 'sendQuestionStateEmail', + data + ) + + if (result instanceof Error) { + return result + } else { + return { + toAddresses: receiverEmails, + sourceEmail: config.emailSource, + replyToAddresses: [config.helpDeskEmail], + subject: `${ + config.stage !== 'prod' ? `[${config.stage}] ` : '' + }New questions about ${packageName}`, + bodyText: stripHTMLFromTemplate(result), + bodyHTML: result, + } + } +} diff --git a/services/app-api/src/emailer/emails/unlockPackageCMSEmail.test.ts b/services/app-api/src/emailer/emails/unlockPackageCMSEmail.test.ts index 2360a1d67e..73c334fbb0 100644 --- a/services/app-api/src/emailer/emails/unlockPackageCMSEmail.test.ts +++ b/services/app-api/src/emailer/emails/unlockPackageCMSEmail.test.ts @@ -30,7 +30,6 @@ const sub: UnlockedHealthPlanFormDataType = { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -74,8 +73,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -94,8 +92,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -148,7 +145,6 @@ describe('unlockPackageCMSEmail', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -177,7 +173,6 @@ describe('unlockPackageCMSEmail', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -206,7 +201,6 @@ describe('unlockPackageCMSEmail', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -245,8 +239,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -321,8 +314,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } testEmailConfig().oactEmails.forEach((emailAddress) => { @@ -333,7 +325,7 @@ describe('unlockPackageCMSEmail', () => { ) }) - testEmailConfig().dmcpEmails.forEach((emailAddress) => { + testEmailConfig().dmcpSubmissionEmails.forEach((emailAddress) => { expect(template).toEqual( expect.objectContaining({ toAddresses: expect.arrayContaining([emailAddress]), @@ -361,8 +353,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -392,8 +383,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } testStateAnalystEmails.forEach((emailAddress) => { @@ -419,8 +409,7 @@ describe('unlockPackageCMSEmail', () => { ] if (template instanceof Error) { - console.error(template) - return + throw template } reviewerEmails.forEach((emailAddress) => { @@ -457,8 +446,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } testStateAnalystEmails.forEach((emailAddress) => { @@ -480,8 +468,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } const ratesReviewerEmails = [...testEmailConfig().oactEmails] @@ -504,8 +491,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } testStateAnalystEmails.forEach((emailAddress) => { @@ -531,8 +517,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } testStateAnalystEmails.forEach((emailAddress) => { @@ -559,8 +544,7 @@ describe('unlockPackageCMSEmail', () => { const excludedEmails = [...testEmailConfig().oactEmails] if (template instanceof Error) { - console.error(template) - return + throw template } excludedEmails.forEach((emailAddress) => { @@ -590,7 +574,6 @@ describe('unlockPackageCMSEmail', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -622,8 +605,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } testStateAnalystEmails.forEach((emailAddress) => { @@ -646,7 +628,6 @@ describe('unlockPackageCMSEmail', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -679,8 +660,7 @@ describe('unlockPackageCMSEmail', () => { const excludedEmails = [...testEmailConfig().oactEmails] if (template instanceof Error) { - console.error(template) - return + throw template } excludedEmails.forEach((emailAddress) => { @@ -709,8 +689,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -731,8 +710,7 @@ describe('unlockPackageCMSEmail', () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template.bodyHTML).toMatchSnapshot() diff --git a/services/app-api/src/emailer/emails/unlockPackageStateEmail.test.ts b/services/app-api/src/emailer/emails/unlockPackageStateEmail.test.ts index ab9bc17b99..26b7f6a107 100644 --- a/services/app-api/src/emailer/emails/unlockPackageStateEmail.test.ts +++ b/services/app-api/src/emailer/emails/unlockPackageStateEmail.test.ts @@ -27,7 +27,6 @@ const sub: UnlockedHealthPlanFormDataType = { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -71,8 +70,7 @@ test('subject line is correct and clearly states submission is unlocked', async ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -94,8 +92,7 @@ test('includes expected data summary for a contract and rates submission unlock ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -150,7 +147,6 @@ test('includes expected data summary for a multi-rate contract and rates submiss s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -179,7 +175,6 @@ test('includes expected data summary for a multi-rate contract and rates submiss s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -208,7 +203,6 @@ test('includes expected data summary for a multi-rate contract and rates submiss s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -247,8 +241,7 @@ test('includes expected data summary for a multi-rate contract and rates submiss ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template).toEqual( @@ -343,7 +336,6 @@ test('renders overall email as expected', async () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -372,7 +364,6 @@ test('renders overall email as expected', async () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -405,8 +396,7 @@ test('renders overall email as expected', async () => { ) if (template instanceof Error) { - console.error(template) - return + throw template } expect(template.bodyHTML).toMatchSnapshot() diff --git a/services/app-api/src/emailer/etaTemplates/sendQuestionCMSEmail.eta b/services/app-api/src/emailer/etaTemplates/sendQuestionCMSEmail.eta new file mode 100644 index 0000000000..b3079091da --- /dev/null +++ b/services/app-api/src/emailer/etaTemplates/sendQuestionCMSEmail.eta @@ -0,0 +1,8 @@ +<%= it.cmsRequestorDivision %> sent questions to the state for submission <%= it.packageName %>
+Sent by: <%= it.cmsRequestorName %> (<%= it.cmsRequestorDivision %>) <%= it.cmsRequestorEmail %> +
+Round: <%= it.questionRound %> +
+Date: <%= it.dateAsked %>
+
+View submission Q&A diff --git a/services/app-api/src/emailer/etaTemplates/sendQuestionResponseCMSEmail.eta b/services/app-api/src/emailer/etaTemplates/sendQuestionResponseCMSEmail.eta new file mode 100644 index 0000000000..2f89ca4162 --- /dev/null +++ b/services/app-api/src/emailer/etaTemplates/sendQuestionResponseCMSEmail.eta @@ -0,0 +1,6 @@ +The state submitted responses to <%= it.cmsRequestorDivision %>'s questions about <%= it.packageName %>
+Submitted by: <%= it.stateResponseSubmitterName %> <%= it.stateResponseSubmitterEmail %>
+Round: <%= it.questionRound %>
+Questions sent on: <%= it.dateAsked %>
+
+View submission Q&A diff --git a/services/app-api/src/emailer/etaTemplates/sendQuestionResponseStateEmail.eta b/services/app-api/src/emailer/etaTemplates/sendQuestionResponseStateEmail.eta new file mode 100644 index 0000000000..0b28d8ca4a --- /dev/null +++ b/services/app-api/src/emailer/etaTemplates/sendQuestionResponseStateEmail.eta @@ -0,0 +1,21 @@ +<%= it.cmsRequestorDivision %> round <%= it.questionRound %> response was successfully submitted
+
+Date: <%= it.dateAsked %> +
+
+View response +
+
+What comes next:
+
    +
  1. Questions: You may receive additional questions from CMS as they conduct their reviews.
  2. +
  3. Decision: Once all questions have been addressed, CMS will contact you with their final recommendation.
  4. +
+
+If you need assistance or to make changes to your submission:
+
    +
  • For assistance with programmatic, contractual, or operational issues, please reach out to MCOGDMCOActions@cms.hhs.gov and/or your CMS primary contact.
  • +
  • For assistance on policy and actuarial issues, please reach out to MMCratesetting@cms.hhs.gov.
  • +
  • For issues related to MC-Review or all other inquiries, please reach out to MC_Review_HelpDesk@cms.hhs.gov.
  • +
+ diff --git a/services/app-api/src/emailer/etaTemplates/sendQuestionStateEmail.eta b/services/app-api/src/emailer/etaTemplates/sendQuestionStateEmail.eta new file mode 100644 index 0000000000..c6e90f1bc0 --- /dev/null +++ b/services/app-api/src/emailer/etaTemplates/sendQuestionStateEmail.eta @@ -0,0 +1,9 @@ +CMS asked questions about <%= it.packageName %>
+Sent by: <%= it.cmsRequestorName %> (<%= it.cmsRequestorDivision %>) <%= it.cmsRequestorEmail %> +
+Date: <%= it.dateAsked %>
+
+You must answer the question before CMS can continue reviewing it.
+
+Open the submission in MC-Review to answer questions + diff --git a/services/app-api/src/emailer/generateURLs.ts b/services/app-api/src/emailer/generateURLs.ts index e84284acc0..1e933bd3d5 100644 --- a/services/app-api/src/emailer/generateURLs.ts +++ b/services/app-api/src/emailer/generateURLs.ts @@ -27,4 +27,17 @@ function submissionSummaryURL(id: string, base: string): string { return url } -export { reviewAndSubmitURL, submissionSummaryURL } +function submissionQuestionResponseURL(id: string, base: string): string { + const pattern = RoutesRecord.SUBMISSIONS_QUESTIONS_AND_ANSWERS + const toPath = compile(pattern, { encode: encodeURIComponent }) + const path = toPath({ id }) + const url = new URL(path, base).href + + return url +} + +export { + reviewAndSubmitURL, + submissionSummaryURL, + submissionQuestionResponseURL, +} diff --git a/services/app-api/src/emailer/index.ts b/services/app-api/src/emailer/index.ts index e1a1b72001..c20cbc97ed 100644 --- a/services/app-api/src/emailer/index.ts +++ b/services/app-api/src/emailer/index.ts @@ -1,5 +1,5 @@ export { getSESEmailParams, sendSESEmail } from './awsSES' -export { newLocalEmailer, newSESEmailer } from './emailer' +export { newLocalEmailer, newSESEmailer, emailer } from './emailer' export { newPackageCMSEmail, newPackageStateEmail, @@ -7,6 +7,10 @@ export { unlockPackageStateEmail, resubmitPackageStateEmail, resubmitPackageCMSEmail, + sendQuestionStateEmail, + sendQuestionCMSEmail, + sendQuestionResponseCMSEmail, + sendQuestionResponseStateEmail, } from './emails' export type { EmailConfiguration, diff --git a/services/app-api/src/emailer/templateHelpers.test.ts b/services/app-api/src/emailer/templateHelpers.test.ts index a0d6590421..ef73a62e21 100644 --- a/services/app-api/src/emailer/templateHelpers.test.ts +++ b/services/app-api/src/emailer/templateHelpers.test.ts @@ -1,18 +1,23 @@ import { filterChipAndPRSubmissionReviewers, + findContractPrograms, generateCMSReviewerEmails, + getQuestionRound, handleAsCHIPSubmission, } from './templateHelpers' import type { UnlockedHealthPlanFormDataType } from '../../../app-web/src/common-code/healthPlanFormDataType' import { mockUnlockedContractAndRatesFormData, mockUnlockedContractOnlyFormData, + mockContractRev, testEmailConfig, testStateAnalystsEmails, + mockQuestionAndResponses, } from '../testHelpers/emailerHelpers' import type { EmailConfiguration, StateAnalystsEmails } from './emailer' +import type { ProgramType } from '../domain-models' -describe('templateHelpers', () => { +describe('generateCMSReviewerEmails', () => { const contractOnlyWithValidRateData: { submission: UnlockedHealthPlanFormDataType emailConfig: EmailConfiguration @@ -24,22 +29,22 @@ describe('templateHelpers', () => { submission: mockUnlockedContractOnlyFormData(), emailConfig: testEmailConfig(), stateAnalystsEmails: testStateAnalystsEmails, - testDescription: 'Contract only submission', + testDescription: 'contract only submission', expectedResult: [ ...testEmailConfig().devReviewTeamEmails, ...testStateAnalystsEmails, - ...testEmailConfig().dmcpEmails, + ...testEmailConfig().dmcpSubmissionEmails, ], }, { submission: mockUnlockedContractAndRatesFormData(), emailConfig: testEmailConfig(), stateAnalystsEmails: testStateAnalystsEmails, - testDescription: 'Contract and rates submission', + testDescription: 'contract and rates submission', expectedResult: [ ...testEmailConfig().devReviewTeamEmails, ...testStateAnalystsEmails, - ...testEmailConfig().dmcpEmails, + ...testEmailConfig().dmcpSubmissionEmails, ...testEmailConfig().oactEmails, ], }, @@ -51,7 +56,7 @@ describe('templateHelpers', () => { emailConfig: testEmailConfig(), stateAnalystsEmails: testStateAnalystsEmails, testDescription: - 'Submission with CHIP program specified for contract certification', + 'submission with CHIP program specified for contract certification', expectedResult: [ 'devreview1@example.com', 'devreview2@example.com', @@ -70,7 +75,6 @@ describe('templateHelpers', () => { s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -97,7 +101,7 @@ describe('templateHelpers', () => { emailConfig: testEmailConfig(), stateAnalystsEmails: testStateAnalystsEmails, testDescription: - 'Submission with CHIP program specified for rate certification', + 'submission with CHIP program specified for rate certification', expectedResult: [ 'devreview1@example.com', 'devreview2@example.com', @@ -125,14 +129,14 @@ describe('templateHelpers', () => { }), emailConfig: testEmailConfig(), stateAnalystsEmails: testStateAnalystsEmails, - testDescription: 'Error result.', + testDescription: 'error result.', expectedResult: new Error( - `generateCMSReviewerEmails does not currently support submission type: undefined.` + `does not currently support submission type: undefined.` ), }, ] test.each(contractOnlyWithValidRateData)( - 'Generate CMS Reviewer email: $testDescription', + '$testDescription', ({ submission, emailConfig, stateAnalystsEmails, expectedResult }) => { expect( generateCMSReviewerEmails( @@ -143,7 +147,9 @@ describe('templateHelpers', () => { ).toEqual(expect.objectContaining(expectedResult)) } ) +}) +describe('handleAsCHIPSubmission', () => { test.each([ { pkg: mockUnlockedContractAndRatesFormData({ @@ -177,13 +183,12 @@ describe('templateHelpers', () => { testDescription: 'for non CHIP submission', expectedResult: false, }, - ])( - 'handleAsCHIPSubmission: $testDescription', - ({ pkg, expectedResult }) => { - expect(handleAsCHIPSubmission(pkg)).toEqual(expectedResult) - } - ) + ])('$testDescription', ({ pkg, expectedResult }) => { + expect(handleAsCHIPSubmission(pkg)).toEqual(expectedResult) + }) +}) +describe('filterChipAndPRSubmissionReviewers', () => { test.each([ { reviewers: [ @@ -199,7 +204,7 @@ describe('templateHelpers', () => { reviewers: [ 'Bobloblaw@example.com', 'Lucille.Bluth@example.com', - testEmailConfig().dmcpEmails[0], + testEmailConfig().dmcpSubmissionEmails[0], ], config: testEmailConfig(), testDescription: 'removes dmcp emails', @@ -208,12 +213,171 @@ describe('templateHelpers', () => { 'Lucille.Bluth@example.com', ], }, - ])( - 'filterChipAndPRSubmissionReviewers: $testDescription', - ({ reviewers, config, expectedResult }) => { - expect( - filterChipAndPRSubmissionReviewers(reviewers, config) - ).toEqual(expectedResult) + ])('$testDescription', ({ reviewers, config, expectedResult }) => { + expect(filterChipAndPRSubmissionReviewers(reviewers, config)).toEqual( + expectedResult + ) + }) +}) + +describe('findContractPrograms', () => { + test('successfully returns programs for a contract', async () => { + const sub = mockContractRev() + const statePrograms: [ProgramType] = [ + { + id: 'abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce', + name: 'CHIP', + fullName: 'MN CHIP', + }, + ] + + const programs = findContractPrograms(sub, statePrograms) + + expect(programs).toEqual(statePrograms) + }) + + test('throws error if state and contract program ids do not match', async () => { + const sub = mockContractRev() + const statePrograms: [ProgramType] = [ + { + id: 'unmatched-id', + name: 'CHIP', + fullName: 'MN CHIP', + }, + ] + + const result = findContractPrograms(sub, statePrograms) + if (!(result instanceof Error)) { + throw new Error('must be an error') } - ) + expect(result.message).toContain( + "Can't find programs abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce from state MN" + ) + }) +}) + +describe('getQuestionRound', () => { + test.each([ + { + questions: [ + mockQuestionAndResponses({ + id: 'target-question', + division: 'OACT', + createdAt: new Date('03/01/2024'), + }), + mockQuestionAndResponses({ + division: 'DMCO', + createdAt: new Date('03/01/2024'), + }), + mockQuestionAndResponses({ + division: 'OACT', + createdAt: new Date('02/01/2024'), + }), + mockQuestionAndResponses({ + division: 'OACT', + createdAt: new Date('01/01/2024'), + }), + ], + currentQuestion: mockQuestionAndResponses({ + id: 'target-question', + division: 'OACT', + createdAt: new Date('03/01/2024'), + }), + expectedResult: 3, + testDescription: 'Gets correct round for latest question from OACT', + }, + { + questions: [ + mockQuestionAndResponses({ + division: 'OACT', + createdAt: new Date('03/01/2024'), + }), + mockQuestionAndResponses({ + division: 'DMCO', + createdAt: new Date('03/01/2024'), + }), + mockQuestionAndResponses({ + id: 'target-question', + division: 'OACT', + createdAt: new Date('02/01/2024'), + }), + mockQuestionAndResponses({ + division: 'OACT', + createdAt: new Date('01/01/2024'), + }), + ], + currentQuestion: mockQuestionAndResponses({ + id: 'target-question', + division: 'OACT', + createdAt: new Date('03/01/2024'), + }), + expectedResult: 2, + testDescription: 'Gets correct round for second question from OACT', + }, + { + questions: [ + mockQuestionAndResponses({ + division: 'OACT', + createdAt: new Date('03/01/2024'), + }), + mockQuestionAndResponses({ + division: 'DMCO', + createdAt: new Date('03/01/2024'), + }), + mockQuestionAndResponses({ + id: 'target-question', + division: 'OACT', + createdAt: new Date('02/01/2024'), + }), + mockQuestionAndResponses({ + division: 'OACT', + createdAt: new Date('01/01/2024'), + }), + ], + currentQuestion: mockQuestionAndResponses({ + id: 'not-found-question', + division: 'OACT', + createdAt: new Date('03/01/2024'), + }), + expectedResult: new Error( + 'Error getting question round, current question index not found' + ), + testDescription: + 'Returns error if question is not found in questions', + }, + { + questions: [ + mockQuestionAndResponses({ + division: 'OACT', + createdAt: new Date('03/01/2024'), + }), + mockQuestionAndResponses({ + division: 'DMCO', + createdAt: new Date('03/01/2024'), + }), + mockQuestionAndResponses({ + id: 'target-question', + division: 'OACT', + createdAt: new Date('02/01/2024'), + }), + mockQuestionAndResponses({ + division: 'OACT', + createdAt: new Date('01/01/2024'), + }), + ], + currentQuestion: mockQuestionAndResponses({ + id: 'not-found-question', + division: 'DMCP', + createdAt: new Date('03/01/2024'), + }), + expectedResult: new Error( + 'Error getting question round, current question not found' + ), + testDescription: 'Returns error if divison has no questions', + }, + ])('$testDescription', ({ questions, currentQuestion, expectedResult }) => { + expect(getQuestionRound(questions, currentQuestion)).toEqual( + expectedResult + ) + }) }) diff --git a/services/app-api/src/emailer/templateHelpers.ts b/services/app-api/src/emailer/templateHelpers.ts index 066c84054a..d8fa02d92f 100644 --- a/services/app-api/src/emailer/templateHelpers.ts +++ b/services/app-api/src/emailer/templateHelpers.ts @@ -7,9 +7,13 @@ import type { SubmissionType, } from '../../../app-web/src/common-code/healthPlanFormDataType' import type { EmailConfiguration, StateAnalystsEmails } from '.' -import type { ProgramType } from '../domain-models' +import type { + ContractRevisionWithRatesType, + ProgramType, +} from '../domain-models' import { logError } from '../logger' import { pruneDuplicateEmails } from './formatters' +import type { Question } from '../domain-models' // ETA SETUP Eta.configure({ @@ -80,17 +84,18 @@ const filterChipAndPRSubmissionReviewers = ( reviewers: string[], config: EmailConfiguration ) => { - const { oactEmails, dmcpEmails } = config + const { oactEmails, dmcpSubmissionEmails } = config return reviewers.filter( - (email) => !dmcpEmails.includes(email) && !oactEmails.includes(email) + (email) => + !dmcpSubmissionEmails.includes(email) && !oactEmails.includes(email) ) } /* Determine reviewers for a given health plan package and state - devReviewTeamEmails added to all emails by default - - dmcpEmails added in both CONTRACT_ONLY and CONTRACT_AND_RATES + - dmcpSubmissionEmails added in both CONTRACT_ONLY and CONTRACT_AND_RATES - oactEmails added for CONTRACT_AND_RATES - dmco is added to emails via state analysts @@ -110,7 +115,7 @@ const generateCMSReviewerEmails = ( ) } - const { oactEmails, dmcpEmails } = config + const { oactEmails, dmcpSubmissionEmails } = config let reviewers: string[] = [] if (pkg.submissionType === 'CONTRACT_ONLY') { @@ -118,14 +123,14 @@ const generateCMSReviewerEmails = ( reviewers = [ ...config.devReviewTeamEmails, ...stateAnalystsEmails, - ...dmcpEmails, + ...dmcpSubmissionEmails, ] } else if (pkg.submissionType === 'CONTRACT_AND_RATES') { //Contract and rate submissions reviewer emails. reviewers = [ ...config.devReviewTeamEmails, ...stateAnalystsEmails, - ...dmcpEmails, + ...dmcpSubmissionEmails, ...oactEmails, ] } @@ -174,6 +179,24 @@ const findPackagePrograms = ( return programs } +//Find state programs from contract with rates +const findContractPrograms = ( + contractRev: ContractRevisionWithRatesType, + statePrograms: ProgramType[] +): ProgramType[] | Error => { + const programIDs = contractRev.formData.programIDs + const programs = statePrograms.filter((program) => + programIDs.includes(program.id) + ) + if (!programs || programs.length !== programIDs.length) { + const errMessage = `Can't find programs ${programIDs} from state ${contractRev.contract.stateCode}` + logError('newPackageCMSEmail', errMessage) + return new Error(errMessage) + } + + return programs +} + // Clean out HTML tags from an HTML based template // this way we still have a text alternative for email client rendering html in plaintext // plaintext is also referenced for unit testing @@ -191,6 +214,34 @@ const stripHTMLFromTemplate = (template: string) => { return formatted.replace(/(<([^>]+)>)/gi, '') } +const getQuestionRound = ( + allQuestions: Question[], + currentQuestion: Question +): number | Error => { + // Filter out other divisions question and sort by created at in ascending order + const divisionQuestions = allQuestions + .filter((question) => question.division === currentQuestion.division) + .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) + + if (divisionQuestions.length === 0) { + return new Error( + 'Error getting question round, current question not found' + ) + } + + // Find index of the current question, this is it's round. First, index 0, in the array is round 1 + const questionIndex = divisionQuestions.findIndex( + (question) => question.id === currentQuestion.id + ) + if (questionIndex === -1) { + return new Error( + 'Error getting question round, current question index not found' + ) + } + + return questionIndex + 1 +} + export { stripHTMLFromTemplate, handleAsCHIPSubmission, @@ -199,5 +250,7 @@ export { SubmissionTypeRecord, findAllPackageProgramIds, findPackagePrograms, + findContractPrograms, filterChipAndPRSubmissionReviewers, + getQuestionRound, } diff --git a/services/app-api/src/handlers/add_sha.test.ts b/services/app-api/src/handlers/add_sha.test.ts deleted file mode 100644 index 48442809a9..0000000000 --- a/services/app-api/src/handlers/add_sha.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { main } from './add_sha' -import * as add_sha from './add_sha' -import type { SubmissionDocument } from 'app-web/src/common-code/healthPlanFormDataType' -import { unlockedWithALittleBitOfEverything } from 'app-web/src/common-code/healthPlanFormDataMocks' -import type { Context } from 'aws-lambda' -import type { HealthPlanRevisionTable } from '@prisma/client' -import type { Store } from '../postgres' -import type { Event } from '@aws-sdk/client-s3' -import { toProtoBuffer } from 'app-web/src/common-code/proto/healthPlanFormDataProto' - -const mockStore: Store = { - findAllRevisions: jest.fn(), - updateHealthPlanRevision: jest.fn(), -} as unknown as Store - -jest.mock('@aws-sdk/client-secrets-manager', () => { - return { - SecretsManagerClient: jest.fn(() => { - /* empty callback */ - }), - GetSecretValueCommand: jest.fn(() => { - /* empty callback */ - }), - } -}) - -describe('add_sha', () => { - beforeEach(() => { - jest.resetAllMocks() - jest.spyOn(add_sha, 'getDatabaseConnection').mockImplementation(() => - Promise.resolve(mockStore) - ) - jest.spyOn(add_sha, 'calculateSHA256').mockImplementation(() => { - return Promise.resolve('mockSHA256') - }) - }) - - function createRevisions(documents: SubmissionDocument[]) { - return [ - { - id: 'mockId', - createdAt: new Date(), - pkgID: 'mockPkgID', - formDataProto: Buffer.from( - toProtoBuffer({ - ...unlockedWithALittleBitOfEverything(), - documents, - }) - ), - submittedAt: new Date(), - unlockedAt: new Date(), - unlockedBy: 'mockUnlockedBy', - unlockedReason: 'mockUnlockedReason', - submittedBy: 'mockSubmittedBy', - submittedReason: 'mockSubmittedReason', - }, - ] - } - - it('should not overwrite a sha256 property when it already exists', async () => { - // Create a revision with an existing sha256 property in the document - const revisions: HealthPlanRevisionTable[] = createRevisions([ - { - s3URL: 's3://bucketname/key/foo.png', - name: 'contract doc', - documentCategories: ['CONTRACT_RELATED'], - sha256: 'doNotReplaceMe', - }, - ]) - - const storeFindAllRevisionsSpy = jest.spyOn( - mockStore, - 'findAllRevisions' - ) - storeFindAllRevisionsSpy.mockResolvedValue(revisions) - - const updateHealthPlanRevisionSpy = jest.spyOn( - mockStore, - 'updateHealthPlanRevision' - ) - - await main({} as Event, {} as Context, () => { - /*empty callback*/ - }) - - // the sha should remain unchanged, and not be overwritten by the mock value defined above - expect(updateHealthPlanRevisionSpy).toHaveBeenCalledWith( - 'mockPkgID', - 'mockId', - expect.objectContaining({ - documents: expect.arrayContaining([ - expect.objectContaining({ - sha256: 'doNotReplaceMe', - }), - ]), - }) - ) - }) -}) diff --git a/services/app-api/src/handlers/add_sha.ts b/services/app-api/src/handlers/add_sha.ts deleted file mode 100644 index 1aa740c8ab..0000000000 --- a/services/app-api/src/handlers/add_sha.ts +++ /dev/null @@ -1,215 +0,0 @@ -import type { Handler } from 'aws-lambda' -import type { Readable } from 'stream' -import { configurePostgres } from './configuration' -import { NewPostgresStore } from '../postgres/postgresStore' -import type { HealthPlanRevisionTable } from '@prisma/client' -import type { - HealthPlanFormDataType, - SubmissionDocument, -} from '../../../app-web/src/common-code/healthPlanFormDataType' -import { toDomain } from '../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import type { StoreError } from '../postgres/storeError' -import { isStoreError } from '../postgres/storeError' -import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3' -import { createHash } from 'crypto' -import type { Store } from '../postgres' -import { - parseKey, - parseBucketName, -} from '../../../app-web/src/common-code/s3URLEncoding' -import { - initTracer, - initMeter, - recordException, -} from '../../../uploads/src/lib/otel' - -const s3 = new S3Client({ region: 'us-east-1' }) - -export const streamToBuffer = async (stream: Readable): Promise => { - return new Promise((resolve, reject) => { - const chunks: Uint8Array[] = [] - stream.on('data', (chunk) => chunks.push(chunk)) - stream.on('error', reject) - stream.on('end', () => resolve(Buffer.concat(chunks))) - }) -} - -export const calculateSHA256 = async (s3URL: string): Promise => { - try { - const getObjectCommand = new GetObjectCommand({ - Bucket: parseBucketName(s3URL) as string, - Key: `allusers/${parseKey(s3URL)}`, - }) - const s3Object = await s3.send(getObjectCommand) - const buffer = await streamToBuffer(s3Object.Body as Readable) - - const hash = createHash('sha256') - hash.update(buffer) - return hash.digest('hex') - } catch (err) { - console.error(`Error in calculateSHA256 for ${s3URL}: ${err}`) - return '' - } -} - -export const updateDocumentsSHA256 = async ( - documents: SubmissionDocument[], - serviceName: string -): Promise => { - try { - const updatedDocuments = await Promise.all( - documents.map(async (document) => { - if ( - !Object.prototype.hasOwnProperty.call(document, 'sha256') || - !document.sha256 - ) { - try { - const sha256 = await calculateSHA256(document.s3URL) - const updatedDocument = { - ...document, - sha256: `${sha256}`, - } - return updatedDocument - } catch (error) { - console.error('Error in updateDocumentsSHA256:', error) - recordException(error, serviceName, 'calculateSHA256') - // Return the original document if an error occurs - return document - } - } else { - return document - } - }) - ) - return updatedDocuments - } catch (error) { - console.error('Error in updateDocumentsSHA256:', error) - recordException(error, serviceName, 'updateDocumentsSHA256') - throw error - } -} - -export const processRevisions = async ( - store: Store, - revisions: HealthPlanRevisionTable[], - serviceName: string -): Promise => { - for (const revision of revisions) { - const pkgID = revision.pkgID - const decodedFormDataProto = toDomain(revision.formDataProto) - if (!(decodedFormDataProto instanceof Error)) { - const formData = decodedFormDataProto as HealthPlanFormDataType - formData.documents = await updateDocumentsSHA256( - formData.documents, - serviceName - ) - formData.contractDocuments = await updateDocumentsSHA256( - formData.contractDocuments, - serviceName - ) - for (const rateInfo of formData.rateInfos) { - rateInfo.rateDocuments = await updateDocumentsSHA256( - rateInfo.rateDocuments, - serviceName - ) - } - try { - const update = await store.updateHealthPlanRevision( - pkgID, - revision.id, - formData - ) - if (isStoreError(update)) { - console.error( - `StoreError updating revision ${ - revision.id - }: ${JSON.stringify(update)}` - ) - throw new Error('Error updating revision') - } - } catch (err) { - console.error(`Error updating revision ${revision.id}: ${err}`) - throw err - } - } else { - console.error( - `Error decoding formDataProto for revision ${revision.id} in sha migration: ${decodedFormDataProto}` - ) - recordException( - `Error decoding formDataProto for revision ${revision.id} in sha migration: ${decodedFormDataProto}`, - serviceName, - 'processRevisions' - ) - } - } -} - -export const getDatabaseConnection = async (): Promise => { - const dbURL = process.env.DATABASE_URL - const secretsManagerSecret = process.env.SECRETS_MANAGER_SECRET - - if (!dbURL) { - console.error('DATABASE_URL not set') - throw new Error('Init Error: DATABASE_URL is required to run app-api') - } - if (!secretsManagerSecret) { - console.error('SECRETS_MANAGER_SECRET not set') - } - - const pgResult = await configurePostgres(dbURL, secretsManagerSecret) - if (pgResult instanceof Error) { - console.error( - "Init Error: Postgres couldn't be configured in data exporter" - ) - throw pgResult - } else { - console.info('Postgres configured in data exporter') - } - const store = NewPostgresStore(pgResult) - - return store -} - -export const getRevisions = async ( - store: Store -): Promise => { - const result: HealthPlanRevisionTable[] | StoreError = - await store.findAllRevisions() - if (isStoreError(result)) { - console.error( - `Error getting revisions from db ${JSON.stringify(result)}` - ) - throw new Error('Error getting records; cannot generate report') - } - - return result -} - -export const main: Handler = async (event, context) => { - // Check on the values for our required config - const stageName = process.env.stage ?? 'stageNotSet' - const serviceName = `add_sha_lambda-${stageName}` - const otelCollectorURL = process.env.REACT_APP_OTEL_COLLECTOR_URL - if (otelCollectorURL) { - initTracer(serviceName, otelCollectorURL) - } else { - console.error( - 'Configuration Error: REACT_APP_OTEL_COLLECTOR_URL must be set' - ) - } - - initMeter(serviceName) - const store = await getDatabaseConnection() - - const revisions = await getRevisions(store) - // Get the pkgID from the first revision in the list - const pkgID = revisions[0].pkgID - if (!pkgID) { - console.error('Package ID is missing in the revisions') - throw new Error('Package ID is required') - } - - await processRevisions(store, revisions, serviceName) - - console.info('SHA256 update complete') -} diff --git a/services/app-api/src/handlers/apollo_gql.ts b/services/app-api/src/handlers/apollo_gql.ts index 2abf459b38..c707be17ce 100644 --- a/services/app-api/src/handlers/apollo_gql.ts +++ b/services/app-api/src/handlers/apollo_gql.ts @@ -240,7 +240,9 @@ async function initializeGQLHandler(): Promise { const cmsRateHelpEmailAddress = await emailParameterStore.getCmsRateHelpEmail() const oactEmails = await emailParameterStore.getOACTEmails() - const dmcpEmails = await emailParameterStore.getDMCPEmails() + const dmcpReviewEmails = await emailParameterStore.getDMCPReviewEmails() + const dmcpSubmissionEmails = + await emailParameterStore.getDMCPSubmissionEmails() const dmcoEmails = await emailParameterStore.getDMCOEmails() if (emailSource instanceof Error) @@ -267,8 +269,11 @@ async function initializeGQLHandler(): Promise { if (oactEmails instanceof Error) throw new Error(`Configuration Error: ${oactEmails.message}`) - if (dmcpEmails instanceof Error) - throw new Error(`Configuration Error: ${dmcpEmails.message}`) + if (dmcpReviewEmails instanceof Error) + throw new Error(`Configuration Error: ${dmcpReviewEmails.message}`) + + if (dmcpSubmissionEmails instanceof Error) + throw new Error(`Configuration Error: ${dmcpSubmissionEmails.message}`) if (dmcoEmails instanceof Error) throw new Error(`Configuration Error: ${dmcoEmails.message}`) @@ -324,7 +329,8 @@ async function initializeGQLHandler(): Promise { cmsReviewHelpEmailAddress, cmsRateHelpEmailAddress, oactEmails, - dmcpEmails, + dmcpReviewEmails, + dmcpSubmissionEmails, dmcoEmails, helpDeskEmail, }) @@ -336,7 +342,8 @@ async function initializeGQLHandler(): Promise { cmsReviewHelpEmailAddress, cmsRateHelpEmailAddress, oactEmails, - dmcpEmails, + dmcpReviewEmails, + dmcpSubmissionEmails, dmcoEmails, helpDeskEmail, }) diff --git a/services/app-api/src/handlers/migrate_rate_documents.test.ts b/services/app-api/src/handlers/migrate_rate_documents.test.ts deleted file mode 100644 index a70b338be9..0000000000 --- a/services/app-api/src/handlers/migrate_rate_documents.test.ts +++ /dev/null @@ -1,439 +0,0 @@ -import { main } from './migrate_rate_documents' -import * as migrate_rate_documents from './migrate_rate_documents' -import type { - ActuaryCommunicationType, - DocumentCategoryType, - RateCapitationType, - RateType, - SubmissionDocument, -} from 'app-web/src/common-code/healthPlanFormDataType' -import { unlockedWithALittleBitOfEverything } from 'app-web/src/common-code/healthPlanFormDataMocks' -import type { Context } from 'aws-lambda' -import type { HealthPlanRevisionTable } from '@prisma/client' -import type { Store } from '../postgres' -import type { Event } from '@aws-sdk/client-s3' -import { toProtoBuffer } from 'app-web/src/common-code/proto/healthPlanFormDataProto' - -const mockStore: Store = { - findAllRevisions: jest.fn(), - updateHealthPlanRevision: jest.fn(), -} as unknown as Store - -jest.mock('@aws-sdk/client-secrets-manager', () => { - return { - SecretsManagerClient: jest.fn(() => { - /* empty callback */ - }), - GetSecretValueCommand: jest.fn(() => { - /* empty callback */ - }), - } -}) - -describe('migrate_rate_documents', () => { - beforeEach(() => { - jest.resetAllMocks() - jest.spyOn( - migrate_rate_documents, - 'getDatabaseConnection' - ).mockImplementation(() => Promise.resolve(mockStore)) - }) - - const extraRateInfos = [ - { - id: 'test-rate-certification-one', - rateType: 'AMENDMENT' as RateType, - rateCapitationType: 'RATE_CELL' as RateCapitationType, - rateDocuments: [ - { - s3URL: 's3://bucketname/key/foo.png', - name: 'rates cert 1', - sha256: 'fakesha', - documentCategories: [ - 'RATES_RELATED', - ] as DocumentCategoryType[], - }, - { - s3URL: 's3://bucketname/key/foo.png', - name: 'rates cert 2', - sha256: 'fakesha', - documentCategories: [ - 'RATES_RELATED', - ] as DocumentCategoryType[], - }, - ], - supportingDocuments: [], - rateDateStart: new Date(Date.UTC(2021, 4, 22)), - rateDateEnd: new Date(Date.UTC(2022, 3, 29)), - rateDateCertified: new Date(Date.UTC(2021, 4, 23)), - rateAmendmentInfo: { - effectiveDateStart: new Date(Date.UTC(2022, 5, 21)), - effectiveDateEnd: new Date(Date.UTC(2022, 9, 21)), - }, - rateProgramIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], - rateCertificationName: - 'MCR-MN-0005-SNBC-RATE-20220621-20221021-AMENDMENT-20210523', - actuaryContacts: [ - { - name: 'foo bar', - titleRole: 'manager', - email: 'soandso@example.com', - actuarialFirm: 'OTHER' as const, - actuarialFirmOther: 'ACME', - }, - { - name: 'Fine Bab', - titleRole: 'supervisor', - email: 'lodar@example.com', - actuarialFirm: 'MERCER' as const, - }, - ], - actuaryCommunicationPreference: - 'OACT_TO_ACTUARY' as ActuaryCommunicationType, - packagesWithSharedRateCerts: [], - }, - { - id: 'test-rate-certification-two', - rateType: 'AMENDMENT' as RateType, - rateCapitationType: 'RATE_CELL' as RateCapitationType, - rateDocuments: [ - { - s3URL: 's3://bucketname/key/foo1.png', - name: 'rates cert 1', - sha256: 'fakesha', - documentCategories: [ - 'RATES_RELATED', - ] as DocumentCategoryType[], - }, - { - s3URL: 's3://bucketname/key/foo2.png', - name: 'rates cert 2', - sha256: 'fakesha', - documentCategories: [ - 'RATES_RELATED', - ] as DocumentCategoryType[], - }, - ], - supportingDocuments: [], - rateDateStart: new Date(Date.UTC(2021, 4, 22)), - rateDateEnd: new Date(Date.UTC(2022, 3, 29)), - rateDateCertified: new Date(Date.UTC(2021, 4, 23)), - rateAmendmentInfo: { - effectiveDateStart: new Date(Date.UTC(2022, 5, 21)), - effectiveDateEnd: new Date(Date.UTC(2022, 9, 21)), - }, - rateProgramIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51ggf'], - rateCertificationName: - 'MCR-MN-0005-PMAP-RATE-20220621-20221021-AMENDMENT-20210523', - actuaryContacts: [ - { - name: 'foo bar', - titleRole: 'manager', - email: 'soandso@example.com', - actuarialFirm: 'OTHER' as const, - actuarialFirmOther: 'ACME', - }, - { - name: 'Fine Bab', - titleRole: 'supervisor', - email: 'lodar@example.com', - actuarialFirm: 'MERCER' as const, - }, - ], - actuaryCommunicationPreference: - 'OACT_TO_ACTUARY' as ActuaryCommunicationType, - packagesWithSharedRateCerts: [], - }, - ] - - function createRevisions(documents: SubmissionDocument[], id = 'mockId') { - return [ - { - id, - createdAt: new Date(), - pkgID: 'mockPkgID', - formDataProto: Buffer.from( - toProtoBuffer({ - ...unlockedWithALittleBitOfEverything(), - rateInfos: extraRateInfos, - documents, - id, - }) - ), - submittedAt: new Date(), - unlockedAt: new Date(), - unlockedBy: 'mockUnlockedBy', - unlockedReason: 'mockUnlockedReason', - submittedBy: 'mockSubmittedBy', - submittedReason: 'mockSubmittedReason', - }, - ] - } - - function createBlankRevisions( - documents: SubmissionDocument[], - id = 'mockId' - ) { - return [ - { - id, - createdAt: new Date(), - pkgID: 'mockPkgID', - formDataProto: {} as Buffer, - submittedAt: new Date(), - unlockedAt: new Date(), - unlockedBy: 'mockUnlockedBy', - unlockedReason: 'mockUnlockedReason', - submittedBy: 'mockSubmittedBy', - submittedReason: 'mockSubmittedReason', - }, - { - id, - createdAt: new Date(), - pkgID: 'mockPkgID', - formDataProto: Buffer.from( - toProtoBuffer({ - ...unlockedWithALittleBitOfEverything(), - rateInfos: extraRateInfos, - documents, - id, - }) - ), - submittedAt: new Date(), - unlockedAt: new Date(), - unlockedBy: 'mockUnlockedBy', - unlockedReason: 'mockUnlockedReason', - submittedBy: 'mockSubmittedBy', - submittedReason: 'mockSubmittedReason', - }, - ] - } - - it('should move rate related documents to rateInfos', async () => { - // Create a revision with a rate related document - const revisions: HealthPlanRevisionTable[] = createRevisions([ - { - s3URL: 's3://bucketname/key/foo.png', - name: 'contract doc', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED'], - }, - ]) - - const storeFindAllRevisionsSpy = jest.spyOn( - mockStore, - 'findAllRevisions' - ) - storeFindAllRevisionsSpy.mockResolvedValue(revisions) - - const updateHealthPlanRevisionSpy = jest.spyOn( - mockStore, - 'updateHealthPlanRevision' - ) - - await main({} as Event, {} as Context, () => { - /*empty callback*/ - }) - - // the document should be moved to rateInfos - expect(updateHealthPlanRevisionSpy).toHaveBeenCalledWith( - 'mockPkgID', - 'mockId', - expect.objectContaining({ - rateInfos: expect.arrayContaining([ - expect.objectContaining({ - supportingDocuments: expect.arrayContaining([ - expect.objectContaining({ - name: 'contract doc', - }), - ]), - }), - ]), - documents: expect.not.arrayContaining([ - expect.objectContaining({ - name: 'contract doc', - }), - ]), - }) - ) - }) - - it('should skip a revision with an error and handle all other revisions', async () => { - // Create a revision with a rate related document - const revisions: HealthPlanRevisionTable[] = createBlankRevisions([ - { - s3URL: 's3://bucketname/key/foo.png', - name: 'contract doc', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED'], - }, - ]) - - const storeFindAllRevisionsSpy = jest.spyOn( - mockStore, - 'findAllRevisions' - ) - storeFindAllRevisionsSpy.mockResolvedValue(revisions) - - const updateHealthPlanRevisionSpy = jest.spyOn( - mockStore, - 'updateHealthPlanRevision' - ) - - await main({} as Event, {} as Context, () => { - /*empty callback*/ - }) - - // the document should be moved to rateInfos - expect(updateHealthPlanRevisionSpy).toHaveBeenCalledWith( - 'mockPkgID', - 'mockId', - expect.objectContaining({ - rateInfos: expect.arrayContaining([ - expect.objectContaining({ - supportingDocuments: expect.arrayContaining([ - expect.objectContaining({ - name: 'contract doc', - }), - ]), - }), - ]), - documents: expect.not.arrayContaining([ - expect.objectContaining({ - name: 'contract doc', - }), - ]), - }) - ) - }) - it('should skip submissions with no rate infos', async () => { - const revisions: HealthPlanRevisionTable[] = [ - { - id: 'mockId', - createdAt: new Date(), - pkgID: 'mockPkgID', - formDataProto: Buffer.from( - toProtoBuffer({ - ...unlockedWithALittleBitOfEverything(), - rateInfos: [], - }) - ), - submittedAt: new Date(), - unlockedAt: new Date(), - unlockedBy: 'mockUnlockedBy', - unlockedReason: 'mockUnlockedReason', - submittedBy: 'mockSubmittedBy', - submittedReason: 'mockSubmittedReason', - }, - ] - - const storeFindAllRevisionsSpy = jest.spyOn( - mockStore, - 'findAllRevisions' - ) - storeFindAllRevisionsSpy.mockResolvedValue(revisions) - - const updateHealthPlanRevisionSpy = jest.spyOn( - mockStore, - 'updateHealthPlanRevision' - ) - - await main({} as Event, {} as Context, () => { - /*empty callback*/ - }) - - expect(updateHealthPlanRevisionSpy).not.toHaveBeenCalled() - }) - - it('should move specific documents to specific places in rateInfos', async () => { - // Create a revision with two specific documents - const revisions: HealthPlanRevisionTable[] = createRevisions( - [ - { - s3URL: 's3://bucketname/key/foo.png', - name: 'Report12 - SFY 2022 Preliminary MississippiCAN Capitation Rates - Exhibits.xlsx', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED'], - }, - { - s3URL: 's3://bucketname/key/bar.png', - name: 'Report13 - SFY 2023 Preliminary MississippiCAN Capitation Rates - Exhibits.xlsx', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED'], - }, - { - s3URL: 's3://bucketname/key/baz.png', - name: 'unrelated doc', - sha256: 'fakesha', - documentCategories: ['CONTRACT'], - }, - ], - 'ddd5dde1-0082-4398-90fe-89fc1bc148df' - ) // This is the id specified in the handler for the special case - const storeFindAllRevisionsSpy = jest.spyOn( - mockStore, - 'findAllRevisions' - ) - storeFindAllRevisionsSpy.mockResolvedValue(revisions) - - const updateHealthPlanRevisionSpy = jest.spyOn( - mockStore, - 'updateHealthPlanRevision' - ) - - await main({} as Event, {} as Context, () => { - /*empty callback*/ - }) - - expect(updateHealthPlanRevisionSpy).toHaveBeenCalledTimes(1) - - const formData = updateHealthPlanRevisionSpy.mock.calls[0][2] - // Check that Report12 is moved to rateInfos[0] and not present in documents - expect(formData.rateInfos[0].supportingDocuments).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: 'Report12 - SFY 2022 Preliminary MississippiCAN Capitation Rates - Exhibits.xlsx', - }), - ]) - ) - expect(formData.documents).toEqual( - expect.not.arrayContaining([ - expect.objectContaining({ - name: 'Report12 - SFY 2022 Preliminary MississippiCAN Capitation Rates - Exhibits.xlsx', - }), - ]) - ) - - // Check that Report13 is moved to rateInfos[1] and not present in documents - expect(formData.rateInfos[1].supportingDocuments).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: 'Report13 - SFY 2023 Preliminary MississippiCAN Capitation Rates - Exhibits.xlsx', - }), - ]) - ) - expect(formData.documents).toEqual( - expect.not.arrayContaining([ - expect.objectContaining({ - name: 'Report13 - SFY 2023 Preliminary MississippiCAN Capitation Rates - Exhibits.xlsx', - }), - ]) - ) - - // Check that the unrelated doc remains in documents and not in rateInfos - expect(formData.documents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'unrelated doc' }), - ]) - ) - expect(formData.rateInfos).toEqual( - expect.not.arrayContaining([ - expect.objectContaining({ - supportingDocuments: expect.arrayContaining([ - expect.objectContaining({ name: 'unrelated doc' }), - ]), - }), - ]) - ) - }) -}) diff --git a/services/app-api/src/handlers/migrate_rate_documents.ts b/services/app-api/src/handlers/migrate_rate_documents.ts deleted file mode 100644 index 224be0605b..0000000000 --- a/services/app-api/src/handlers/migrate_rate_documents.ts +++ /dev/null @@ -1,215 +0,0 @@ -import type { Handler } from 'aws-lambda' -import { configurePostgres } from './configuration' -import { NewPostgresStore } from '../postgres/postgresStore' -import type { HealthPlanRevisionTable } from '@prisma/client' -import type { HealthPlanFormDataType } from '../../../app-web/src/common-code/healthPlanFormDataType' -import { toDomain } from '../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import type { StoreError } from '../postgres/storeError' -import { isStoreError } from '../postgres/storeError' -import type { Store } from '../postgres' -import { - initTracer, - initMeter, - recordException, -} from '../../../uploads/src/lib/otel' - -/* We're changing where we store rate documents on the proto. We're moving them from fromDataProto.documents -to formDataProto.rateInfos[0].supportingDocuments. This migration will move the documents from the old location -to the new location, removing the documents from formDataProto.documents. */ - -export const processRevisions = async ( - store: Store, - revisions: HealthPlanRevisionTable[] -): Promise => { - console.info('STARTING process revisions') - const stageName = process.env.stage ?? 'stageNotSet' - const serviceName = `migrate_rate_documents_lambda-${stageName}` - const otelCollectorURL = process.env.REACT_APP_OTEL_COLLECTOR_URL - if (otelCollectorURL) { - initTracer(serviceName, otelCollectorURL) - } else { - console.error( - 'Configuration Error: REACT_APP_OTEL_COLLECTOR_URL must be set' - ) - } - - // Get the pkgID from the first revision in the list - not sure why we need? - const pkgID = revisions[0].pkgID - if (!pkgID) { - console.error('Package ID is missing in the revisions') - throw new Error('Package ID is required') - } - - initMeter(serviceName) - let revisionsEdited = 0 - let revisionsMigrated = 0 - console.info('starting to loop through revisions') - for (const revision of revisions) { - const pkgID = revision.pkgID - const decodedFormDataProto = toDomain(revision.formDataProto) - - if (!(decodedFormDataProto instanceof Error)) { - const formData = decodedFormDataProto as HealthPlanFormDataType - if ( - formData.submissionType === 'CONTRACT_ONLY' || - !formData.rateInfos[0] - ) { - continue // no need to migrate these - } - // skip the submission with two rates - if (formData.id !== 'ddd5dde1-0082-4398-90fe-89fc1bc148df') { - if (formData.rateInfos.length > 1 && formData.documents) { - console.info( - `UNEXPECTED: There is an additional submission on this environment with rate supporting docs to be migrated. ID: ${formData.id}` - ) - } - - // we know the other submissions have only a single rate to handle - const ratesRelatedDocument = formData.documents.filter((doc) => - doc.documentCategories.includes('RATES_RELATED') - ) - - formData.rateInfos[0].supportingDocuments = ratesRelatedDocument - formData.documents = formData.documents.filter( - (doc) => !ratesRelatedDocument.includes(doc) - ) - revisionsEdited++ - } else { - // now handle the submission with two rates - const firstRateRelatedDocument = formData.documents.filter( - (doc) => - doc.name === - 'Report12 - SFY 2022 Preliminary MississippiCAN Capitation Rates - Exhibits.xlsx' - ) - - const secondRateRelatedDocument = formData.documents.filter( - (doc) => - doc.name === - 'Report13 - SFY 2023 Preliminary MississippiCAN Capitation Rates - Exhibits.xlsx' - ) - - if ( - firstRateRelatedDocument.length === 0 || - secondRateRelatedDocument.length === 0 - ) { - console.info( - 'UNEXPECTED: Rate related documents for odd duck submission are incorrect', - firstRateRelatedDocument, - secondRateRelatedDocument - ) - } - if ( - !formData.rateInfos[0] || - !formData.rateInfos[1].supportingDocuments - ) { - console.info( - 'UNEXPECTED: Rate infos for odd duck submission are incorrect', - formData.rateInfos - ) - } - - formData.rateInfos[0].supportingDocuments = - firstRateRelatedDocument - formData.rateInfos[1].supportingDocuments = - secondRateRelatedDocument - formData.documents = formData.documents.filter( - (doc) => - !firstRateRelatedDocument.includes(doc) && - !secondRateRelatedDocument.includes(doc) - ) - revisionsEdited++ - console.info('in the loop of editing revisions') - } - - try { - console.info(`updating submission ${pkgID}`) - const update = await store.updateHealthPlanRevision( - pkgID, - revision.id, - formData - ) - if (isStoreError(update)) { - console.error( - `StoreError updating revision ${ - revision.id - }: ${JSON.stringify(update)}` - ) - throw new Error('Error updating revision') - } else { - revisionsMigrated++ - } - } catch (err) { - console.error(`Error updating revision ${revision.id}: ${err}`) - throw err - } - } else { - const error = `UNEXPECTED: Error decoding formDataProto for revision ${revision.id} in package ${revision.pkgID} in rate migration: ${decodedFormDataProto}` - console.error(error) - recordException(error, serviceName, 'migrate_rate_documents') - } - } - console.info( - `There were ${revisionsEdited}/${revisions.length} were flagged for changes` - ) - console.info( - `And ${revisionsMigrated}/ ${revisions.length} successfully migrated` - ) -} - -export const getDatabaseConnection = async (): Promise => { - const dbURL = process.env.DATABASE_URL - const secretsManagerSecret = process.env.SECRETS_MANAGER_SECRET - - if (!dbURL) { - console.error('DATABASE_URL not set') - throw new Error('Init Error: DATABASE_URL is required to run app-api') - } - if (!secretsManagerSecret) { - console.error('SECRETS_MANAGER_SECRET not set') - } - - const pgResult = await configurePostgres(dbURL, secretsManagerSecret) - if (pgResult instanceof Error) { - console.error( - "Init Error: Postgres couldn't be configured in data exporter" - ) - throw pgResult - } else { - console.info('Postgres configured in data exporter') - } - const store = NewPostgresStore(pgResult) - - return store -} - -export const getRevisions = async ( - store: Store -): Promise => { - const result: HealthPlanRevisionTable[] | StoreError = - await store.findAllRevisions() - if (isStoreError(result)) { - console.error( - `Error getting revisions from db ${JSON.stringify(result)}` - ) - throw new Error('Error getting records; cannot generate report') - } - return result -} - -export const main: Handler = async (event, context) => { - console.info('STARTING') - try { - const store = await getDatabaseConnection() - const revisions = await getRevisions(store) - - try { - await processRevisions(store, revisions) - } catch (processRevisionsError) { - console.error(`ERROR process revisions: ${processRevisionsError}`) - } - - console.info('rate document migration complete') - } catch (error) { - console.error(`ERROR: ${error}`) - } -} diff --git a/services/app-api/src/handlers/postgres_migrate.ts b/services/app-api/src/handlers/postgres_migrate.ts index 99b7448e9e..64c36b2e1a 100644 --- a/services/app-api/src/handlers/postgres_migrate.ts +++ b/services/app-api/src/handlers/postgres_migrate.ts @@ -120,43 +120,6 @@ export const main: Handler = async (): Promise => { } } - // run the proto data migration. this will run any data changes to the protobufs stored in postgres - try { - const connectTimeout = process.env.CONNECT_TIMEOUT ?? '60' - const migrateProtosResult = spawnSync( - `${process.execPath}`, - [ - '/opt/nodejs/protoMigrator/migrate_protos.js', - 'db', - '/opt/nodejs/protoMigrator/healthPlanFormDataMigrations', - ], - { - env: { - DATABASE_URL: - dbConnectionURL + `&connect_timeout=${connectTimeout}`, - }, - } - ) - - console.info( - 'stderror', - migrateProtosResult.stderr && migrateProtosResult.stderr.toString() - ) - console.info( - 'stdout', - migrateProtosResult.stdout && migrateProtosResult.stdout.toString() - ) - if (migrateProtosResult.status !== 0) { - const errMsg = `Could not run migrate_protos db: ${migrateProtosResult.stderr.toString()}` - recordException(errMsg, serviceName, 'migrate_protos db') - return fmtMigrateError(errMsg) - } - } catch (err) { - const errMsg = `Could not migrate the database protobufs: ${err}` - recordException(errMsg, serviceName, 'migrate protos db') - return fmtMigrateError(errMsg) - } - // Run the prisma dataMigrations. // these are compiled in app-api so we can call them directly diff --git a/services/app-api/src/handlers/proto_to_db.test.ts b/services/app-api/src/handlers/proto_to_db.test.ts deleted file mode 100644 index d1ffbe7ca1..0000000000 --- a/services/app-api/src/handlers/proto_to_db.test.ts +++ /dev/null @@ -1,729 +0,0 @@ -import { todaysDate } from '../testHelpers/dateHelpers' -import { - constructTestPostgresServer, - createAndSubmitTestHealthPlanPackage, - createAndUpdateTestHealthPlanPackage, - defaultFloridaProgram, - fetchTestHealthPlanPackageById, - resubmitTestHealthPlanPackage, - submitTestHealthPlanPackage, - unlockTestHealthPlanPackage, - updateTestHealthPlanFormData, -} from '../testHelpers/gqlHelpers' -import { v4 as uuidv4 } from 'uuid' -import { latestFormData } from '../testHelpers/healthPlanPackageHelpers' -import { testLDService } from '../testHelpers/launchDarklyHelpers' -import { testCMSUser } from '../testHelpers/userHelpers' -import UNLOCK_HEALTH_PLAN_PACKAGE from 'app-graphql/src/mutations/unlockHealthPlanPackage.graphql' -import { - cleanupPreviousProtoMigrate, - decodeFormDataProto, - migrateHealthPlanRevisions, -} from './proto_to_db' -import { sharedTestPrismaClient } from '../testHelpers/storeHelpers' -import { findAllRevisions } from '../postgres/healthPlanPackage' -import { isStoreError } from '../postgres' -import { - base64ToDomain, - toProtoBuffer, -} from '../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import assert from 'assert' -import { packageName } from '../../../app-web/src/common-code/healthPlanFormDataType' -import type { - HealthPlanFormDataType, - LockedHealthPlanFormDataType, -} from '../../../app-web/src/common-code/healthPlanFormDataType' - -describe.skip('test that we migrate things', () => { - const mockPreRefactorLDService = testLDService({ - 'rates-db-refactor': false, - }) - const mockPostRefactorLDService = testLDService({ - 'rates-db-refactor': true, - }) - - const cmsUser = testCMSUser() - - it('allows for multiple edits, editing the set of revisions correctly', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockPreRefactorLDService, - }) - - // First, create a new submitted submission - const stateDraft = await createAndSubmitTestHealthPlanPackage( - stateServer - // unlockedWithFullRates(), - ) - - const cmsServer = await constructTestPostgresServer({ - ldService: mockPreRefactorLDService, - context: { - user: cmsUser, - }, - }) - - // Unlock - const unlockResult = await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateDraft.id, - unlockedReason: 'Super duper good reason.', - }, - }, - }) - - expect(unlockResult.errors).toBeUndefined() - const unlockedSub = unlockResult?.data?.unlockHealthPlanPackage.pkg - - // After unlock, we should get a draft submission back - expect(unlockedSub.status).toBe('UNLOCKED') - expect(unlockedSub.revisions[0].node.unlockInfo).toBeDefined() - expect(unlockedSub.revisions[0].node.unlockInfo?.updatedBy).toBe( - 'zuko@example.com' - ) - expect(unlockedSub.revisions[0].node.unlockInfo?.updatedReason).toBe( - 'Super duper good reason.' - ) - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain(todaysDate()) - // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain('Z') - - const formData = latestFormData(unlockedSub) - - // after unlock we should be able to update that draft submission and get the results - formData.submissionDescription = 'UPDATED_AFTER_UNLOCK' - - formData.rateInfos.push( - { - id: uuidv4(), - rateDateStart: new Date(), - rateDateEnd: new Date(), - rateProgramIDs: ['5c10fe9f-bec9-416f-a20c-718b152ad633'], - rateType: 'NEW', - rateDateCertified: new Date(), - rateDocuments: [ - { - name: 'fake doc', - s3URL: 'foo://bar', - documentCategories: ['RATES'], - sha256: 'fakesha', - }, - ], - supportingDocuments: [], - actuaryContacts: [ - { - name: 'Enrico Soletzo', - titleRole: 'person', - email: 'en@example.com', - actuarialFirm: 'MERCER', - }, - ], - }, - { - id: uuidv4(), - rateDateStart: new Date(), - rateDateEnd: new Date(), - rateProgramIDs: ['5c10fe9f-bec9-416f-a20c-718b152ad633'], - rateType: 'NEW', - rateDateCertified: new Date(), - rateDocuments: [ - { - name: 'fake doc number two', - s3URL: 'foo://bar/two', - documentCategories: ['RATES'], - sha256: 'fakesha', - }, - { - name: 'fake doc number three', - s3URL: 'foo://bar/three', - documentCategories: ['RATES'], - sha256: 'fakesha', - }, - ], - supportingDocuments: [], - actuaryContacts: [ - { - name: 'Enrico Soletzo', - titleRole: 'person', - email: 'en@example.com', - actuarialFirm: 'MERCER', - }, - ], - } - ) - - await updateTestHealthPlanFormData(stateServer, formData) - - const refetched = await fetchTestHealthPlanPackageById( - stateServer, - stateDraft.id - ) - - const refetchedFormData = latestFormData(refetched) - - expect(refetchedFormData.submissionDescription).toBe( - 'UPDATED_AFTER_UNLOCK' - ) - - expect(refetchedFormData.rateInfos).toHaveLength(3) - - const rateDocs = refetchedFormData.rateInfos.map( - (r) => r.rateDocuments[0].name - ) - expect(rateDocs).toEqual([ - 'rateDocument.pdf', - 'fake doc', - 'fake doc number two', - ]) - - await resubmitTestHealthPlanPackage( - stateServer, - stateDraft.id, - 'Test first resubmission reason' - ) - - const unlockedPKG = await unlockTestHealthPlanPackage( - cmsServer, - stateDraft.id, - 'unlock to remove rate' - ) - - const unlockedFormData = latestFormData(unlockedPKG) - - // remove the first rate - unlockedFormData.submissionDescription = 'FINAL_DESCRIPTION' - unlockedFormData.rateInfos = unlockedFormData.rateInfos.slice(1) - - await updateTestHealthPlanFormData(stateServer, unlockedFormData) - - const finallySubmittedPKG = await resubmitTestHealthPlanPackage( - stateServer, - stateDraft.id, - 'Test second resubmission reason' - ) - - const finallySubmittedFormData = latestFormData(finallySubmittedPKG) - - expect(finallySubmittedFormData.rateInfos).toHaveLength(2) - const finalRateDocs = finallySubmittedFormData.rateInfos.map( - (r) => r.rateDocuments[0].name - ) - expect(finalRateDocs).toEqual(['fake doc', 'fake doc number two']) - - // Now that we have a fully submitted package, we run the proto migrator on it - const prismaClient = await sharedTestPrismaClient() - - // first reset us to the pre-proto migration tables state - const cleanResult = await cleanupPreviousProtoMigrate(prismaClient) - if (cleanResult instanceof Error) { - const error = new Error( - `Could not reset the DB: ${cleanResult.message}` - ) - throw error - } - - // look up the HPP using prisma methods. The migrator relies on finding all the - // revisions in the DB and uses the Prisma type. - const allRevisions = await findAllRevisions(prismaClient) - if (isStoreError(allRevisions)) { - const error = new Error( - `Could not fetch revisions from DB: ${allRevisions.message}` - ) - throw error - } - - // for our test we just want the test data we made above to make expects on, not - // absolutely everything in the local DB - const revisionsToMigrate = [] - for (const revision of allRevisions) { - const formData = decodeFormDataProto(revision) - if (formData instanceof Error) { - const error = new Error( - `Could not decode form data from revision in test: ${formData.message}` - ) - throw error - } - if (formData.id === finallySubmittedPKG.id) { - revisionsToMigrate.push(revision) - } - } - - const migrateErrors = await migrateHealthPlanRevisions( - prismaClient, - revisionsToMigrate - ) - if (migrateErrors.length > 0) { - const error = new Error( - `Could not get a migrated revision back: ${migrateErrors}` - ) - console.error(error) - throw error - } - - const stateServerPost = await constructTestPostgresServer({ - ldService: mockPostRefactorLDService, - }) - - // let's fetch the HPP from the new contract and revision tables - const fetchedHPP = await fetchTestHealthPlanPackageById( - stateServerPost, - finallySubmittedPKG.id - ) - - // check HPP post refactor to HPP pre refactor - // finallySubmittedPKG is what came back from the last submission - // fetchedHPP is what came back from fetchHPP with the rate refactor flag on - expect(finallySubmittedPKG.revisions).toHaveLength( - fetchedHPP.revisions.length - ) - - const preFDS: HealthPlanFormDataType[] = [] - const postFDS: HealthPlanFormDataType[] = [] - for (let i = 0; i < finallySubmittedPKG.revisions.length; i++) { - const preRev = finallySubmittedPKG.revisions[i].node - const postRev = fetchedHPP.revisions[i].node - - const preFD = base64ToDomain(preRev.formDataProto) - const postFD = base64ToDomain(postRev.formDataProto) - - if (preFD instanceof Error || postFD instanceof Error) { - throw new Error('Got an error decoding') - } - - preFDS.push(preFD) - postFDS.push(postFD) - } - - console.info( - 'COMPARE ORDER', - preFDS.map((fd) => [fd.submissionDescription, fd.createdAt]), - postFDS.map((fd) => [fd.submissionDescription, fd.createdAt]) - ) - - for (let i = 0; i < finallySubmittedPKG.revisions.length; i++) { - const preFD = preFDS[i] - const postFD = postFDS[i] - - // deepStrictEqual args: actual comes first then expected - assert.deepStrictEqual(postFD, preFD, 'form data not equal') - } - }, 20000) - - it('sets submitted at at correctly.', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockPreRefactorLDService, - }) - - // First, create a new submitted submission - const stateDraft = await createAndSubmitTestHealthPlanPackage( - stateServer - // unlockedWithFullRates(), - ) - - const prismaClient = await sharedTestPrismaClient() - - // Modify submitted at on the first revision to not equal the most recent updatedAt date. - // First in the proto - const oldRevFD = latestFormData( - stateDraft - ) as LockedHealthPlanFormDataType - oldRevFD.submittedAt = new Date(2023, 0, 1) - const newFD = Buffer.from(toProtoBuffer(oldRevFD)) - - // now in the submit info. - await prismaClient.healthPlanRevisionTable.update({ - where: { id: stateDraft.revisions[0].node.id }, - data: { - submittedAt: new Date(2023, 0, 1), - formDataProto: newFD, - }, - }) - - // Now, run the migrator - // first reset us to the pre-proto migration tables state - const cleanResult = await cleanupPreviousProtoMigrate(prismaClient) - if (cleanResult instanceof Error) { - const error = new Error( - `Could not reset the DB: ${cleanResult.message}` - ) - throw error - } - - // look up the HPP using prisma methods. The migrator relies on finding all the - // revisions in the DB and uses the Prisma type. - const allRevisions = await findAllRevisions(prismaClient) - if (isStoreError(allRevisions)) { - const error = new Error( - `Could not fetch revisions from DB: ${allRevisions.message}` - ) - throw error - } - - // for our test we just want the test data we made above to make expects on, not - // absolutely everything in the local DB - const revisionsToMigrate = [] - for (const revision of allRevisions) { - const formData = decodeFormDataProto(revision) - if (formData instanceof Error) { - const error = new Error( - `Could not decode form data from revision in test: ${formData.message}` - ) - throw error - } - if (formData.id === stateDraft.id) { - revisionsToMigrate.push(revision) - } - } - - const migrateErrors = await migrateHealthPlanRevisions( - prismaClient, - revisionsToMigrate - ) - if (migrateErrors.length > 0) { - const error = new Error( - `Could not get a migrated revision back: ${migrateErrors}` - ) - console.error(error) - throw error - } - - // Now compare new to old. - const oldHPP = await fetchTestHealthPlanPackageById( - stateServer, - stateDraft.id - ) - - const stateServerPost = await constructTestPostgresServer({ - ldService: mockPostRefactorLDService, - }) - - // let's fetch the HPP from the new contract and revision tables - const fetchedHPP = await fetchTestHealthPlanPackageById( - stateServerPost, - stateDraft.id - ) - - expect(fetchedHPP.initiallySubmittedAt).toEqual( - oldHPP.initiallySubmittedAt - ) - expect(fetchedHPP.revisions[0].node.submitInfo?.updatedAt).toEqual( - oldHPP.revisions[0].node.submitInfo?.updatedAt - ) - - // check HPP post refactor to HPP pre refactor - // finallySubmittedPKG is what came back from the last submission - // fetchedHPP is what came back from fetchHPP with the rate refactor flag on - expect(oldHPP.revisions).toHaveLength(fetchedHPP.revisions.length) - - const preFDS: HealthPlanFormDataType[] = [] - const postFDS: HealthPlanFormDataType[] = [] - for (let i = 0; i < oldHPP.revisions.length; i++) { - const preRev = oldHPP.revisions[i].node - const postRev = fetchedHPP.revisions[i].node - - const preFD = base64ToDomain(preRev.formDataProto) - const postFD = base64ToDomain(postRev.formDataProto) - - if (preFD instanceof Error || postFD instanceof Error) { - throw new Error('Got an error decoding') - } - - preFDS.push(preFD) - postFDS.push(postFD) - } - - console.info( - 'COMPARE ORDER', - preFDS.map((fd) => [fd.submissionDescription, fd.createdAt]), - postFDS.map((fd) => [fd.submissionDescription, fd.createdAt]) - ) - - for (let i = 0; i < oldHPP.revisions.length; i++) { - const preFD = preFDS[i] - const postFD = postFDS[i] - - // deepStrictEqual args: actual comes first then expected - assert.deepStrictEqual(postFD, preFD, 'form data not equal') - } - }, 20000) - - it('sets unlocked at correctly.', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockPreRefactorLDService, - }) - - // First, create a new submitted submission - const stateDraft = await createAndSubmitTestHealthPlanPackage( - stateServer - // unlockedWithFullRates(), - ) - - const cmsServer = await constructTestPostgresServer({ - ldService: mockPreRefactorLDService, - context: { - user: cmsUser, - }, - }) - - // Unlock - await unlockTestHealthPlanPackage( - cmsServer, - stateDraft.id, - 'Super duper good reason.' - ) - - const prismaClient = await sharedTestPrismaClient() - - // Now, run the migrator - // first reset us to the pre-proto migration tables state - const cleanResult = await cleanupPreviousProtoMigrate(prismaClient) - if (cleanResult instanceof Error) { - const error = new Error( - `Could not reset the DB: ${cleanResult.message}` - ) - throw error - } - - // look up the HPP using prisma methods. The migrator relies on finding all the - // revisions in the DB and uses the Prisma type. - const allRevisions = await findAllRevisions(prismaClient) - if (isStoreError(allRevisions)) { - const error = new Error( - `Could not fetch revisions from DB: ${allRevisions.message}` - ) - throw error - } - - // for our test we just want the test data we made above to make expects on, not - // absolutely everything in the local DB - const revisionsToMigrate = [] - for (const revision of allRevisions) { - const formData = decodeFormDataProto(revision) - if (formData instanceof Error) { - const error = new Error( - `Could not decode form data from revision in test: ${formData.message}` - ) - throw error - } - if (formData.id === stateDraft.id) { - revisionsToMigrate.push(revision) - } - } - - const migrateErrors = await migrateHealthPlanRevisions( - prismaClient, - revisionsToMigrate - ) - if (migrateErrors.length > 0) { - const error = new Error( - `Could not get a migrated revision back: ${migrateErrors}` - ) - console.error(error) - throw error - } - - // Now compare new to old. - const oldHPP = await fetchTestHealthPlanPackageById( - stateServer, - stateDraft.id - ) - - const stateServerPost = await constructTestPostgresServer({ - ldService: mockPostRefactorLDService, - }) - - // let's fetch the HPP from the new contract and revision tables - const fetchedHPP = await fetchTestHealthPlanPackageById( - stateServerPost, - stateDraft.id - ) - - expect(fetchedHPP.initiallySubmittedAt).toEqual( - oldHPP.initiallySubmittedAt - ) - expect(fetchedHPP.revisions[0].node.unlockInfo?.updatedAt).toEqual( - oldHPP.revisions[0].node.unlockInfo?.updatedAt - ) - - // check HPP post refactor to HPP pre refactor - // finallySubmittedPKG is what came back from the last submission - // fetchedHPP is what came back from fetchHPP with the rate refactor flag on - expect(oldHPP.revisions).toHaveLength(fetchedHPP.revisions.length) - - const preFDS: HealthPlanFormDataType[] = [] - const postFDS: HealthPlanFormDataType[] = [] - for (let i = 0; i < oldHPP.revisions.length; i++) { - const preRev = oldHPP.revisions[i].node - const postRev = fetchedHPP.revisions[i].node - - const preFD = base64ToDomain(preRev.formDataProto) - const postFD = base64ToDomain(postRev.formDataProto) - - if (preFD instanceof Error || postFD instanceof Error) { - throw new Error('Got an error decoding') - } - - preFDS.push(preFD) - postFDS.push(postFD) - } - - console.info( - 'COMPARE ORDER', - preFDS.map((fd) => [fd.submissionDescription, fd.createdAt]), - postFDS.map((fd) => [fd.submissionDescription, fd.createdAt]) - ) - - for (let i = 0; i < oldHPP.revisions.length; i++) { - const preFD = preFDS[i] - const postFD = postFDS[i] - - // who cares about updated at. - preFD.updatedAt = new Date() - postFD.updatedAt = preFD.updatedAt - - // deepStrictEqual args: actual comes first then expected - assert.deepStrictEqual(postFD, preFD, 'form data not equal') - } - }, 20000) - - it('deals with related rates correctly.', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockPreRefactorLDService, - }) - - // First, create a new submitted submission - const stateDraft = await createAndSubmitTestHealthPlanPackage( - stateServer - ) - - const stateFD = latestFormData(stateDraft) - - const draftWithRelatedRate = await createAndUpdateTestHealthPlanPackage( - stateServer - ) - - const draftRate = latestFormData(draftWithRelatedRate) - draftRate.submissionDescription = 'This has a Related Rate' - draftRate.rateInfos[0].packagesWithSharedRateCerts = [ - { - packageId: stateDraft.id, - packageName: packageName( - stateDraft.stateCode, - stateFD.stateNumber, - stateFD.programIDs, - [defaultFloridaProgram()] - ), - }, - ] - - await updateTestHealthPlanFormData(stateServer, draftRate) - - await submitTestHealthPlanPackage(stateServer, draftWithRelatedRate.id) - - const prismaClient = await sharedTestPrismaClient() - // Now, run the migrator - // first reset us to the pre-proto migration tables state - const cleanResult = await cleanupPreviousProtoMigrate(prismaClient) - if (cleanResult instanceof Error) { - const error = new Error( - `Could not reset the DB: ${cleanResult.message}` - ) - throw error - } - - // look up the HPP using prisma methods. The migrator relies on finding all the - // revisions in the DB and uses the Prisma type. - const allRevisions = await findAllRevisions(prismaClient) - if (isStoreError(allRevisions)) { - const error = new Error( - `Could not fetch revisions from DB: ${allRevisions.message}` - ) - throw error - } - - // for our test we just want the test data we made above to make expects on, not - // absolutely everything in the local DB - const revisionsToMigrate = [] - for (const revision of allRevisions) { - const formData = decodeFormDataProto(revision) - if (formData instanceof Error) { - const error = new Error( - `Could not decode form data from revision in test: ${formData.message}` - ) - throw error - } - if ( - formData.id === stateDraft.id || - formData.id === draftWithRelatedRate.id - ) { - revisionsToMigrate.push(revision) - } - } - - const migrateErrors = await migrateHealthPlanRevisions( - prismaClient, - revisionsToMigrate - ) - if (migrateErrors.length > 0) { - const error = new Error( - `Could not get a migrated revision back: ${migrateErrors}` - ) - console.error(error) - throw error - } - - // Now compare new to old. - const oldHPP = await fetchTestHealthPlanPackageById( - stateServer, - draftWithRelatedRate.id - ) - - const stateServerPost = await constructTestPostgresServer({ - ldService: mockPostRefactorLDService, - }) - - // let's fetch the HPP from the new contract and revision tables - const fetchedHPP = await fetchTestHealthPlanPackageById( - stateServerPost, - draftWithRelatedRate.id - ) - - // check HPP post refactor to HPP pre refactor - // finallySubmittedPKG is what came back from the last submission - // fetchedHPP is what came back from fetchHPP with the rate refactor flag on - expect(oldHPP.revisions).toHaveLength(fetchedHPP.revisions.length) - - const preFDS: HealthPlanFormDataType[] = [] - const postFDS: HealthPlanFormDataType[] = [] - for (let i = 0; i < oldHPP.revisions.length; i++) { - const preRev = oldHPP.revisions[i].node - const postRev = fetchedHPP.revisions[i].node - - const preFD = base64ToDomain(preRev.formDataProto) - const postFD = base64ToDomain(postRev.formDataProto) - - if (preFD instanceof Error || postFD instanceof Error) { - throw new Error('Got an error decoding') - } - - preFDS.push(preFD) - postFDS.push(postFD) - } - - // in this case, because we're submitting a rate, we're getting back fake revisions - const preFD = preFDS[0] as LockedHealthPlanFormDataType - const postFD = postFDS[0] as LockedHealthPlanFormDataType - - // ignore submittedAt and updatedAt - postFD.updatedAt = preFD.updatedAt - postFD.submittedAt = preFD.submittedAt - - // deepStrictEqual args: actual comes first then expected - assert.deepStrictEqual(postFD, preFD, 'form data not equal') - }, 20000) -}) diff --git a/services/app-api/src/handlers/proto_to_db.ts b/services/app-api/src/handlers/proto_to_db.ts deleted file mode 100644 index 374c23f4eb..0000000000 --- a/services/app-api/src/handlers/proto_to_db.ts +++ /dev/null @@ -1,316 +0,0 @@ -/* -There are comments throughout this file, placed before where we call each migration function, -that try to explain my assumptions and strategies. As I note below, -my confidence in the join table migration and document associations is lower than -for the other migrations. I think they're worth discussing as a team. - -In its current state, I think all the tables that we want to populate are being populated, -but there's a lot of work left on the ticket - -1. What needs to be wrapped in transactions? -2. Are drafts vs submissions being handled correctly? -3. Are all the unnecessary duplicate insertions being prevented? -4. A review of error handling. I was more focused on debugging. -5. Tests. I've been testing this by running it locally and checking the database. Those -steps will be transmitted elsewhere. -*/ -import type { Handler, APIGatewayProxyResultV2 } from 'aws-lambda' -import { initTracer, recordException } from '../../../uploads/src/lib/otel' -import { configurePostgres } from './configuration' -import { NewPostgresStore } from '../postgres/postgresStore' -import type { Store } from '../postgres' -import type { HealthPlanRevisionTable } from '@prisma/client' -import type { PrismaClient } from '@prisma/client' -import type { ContractTable, ContractRevisionTable } from '@prisma/client' -import type { StoreError } from '../postgres/storeError' -import { isStoreError } from '../postgres/storeError' -import { migrateContractRevision } from '../postgres/contractAndRates/proto_to_db_ContractRevisions' -import { migrateRateInfo } from '../postgres/contractAndRates/proto_to_db_RateRevisions' -import { insertContractId } from '../postgres/contractAndRates/proto_to_db_ContractId' -import { cleanupLastMigration } from '../postgres/contractAndRates/proto_to_db_CleanupLastMigration' -import { toDomain } from '../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import type { HealthPlanFormDataType } from '../../../app-web/src/common-code/healthPlanFormDataType' - -export const getDatabaseConnection = async (): Promise<{ - store: Store - prismaClient: PrismaClient -}> => { - const dbURL = process.env.DATABASE_URL - const secretsManagerSecret = process.env.SECRETS_MANAGER_SECRET - - if (!dbURL) { - console.error('DATABASE_URL not set') - throw new Error( - 'Init Error: DATABASE_URL is required to run migrations' - ) - } - if (!secretsManagerSecret) { - console.error('SECRETS_MANAGER_SECRET not set') - throw new Error( - 'Init Error: SECRETS_MANAGER_SECRET is required to run migrations' - ) - } - - const pgResult = await configurePostgres(dbURL, secretsManagerSecret) - if (pgResult instanceof Error) { - console.error( - "Init Error: Postgres couldn't be configured in data exporter" - ) - throw pgResult - } else { - console.info('Postgres configured in data exporter') - } - const store = NewPostgresStore(pgResult) - - return { store, prismaClient: pgResult } -} - -export const getRevisions = async ( - store: Store -): Promise => { - const result: HealthPlanRevisionTable[] | StoreError = - await store.findAllRevisions() - if (isStoreError(result)) { - console.error( - `Error getting revisions from db ${JSON.stringify(result)}` - ) - throw new Error('Error getting records; cannot generate report') - } - - return result -} - -export function decodeFormDataProto( - revision: HealthPlanRevisionTable -): HealthPlanFormDataType | Error { - // decode the proto - const decodedFormDataProto = toDomain(revision.formDataProto) - if (decodedFormDataProto instanceof Error) { - const error = new Error( - `Error in toDomain for ${revision.id}: ${decodedFormDataProto.message}` - ) - return error - } - return decodedFormDataProto as HealthPlanFormDataType -} - -export type MigrateRevisionResult = { - contract: ContractTable - rateInvocation: MigrateRatesInvocation -} - -interface MigrateRatesInvocation { - revision: HealthPlanRevisionTable - formData: HealthPlanFormDataType - contractRevision: ContractRevisionTable -} - -async function migrateRevision( - client: PrismaClient, - revision: HealthPlanRevisionTable -): Promise { - /* The order in which we call the helpers in this file matters */ - const formData = decodeFormDataProto(revision) - if (formData instanceof Error) { - return formData - } - - // migrate the contract part - const migrateContractResult = await migrateContract( - client, - revision, - formData - ) - if (migrateContractResult instanceof Error) { - console.error(migrateContractResult) - return migrateContractResult - } - - const rateInvocation: MigrateRatesInvocation = { - revision, - formData, - contractRevision: migrateContractResult.contractRevision, - } - - return { - contract: migrateContractResult.contract, - rateInvocation, - } -} - -type ContractMigrationResult = - | { - contract: ContractTable - contractRevision: ContractRevisionTable - } - | Error - -export async function migrateContract( - client: PrismaClient, - revision: HealthPlanRevisionTable, - formData: HealthPlanFormDataType -): Promise { - /* The field 'contractId' in the ContractRevisionTable matches the field 'id' in the ContractTable - so the ContractTable has to be populated before the revisions can be inserted - Note two things: - 1. This value is originally the pkgID in the HealthPlanRevisionTable - 2. So it's really acting as a foreign key that ties many of these tables together - 3. I think this is working as I expected, but if something goes very wrong - somewhere along the line, look here first. */ - const insertContractResult = await insertContractId( - client, - revision, - formData - ) - if (insertContractResult instanceof Error) { - const error = new Error( - `Error creating contract for ${revision.id}: ${insertContractResult.message}` - ) - return error - } - - const migrateContractResult = await migrateContractRevision( - client, - revision, - formData, - insertContractResult - ) - if (migrateContractResult instanceof Error) { - const error = new Error( - `Error in migrateContractRevision for ${revision.id}: ${migrateContractResult.message}` - ) - return error - } - - return { - contract: insertContractResult, - contractRevision: migrateContractResult, - } -} - -// cleanupPreviousProtoMigrate resets us back to the state prior to running this migration -export async function cleanupPreviousProtoMigrate( - client: PrismaClient -): Promise { - const cleanResult = await cleanupLastMigration(client) - if (cleanResult instanceof Error) { - console.error(cleanResult) - return cleanResult - } - return -} - -// We must migrate the contracts over and then the rates. -export async function migrateHealthPlanRevisions( - client: PrismaClient, - revisions: HealthPlanRevisionTable[] -): Promise { - const revisionsErrors: Error[] = [] - - const rateInvocations: MigrateRatesInvocation[] = [] - - // We have to migrate the rates after the contracts b/c of the links - for (const revision of revisions) { - console.info(`Migrating Contract HealthPlanRevision ${revision.id}`) - const migrateResult = await migrateRevision(client, revision) - if (migrateResult instanceof Error) { - revisionsErrors.push(migrateResult) - console.error(migrateResult) - continue - } - - rateInvocations.push(migrateResult.rateInvocation) - - console.info( - `Migrated Contract HealthPlanRevision ${revision.id} successfully...` - ) - } - - for (const rateInvocation of rateInvocations) { - /* Just as with the Contract and ContractRevision tables noted above, we take the - original HealthPlanRevision 'pkgID' and tie the RateTable ('id') to the RateRevisionTable ('rateID') - (the contract stuff happens in two files; the rate stuff happens in one file; you'll probably want to change this) */ - console.info( - `Migrating Rates HealthPlanRevision ${rateInvocation.revision.id}` - ) - const ratesResult = await migrateRateInfo( - client, - rateInvocation.revision, - rateInvocation.formData, - rateInvocation.contractRevision - ) - if (ratesResult instanceof Error) { - revisionsErrors.push(ratesResult) - const error = new Error( - `Error migrating ${rateInvocation.revision.id} rates: ${ratesResult.message}` - ) - console.error(error) - continue - } - - console.info( - `Migrated Rates HealthPlanRevision ${rateInvocation.revision.id} successfully...` - ) - } - - return revisionsErrors -} - -export const main: Handler = async (): Promise => { - // setup otel tracing - const stageName = process.env.stage ?? 'stageNotSet' - const serviceName = `proto_to_db_lambda-${stageName}` - const otelCollectorURL = process.env.REACT_APP_OTEL_COLLECTOR_URL - if (otelCollectorURL) { - initTracer(serviceName, otelCollectorURL) - } else { - console.error( - 'Configuration Error: REACT_APP_OTEL_COLLECTOR_URL must be set' - ) - } - - // setup db connections, clean last migration run, and get revisions - const { store, prismaClient } = await getDatabaseConnection() - const cleanResult = await cleanupPreviousProtoMigrate(prismaClient) - if (cleanResult instanceof Error) { - return { - statusCode: 500, - body: JSON.stringify({ - message: - 'Could not cleanup after previous migrations. Aborting.', - }), - } - } - - const revisions = await getRevisions(store) - - // go through the list of revisions and migrate - console.info(`Found ${revisions.length} revisions to migrate...`) - - const migrationErrors = await migrateHealthPlanRevisions( - prismaClient, - revisions - ) - - if (migrationErrors.length > 0) { - console.error(`Encountered ${migrationErrors.length} Errors Migrating`) - for (const migrationError of migrationErrors) { - recordException(migrationError, serviceName, 'migrateRevision') - } - - return { - statusCode: 500, - body: JSON.stringify({ - message: 'Migration complete, Errored.', - }), - } - } - console.info('Successfully migrated rates and contracts.') - - return { - statusCode: 200, - body: JSON.stringify({ - message: 'Lambda function executed successfully', - }), - } -} diff --git a/services/app-api/src/parameterStore/emailParameterStore/dmcpEmails/getDMCPEmails.ts b/services/app-api/src/parameterStore/emailParameterStore/dmcpEmails/getDMCPReviewEmails.ts similarity index 66% rename from services/app-api/src/parameterStore/emailParameterStore/dmcpEmails/getDMCPEmails.ts rename to services/app-api/src/parameterStore/emailParameterStore/dmcpEmails/getDMCPReviewEmails.ts index 92d46b5f60..9dc81cef99 100644 --- a/services/app-api/src/parameterStore/emailParameterStore/dmcpEmails/getDMCPEmails.ts +++ b/services/app-api/src/parameterStore/emailParameterStore/dmcpEmails/getDMCPReviewEmails.ts @@ -1,13 +1,13 @@ import { ParameterStore } from '../../awsParameterStore' import { validateAndReturnValueArray } from '../helpers' -export const getDMCPEmails = async (): Promise => { - const name = `/configuration/email/dmcp` +export const getDMCPReviewEmails = async (): Promise => { + const name = `/configuration/email/dmcpReview` const dmcpTeamAddresses = await ParameterStore.getParameter(name) return validateAndReturnValueArray(dmcpTeamAddresses, name) } -export const getDMCPEmailsLocal = async (): Promise => [ +export const getDMCPReviewEmailsLocal = async (): Promise => [ `"DMCP Reviewer 1" `, `"DMCP Reviewer 2" `, `"DMCP Reviewer 3" `, diff --git a/services/app-api/src/parameterStore/emailParameterStore/dmcpEmails/getDMCPSubmissionEmails.ts b/services/app-api/src/parameterStore/emailParameterStore/dmcpEmails/getDMCPSubmissionEmails.ts new file mode 100644 index 0000000000..7d81f24475 --- /dev/null +++ b/services/app-api/src/parameterStore/emailParameterStore/dmcpEmails/getDMCPSubmissionEmails.ts @@ -0,0 +1,16 @@ +import { ParameterStore } from '../../awsParameterStore' +import { validateAndReturnValueArray } from '../helpers' + +export const getDMCPSubmissionEmails = async (): Promise => { + const name = `/configuration/email/dmcpSubmission` + const dmcpTeamAddresses = await ParameterStore.getParameter(name) + return validateAndReturnValueArray(dmcpTeamAddresses, name) +} + +export const getDMCPSubmissionEmailsLocal = async (): Promise< + string[] | Error +> => [ + `"DMCP Submission Reviewer 1" `, + `"DMCP Submission Reviewer 2" `, + `"DMCP Submission Reviewer 3" `, +] diff --git a/services/app-api/src/parameterStore/emailParameterStore/emailParameterStore.ts b/services/app-api/src/parameterStore/emailParameterStore/emailParameterStore.ts index 026bfb095d..26fb63601c 100644 --- a/services/app-api/src/parameterStore/emailParameterStore/emailParameterStore.ts +++ b/services/app-api/src/parameterStore/emailParameterStore/emailParameterStore.ts @@ -8,8 +8,10 @@ import { getCmsReviewHelpEmailLocal, getCmsRateHelpEmail, getCmsRateHelpEmailLocal, - getDMCPEmails, - getDMCPEmailsLocal, + getDMCPReviewEmails, + getDMCPSubmissionEmails, + getDMCPReviewEmailsLocal, + getDMCPSubmissionEmailsLocal, getOACTEmails, getOACTEmailsLocal, getDMCOEmails, @@ -31,7 +33,8 @@ export type EmailParameterStore = { getDevReviewTeamEmails: () => Promise getCmsReviewHelpEmail: () => Promise getCmsRateHelpEmail: () => Promise - getDMCPEmails: () => Promise + getDMCPReviewEmails: () => Promise + getDMCPSubmissionEmails: () => Promise getOACTEmails: () => Promise getDMCOEmails: () => Promise getSourceEmail: () => Promise @@ -45,7 +48,8 @@ function newLocalEmailParameterStore(): EmailParameterStore { getDevReviewTeamEmails: getDevReviewTeamEmailsLocal, getCmsReviewHelpEmail: getCmsReviewHelpEmailLocal, getCmsRateHelpEmail: getCmsRateHelpEmailLocal, - getDMCPEmails: getDMCPEmailsLocal, + getDMCPReviewEmails: getDMCPReviewEmailsLocal, + getDMCPSubmissionEmails: getDMCPSubmissionEmailsLocal, getOACTEmails: getOACTEmailsLocal, getDMCOEmails: getDMCOEmailsLocal, getSourceEmail: getSourceEmailLocal, @@ -61,7 +65,8 @@ function newAWSEmailParameterStore(): EmailParameterStore { getCmsReviewHelpEmail: getCmsReviewHelpEmail, getCmsRateHelpEmail: getCmsRateHelpEmail, getDMCOEmails: getDMCOEmails, - getDMCPEmails: getDMCPEmails, + getDMCPReviewEmails: getDMCPReviewEmails, + getDMCPSubmissionEmails: getDMCPSubmissionEmails, getOACTEmails: getOACTEmails, getSourceEmail: getSourceEmail, getHelpDeskEmail: getHelpDeskEmail, diff --git a/services/app-api/src/parameterStore/emailParameterStore/index.ts b/services/app-api/src/parameterStore/emailParameterStore/index.ts index 32fa0c28fb..9ab40db73f 100644 --- a/services/app-api/src/parameterStore/emailParameterStore/index.ts +++ b/services/app-api/src/parameterStore/emailParameterStore/index.ts @@ -18,7 +18,14 @@ export { } from './cmsRateHelpEmail/getCmsRateHelpEmail' export { getDMCOEmails, getDMCOEmailsLocal } from './dmcoEmails/getDMCOEmails' export { getOACTEmails, getOACTEmailsLocal } from './oactEmails/getOACTEmails' -export { getDMCPEmails, getDMCPEmailsLocal } from './dmcpEmails/getDMCPEmails' +export { + getDMCPReviewEmails, + getDMCPReviewEmailsLocal, +} from './dmcpEmails/getDMCPReviewEmails' +export { + getDMCPSubmissionEmails, + getDMCPSubmissionEmailsLocal, +} from './dmcpEmails/getDMCPSubmissionEmails' export { getSourceEmail, getSourceEmailLocal, diff --git a/services/app-api/src/postgres/contractAndRates/findAllContractsWithHistoryByState.ts b/services/app-api/src/postgres/contractAndRates/findAllContractsWithHistoryByState.ts index 807c1325a6..6675c42e75 100644 --- a/services/app-api/src/postgres/contractAndRates/findAllContractsWithHistoryByState.ts +++ b/services/app-api/src/postgres/contractAndRates/findAllContractsWithHistoryByState.ts @@ -1,6 +1,6 @@ import type { PrismaTransactionType } from '../prismaTypes' import type { ContractType } from '../../domain-models/contractAndRates' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' import { parseContractWithHistory } from './parseContractWithHistory' import { includeFullContract } from './prismaSubmittedContractHelpers' diff --git a/services/app-api/src/postgres/contractAndRates/findAllContractsWithHistoryBySubmitInfo.ts b/services/app-api/src/postgres/contractAndRates/findAllContractsWithHistoryBySubmitInfo.ts index 836f3de513..ec6a1424ee 100644 --- a/services/app-api/src/postgres/contractAndRates/findAllContractsWithHistoryBySubmitInfo.ts +++ b/services/app-api/src/postgres/contractAndRates/findAllContractsWithHistoryBySubmitInfo.ts @@ -1,5 +1,5 @@ import type { PrismaTransactionType } from '../prismaTypes' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' import { parseContractWithHistory } from './parseContractWithHistory' import { includeFullContract } from './prismaSubmittedContractHelpers' import type { ContractOrErrorArrayType } from './findAllContractsWithHistoryByState' diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts index 9e9f42808a..1cd2ea3847 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts @@ -1,6 +1,6 @@ import type { PrismaTransactionType } from '../prismaTypes' import type { ContractType } from '../../domain-models/contractAndRates' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' import { parseContractWithHistory } from './parseContractWithHistory' import { includeFullContract } from './prismaSubmittedContractHelpers' diff --git a/services/app-api/src/postgres/contractAndRates/findRateWithHistory.ts b/services/app-api/src/postgres/contractAndRates/findRateWithHistory.ts index 0512bd0a5e..38f66c5d39 100644 --- a/services/app-api/src/postgres/contractAndRates/findRateWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/findRateWithHistory.ts @@ -1,6 +1,6 @@ import type { PrismaTransactionType } from '../prismaTypes' import type { RateType } from '../../domain-models/contractAndRates' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' import { includeFullRate } from './prismaSubmittedRateHelpers' import { parseRateWithHistory } from './parseRateWithHistory' diff --git a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts index 4ed0364500..2fd501af8c 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaSharedContractRateHelpers.ts @@ -1,5 +1,4 @@ import type { Prisma } from '@prisma/client' -import type { DocumentCategoryType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import type { ProgramType } from '../../domain-models' import type { ContractFormDataType, @@ -168,7 +167,6 @@ function rateFormDataToDomainModel( name: doc.name, s3URL: doc.s3URL, sha256: doc.sha256, - documentCategories: ['RATES'] as DocumentCategoryType[], })) : [], supportingDocuments: rateRevision.supportingDocuments @@ -176,9 +174,6 @@ function rateFormDataToDomainModel( name: doc.name, s3URL: doc.s3URL, sha256: doc.sha256, - documentCategories: [ - 'RATES_RELATED', - ] as DocumentCategoryType[], })) : [], rateDateStart: rateRevision.rateDateStart ?? undefined, diff --git a/services/app-api/src/postgres/contractAndRates/proto_to_db_CleanupLastMigration.ts b/services/app-api/src/postgres/contractAndRates/proto_to_db_CleanupLastMigration.ts deleted file mode 100644 index 99c1de2c82..0000000000 --- a/services/app-api/src/postgres/contractAndRates/proto_to_db_CleanupLastMigration.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { PrismaClient } from '@prisma/client' - -// this function removes all the data that was previously migrated from Protos. -// the motivation here is that we want to run the migrator as many times as we want, -// but keeping track of what was migrated already and what has not been is complicated. -// Instead, remove it all and start fresh. -export async function cleanupLastMigration( - client: PrismaClient -): Promise { - try { - const deleteManyResult = await client.$transaction([ - client.contractDocument.deleteMany(), - client.rateDocument.deleteMany(), - client.stateContact.deleteMany(), - client.actuaryContact.deleteMany(), - client.contractSupportingDocument.deleteMany(), - client.rateSupportingDocument.deleteMany(), - client.rateRevisionsOnContractRevisionsTable.deleteMany(), - client.contractRevisionTable.deleteMany(), - client.rateRevisionTable.deleteMany(), - client.updateInfoTable.deleteMany(), - - // must be last due to foreign keys - client.rateTable.deleteMany(), - client.contractTable.deleteMany(), - ]) - - if (deleteManyResult instanceof Error) { - const error = new Error( - `Error removing data from tables: ${deleteManyResult.message}` - ) - console.error(error) - return error - } - return - } catch (err) { - console.error(err) - return err - } -} diff --git a/services/app-api/src/postgres/contractAndRates/proto_to_db_ContractId.test.ts b/services/app-api/src/postgres/contractAndRates/proto_to_db_ContractId.test.ts deleted file mode 100644 index 3a1da0b245..0000000000 --- a/services/app-api/src/postgres/contractAndRates/proto_to_db_ContractId.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { StateCodeType } from '../../../../app-web/src/common-code/healthPlanFormDataType' - -import { sharedTestPrismaClient } from '../../testHelpers/storeHelpers' -import { createMockRevision } from '../../testHelpers/protoMigratorHelpers' -import { insertContractId } from './proto_to_db_ContractId' -import { decodeFormDataProto } from '../../handlers/proto_to_db' - -describe('proto_to_db_ContractId', () => { - it('inserts the contract and returns a ContractTable from the revision', async () => { - const client = await sharedTestPrismaClient() - const mockRevision = createMockRevision() - - const formData = decodeFormDataProto(mockRevision) - if (formData instanceof Error) { - return formData - } - - const contractData = await insertContractId( - client, - mockRevision, - formData - ) - expect(contractData).toEqual( - expect.objectContaining({ - id: expect.any(String), - stateCode: 'MN', - stateNumber: expect.any(Number), - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }) - ) - }) - - it('returns an error when an invalid state code is provided', async () => { - const client = await sharedTestPrismaClient() - const mockRevision = createMockRevision() - - const formData = decodeFormDataProto(mockRevision) - if (formData instanceof Error) { - return formData - } - - formData.stateCode = 'MEXICO' as StateCodeType - - const contractData = await insertContractId( - client, - mockRevision, - formData - ) - - // Expect a prisma error - expect(contractData).toBeInstanceOf(Error) - }) -}) diff --git a/services/app-api/src/postgres/contractAndRates/proto_to_db_ContractId.ts b/services/app-api/src/postgres/contractAndRates/proto_to_db_ContractId.ts deleted file mode 100644 index b2582711f8..0000000000 --- a/services/app-api/src/postgres/contractAndRates/proto_to_db_ContractId.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { - PrismaClient, - ContractTable, - HealthPlanRevisionTable, -} from '@prisma/client' -import type { HealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' - -async function insertContractId( - client: PrismaClient, - revision: HealthPlanRevisionTable, - formData: HealthPlanFormDataType -): Promise { - try { - const stateCode = formData.stateCode - const stateNumber = formData.stateNumber - - const state = await client.state.findUnique({ - where: { stateCode: stateCode }, - }) - - if (!state) { - const error = new Error(`State with code ${stateCode} not found`) - return error - } - - const contract = await client.contractTable.upsert({ - where: { id: formData.id }, - create: { - id: formData.id, - state: { - connect: { - stateCode: stateCode, - }, - }, - stateNumber: stateNumber, - createdAt: revision.createdAt, - }, - - update: { - stateNumber: stateNumber, - }, - }) - - return contract - } catch (err) { - const error = new Error( - `Error creating contract ${JSON.stringify(err)}` - ) - return error - } -} - -export { insertContractId } diff --git a/services/app-api/src/postgres/contractAndRates/proto_to_db_ContractRevisions.ts b/services/app-api/src/postgres/contractAndRates/proto_to_db_ContractRevisions.ts deleted file mode 100644 index 04ebdf55ea..0000000000 --- a/services/app-api/src/postgres/contractAndRates/proto_to_db_ContractRevisions.ts +++ /dev/null @@ -1,226 +0,0 @@ -import type { - PrismaClient, - ContractRevisionTable, - ManagedCareEntity, - ContractTable, - Prisma, - HealthPlanRevisionTable, -} from '@prisma/client' - -import type { HealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' - -async function migrateContractRevision( - client: PrismaClient, - revision: HealthPlanRevisionTable, - formData: HealthPlanFormDataType, - contract: ContractTable -): Promise { - try { - const existingRevision = await client.contractRevisionTable.findUnique({ - where: { id: revision.id }, - }) - - if (existingRevision) { - console.info( - `Contract revision with ID ${revision.id} already exists. Skipping...` - ) - return existingRevision - } - - // begin constructing the contract revision - const createDataObject: Prisma.ContractRevisionTableCreateInput = { - contract: { - connect: { - id: contract.id, - }, - }, - id: revision.id, - createdAt: revision.createdAt, - updatedAt: - formData.updatedAt > revision.createdAt - ? formData.updatedAt - : revision.createdAt, - submissionType: formData.submissionType, - submissionDescription: formData.submissionDescription, - programIDs: formData.programIDs, - populationCovered: formData.populationCovered ?? null, - riskBasedContract: formData.riskBasedContract ?? null, - contractType: formData.contractType ?? 'BASE', - contractExecutionStatus: formData.contractExecutionStatus ?? null, - contractDateStart: formData.contractDateStart ?? null, - contractDateEnd: formData.contractDateEnd ?? null, - managedCareEntities: - formData.managedCareEntities as ManagedCareEntity[], - federalAuthorities: formData.federalAuthorities, - modifiedBenefitsProvided: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedBenefitsProvided ?? null, - modifiedGeoAreaServed: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedGeoAreaServed ?? null, - modifiedMedicaidBeneficiaries: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedMedicaidBeneficiaries ?? null, - modifiedRiskSharingStrategy: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedRiskSharingStrategy ?? null, - modifiedIncentiveArrangements: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedIncentiveArrangements ?? null, - modifiedWitholdAgreements: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedWitholdAgreements ?? null, - modifiedStateDirectedPayments: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedStateDirectedPayments ?? null, - modifiedPassThroughPayments: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedPassThroughPayments ?? null, - modifiedPaymentsForMentalDiseaseInstitutions: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedPaymentsForMentalDiseaseInstitutions ?? null, - modifiedMedicalLossRatioStandards: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedMedicalLossRatioStandards ?? null, - modifiedOtherFinancialPaymentIncentive: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedOtherFinancialPaymentIncentive ?? null, - modifiedEnrollmentProcess: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedEnrollmentProcess ?? null, - modifiedGrevienceAndAppeal: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedGrevienceAndAppeal ?? null, - modifiedNetworkAdequacyStandards: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedNetworkAdequacyStandards ?? null, - modifiedLengthOfContract: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedLengthOfContract ?? null, - modifiedNonRiskPaymentArrangements: - formData.contractAmendmentInfo?.modifiedProvisions - ?.modifiedNonRiskPaymentArrangements ?? null, - inLieuServicesAndSettings: - formData.contractAmendmentInfo?.modifiedProvisions - ?.inLieuServicesAndSettings ?? null, - } - - // Add the unlocked info to the table if it exists - if ( - revision.unlockedAt && - revision.unlockedBy && - revision.unlockedReason - ) { - const user = await client.user.findFirst({ - where: { email: revision.unlockedBy }, - }) - - if (user) { - createDataObject.unlockInfo = { - create: { - updatedAt: revision.unlockedAt, - updatedByID: user.id, - updatedReason: revision.unlockedReason, - }, - } - } else { - console.warn( - `User with email ${revision.unlockedBy} does not exist. Skipping unlockInfo creation.` - ) - } - } - - // add the submit info to the table if it exists - if (revision.submittedAt && revision.submittedBy) { - const user = await client.user.findFirst({ - where: { email: revision.submittedBy }, - }) - if (user) { - createDataObject.submitInfo = { - create: { - updatedAt: revision.submittedAt, - updatedByID: user.id, - updatedReason: - revision.submittedReason ?? - 'Migrated from previous system', - }, - } - } else { - console.warn( - `User with email ${revision.submittedBy} does not exist. Skipping submitInfo creation.` - ) - } - } - - // add the contract documents - let contractDocPos = 0 - const contractDocumentsArray = [] - for (const doc of formData.contractDocuments) { - const contractDoc: Prisma.ContractDocumentCreateWithoutContractRevisionInput = - { - createdAt: revision.createdAt, - updatedAt: new Date(), - name: doc.name, - s3URL: doc.s3URL, - sha256: doc.sha256, - position: contractDocPos, - } - contractDocumentsArray.push(contractDoc) - contractDocPos = contractDocPos + 1 - } - - createDataObject.contractDocuments = { - create: contractDocumentsArray, - } - - // add the contract supporting documents - let supportingDocPos = 0 - const supportingDocumentsArray = [] - for (const supportDoc of formData.documents) { - const contractSupportDoc: Prisma.ContractSupportingDocumentCreateWithoutContractRevisionInput = - { - createdAt: revision.createdAt, - updatedAt: new Date(), - name: supportDoc.name, - s3URL: supportDoc.s3URL, - sha256: supportDoc.sha256, - position: supportingDocPos, - } - supportingDocumentsArray.push(contractSupportDoc) - supportingDocPos++ - } - createDataObject.supportingDocuments = { - create: supportingDocumentsArray, - } - - // add the state contacts - let stateContactsPos = 0 - const stateContactsArray = [] - for (const stateContact of formData.stateContacts) { - const newStateContact: Prisma.StateContactCreateWithoutContractRevisionInput = - { - createdAt: revision.createdAt, - updatedAt: new Date(), - name: stateContact.name, - email: stateContact.email, - titleRole: stateContact.titleRole, - position: stateContactsPos, - } - stateContactsArray.push(newStateContact) - stateContactsPos++ - } - createDataObject.stateContacts = { - create: stateContactsArray, - } - - return await client.contractRevisionTable.create({ - data: createDataObject, - }) - } catch (err) { - return new Error( - `Error creating contract revision for ID ${revision.id}: ${err.message}` - ) - } -} - -export { migrateContractRevision } diff --git a/services/app-api/src/postgres/contractAndRates/proto_to_db_RateRevisions.ts b/services/app-api/src/postgres/contractAndRates/proto_to_db_RateRevisions.ts deleted file mode 100644 index 56d3c5762a..0000000000 --- a/services/app-api/src/postgres/contractAndRates/proto_to_db_RateRevisions.ts +++ /dev/null @@ -1,294 +0,0 @@ -import type { - PrismaClient, - HealthPlanRevisionTable, - Prisma, - ContractRevisionTable, -} from '@prisma/client' -import type { HealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' - -export async function migrateRateInfo( - client: PrismaClient, - revision: HealthPlanRevisionTable, - formData: HealthPlanFormDataType, - contractRevision: ContractRevisionTable -): Promise { - // get the state info - const stateCode = formData.stateCode - - const state = await client.state.findUnique({ - where: { stateCode: stateCode }, - }) - - if (!state) { - const error = new Error(`State with code ${stateCode} not found`) - return error - } - - for (const rateInfo of formData.rateInfos) { - const dataToCopy: Prisma.RateRevisionTableCreateWithoutRateInput = { - createdAt: revision.createdAt, - amendmentEffectiveDateStart: - rateInfo.rateAmendmentInfo?.effectiveDateStart ?? null, - amendmentEffectiveDateEnd: - rateInfo.rateAmendmentInfo?.effectiveDateEnd ?? null, - actuaryCommunicationPreference: - rateInfo.actuaryCommunicationPreference ?? null, - rateType: rateInfo.rateType ?? null, - rateCapitationType: rateInfo.rateCapitationType ?? null, - rateDateStart: rateInfo.rateDateStart ?? null, - rateDateEnd: rateInfo.rateDateEnd ?? null, - rateDateCertified: rateInfo.rateDateCertified ?? null, - rateProgramIDs: rateInfo.rateProgramIDs ?? [], - rateCertificationName: rateInfo.rateCertificationName ?? null, - } - - // Add the unlocked info to the table if it exists - if (formData.status === 'SUBMITTED' && revision.unlockedBy) { - const user = await client.user.findFirst({ - where: { email: revision.unlockedBy }, - }) - if (user) { - dataToCopy.unlockInfo = { - create: { - updatedAt: revision.unlockedAt ?? formData.updatedAt, //TODO: not sure what we want to fall back to here - updatedByID: user.id, - updatedReason: - revision.unlockedReason ?? - 'Migrated from previous system', - }, - } - } else { - console.warn( - `User with email ${revision.unlockedBy} does not exist. Skipping unlockInfo creation.` - ) - } - } - - // add the submit info to the table if it exists - if (formData.status === 'SUBMITTED' && revision.submittedBy) { - const user = await client.user.findFirst({ - where: { email: revision.submittedBy }, - }) - if (user) { - dataToCopy.submitInfo = { - create: { - updatedAt: formData.updatedAt, - updatedByID: user.id, - updatedReason: - revision.submittedReason ?? - 'Migrated from previous system', - }, - } - } else { - console.warn( - `User with email ${revision.submittedBy} does not exist. Skipping submitInfo creation.` - ) - } - } - - // add the actuary contacts - if (rateInfo.actuaryContacts) { - let actuaryContactsPos = 0 - const actuaryContactsArray = [] - for (const actuaryContact of rateInfo.actuaryContacts) { - const newActuaryContact: Prisma.ActuaryContactCreateInput = { - id: actuaryContact.id, - name: actuaryContact.name, - titleRole: actuaryContact.titleRole, - email: actuaryContact.email, - actuarialFirm: actuaryContact.actuarialFirm, - actuarialFirmOther: actuaryContact.actuarialFirmOther, - position: actuaryContactsPos, - } - actuaryContactsArray.push(newActuaryContact) - actuaryContactsPos++ - } - dataToCopy.certifyingActuaryContacts = { - create: actuaryContactsArray, - } - } - - if (formData.addtlActuaryContacts) { - let addtlActuaryContactsPos = 0 - const addtlActuaryContactsArray = [] - for (const addtlActuaryContact of formData.addtlActuaryContacts) { - const newAddtlActuaryContact: Prisma.ActuaryContactCreateInput = - { - id: addtlActuaryContact.id, - name: addtlActuaryContact.name, - titleRole: addtlActuaryContact.titleRole, - email: addtlActuaryContact.email, - actuarialFirm: addtlActuaryContact.actuarialFirm, - actuarialFirmOther: - addtlActuaryContact.actuarialFirmOther, - position: addtlActuaryContactsPos, - } - addtlActuaryContactsArray.push(newAddtlActuaryContact) - addtlActuaryContactsPos++ - } - - dataToCopy.addtlActuaryContacts = { - create: addtlActuaryContactsArray, - } - } - - // handle rate documents - let rateDocPos = 0 - const rateDocsArray = [] - for (const rateDoc of rateInfo.rateDocuments) { - const rateDocument: Prisma.RateDocumentCreateWithoutRateRevisionInput = - { - createdAt: revision.createdAt, - updatedAt: new Date(), - name: rateDoc.name, - s3URL: rateDoc.s3URL, - sha256: rateDoc.sha256, - position: rateDocPos, - } - rateDocsArray.push(rateDocument) - rateDocPos++ - } - dataToCopy.rateDocuments = { - create: rateDocsArray, - } - - // handle rate revision documents - let rateRevDocPos = 0 - const rateRevDocsArray = [] - for (const supportRateDoc of rateInfo.supportingDocuments) { - const rateSupportDocument: Prisma.RateSupportingDocumentCreateWithoutRateRevisionInput = - { - createdAt: revision.createdAt, - updatedAt: new Date(), - name: supportRateDoc.name, - s3URL: supportRateDoc.s3URL, - sha256: supportRateDoc.sha256, - position: rateRevDocPos, - } - rateRevDocsArray.push(rateSupportDocument) - rateRevDocPos++ - } - dataToCopy.supportingDocuments = { - create: rateRevDocsArray, - } - - // Connect to shared contracts - if (rateInfo.packagesWithSharedRateCerts) { - const sharedContractIDs = rateInfo.packagesWithSharedRateCerts.map( - (pkg) => pkg.packageId - ) - dataToCopy.contractsWithSharedRateRevision = { - connect: sharedContractIDs.map((cid) => ({ id: cid })), - } - } - - // if this package is unlocked, so are the rates and contracts - // the only connection should be draftRates/draftContracts - if (!revision.submittedAt) { - dataToCopy.draftContracts = { - connect: { - id: contractRevision.contractID, - }, - } - } else { - // if this package has been submitted, then we set the revision join table. - // rate revisions on contract revisions join table - dataToCopy.contractRevisions = { - create: { - contractRevisionID: contractRevision.id, - validAfter: new Date(), - }, - } - } - - // each rate revision data here belongs to a different Rate, so we need - // to upsert that rate. - // Critically, the ID in the HPFormData is the ID we want to use for that rate - // since it should be stable across revisions. - - await client.$transaction(async (tx) => { - try { - // check if this rate exists - const findRateResult = await tx.rateTable.findFirst({ - where: { - id: rateInfo.id, - }, - }) - - if (findRateResult === undefined || findRateResult === null) { - // we have to create this rate now. - - // get the current state number: - const state = await tx.state.findUnique({ - where: { stateCode: stateCode }, - }) - - if (!state) { - const error = new Error( - `State with code ${stateCode} not found` - ) - return error - } - - const newRateCertNumber = - state.latestStateRateCertNumber + 1 - - await tx.state.update({ - where: { stateCode: stateCode }, - data: { - latestStateRateCertNumber: newRateCertNumber, - }, - }) - - await tx.rateTable.create({ - data: { - id: rateInfo.id, - state: { - connect: { - stateCode: stateCode, - }, - }, - stateNumber: newRateCertNumber, - revisions: { - create: dataToCopy, - }, - }, - }) - } else { - // the rate already exists, add a new revision - await tx.rateTable.update({ - where: { id: rateInfo.id }, - data: { - revisions: { - create: dataToCopy, - }, - }, - }) - } - } catch (e) { - console.error('Failed to upsert Rate', e) - return e - } - }) - } - - // and finally, if this is an unlocked contract, set the draftRates. - if (!revision.submittedAt) { - const rateIDs = formData.rateInfos.map((r) => r.id) - - await client.contractRevisionTable.update({ - where: { id: contractRevision.id }, - data: { - updatedAt: - formData.updatedAt > revision.createdAt - ? formData.updatedAt - : revision.createdAt, - draftRates: { - connect: rateIDs.map((rid) => ({ - id: rid, - })), - }, - }, - }) - } -} diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts index 51356e57b0..1676eccb0b 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts @@ -6,7 +6,7 @@ import { insertDraftRate } from './insertRate' import { submitRate } from './submitRate' import { updateDraftRate } from './updateDraftRate' import { must, createInsertContractData } from '../../testHelpers' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' describe('submitContract', () => { it('creates a submission from a draft', async () => { diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.ts b/services/app-api/src/postgres/contractAndRates/submitContract.ts index 3f4f8fa4c8..d52fe3b82b 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.ts @@ -1,7 +1,7 @@ import type { PrismaClient } from '@prisma/client' import type { ContractType } from '../../domain-models/contractAndRates' import { findContractWithHistory } from './findContractWithHistory' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' import type { UpdateInfoType } from '../../domain-models' import { includeLatestSubmittedRateRev } from './prismaSubmittedRateHelpers' diff --git a/services/app-api/src/postgres/contractAndRates/submitRate.test.ts b/services/app-api/src/postgres/contractAndRates/submitRate.test.ts index 742c07621a..bb8290ebad 100644 --- a/services/app-api/src/postgres/contractAndRates/submitRate.test.ts +++ b/services/app-api/src/postgres/contractAndRates/submitRate.test.ts @@ -1,7 +1,7 @@ import { sharedTestPrismaClient } from '../../testHelpers/storeHelpers' import { v4 as uuidv4 } from 'uuid' import { submitRate } from './submitRate' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' import { createInsertRateData } from '../../testHelpers/contractAndRates/rateHelpers' import { createInsertContractData, must } from '../../testHelpers' import { insertDraftRate } from './insertRate' diff --git a/services/app-api/src/postgres/contractAndRates/submitRate.ts b/services/app-api/src/postgres/contractAndRates/submitRate.ts index 4a64d2e7b4..d5c0d673d0 100644 --- a/services/app-api/src/postgres/contractAndRates/submitRate.ts +++ b/services/app-api/src/postgres/contractAndRates/submitRate.ts @@ -3,7 +3,7 @@ import type { UpdateInfoType } from '../../domain-models' import type { PrismaClient } from '@prisma/client' import type { RateType } from '../../domain-models/contractAndRates' import { includeLatestSubmittedRateRev } from './prismaSubmittedContractHelpers' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' type SubmitRateArgsType = { rateID?: string diff --git a/services/app-api/src/postgres/contractAndRates/unlockContract.ts b/services/app-api/src/postgres/contractAndRates/unlockContract.ts index 139c339f3f..2a4e4b2a77 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockContract.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockContract.ts @@ -1,7 +1,7 @@ import type { PrismaClient } from '@prisma/client' import type { ContractType } from '../../domain-models/contractAndRates' import { findContractWithHistory } from './findContractWithHistory' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' type UnlockContractArgsType = { contractID: string diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts index 41280f7884..90d039657c 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContractWithRates.ts @@ -1,5 +1,5 @@ import { findContractWithHistory } from './findContractWithHistory' -import { NotFoundError } from '../storeError' +import { NotFoundError } from '../postgresErrors' import type { PrismaClient } from '@prisma/client' import type { ContractFormDataType, diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftRate.ts b/services/app-api/src/postgres/contractAndRates/updateDraftRate.ts index 5e87b63d2e..dfa72acceb 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftRate.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftRate.ts @@ -1,5 +1,5 @@ import { findRateWithHistory } from './findRateWithHistory' -import type { NotFoundError } from '../storeError' +import type { NotFoundError } from '../postgresErrors' import type { RateFormDataType, RateType, diff --git a/services/app-api/src/postgres/contractAndRates/updateMCCRSID.ts b/services/app-api/src/postgres/contractAndRates/updateMCCRSID.ts index 5f880074f5..a1e908aea5 100644 --- a/services/app-api/src/postgres/contractAndRates/updateMCCRSID.ts +++ b/services/app-api/src/postgres/contractAndRates/updateMCCRSID.ts @@ -1,5 +1,5 @@ import { findContractWithHistory } from './findContractWithHistory' -import type { NotFoundError } from '../storeError' +import type { NotFoundError } from '../postgresErrors' import type { PrismaClient } from '@prisma/client' import type { ContractType } from '../../domain-models/contractAndRates' import { nullify } from '../prismaDomainAdaptors' diff --git a/services/app-api/src/postgres/healthPlanPackage/findAllHealthPlanPackagesByState.ts b/services/app-api/src/postgres/healthPlanPackage/findAllHealthPlanPackagesByState.ts deleted file mode 100644 index 58e48c352f..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/findAllHealthPlanPackagesByState.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { PrismaClient } from '@prisma/client' -import type { HealthPlanPackageType } from '../../domain-models' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError, isStoreError } from '../storeError' -import type { HealthPlanPackageWithRevisionsTable } from './healthPlanPackageHelpers' -import { - convertToHealthPlanPackageType, - getCurrentRevision, -} from './healthPlanPackageHelpers' - -export async function findAllPackagesWrapper( - client: PrismaClient, - stateCode: string -): Promise { - try { - const result = await client.healthPlanPackageTable.findMany({ - where: { - stateCode: { - equals: stateCode, - }, - }, - include: { - revisions: { - orderBy: { - createdAt: 'desc', // We expect our revisions most-recent-first - }, - }, - }, - }) - return result - } catch (e: unknown) { - console.info('failed to findAll', e) - return convertPrismaErrorToStoreError(e) - } -} - -export async function findAllHealthPlanPackagesByState( - client: PrismaClient, - stateCode: string -): Promise { - const findResult = await findAllPackagesWrapper(client, stateCode) - - if (isStoreError(findResult)) { - return findResult - } - - if (findResult === undefined) { - return findResult - } - - const submissions: HealthPlanPackageType[] = [] - const errors: Error | StoreError[] = [] - findResult.forEach((submissionWithRevisions) => { - // check for current revision, if it doesn't exist, log an error - const currentRevisionOrError = getCurrentRevision( - submissionWithRevisions.id, - submissionWithRevisions - ) - if (isStoreError(currentRevisionOrError)) { - console.info( - `ERROR submission ${submissionWithRevisions.id} does not have a current revision` - ) - console.info( - `ERROR findAllSubmissionsWithRevisions for ${stateCode} has ${errors.length} error(s)` - ) - return - } - const submission = convertToHealthPlanPackageType( - submissionWithRevisions - ) - submissions.push(submission) - }) - // only return packages with valid revisions - return submissions -} diff --git a/services/app-api/src/postgres/healthPlanPackage/findAllHealthPlanPackagesBySubmittedAt.ts b/services/app-api/src/postgres/healthPlanPackage/findAllHealthPlanPackagesBySubmittedAt.ts deleted file mode 100644 index 6366aad0b5..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/findAllHealthPlanPackagesBySubmittedAt.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { PrismaClient } from '@prisma/client' -import type { HealthPlanPackageType } from '../../domain-models' -import { packageStatus } from '../../domain-models' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError, isStoreError } from '../storeError' -import type { HealthPlanPackageWithRevisionsTable } from './healthPlanPackageHelpers' -import { - convertToHealthPlanPackageType, - getCurrentRevision, -} from './healthPlanPackageHelpers' - -export async function findAllPackagesWrapper( - client: PrismaClient -): Promise { - try { - const result = await client.healthPlanPackageTable.findMany({ - where: { - revisions: { some: { submittedAt: { not: null } } }, // drafts have no submission status - stateCode: { not: 'AS' }, // exclude test state as per ADR 019 - }, - include: { - revisions: { - orderBy: { - createdAt: 'desc', // We expect our revisions most-recent-first - }, - }, - }, - }) - - return result - } catch (e: unknown) { - console.error('failed to findAll', e) - return convertPrismaErrorToStoreError(e) - } -} - -export async function findAllHealthPlanPackagesBySubmittedAt( - client: PrismaClient -): Promise { - const findResult = await findAllPackagesWrapper(client) - - if (isStoreError(findResult)) { - return findResult - } - - if (findResult === undefined) { - return findResult - } - - const submissions: HealthPlanPackageType[] = [] - const errors: Error | StoreError[] = [] - findResult.forEach((submissionWithRevisions) => { - // check for current revision, if it doesn't exist, log an error - const currentRevisionOrError = getCurrentRevision( - submissionWithRevisions.id, - submissionWithRevisions - ) - if (isStoreError(currentRevisionOrError)) { - console.error( - `ERROR submission ${submissionWithRevisions.id} does not have a current revision` - ) - console.error( - `ERROR findAllHealthPlanPackagesBySubmittedAt has no revisions. There are ${errors.length} error(s)` - ) - return - } - - const submission = convertToHealthPlanPackageType( - submissionWithRevisions - ) - - if (packageStatus(submission) === 'DRAFT') { - console.error( - 'We should not be fetching draft submissions for CMS Users' - ) - } else { - submissions.push(submission) - } - }) - - // only return packages with valid revisions - return submissions -} diff --git a/services/app-api/src/postgres/healthPlanPackage/findAllRevisions.ts b/services/app-api/src/postgres/healthPlanPackage/findAllRevisions.ts deleted file mode 100644 index dfb3743e87..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/findAllRevisions.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { PrismaClient } from '@prisma/client' -import type { - HealthPlanPackageTable, - HealthPlanRevisionTable, -} from '@prisma/client' -import type { StoreError } from '../storeError' -export type PackagesAndRevisions = (HealthPlanPackageTable & { - revisions: HealthPlanRevisionTable[] -})[] - -export async function findAllRevisions( - client: PrismaClient -): Promise { - const allRevisions: HealthPlanRevisionTable[] = - await client.healthPlanRevisionTable.findMany() - if (allRevisions instanceof Error) { - console.error('findAllRevisions error:', allRevisions) - } - return allRevisions -} diff --git a/services/app-api/src/postgres/healthPlanPackage/findHealthPlanPackage.ts b/services/app-api/src/postgres/healthPlanPackage/findHealthPlanPackage.ts deleted file mode 100644 index 7e127409a1..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/findHealthPlanPackage.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { PrismaClient } from '@prisma/client' -import type { HealthPlanPackageType } from '../../domain-models' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError, isStoreError } from '../storeError' -import type { HealthPlanPackageWithRevisionsTable } from './healthPlanPackageHelpers' -import { convertToHealthPlanPackageType } from './healthPlanPackageHelpers' - -export async function findUniqueSubmissionWrapper( - client: PrismaClient, - id: string -): Promise { - try { - const findResult = await client.healthPlanPackageTable.findUnique({ - where: { - id: id, - }, - include: { - revisions: { - orderBy: { - createdAt: 'desc', // We expect our revisions most-recent-first - }, - }, - }, - }) - - if (!findResult) { - return undefined - } - - return findResult - } catch (e: unknown) { - return convertPrismaErrorToStoreError(e) - } -} - -export async function findHealthPlanPackage( - client: PrismaClient, - id: string -): Promise { - const findResult = await findUniqueSubmissionWrapper(client, id) - - if (isStoreError(findResult)) { - return findResult - } - - if (findResult === undefined) { - return findResult - } - - const submission = convertToHealthPlanPackageType(findResult) - - return submission -} diff --git a/services/app-api/src/postgres/healthPlanPackage/healthPlanPackageHelpers.ts b/services/app-api/src/postgres/healthPlanPackage/healthPlanPackageHelpers.ts deleted file mode 100644 index df64f455ec..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/healthPlanPackageHelpers.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { - HealthPlanPackageTable, - HealthPlanRevisionTable, -} from '@prisma/client' -import type { - HealthPlanPackageType, - Question, - UpdateInfoType, -} from '../../domain-models' -import type { StoreError } from '../storeError' - -export type HealthPlanPackageWithRevisionsTable = HealthPlanPackageTable & { - revisions: HealthPlanRevisionTable[] - questions?: Question[] -} - -// getCurrentRevision returns the first revision associated with a package -const getCurrentRevision = ( - pkgID: string, - pkg: HealthPlanPackageWithRevisionsTable | null -): HealthPlanRevisionTable | StoreError => { - if (!pkg) - return { - code: 'UNEXPECTED_EXCEPTION' as const, - message: `No package found for id: ${pkgID}`, - } - - if (!pkg.revisions || pkg.revisions.length < 1) - return { - code: 'UNEXPECTED_EXCEPTION' as const, - message: `No revisions found for package id: ${pkgID}`, - } - - // run through the list of revisions, get the newest one. - // If we ORDERED BY before getting these, we could probably simplify this. - const newestRev = pkg.revisions.reduce((acc, revision) => { - if (revision.createdAt > acc.createdAt) { - return revision - } else { - return acc - } - }, pkg.revisions[0]) - - return newestRev -} - -// convertToHealthPlanPackageType transforms the DB representation of StateSubmissionWithRevisions into our HealthPlanPackageType -function convertToHealthPlanPackageType( - dbPkg: HealthPlanPackageWithRevisionsTable -): HealthPlanPackageType { - return { - id: dbPkg.id, - stateCode: dbPkg.stateCode, - revisions: dbPkg.revisions.map((r) => { - let submitInfo: UpdateInfoType | undefined = undefined - if (r.submittedAt && r.submittedReason && r.submittedBy) { - submitInfo = { - updatedAt: r.submittedAt, - updatedReason: r.submittedReason, - updatedBy: r.submittedBy, - } - } - - let unlockInfo: UpdateInfoType | undefined = undefined - if (r.unlockedAt && r.unlockedBy && r.unlockedReason) { - unlockInfo = { - updatedAt: r.unlockedAt, - updatedBy: r.unlockedBy, - updatedReason: r.unlockedReason, - } - } - - return { - id: r.id, - unlockInfo, - submitInfo, - createdAt: r.createdAt, - formDataProto: r.formDataProto, - } - }), - } -} - -export { getCurrentRevision, convertToHealthPlanPackageType } diff --git a/services/app-api/src/postgres/healthPlanPackage/index.ts b/services/app-api/src/postgres/healthPlanPackage/index.ts deleted file mode 100644 index db159f71c8..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { findAllHealthPlanPackagesByState } from './findAllHealthPlanPackagesByState' -export { findAllHealthPlanPackagesBySubmittedAt } from './findAllHealthPlanPackagesBySubmittedAt' -export { findHealthPlanPackage } from './findHealthPlanPackage' -export type { InsertHealthPlanPackageArgsType } from './insertHealthPlanPackage' -export { insertHealthPlanPackage } from './insertHealthPlanPackage' -export { insertHealthPlanRevision } from './insertHealthPlanRevision' -export { updateHealthPlanRevision } from './updateHealthPlanRevision' -export { findAllRevisions } from './findAllRevisions' diff --git a/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.test.ts b/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.test.ts deleted file mode 100644 index dcaf141640..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { HealthPlanPackageType } from '../../domain-models' -import { toDomain } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import { sharedTestPrismaClient } from '../../testHelpers/storeHelpers' -import type { InsertHealthPlanPackageArgsType } from './insertHealthPlanPackage' -import { insertHealthPlanPackage } from './insertHealthPlanPackage' -import { isStoreError } from '../storeError' - -describe('insertHealthPlanPackage', () => { - // TODO this test needs to be improved its not testing anything - // eslint-disable-next-line jest/expect-expect - it('increases state number with every insertion', async () => { - // this test attempts to create a number of drafts concurrently. - // if any of the state numbers in the resultant drafts are duplicates, we have a bug. - - const client = await sharedTestPrismaClient() - - const args: InsertHealthPlanPackageArgsType = { - stateCode: 'FL', - populationCovered: 'MEDICAID', - programIDs: ['smmc'], - riskBasedContract: false, - submissionType: 'CONTRACT_ONLY', - submissionDescription: 'concurrency state code test', - contractType: 'BASE', - } - - const resultPromises = [] - for (let i = 0; i < 30; i++) { - resultPromises.push(insertHealthPlanPackage(client, args)) - } - - const results = await Promise.all(resultPromises) - if (results.some((result) => isStoreError(result))) { - console.info('RESULTS', results) - throw new Error('some of our inserts failed') - } - - // Because we are erroring above if _any_ of our results are a store error - // we can tell the type system that all of our results are UnlockedHealthPlanFormDataType - const drafts = results as HealthPlanPackageType[] - - const formDatum = drafts.map((d) => { - const formDataResult = toDomain(d.revisions[0].formDataProto) - if (formDataResult instanceof Error) { - throw formDataResult - } - return formDataResult - }) - - // Quick way to see if there are any duplicates, throw the state numbers into - // a set and check that the set and the array have the same number of elements - const stateNumbers = formDatum.map((d) => d.stateNumber) - const stateNumberSet = new Set(stateNumbers) - - if (stateNumbers.length !== stateNumberSet.size) { - console.info( - 'We got some duplicates: ', - stateNumbers.sort(), - stateNumberSet.size - ) - throw new Error('got some duplicate state numbers.') - } - }) -}) diff --git a/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.ts b/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.ts deleted file mode 100644 index 480bd8f523..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanPackage.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { PrismaClient } from '@prisma/client' -import { Buffer } from 'buffer' -import { v4 as uuidv4 } from 'uuid' -import type { - UnlockedHealthPlanFormDataType, - SubmissionType, - ContractType, -} from '../../../../app-web/src/common-code/healthPlanFormDataType' -import type { HealthPlanPackageType } from '../../domain-models' -import { toProtoBuffer } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError, isStoreError } from '../storeError' -import { convertToHealthPlanPackageType } from './healthPlanPackageHelpers' -import type { PopulationCoveredType } from '../../gen/gqlServer' - -export type InsertHealthPlanPackageArgsType = { - stateCode: string - populationCovered: PopulationCoveredType - programIDs: string[] - riskBasedContract?: boolean - submissionType: SubmissionType - submissionDescription: string - contractType: ContractType -} - -// By using Prisma's "increment" syntax here, we ensure that we are atomically increasing -// the state number every time we call this function. -async function incrementAndGetStateNumber( - client: PrismaClient, - stateCode: string -): Promise { - try { - const stateNumberResult = await client.state.update({ - data: { - latestStateSubmissionNumber: { - increment: 1, - }, - }, - where: { - stateCode: stateCode, - }, - }) - - return stateNumberResult.latestStateSubmissionNumber - } catch (e) { - return convertPrismaErrorToStoreError(e) - } -} - -export async function insertHealthPlanPackage( - client: PrismaClient, - args: InsertHealthPlanPackageArgsType -): Promise { - const stateNumberResult = await incrementAndGetStateNumber( - client, - args.stateCode - ) - - if (isStoreError(stateNumberResult)) { - console.info('Error: Getting New State Number', stateNumberResult) - return stateNumberResult - } - - const stateNumber: number = stateNumberResult - - // construct a new Draft Submission - const draft: UnlockedHealthPlanFormDataType = { - id: uuidv4(), - createdAt: new Date(), - updatedAt: new Date(), - stateNumber, - status: 'DRAFT', - populationCovered: args.populationCovered, - submissionType: args.submissionType, - riskBasedContract: args.riskBasedContract, - programIDs: args.programIDs, - submissionDescription: args.submissionDescription, - stateCode: args.stateCode, - contractType: args.contractType, - rateInfos: [], - documents: [], - contractDocuments: [], - stateContacts: [], - addtlActuaryContacts: [], - managedCareEntities: [], - federalAuthorities: [], - } - const protobuf = toProtoBuffer(draft) - - const buffer = Buffer.from(protobuf) - - try { - const pkg = await client.healthPlanPackageTable.create({ - data: { - id: draft.id, - stateCode: draft.stateCode, - revisions: { - create: { - id: uuidv4(), - createdAt: new Date(), - formDataProto: buffer, - }, - }, - }, - include: { - revisions: { - orderBy: { - createdAt: 'desc', // We expect our revisions most-recent-first - }, - }, - }, - }) - - return convertToHealthPlanPackageType(pkg) - } catch (e: unknown) { - console.info('ERROR: inserting into to the database: ', e) - - return convertPrismaErrorToStoreError(e) - } -} diff --git a/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanRevision.ts b/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanRevision.ts deleted file mode 100644 index 9fce0cff84..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/insertHealthPlanRevision.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { PrismaClient } from '@prisma/client' -import { v4 as uuidv4 } from 'uuid' -import type { UnlockedHealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' -import type { HealthPlanPackageType, UpdateInfoType } from '../../domain-models' -import { toProtoBuffer } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError } from '../storeError' -import { convertToHealthPlanPackageType } from './healthPlanPackageHelpers' - -export type InsertHealthPlanRevisionArgsType = { - pkgID: string - unlockInfo: UpdateInfoType - draft: UnlockedHealthPlanFormDataType -} - -export async function insertHealthPlanRevision( - client: PrismaClient, - args: InsertHealthPlanRevisionArgsType -): Promise { - args.draft.updatedAt = new Date() - - const protobuf = toProtoBuffer(args.draft) - const buffer = Buffer.from(protobuf) - - const { unlockInfo, pkgID } = args - - try { - const submission = await client.healthPlanPackageTable.update({ - where: { - id: pkgID, - }, - data: { - revisions: { - create: [ - { - id: uuidv4(), - createdAt: new Date(), - formDataProto: buffer, - unlockedAt: unlockInfo.updatedAt, - unlockedBy: unlockInfo.updatedBy, - unlockedReason: unlockInfo.updatedReason, - }, - ], - }, - }, - include: { - revisions: { - orderBy: { - createdAt: 'desc', // We expect our revisions most-recent-first - }, - }, - }, - }) - - return convertToHealthPlanPackageType(submission) - } catch (e: unknown) { - console.info('ERROR: inserting into to the database: ', e) - - return convertPrismaErrorToStoreError(e) - } -} diff --git a/services/app-api/src/postgres/healthPlanPackage/updateHealthPlanRevision.ts b/services/app-api/src/postgres/healthPlanPackage/updateHealthPlanRevision.ts deleted file mode 100644 index b429407eb6..0000000000 --- a/services/app-api/src/postgres/healthPlanPackage/updateHealthPlanRevision.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { HealthPlanRevisionTable, PrismaClient } from '@prisma/client' -import type { HealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' -import { toProtoBuffer } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import type { UpdateInfoType, HealthPlanPackageType } from '../../domain-models' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError, isStoreError } from '../storeError' -import type { HealthPlanPackageWithRevisionsTable } from './healthPlanPackageHelpers' -import { convertToHealthPlanPackageType } from './healthPlanPackageHelpers' - -export async function updateRevisionWrapper( - client: PrismaClient, - pkgID: string, - revisionID: string, - proto: Buffer, - submitInfo?: UpdateInfoType -): Promise { - const revisionBody: Partial = { - formDataProto: proto, - } - - if (submitInfo) { - revisionBody.submittedAt = submitInfo.updatedAt - revisionBody.submittedBy = submitInfo.updatedBy - revisionBody.submittedReason = submitInfo.updatedReason - } - - try { - const updateResult = await client.healthPlanPackageTable.update({ - where: { - id: pkgID, - }, - data: { - revisions: { - update: { - where: { - id: revisionID, - }, - data: revisionBody, - }, - }, - }, - include: { - revisions: { - orderBy: { - createdAt: 'desc', // We expect our revisions most-recent-first - }, - }, - }, - }) - - return updateResult - } catch (updateError) { - return convertPrismaErrorToStoreError(updateError) - } -} - -export async function updateHealthPlanRevision( - client: PrismaClient, - pkgID: string, - revisionID: string, - formData: HealthPlanFormDataType, - submitInfo?: UpdateInfoType -): Promise { - formData.updatedAt = new Date() - - const proto = toProtoBuffer(formData) - const buffer = Buffer.from(proto) - - const updateResult = await updateRevisionWrapper( - client, - pkgID, - revisionID, - buffer, - submitInfo - ) - - if (isStoreError(updateResult)) { - return updateResult - } - - return convertToHealthPlanPackageType(updateResult) -} diff --git a/services/app-api/src/postgres/index.ts b/services/app-api/src/postgres/index.ts index 4e80434d55..3142c42d0a 100644 --- a/services/app-api/src/postgres/index.ts +++ b/services/app-api/src/postgres/index.ts @@ -1,9 +1,8 @@ export { findPrograms } from './state/findPrograms' -export type { InsertHealthPlanPackageArgsType } from './healthPlanPackage' export type { InsertUserArgsType } from './user' +export type { InsertContractArgsType } from './contractAndRates/insertContract' export type { Store } from './postgresStore' export { NewPostgresStore } from './postgresStore' export { NewPrismaClient } from './prismaClient' -export type { StoreError } from './storeError' -export { isStoreError, NotFoundError } from './storeError' +export { NotFoundError } from './postgresErrors' export { findStatePrograms } from './state/findStatePrograms' diff --git a/services/app-api/src/postgres/postgresErrors.ts b/services/app-api/src/postgres/postgresErrors.ts new file mode 100644 index 0000000000..11d1c62aa1 --- /dev/null +++ b/services/app-api/src/postgres/postgresErrors.ts @@ -0,0 +1,10 @@ +// NotFoundError is an Error subclass that indicates that we failed to find the request record in the db +class NotFoundError extends Error { + constructor(message: string) { + super(message) + + Object.setPrototypeOf(this, NotFoundError.prototype) + } +} + +export { NotFoundError } diff --git a/services/app-api/src/postgres/postgresStore.ts b/services/app-api/src/postgres/postgresStore.ts index 5a00ae1a41..21647c00a5 100644 --- a/services/app-api/src/postgres/postgresStore.ts +++ b/services/app-api/src/postgres/postgresStore.ts @@ -1,39 +1,17 @@ -import type { - PrismaClient, - HealthPlanRevisionTable, - Division, -} from '@prisma/client' -import type { - UnlockedHealthPlanFormDataType, - HealthPlanFormDataType, - StateCodeType, -} from '../../../app-web/src/common-code/healthPlanFormDataType' +import type { PrismaClient, Division } from '@prisma/client' +import type { StateCodeType } from '../../../app-web/src/common-code/healthPlanFormDataType' import type { ProgramType, - HealthPlanPackageType, - UpdateInfoType, UserType, CMSUserType, StateUserType, Question, CreateQuestionInput, - QuestionResponseType, InsertQuestionResponseArgs, StateType, RateType, } from '../domain-models' import { findPrograms, findStatePrograms } from '../postgres' -import type { StoreError } from './storeError' -import type { InsertHealthPlanPackageArgsType } from './healthPlanPackage' -import { - findAllHealthPlanPackagesByState, - findAllHealthPlanPackagesBySubmittedAt, - findHealthPlanPackage, - insertHealthPlanPackage, - insertHealthPlanRevision, - updateHealthPlanRevision, - findAllRevisions, -} from './healthPlanPackage' import type { InsertUserArgsType } from './user' import { findUser, @@ -83,48 +61,17 @@ type Store = { findStatePrograms: (stateCode: string) => ProgramType[] | Error - findAllSupportedStates: () => Promise - - findAllRevisions: () => Promise - - findAllUsers: () => Promise - - findHealthPlanPackage: ( - draftUUID: string - ) => Promise - - findAllHealthPlanPackagesByState: ( - stateCode: string - ) => Promise - - findAllHealthPlanPackagesBySubmittedAt: () => Promise< - HealthPlanPackageType[] | StoreError - > - - insertHealthPlanPackage: ( - args: InsertHealthPlanPackageArgsType - ) => Promise - - updateHealthPlanRevision: ( - pkgID: string, - revisionID: string, - formData: HealthPlanFormDataType, - submitInfo?: UpdateInfoType - ) => Promise + findAllSupportedStates: () => Promise - insertHealthPlanRevision: ( - pkgID: string, - unlockInfo: UpdateInfoType, - draft: UnlockedHealthPlanFormDataType - ) => Promise + findAllUsers: () => Promise - findUser: (id: string) => Promise + findUser: (id: string) => Promise - insertUser: (user: InsertUserArgsType) => Promise + insertUser: (user: InsertUserArgsType) => Promise insertManyUsers: ( users: InsertUserArgsType[] - ) => Promise + ) => Promise updateCmsUserProperties: ( userID: string, @@ -132,23 +79,20 @@ type Store = { idOfUserPerformingUpdate: string, divisionAssignment?: Division, description?: string | null - ) => Promise + ) => Promise insertQuestion: ( questionInput: CreateQuestionInput, user: CMSUserType - ) => Promise + ) => Promise findAllQuestionsByContract: (pkgID: string) => Promise insertQuestionResponse: ( questionInput: InsertQuestionResponseArgs, user: StateUserType - ) => Promise + ) => Promise - /** - * Rates database refactor prisma functions - */ insertDraftContract: ( args: InsertContractArgsType ) => Promise @@ -192,27 +136,6 @@ type Store = { function NewPostgresStore(client: PrismaClient): Store { return { - insertHealthPlanPackage: (args) => - insertHealthPlanPackage(client, args), - findHealthPlanPackage: (id) => findHealthPlanPackage(client, id), - findAllHealthPlanPackagesByState: (stateCode) => - findAllHealthPlanPackagesByState(client, stateCode), - findAllHealthPlanPackagesBySubmittedAt: () => - findAllHealthPlanPackagesBySubmittedAt(client), - updateHealthPlanRevision: (pkgID, revisionID, formData, submitInfo) => - updateHealthPlanRevision( - client, - pkgID, - revisionID, - formData, - submitInfo - ), - insertHealthPlanRevision: (pkgID, unlockInfo, draft) => - insertHealthPlanRevision(client, { - pkgID, - unlockInfo, - draft, - }), findPrograms: findPrograms, findUser: (id) => findUser(client, id), insertUser: (args) => insertUser(client, args), @@ -234,17 +157,15 @@ function NewPostgresStore(client: PrismaClient): Store { ), findStatePrograms: findStatePrograms, findAllSupportedStates: () => findAllSupportedStates(client), - findAllRevisions: () => findAllRevisions(client), findAllUsers: () => findAllUsers(client), + insertQuestion: (questionInput, user) => insertQuestion(client, questionInput, user), findAllQuestionsByContract: (pkgID) => findAllQuestionsByContract(client, pkgID), insertQuestionResponse: (questionInput, user) => insertQuestionResponse(client, questionInput, user), - /** - * Rates database refactor prisma functions - */ + insertDraftContract: (args) => insertDraftContract(client, args), findContractWithHistory: (args) => findContractWithHistory(client, args), diff --git a/services/app-api/src/postgres/questionResponse/findAllQuestionsByContract.ts b/services/app-api/src/postgres/questionResponse/findAllQuestionsByContract.ts index 9f24fa4f07..bb3dce6bed 100644 --- a/services/app-api/src/postgres/questionResponse/findAllQuestionsByContract.ts +++ b/services/app-api/src/postgres/questionResponse/findAllQuestionsByContract.ts @@ -1,9 +1,6 @@ import type { PrismaClient } from '@prisma/client' -import type { - CMSUserType, - Question, - QuestionResponseType, -} from '../../domain-models' +import type { Question } from '../../domain-models' +import { questionPrismaToDomainType, questionInclude } from './questionHelpers' export async function findAllQuestionsByContract( client: PrismaClient, @@ -14,36 +11,15 @@ export async function findAllQuestionsByContract( where: { contractID: contractID, }, - include: { - documents: { - orderBy: { - createdAt: 'desc', - }, - }, - responses: { - include: { - addedBy: true, - documents: true, - }, - orderBy: { - createdAt: 'desc', - }, - }, - addedBy: true, - }, + include: questionInclude, orderBy: { createdAt: 'desc', }, }) - const questions: Question[] = findResult.map((question) => ({ - ...question, - addedBy: { - ...question.addedBy, - stateAssignments: [], - } as CMSUserType, - responses: question.responses as QuestionResponseType[], - })) + const questions: Question[] = findResult.map((question) => + questionPrismaToDomainType(question) + ) return questions } catch (e: unknown) { diff --git a/services/app-api/src/postgres/questionResponse/insertQuestion.ts b/services/app-api/src/postgres/questionResponse/insertQuestion.ts index 17d730bcdc..5c3483a8e0 100644 --- a/services/app-api/src/postgres/questionResponse/insertQuestion.ts +++ b/services/app-api/src/postgres/questionResponse/insertQuestion.ts @@ -1,20 +1,18 @@ import type { PrismaClient } from '@prisma/client' import type { CMSUserType, - QuestionResponseType, Question, CreateQuestionInput, DivisionType, } from '../../domain-models' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError } from '../storeError' import { v4 as uuidv4 } from 'uuid' +import { questionPrismaToDomainType, questionInclude } from './questionHelpers' export async function insertQuestion( client: PrismaClient, questionInput: CreateQuestionInput, user: CMSUserType -): Promise { +): Promise { const documents = questionInput.documents.map((document) => ({ id: uuidv4(), name: document.name, @@ -40,34 +38,11 @@ export async function insertQuestion( }, division: user.divisionAssignment as DivisionType, }, - include: { - documents: { - orderBy: { - createdAt: 'desc', - }, - }, - responses: { - include: { - addedBy: true, - documents: true, - }, - orderBy: { - createdAt: 'desc', - }, - }, - }, + include: questionInclude, }) - const createdQuestion: Question = { - ...result, - addedBy: user, - responses: result.responses.map( - (response) => response as QuestionResponseType - ), - } - - return createdQuestion - } catch (e: unknown) { - return convertPrismaErrorToStoreError(e) + return questionPrismaToDomainType(result) + } catch (e) { + return e } } diff --git a/services/app-api/src/postgres/questionResponse/insertQuestionResponse.ts b/services/app-api/src/postgres/questionResponse/insertQuestionResponse.ts index b27b5652ec..630c5eb56a 100644 --- a/services/app-api/src/postgres/questionResponse/insertQuestionResponse.ts +++ b/services/app-api/src/postgres/questionResponse/insertQuestionResponse.ts @@ -1,18 +1,18 @@ import type { PrismaClient } from '@prisma/client' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError } from '../storeError' import type { InsertQuestionResponseArgs, - QuestionResponseType, StateUserType, + Question, } from '../../domain-models' import { v4 as uuidv4 } from 'uuid' +import { questionInclude, questionPrismaToDomainType } from './questionHelpers' +import { NotFoundError } from '../postgresErrors' export async function insertQuestionResponse( client: PrismaClient, response: InsertQuestionResponseArgs, user: StateUserType -): Promise { +): Promise { const documents = response.documents.map((document) => ({ id: uuidv4(), name: document.name, @@ -20,35 +20,37 @@ export async function insertQuestionResponse( })) try { - const result = await client.questionResponse.create({ + const result = await client.question.update({ + where: { + id: response.questionID, + }, data: { - id: uuidv4(), - question: { - connect: { - id: response.questionID, - }, - }, - addedBy: { - connect: { - id: user.id, + responses: { + create: { + id: uuidv4(), + addedBy: { + connect: { + id: user.id, + }, + }, + documents: { + create: documents, + }, }, }, - documents: { - create: documents, - }, - }, - include: { - documents: true, }, + include: questionInclude, }) - const createdResponse: QuestionResponseType = { - ...result, - addedBy: user, + return questionPrismaToDomainType(result) + } catch (e) { + // Return a NotFoundError if prisma fails on the primary key constraint + // An operation failed because it depends on one or more records + // that were required but not found. + if (e.code === 'P2025') { + return new NotFoundError('Question was not found to respond to') } - return createdResponse - } catch (e: unknown) { - return convertPrismaErrorToStoreError(e) + return e } } diff --git a/services/app-api/src/postgres/questionResponse/questionHelpers.ts b/services/app-api/src/postgres/questionResponse/questionHelpers.ts index f5af64b519..af1656f42e 100644 --- a/services/app-api/src/postgres/questionResponse/questionHelpers.ts +++ b/services/app-api/src/postgres/questionResponse/questionHelpers.ts @@ -1,6 +1,40 @@ import type { IndexQuestionsPayload, Question } from '../../domain-models' +import type { Prisma } from '@prisma/client' -export const convertToIndexQuestionsPayload = ( +const questionInclude = { + documents: { + orderBy: { + createdAt: 'desc', + }, + }, + responses: { + include: { + addedBy: true, + documents: true, + }, + orderBy: { + createdAt: 'desc', + }, + }, + addedBy: true, +} satisfies Prisma.QuestionInclude + +type PrismaQuestionType = Prisma.QuestionGetPayload<{ + include: typeof questionInclude +}> + +const questionPrismaToDomainType = ( + prismaQuestion: PrismaQuestionType +): Question => ({ + ...prismaQuestion, + addedBy: { + ...prismaQuestion.addedBy, + stateAssignments: [], + } as Question['addedBy'], + responses: prismaQuestion.responses as Question['responses'], +}) + +const convertToIndexQuestionsPayload = ( questions: Question[] ): IndexQuestionsPayload => { const questionsPayload: IndexQuestionsPayload = { @@ -33,3 +67,9 @@ export const convertToIndexQuestionsPayload = ( return questionsPayload } + +export { + questionInclude, + questionPrismaToDomainType, + convertToIndexQuestionsPayload, +} diff --git a/services/app-api/src/postgres/state/findAllSupportedStates.ts b/services/app-api/src/postgres/state/findAllSupportedStates.ts index 078a89f9bd..7a528e9605 100644 --- a/services/app-api/src/postgres/state/findAllSupportedStates.ts +++ b/services/app-api/src/postgres/state/findAllSupportedStates.ts @@ -1,14 +1,12 @@ import type { StateType } from '../../domain-models' import statePrograms from '../../../../app-web/src/common-code/data/statePrograms.json' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError } from '../storeError' import type { PrismaClient } from '@prisma/client' // Returns postgres state info for the states that currently supported for pilot. // Supported states are state that have had their programs added to the statePrograms json file. export async function findAllSupportedStates( client: PrismaClient -): Promise { +): Promise { const pilotStateCodes = statePrograms.states.map((state) => state.code) try { @@ -18,18 +16,11 @@ export async function findAllSupportedStates( }, }) - if (allStates instanceof Error) { - return { - code: 'USER_FORMAT_ERROR', - message: allStates.message, - } - } - return allStates.filter((state) => pilotStateCodes.includes(state.stateCode) ) } catch (err) { console.error(err) - return convertPrismaErrorToStoreError(err) + return err } } diff --git a/services/app-api/src/postgres/storeError.test.ts b/services/app-api/src/postgres/storeError.test.ts deleted file mode 100644 index 5908236dd3..0000000000 --- a/services/app-api/src/postgres/storeError.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable jest/no-conditional-expect */ -import { v4 as uuidv4 } from 'uuid' -import { PrismaClient } from '@prisma/client' -import type { UnlockedHealthPlanFormDataType } from '../../../app-web/src/common-code/healthPlanFormDataType' -import { toProtoBuffer } from '../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import { sharedTestPrismaClient } from '../testHelpers/storeHelpers' -import { convertPrismaErrorToStoreError } from './storeError' - -describe('storeError', () => { - it('errors on a invalid connection string', async () => { - const badPrismaClient = new PrismaClient({ - datasources: { - db: { - url: 'localhost:99999', - }, - }, - }) - - try { - await badPrismaClient.healthPlanPackageTable.findFirst() - - throw new Error('should not be able to connect to bad port') - } catch (e: unknown) { - const storeErr = convertPrismaErrorToStoreError(e) - - expect(storeErr.code).toBe('CONNECTION_ERROR') - } - }) - - it('errors on a bad connection', async () => { - const badPrismaClient = new PrismaClient({ - datasources: { - db: { - url: 'postgres://localhost:9999/foobar&pool_timeout=1', - }, - }, - }) - - try { - await badPrismaClient.healthPlanPackageTable.findFirst() - - throw new Error('should not be able to connect to bad port') - } catch (e: unknown) { - const storeErr = convertPrismaErrorToStoreError(e) - - expect(storeErr.code).toBe('CONNECTION_ERROR') - } - }) - - it('errors on double insert', async () => { - const client = await sharedTestPrismaClient() - - const doubledID = uuidv4() - - const draft: UnlockedHealthPlanFormDataType = { - id: doubledID, - createdAt: new Date(), - updatedAt: new Date(), - stateNumber: 4, - status: 'DRAFT', - submissionType: 'CONTRACT_ONLY', - riskBasedContract: false, - programIDs: ['smmc'], - submissionDescription: 'description', - stateCode: 'FL', - rateInfos: [], - documents: [], - contractDocuments: [], - stateContacts: [], - addtlActuaryContacts: [], - managedCareEntities: [], - federalAuthorities: [], - } - - // we want to figure out what error is returned for a constraint violation. - const protobuf = toProtoBuffer(draft) - - const buffer = Buffer.from(protobuf) - - try { - await client.healthPlanPackageTable.create({ - data: { - id: draft.id, - stateCode: draft.stateCode, - revisions: { - create: { - id: uuidv4(), - createdAt: new Date(), - formDataProto: buffer, - }, - }, - }, - }) - - await client.healthPlanPackageTable.create({ - data: { - id: draft.id, - stateCode: draft.stateCode, - revisions: { - create: { - id: uuidv4(), - createdAt: new Date(), - formDataProto: buffer, - }, - }, - }, - }) - - throw new Error('Inserting the same ID twice should error.') - } catch (e) { - const storeError = convertPrismaErrorToStoreError(e) - - expect(storeError).toEqual({ - code: 'INSERT_ERROR', - message: 'insert failed because of invalid unique constraint', - }) - } - }) - - it('returns unknown for non prisma errors', async () => { - const badErr = new Error('not a pg error') - - const storeError = convertPrismaErrorToStoreError(badErr) - - expect(storeError).toEqual({ - code: 'UNEXPECTED_EXCEPTION', - message: 'A completely unexpected prisma exception has occurred', - }) - }) -}) diff --git a/services/app-api/src/postgres/storeError.ts b/services/app-api/src/postgres/storeError.ts deleted file mode 100644 index 36aa9cbe93..0000000000 --- a/services/app-api/src/postgres/storeError.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - PrismaClientInitializationError, - PrismaClientKnownRequestError, -} from '@prisma/client/runtime/library' - -const StoreErrorCodes = [ - 'CONFIGURATION_ERROR', - 'CONNECTION_ERROR', - 'PROTOBUF_ERROR', - 'INSERT_ERROR', - 'USER_FORMAT_ERROR', - 'UNEXPECTED_EXCEPTION', - 'WRONG_STATUS', - 'NOT_FOUND_ERROR', -] as const -type StoreErrorCode = (typeof StoreErrorCodes)[number] // iterable union type - -type StoreError = { - code: StoreErrorCode - message: string -} - -// Wow this seems complicated. If there are cleaner ways to do this I'd like to know it. -function isStoreError(err: unknown): err is StoreError { - if (err && typeof err == 'object') { - if ('code' in err && 'message' in err) { - // This seems ugly but necessary in a type guard. - const hasCode = err as { code: unknown } - if (typeof hasCode.code === 'string') { - if ( - StoreErrorCodes.some((errCode) => hasCode.code === errCode) - ) { - return true - } - } - } - } - return false -} - -// This function is meant to be called from a catch statement after trying -// a prisma command, so it takes unknown as the input -const convertPrismaErrorToStoreError = (prismaErr: unknown): StoreError => { - // PrismaClientKnownRequestError is for errors that are expected to occur based on - // making invalid requests of some kind. - if (prismaErr instanceof PrismaClientKnownRequestError) { - // P2002 is for violating a uniqueness constraint - if (prismaErr.code === 'P2002') { - return { - code: 'INSERT_ERROR', - message: 'insert failed because of invalid unique constraint', - } - } - - // An operation failed because it depends on one or more records - // that were required but not found. - if (prismaErr.code === 'P2025') { - return { - code: 'NOT_FOUND_ERROR', - message: - 'An operation failed because it depends on one or more records that were required but not found.', - } - } - - console.error( - 'ERROR: Unhandled KnownRequestError from prisma: ', - prismaErr - ) - return { - code: 'UNEXPECTED_EXCEPTION', - message: 'An unexpected prisma exception has occurred', - } - } - - // PrismaClientInitializationError is for errors trying to setup a prisma connection - if (prismaErr instanceof PrismaClientInitializationError) { - return { - code: 'CONNECTION_ERROR', - message: prismaErr.message, - } - } - - console.error( - "CODING ERROR: we weren't able to decode the error thrown by prisma correctly", - prismaErr - ) - return { - code: 'UNEXPECTED_EXCEPTION', - message: 'A completely unexpected prisma exception has occurred', - } -} - -class NotFoundError extends Error { - constructor(message: string) { - super(message) - - Object.setPrototypeOf(this, NotFoundError.prototype) - } -} - -export type { StoreError } -export { NotFoundError, isStoreError, convertPrismaErrorToStoreError } diff --git a/services/app-api/src/postgres/user/findAllUsers.ts b/services/app-api/src/postgres/user/findAllUsers.ts index dce96c34d6..2f37705dd7 100644 --- a/services/app-api/src/postgres/user/findAllUsers.ts +++ b/services/app-api/src/postgres/user/findAllUsers.ts @@ -1,12 +1,10 @@ import type { PrismaClient } from '@prisma/client' import type { UserType } from '../../domain-models' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError } from '../storeError' import { parseDomainUsersFromPrismaUsers } from './prismaDomainUser' export async function findAllUsers( client: PrismaClient -): Promise { +): Promise { try { const allUsers = await client.user.findMany({ include: { @@ -20,15 +18,12 @@ export async function findAllUsers( const domainUserResults = parseDomainUsersFromPrismaUsers(allUsers) if (domainUserResults instanceof Error) { - return { - code: 'USER_FORMAT_ERROR', - message: domainUserResults.message, - } + return domainUserResults } return domainUserResults } catch (err) { console.error(err) - return convertPrismaErrorToStoreError(err) + return err } } diff --git a/services/app-api/src/postgres/user/findUser.ts b/services/app-api/src/postgres/user/findUser.ts index ad4c44b7b9..1f2f422462 100644 --- a/services/app-api/src/postgres/user/findUser.ts +++ b/services/app-api/src/postgres/user/findUser.ts @@ -1,13 +1,11 @@ import type { PrismaClient } from '@prisma/client' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError } from '../storeError' import type { UserType } from '../../domain-models' import { domainUserFromPrismaUser } from './prismaDomainUser' export async function findUser( client: PrismaClient, id: string -): Promise { +): Promise { try { const findResult = await client.user.findUnique({ where: { @@ -24,6 +22,6 @@ export async function findUser( return domainUserFromPrismaUser(findResult) } catch (err) { - return convertPrismaErrorToStoreError(err) + return err } } diff --git a/services/app-api/src/postgres/user/insertManyUsers.ts b/services/app-api/src/postgres/user/insertManyUsers.ts index 592b7d57dd..92aaa6d5ad 100644 --- a/services/app-api/src/postgres/user/insertManyUsers.ts +++ b/services/app-api/src/postgres/user/insertManyUsers.ts @@ -1,6 +1,4 @@ import type { PrismaClient } from '@prisma/client' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError } from '../storeError' import type { UserType } from '../../domain-models' import { toDomainUser } from '../../domain-models' import type { InsertUserArgsType } from './insertUser' @@ -8,7 +6,7 @@ import type { InsertUserArgsType } from './insertUser' export async function insertManyUsers( client: PrismaClient, users: InsertUserArgsType[] -): Promise { +): Promise { try { console.info(`Trying to insert ${users.length} users into postgres....`) @@ -39,6 +37,6 @@ export async function insertManyUsers( return usersResult.map((user) => toDomainUser(user)) } catch (err) { - return convertPrismaErrorToStoreError(err) + return err } } diff --git a/services/app-api/src/postgres/user/insertUser.ts b/services/app-api/src/postgres/user/insertUser.ts index b947b4bf74..7e888b3b9e 100644 --- a/services/app-api/src/postgres/user/insertUser.ts +++ b/services/app-api/src/postgres/user/insertUser.ts @@ -1,6 +1,4 @@ import type { PrismaClient, Role } from '@prisma/client' -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError } from '../storeError' import type { DivisionType, UserType } from '../../domain-models' import { toDomainUser } from '../../domain-models' @@ -17,7 +15,7 @@ export type InsertUserArgsType = { export async function insertUser( client: PrismaClient, user: InsertUserArgsType -): Promise { +): Promise { try { console.info('Trying to insert the user to postgres....') const val = await client.user.create({ @@ -34,6 +32,6 @@ export async function insertUser( console.info('insert user return: ' + val) return toDomainUser(val) } catch (err) { - return convertPrismaErrorToStoreError(err) + return err } } diff --git a/services/app-api/src/postgres/user/prismaDomainUser.ts b/services/app-api/src/postgres/user/prismaDomainUser.ts index 2ca57a4a6a..ebd132f428 100644 --- a/services/app-api/src/postgres/user/prismaDomainUser.ts +++ b/services/app-api/src/postgres/user/prismaDomainUser.ts @@ -1,22 +1,19 @@ import type { User, State } from '@prisma/client' import type { UserType } from '../../domain-models' -import type { StoreError } from '../storeError' -import { isStoreError } from '../storeError' // We are storing all the possible values for any of the user types in the same // table in prisma, so we need to parse those into valid UserTypes or error if something // got stored wrong. function domainUserFromPrismaUser( prismaUser: User & { stateAssignments?: State[] } -): UserType | StoreError { +): UserType | Error { const divisionAssignment = prismaUser.divisionAssignment ?? undefined switch (prismaUser.role) { case 'STATE_USER': if (!prismaUser.stateCode) { - return { - code: 'USER_FORMAT_ERROR', - message: `StateUser has no stateCode; id: ${prismaUser.id}`, - } + return new Error( + `StateUser has no stateCode; id: ${prismaUser.id}` + ) } return { @@ -29,10 +26,9 @@ function domainUserFromPrismaUser( } case 'CMS_USER': if (!prismaUser.stateAssignments) { - return { - code: 'USER_FORMAT_ERROR', - message: `CMSUser has no states array, probably a programming error; id: ${prismaUser.id}`, - } + return new Error( + `CMSUser has no states array, probably a programming error; id: ${prismaUser.id}` + ) } return { @@ -73,12 +69,12 @@ function domainUserFromPrismaUser( function parseDomainUsersFromPrismaUsers( prismaUsers: (User & { stateAssignments?: State[] })[] -): UserType[] | StoreError { +): UserType[] | Error { const users: UserType[] = [] - const errors: StoreError[] = [] + const errors: Error[] = [] for (const prismaUser of prismaUsers) { const result = domainUserFromPrismaUser(prismaUser) - if (isStoreError(result)) { + if (result instanceof Error) { errors.push(result) } else { users.push(result) @@ -86,12 +82,10 @@ function parseDomainUsersFromPrismaUsers( } if (errors.length > 0) { - return { - code: 'USER_FORMAT_ERROR', - message: `Some of the fetched users did not have all their required fields in the db: ${errors - .map((e) => e.message) - .join(', ')}`, - } + const msg = `Some of the fetched users did not have all their required fields in the db: ${errors + .map((e) => e.message) + .join(', ')}` + return new Error(msg) } return users diff --git a/services/app-api/src/postgres/user/updateCmsUserProperties.ts b/services/app-api/src/postgres/user/updateCmsUserProperties.ts index 43ca0044e3..256e6bdffe 100644 --- a/services/app-api/src/postgres/user/updateCmsUserProperties.ts +++ b/services/app-api/src/postgres/user/updateCmsUserProperties.ts @@ -1,10 +1,9 @@ -import type { StoreError } from '../storeError' -import { convertPrismaErrorToStoreError, isStoreError } from '../storeError' import type { StateCodeType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import type { Division, PrismaClient } from '@prisma/client' import { AuditAction } from '@prisma/client' import type { CMSUserType } from '../../domain-models' import { domainUserFromPrismaUser } from './prismaDomainUser' +import { NotFoundError } from '../postgresErrors' export async function updateCmsUserProperties( client: PrismaClient, @@ -13,7 +12,7 @@ export async function updateCmsUserProperties( idOfUserPerformingUpdate: string, divisionAssignment?: Division, description?: string | null -): Promise { +): Promise { try { const statesWithCode = stateCodes.map((s) => { return { stateCode: s } @@ -29,7 +28,7 @@ export async function updateCmsUserProperties( /* get the old user values before updating */ let userBeforeUpdate try { - userBeforeUpdate = await client.user.findFirstOrThrow({ + userBeforeUpdate = await client.user.findFirst({ where: { id: userID, role: 'CMS_USER', @@ -39,14 +38,11 @@ export async function updateCmsUserProperties( }, }) } catch (err) { - return convertPrismaErrorToStoreError(err) + return err } if (!userBeforeUpdate) { - return { - code: 'UNEXPECTED_EXCEPTION', - message: 'Unable to retrieve user to be updated', - } + return new NotFoundError('user to update was not found') } /* if all was well with the old values, update the user and make an audit record; @@ -94,19 +90,18 @@ export async function updateCmsUserProperties( const domainUser = domainUserFromPrismaUser(updateResult) - if (isStoreError(domainUser)) { + if (domainUser instanceof Error) { return domainUser } if (domainUser.role !== 'CMS_USER') { - return { - code: 'UNEXPECTED_EXCEPTION', - message: 'Updated user was not a CMS User!', - } + return new Error( + 'UNEXPECTED EXCEPTION: should have gotten a CMS user back' + ) } return domainUser } catch (err) { - return convertPrismaErrorToStoreError(err) + return err } } diff --git a/services/app-api/src/resolvers/configureResolvers.ts b/services/app-api/src/resolvers/configureResolvers.ts index b3bad7264f..916bf60f57 100644 --- a/services/app-api/src/resolvers/configureResolvers.ts +++ b/services/app-api/src/resolvers/configureResolvers.ts @@ -42,14 +42,8 @@ export function configureResolvers( DateTime: GraphQLDateTime, Query: { fetchCurrentUser: fetchCurrentUserResolver(), - fetchHealthPlanPackage: fetchHealthPlanPackageResolver( - store, - launchDarkly - ), - indexHealthPlanPackages: indexHealthPlanPackagesResolver( - store, - launchDarkly - ), + fetchHealthPlanPackage: fetchHealthPlanPackageResolver(store), + indexHealthPlanPackages: indexHealthPlanPackagesResolver(store), indexUsers: indexUsersResolver(store), indexQuestions: indexQuestionsResolver(store), fetchEmailSettings: fetchEmailSettingsResolver( @@ -58,14 +52,11 @@ export function configureResolvers( emailParameterStore ), // Rates refactor - indexRates: indexRatesResolver(store, launchDarkly), - fetchRate: fetchRateResolver(store, launchDarkly), + indexRates: indexRatesResolver(store), + fetchRate: fetchRateResolver(store), }, Mutation: { - createHealthPlanPackage: createHealthPlanPackageResolver( - store, - launchDarkly - ), + createHealthPlanPackage: createHealthPlanPackageResolver(store), updateHealthPlanFormData: updateHealthPlanFormDataResolver( store, launchDarkly @@ -79,13 +70,20 @@ export function configureResolvers( unlockHealthPlanPackage: unlockHealthPlanPackageResolver( store, emailer, - emailParameterStore, - launchDarkly + emailParameterStore ), - updateContract: updateContract(store, launchDarkly), + updateContract: updateContract(store), updateCMSUser: updateCMSUserResolver(store), - createQuestion: createQuestionResolver(store), - createQuestionResponse: createQuestionResponseResolver(store), + createQuestion: createQuestionResolver( + store, + emailParameterStore, + emailer + ), + createQuestionResponse: createQuestionResponseResolver( + store, + emailer, + emailParameterStore + ), }, User: { // resolveType is required to differentiate Unions diff --git a/services/app-api/src/resolvers/contract/updateContract.test.ts b/services/app-api/src/resolvers/contract/updateContract.test.ts index 75a89515bd..056fe182fe 100644 --- a/services/app-api/src/resolvers/contract/updateContract.test.ts +++ b/services/app-api/src/resolvers/contract/updateContract.test.ts @@ -5,24 +5,18 @@ import { createTestHealthPlanPackage, } from '../../testHelpers/gqlHelpers' import { testCMSUser } from '../../testHelpers/userHelpers' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' describe('updateContract', () => { const cmsUser = testCMSUser() - const mockLDService = testLDService({ ['rates-db-refactor']: true }) it('updates the contract', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) const cmsServer = await constructTestPostgresServer({ - ldService: mockLDService, context: { user: cmsUser, }, @@ -61,14 +55,11 @@ describe('updateContract', () => { }) it('errors if the contract is not submitted', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() // First, create a draft submission const draftSubmission = await createTestHealthPlanPackage(stateServer) const cmsServer = await constructTestPostgresServer({ - ldService: mockLDService, context: { user: cmsUser, }, @@ -96,14 +87,11 @@ describe('updateContract', () => { }) it('errors if a State user calls it', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) // Update const updateResult = await stateServer.executeOperation({ query: UPDATE_CONTRACT_MUTATION, diff --git a/services/app-api/src/resolvers/contract/updateContract.ts b/services/app-api/src/resolvers/contract/updateContract.ts index 54d4c08608..c8de411dfe 100644 --- a/services/app-api/src/resolvers/contract/updateContract.ts +++ b/services/app-api/src/resolvers/contract/updateContract.ts @@ -10,120 +10,103 @@ import { setResolverDetailsOnActiveSpan, setErrorAttributesOnActiveSpan, } from '../attributeHelper' -import type { LDService } from '../../launchDarkly/launchDarkly' import { GraphQLError } from 'graphql' import { NotFoundError } from '../../postgres' export function updateContract( - store: Store, - launchDarkly: LDService + store: Store ): MutationResolvers['updateContract'] { return async (_parent, { input }, context) => { const { user, span } = context setResolverDetailsOnActiveSpan('updateContract', user, span) - const ratesDatabaseRefactor = await launchDarkly.getFeatureFlag( - context, - 'rates-db-refactor' - ) - - if (ratesDatabaseRefactor) { - // This resolver is only callable by CMS users - if (!isCMSUser(user)) { - logError( - 'updateContract', - 'user not authorized to update contract' - ) - setErrorAttributesOnActiveSpan( - 'user not authorized to update contract', - span - ) - throw new ForbiddenError( - 'user not authorized to update contract' - ) - } - const contractWithHistory = await store.findContractWithHistory( - input.id + // This resolver is only callable by CMS users + if (!isCMSUser(user)) { + logError('updateContract', 'user not authorized to update contract') + setErrorAttributesOnActiveSpan( + 'user not authorized to update contract', + span ) - if (contractWithHistory instanceof Error) { - throw contractWithHistory - } + throw new ForbiddenError('user not authorized to update contract') + } - if (contractWithHistory instanceof Error) { - const errMessage = `Issue finding a contract with history with id ${input.id}. Message: ${contractWithHistory.message}` - logError('updateContract', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) + const contractWithHistory = await store.findContractWithHistory( + input.id + ) + if (contractWithHistory instanceof Error) { + throw contractWithHistory + } - if (contractWithHistory instanceof NotFoundError) { - throw new GraphQLError(errMessage, { - extensions: { - code: 'NOT_FOUND', - cause: 'DB_ERROR', - }, - }) - } + if (contractWithHistory instanceof Error) { + const errMessage = `Issue finding a contract with history with id ${input.id}. Message: ${contractWithHistory.message}` + logError('updateContract', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + if (contractWithHistory instanceof NotFoundError) { throw new GraphQLError(errMessage, { extensions: { - code: 'INTERNAL_SERVER_ERROR', + code: 'NOT_FOUND', cause: 'DB_ERROR', }, }) } - const isSubmittedOrUnlocked = - contractWithHistory.status === 'SUBMITTED' || - contractWithHistory.status === 'RESUBMITTED' || - contractWithHistory.status === 'UNLOCKED' + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - if (!isSubmittedOrUnlocked) { - const errMessage = `Can not update a contract has not been submitted or unlocked. Fails for contract with ID: ${contractWithHistory.id}` - logError('updateContract', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - argumentName: 'contractID', - cause: 'INVALID_PACKAGE_STATUS', - }) - } + const isSubmittedOrUnlocked = + contractWithHistory.status === 'SUBMITTED' || + contractWithHistory.status === 'RESUBMITTED' || + contractWithHistory.status === 'UNLOCKED' - const updatedContract = await store.updateContract({ - contractID: input.id, - mccrsID: input.mccrsID || undefined, + if (!isSubmittedOrUnlocked) { + const errMessage = `Can not update a contract has not been submitted or unlocked. Fails for contract with ID: ${contractWithHistory.id}` + logError('updateContract', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new UserInputError(errMessage, { + argumentName: 'contractID', + cause: 'INVALID_PACKAGE_STATUS', }) + } - if (updatedContract instanceof Error) { - const errMessage = `Failed to update contract with ID: ${input.id}. Message: ${updatedContract.message}` - logError('updateContract', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } + const updatedContract = await store.updateContract({ + contractID: input.id, + mccrsID: input.mccrsID || undefined, + }) - const convertedPkg = - convertContractWithRatesToUnlockedHPP(updatedContract) + if (updatedContract instanceof Error) { + const errMessage = `Failed to update contract with ID: ${input.id}. Message: ${updatedContract.message}` + logError('updateContract', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - if (convertedPkg instanceof Error) { - const errMessage = `Issue converting contract. Message: ${convertedPkg.message}` - logError('updateContract', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } - return { - pkg: convertedPkg, - } - } else { - throw new ForbiddenError( - 'updateMCCRSID must be used with rates database refactor flag' - ) + const convertedPkg = + convertContractWithRatesToUnlockedHPP(updatedContract) + + if (convertedPkg instanceof Error) { + const errMessage = `Issue converting contract. Message: ${convertedPkg.message}` + logError('updateContract', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) + } + return { + pkg: convertedPkg, } } } diff --git a/services/app-api/src/resolvers/contractAndRates/fetchRate.test.ts b/services/app-api/src/resolvers/contractAndRates/fetchRate.test.ts index 5ceb46f7c8..9b34dc1a39 100644 --- a/services/app-api/src/resolvers/contractAndRates/fetchRate.test.ts +++ b/services/app-api/src/resolvers/contractAndRates/fetchRate.test.ts @@ -8,25 +8,19 @@ import { updateTestHealthPlanPackage, } from '../../testHelpers/gqlHelpers' import { testCMSUser } from '../../testHelpers/userHelpers' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' import { latestFormData } from '../../testHelpers/healthPlanPackageHelpers' import { must } from '../../testHelpers' import { v4 as uuidv4 } from 'uuid' describe('fetchRate', () => { - const mockLDService = testLDService({ 'rates-db-refactor': true }) - it('returns correct rate revisions on resubmit when existing rate is edited', async () => { const cmsUser = testCMSUser() - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const server = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) const initialRateInfos = () => ({ @@ -40,7 +34,6 @@ describe('fetchRate', () => { name: 'rateDocument.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -142,15 +135,12 @@ describe('fetchRate', () => { it('returns correct rate revisions on resubmit when new rate added', async () => { const cmsUser = testCMSUser() - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const server = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) const initialRateInfos = () => ({ @@ -164,7 +154,6 @@ describe('fetchRate', () => { name: 'rateDocument.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -284,20 +273,16 @@ describe('fetchRate', () => { it('returns the right revisions as a rate is unlocked', async () => { const cmsUser = testCMSUser() - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const server = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) - const unlockedSubmission = await createAndSubmitTestHealthPlanPackage( - server - ) + const unlockedSubmission = + await createAndSubmitTestHealthPlanPackage(server) // unlock two await unlockTestHealthPlanPackage( diff --git a/services/app-api/src/resolvers/contractAndRates/fetchRate.ts b/services/app-api/src/resolvers/contractAndRates/fetchRate.ts index 98cb9715c5..90357d8ed4 100644 --- a/services/app-api/src/resolvers/contractAndRates/fetchRate.ts +++ b/services/app-api/src/resolvers/contractAndRates/fetchRate.ts @@ -1,4 +1,3 @@ -import { ForbiddenError } from 'apollo-server-core' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, @@ -6,27 +5,13 @@ import { } from '../attributeHelper' import { NotFoundError } from '../../postgres' import type { QueryResolvers } from '../../gen/gqlServer' -import type { LDService } from '../../launchDarkly/launchDarkly' import type { Store } from '../../postgres' import { GraphQLError } from 'graphql' -export function fetchRateResolver( - store: Store, - launchDarkly: LDService -): QueryResolvers['fetchRate'] { +export function fetchRateResolver(store: Store): QueryResolvers['fetchRate'] { return async (_parent, { input }, context) => { const { user, span } = context setResolverDetailsOnActiveSpan('fetchRate', user, span) - const ratesDatabaseRefactor = await launchDarkly.getFeatureFlag( - context, - 'rates-db-refactor' - ) - - if (!ratesDatabaseRefactor) { - throw new ForbiddenError( - 'fetchRate must be used with rates database refactor flag' - ) - } const rateWithHistory = await store.findRateWithHistory(input.rateID) if (rateWithHistory instanceof Error) { diff --git a/services/app-api/src/resolvers/contractAndRates/indexRates.test.ts b/services/app-api/src/resolvers/contractAndRates/indexRates.test.ts index fa8b4607dc..f7cfbddb44 100644 --- a/services/app-api/src/resolvers/contractAndRates/indexRates.test.ts +++ b/services/app-api/src/resolvers/contractAndRates/indexRates.test.ts @@ -13,17 +13,13 @@ import { } from '../../testHelpers/gqlHelpers' import type { RateEdge, Rate } from '../../gen/gqlServer' import { testCMSUser, testStateUser } from '../../testHelpers/userHelpers' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' import { latestFormData } from '../../testHelpers/healthPlanPackageHelpers' import { formatGQLDate } from 'app-web/src/common-code/dateHelpers' describe.skip('indexRates', () => { - const mockLDService = testLDService({ 'rates-db-refactor': true }) it('returns ForbiddenError for state user', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() // submit packages that include rates await createAndSubmitTestHealthPlanPackage(stateServer) @@ -38,11 +34,8 @@ describe.skip('indexRates', () => { it('returns rate reviews list for cms user with no errors', async () => { const cmsUser = testCMSUser() - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ - ldService: mockLDService, context: { user: cmsUser, }, @@ -77,14 +70,11 @@ describe.skip('indexRates', () => { it('does not return rates still in initial draft', async () => { const cmsUser = testCMSUser() - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) // First, create new submissions const draft1 = await createAndUpdateTestHealthPlanPackage(stateServer) @@ -114,14 +104,11 @@ describe.skip('indexRates', () => { it('does not add rates when contract only packages submitted', async () => { const cmsUser = testCMSUser() - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) // baseline const initial = await cmsServer.executeOperation({ @@ -155,14 +142,11 @@ describe.skip('indexRates', () => { it('does not add rates a for draft contract and rates package that is submitted later as contract only', async () => { const cmsUser = testCMSUser() - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) // baseline @@ -216,15 +200,12 @@ describe.skip('indexRates', () => { it('returns a rate with history with correct data in each revision', async () => { const cmsUser = testCMSUser() - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const server = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) // baseline @@ -244,7 +225,6 @@ describe.skip('indexRates', () => { name: 'rateDocument.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -410,15 +390,12 @@ describe.skip('indexRates', () => { it('synthesizes the right statuses as a rate is submitted/unlocked/etc', async () => { const cmsUser = testCMSUser() - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const server = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) // First, create new submissions @@ -486,15 +463,12 @@ describe.skip('indexRates', () => { it('returns the right revisions as a rate is submitted/unlocked/etc', async () => { const cmsUser = testCMSUser() - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const server = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) // First, create new submissions @@ -588,14 +562,11 @@ describe.skip('indexRates', () => { it('return a list of submitted rates from multiple states', async () => { const cmsUser = testCMSUser() - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) const otherStateServer = await constructTestPostgresServer({ context: { @@ -604,7 +575,6 @@ describe.skip('indexRates', () => { email: 'aang@mn.gov', }), }, - ldService: mockLDService, }) // submit packages from two different states const defaultState1 = await createAndSubmitTestHealthPlanPackage( diff --git a/services/app-api/src/resolvers/contractAndRates/indexRates.ts b/services/app-api/src/resolvers/contractAndRates/indexRates.ts index b447708a7d..b115409781 100644 --- a/services/app-api/src/resolvers/contractAndRates/indexRates.ts +++ b/services/app-api/src/resolvers/contractAndRates/indexRates.ts @@ -8,7 +8,6 @@ import { import { hasAdminPermissions, isCMSUser } from '../../domain-models/user' import { NotFoundError } from '../../postgres' import type { QueryResolvers } from '../../gen/gqlServer' -import type { LDService } from '../../launchDarkly/launchDarkly' import type { Store } from '../../postgres' import type { RateOrErrorArrayType } from '../../postgres/contractAndRates' import { logError } from '../../logger' @@ -41,23 +40,11 @@ const validateAndReturnRates = ( return parsedRates } -export function indexRatesResolver( - store: Store, - launchDarkly: LDService -): QueryResolvers['indexRates'] { +export function indexRatesResolver(store: Store): QueryResolvers['indexRates'] { return async (_parent, _args, context) => { const { user, span } = context setResolverDetailsOnActiveSpan('indexRates', user, span) - const ratesDatabaseRefactor = await launchDarkly.getFeatureFlag( - context, - 'rates-db-refactor' - ) - if (!ratesDatabaseRefactor) { - throw new ForbiddenError( - 'indexRates must be used with rates database refactor flag' - ) - } if (hasAdminPermissions(user) || isCMSUser(user)) { const ratesWithHistory = await store.findAllRatesWithHistoryBySubmitInfo() diff --git a/services/app-api/src/resolvers/email/fetchEmailSettings.ts b/services/app-api/src/resolvers/email/fetchEmailSettings.ts index 07077fa18d..262df55e8f 100644 --- a/services/app-api/src/resolvers/email/fetchEmailSettings.ts +++ b/services/app-api/src/resolvers/email/fetchEmailSettings.ts @@ -11,7 +11,6 @@ import type { QueryResolvers } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' import type { EmailParameterStore } from '../../parameterStore' import type { Store } from '../../postgres' -import { isStoreError } from '../../postgres' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, @@ -46,7 +45,7 @@ export function fetchEmailSettingsResolver( // Then get list of supported states const findAllStatesResult = await store.findAllSupportedStates() - if (isStoreError(findAllStatesResult)) { + if (findAllStatesResult instanceof Error) { logError('indexUsers', findAllStatesResult.message) setErrorAttributesOnActiveSpan(findAllStatesResult.message, span) throw new Error('Unexpected Error Querying Users') diff --git a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts index a4a2a512fb..14ddd6d649 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelper.test.ts @@ -150,7 +150,6 @@ describe('convertHealthPlanPackageRatesToDomain', () => { s3URL: 's3://bucketname/key/rate', name: 'rate', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -181,7 +180,6 @@ describe('convertHealthPlanPackageRatesToDomain', () => { { s3URL: 's3://bucketname/key/supporting-documents', name: 'supporting documents', - documentCategories: ['RATES_RELATED' as const], sha256: 'supportingDocsSha', }, ], diff --git a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts index 51eceee1bf..8a002e1c7b 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/contractAndRates/resolverHelpers.ts @@ -12,7 +12,6 @@ import type { SubmissionDocument, UnlockedHealthPlanFormDataType, } from '../../../../../app-web/src/common-code/healthPlanFormDataType' -import { calculateSHA256 } from '../../../handlers/add_sha' import { rateFormDataSchema } from '../../../domain-models/contractAndRates' import assert from 'assert' import type { ContractOrErrorArrayType } from '../../../postgres/contractAndRates/findAllContractsWithHistoryByState' @@ -72,16 +71,10 @@ const validateContractsAndConvert = ( const convertHPPDocsToDomain = async (docs: SubmissionDocument[]) => await Promise.all( docs.map(async ({ name, s3URL, sha256 }): Promise => { - let sha = sha256 - - if (!sha) { - sha = await calculateSHA256(s3URL) - } - return { name, s3URL, - sha256: sha, + sha256, } }) ) diff --git a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.test.ts index 95f4dc83e0..d626c47fe7 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.test.ts @@ -3,121 +3,89 @@ import CREATE_HEALTH_PLAN_PACKAGE from '../../../../app-graphql/src/mutations/cr import { constructTestPostgresServer } from '../../testHelpers/gqlHelpers' import { latestFormData } from '../../testHelpers/healthPlanPackageHelpers' import { testCMSUser } from '../../testHelpers/userHelpers' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' -import type { - FeatureFlagLDConstant, - FlagValue, -} from '../../../../app-web/src/common-code/featureFlags' -const flagValueTestParameters: { - flagName: FeatureFlagLDConstant - flagValue: FlagValue - testName: string -}[] = [ - { - flagName: 'rates-db-refactor', - flagValue: false, - testName: 'createHealthPlanPackage with all feature flags off', - }, - { - flagName: 'rates-db-refactor', - flagValue: true, - testName: 'createHealthPlanPackage with rates-db-refactor on', - }, -] +describe(`Tests CreateHealthPlanPackage`, () => { + it('returns package with unlocked form data', async () => { + const server = await constructTestPostgresServer() -describe.each(flagValueTestParameters)( - `Tests $testName`, - ({ flagName, flagValue }) => { - const mockLDService = testLDService({ [flagName]: flagValue }) - - it('returns package with unlocked form data', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - const input: CreateHealthPlanPackageInput = { - populationCovered: 'MEDICAID', - programIDs: [ - '5c10fe9f-bec9-416f-a20c-718b152ad633', - '037af66b-81eb-4472-8b80-01edf17d12d9', - ], - riskBasedContract: false, - submissionType: 'CONTRACT_ONLY', - submissionDescription: 'A real submission', - contractType: 'BASE', - } - const res = await server.executeOperation({ - query: CREATE_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) - - expect(res.errors).toBeUndefined() - - const pkg = res.data?.createHealthPlanPackage.pkg - const draft = latestFormData(pkg) - - expect(draft.submissionDescription).toBe('A real submission') - expect(draft.submissionType).toBe('CONTRACT_ONLY') - expect(draft.programIDs).toEqual([ + const input: CreateHealthPlanPackageInput = { + populationCovered: 'MEDICAID', + programIDs: [ '5c10fe9f-bec9-416f-a20c-718b152ad633', '037af66b-81eb-4472-8b80-01edf17d12d9', - ]) - expect(draft.documents).toHaveLength(0) - expect(draft.managedCareEntities).toHaveLength(0) - expect(draft.federalAuthorities).toHaveLength(0) - expect(draft.contractDateStart).toBeUndefined() - expect(draft.contractDateEnd).toBeUndefined() + ], + riskBasedContract: false, + submissionType: 'CONTRACT_ONLY', + submissionDescription: 'A real submission', + contractType: 'BASE', + } + const res = await server.executeOperation({ + query: CREATE_HEALTH_PLAN_PACKAGE, + variables: { input }, }) - it('returns an error if the program id is not in valid', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const input: CreateHealthPlanPackageInput = { - populationCovered: 'MEDICAID', - programIDs: ['xyz123'], - riskBasedContract: false, - submissionType: 'CONTRACT_ONLY', - submissionDescription: 'A real submission', - contractType: 'BASE', - } - const res = await server.executeOperation({ - query: CREATE_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) + expect(res.errors).toBeUndefined() - expect(res.errors).toBeDefined() - expect(res.errors && res.errors[0].message).toBe( - 'The program id xyz123 does not exist in state FL' - ) + const pkg = res.data?.createHealthPlanPackage.pkg + const draft = latestFormData(pkg) + + expect(draft.submissionDescription).toBe('A real submission') + expect(draft.submissionType).toBe('CONTRACT_ONLY') + expect(draft.programIDs).toEqual([ + '5c10fe9f-bec9-416f-a20c-718b152ad633', + '037af66b-81eb-4472-8b80-01edf17d12d9', + ]) + expect(draft.documents).toHaveLength(0) + expect(draft.managedCareEntities).toHaveLength(0) + expect(draft.federalAuthorities).toHaveLength(0) + expect(draft.contractDateStart).toBeUndefined() + expect(draft.contractDateEnd).toBeUndefined() + }) + + it('returns an error if the program id is not in valid', async () => { + const server = await constructTestPostgresServer() + const input: CreateHealthPlanPackageInput = { + populationCovered: 'MEDICAID', + programIDs: ['xyz123'], + riskBasedContract: false, + submissionType: 'CONTRACT_ONLY', + submissionDescription: 'A real submission', + contractType: 'BASE', + } + const res = await server.executeOperation({ + query: CREATE_HEALTH_PLAN_PACKAGE, + variables: { input }, }) - it('returns an error if a CMS user attempts to create', async () => { - const server = await constructTestPostgresServer({ - context: { - user: testCMSUser(), - }, - ldService: mockLDService, - }) + expect(res.errors).toBeDefined() + expect(res.errors && res.errors[0].message).toBe( + 'The program id xyz123 does not exist in state FL' + ) + }) - const input: CreateHealthPlanPackageInput = { - populationCovered: 'MEDICAID', - programIDs: ['xyz123'], - riskBasedContract: false, - submissionType: 'CONTRACT_ONLY', - submissionDescription: 'A real submission', - contractType: 'BASE', - } - const res = await server.executeOperation({ - query: CREATE_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) + it('returns an error if a CMS user attempts to create', async () => { + const server = await constructTestPostgresServer({ + context: { + user: testCMSUser(), + }, + }) - expect(res.errors).toBeDefined() - expect(res.errors && res.errors[0].message).toBe( - 'user not authorized to create state data' - ) + const input: CreateHealthPlanPackageInput = { + populationCovered: 'MEDICAID', + programIDs: ['xyz123'], + riskBasedContract: false, + submissionType: 'CONTRACT_ONLY', + submissionDescription: 'A real submission', + contractType: 'BASE', + } + const res = await server.executeOperation({ + query: CREATE_HEALTH_PLAN_PACKAGE, + variables: { input }, }) - } -) + + expect(res.errors).toBeDefined() + expect(res.errors && res.errors[0].message).toBe( + 'user not authorized to create state data' + ) + }) +}) diff --git a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts index 3dd8d38195..223262da79 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts @@ -2,8 +2,7 @@ import { ForbiddenError, UserInputError } from 'apollo-server-lambda' import { isStateUser } from '../../domain-models' import type { MutationResolvers, State } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' -import type { InsertHealthPlanPackageArgsType, Store } from '../../postgres' -import { isStoreError } from '../../postgres' +import type { InsertContractArgsType, Store } from '../../postgres' import { pluralize } from '../../../../app-web/src/common-code/formatters' import { setResolverDetailsOnActiveSpan, @@ -11,22 +10,15 @@ import { setSuccessAttributesOnActiveSpan, } from '../attributeHelper' import { GraphQLError } from 'graphql/index' -import type { LDService } from '../../launchDarkly/launchDarkly' import { convertContractWithRatesToUnlockedHPP } from '../../domain-models' export function createHealthPlanPackageResolver( - store: Store, - launchDarkly: LDService + store: Store ): MutationResolvers['createHealthPlanPackage'] { return async (_parent, { input }, context) => { const { user, span } = context setResolverDetailsOnActiveSpan('createHealthPlanPackage', user, span) - const ratesDatabaseRefactor = await launchDarkly.getFeatureFlag( - context, - 'rates-db-refactor' - ) - // This resolver is only callable by state users if (!isStateUser(user)) { logError( @@ -63,73 +55,53 @@ export function createHealthPlanPackageResolver( }) } - const insertArgs: InsertHealthPlanPackageArgsType = { + // Why do we need to do this? I feel like I don't understand Maybe here exactly. + const riskBasedContract = + input.riskBasedContract === undefined + ? undefined + : input.riskBasedContract?.valueOf() + + const insertArgs: InsertContractArgsType = { stateCode: stateFromCurrentUser, - populationCovered: - input.populationCovered as InsertHealthPlanPackageArgsType['populationCovered'], + populationCovered: input.populationCovered, programIDs: input.programIDs, - riskBasedContract: - input.riskBasedContract as InsertHealthPlanPackageArgsType['riskBasedContract'], + riskBasedContract: riskBasedContract, submissionDescription: input.submissionDescription, - submissionType: - input.submissionType as InsertHealthPlanPackageArgsType['submissionType'], + submissionType: input.submissionType, contractType: input.contractType, } - //Here is where we flag the insert - if (ratesDatabaseRefactor) { - const contractResult = await store.insertDraftContract(insertArgs) - if (contractResult instanceof Error) { - const errMessage = `Error creating a draft contract. Message: ${contractResult.message}` - logError('createHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - - // Now we do the conversions - const pkg = convertContractWithRatesToUnlockedHPP(contractResult) - - if (pkg instanceof Error) { - const errMessage = `Error converting draft contract. Message: ${pkg.message}` - logError('createHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } + const contractResult = await store.insertDraftContract(insertArgs) + if (contractResult instanceof Error) { + const errMessage = `Error creating a draft contract. Message: ${contractResult.message}` + logError('createHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - logSuccess('createHealthPlanPackage') - setSuccessAttributesOnActiveSpan(span) + // Now we do the conversions + const pkg = convertContractWithRatesToUnlockedHPP(contractResult) - return { pkg } - } else { - const pkgResult = await store.insertHealthPlanPackage(insertArgs) - if (isStoreError(pkgResult)) { - const errMessage = `Error creating a package of type ${pkgResult.code}. Message: ${pkgResult.message}` - logError('createHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } + if (pkg instanceof Error) { + const errMessage = `Error converting draft contract. Message: ${pkg.message}` + logError('createHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) + } - logSuccess('createHealthPlanPackage') - setSuccessAttributesOnActiveSpan(span) + logSuccess('createHealthPlanPackage') + setSuccessAttributesOnActiveSpan(span) - return { - pkg: pkgResult, - } - } + return { pkg } } } diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts index aff583261b..255e3d0ff3 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts @@ -9,482 +9,417 @@ import { resubmitTestHealthPlanPackage, } from '../../testHelpers/gqlHelpers' import { testCMSUser, testStateUser } from '../../testHelpers/userHelpers' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' -import type { - FeatureFlagLDConstant, - FlagValue, -} from '../../../../app-web/src/common-code/featureFlags' - -const flagValueTestParameters: { - flagName: FeatureFlagLDConstant - flagValue: FlagValue - testName: string -}[] = [ - { - flagName: 'rates-db-refactor', - flagValue: false, - testName: 'fetchHealthPlanPackage with all feature flags off', - }, - { - flagName: 'rates-db-refactor', - flagValue: true, - testName: 'fetchHealthPlanPackage with rates-db-refactor on', - }, -] - -describe.each(flagValueTestParameters)( - `fetchHealthPlanPackage $testName`, - ({ flagName, flagValue }) => { - const mockLDService = testLDService({ [flagName]: flagValue }) - - const testUserCMS = testCMSUser() - - const testUserState = testStateUser() - - it('returns package with one revision', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - // First, create a new submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - server - ) - const createdID = stateSubmission.id - - // then see if we can fetch that same submission - const input = { - pkgID: createdID, - } - - const result = await server.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) - - expect(result.errors).toBeUndefined() - - const resultSub = result.data?.fetchHealthPlanPackage.pkg - expect(resultSub.id).toEqual(createdID) - expect(resultSub.revisions).toHaveLength(1) - - const revision = resultSub.revisions[0].node - - const subData = base64ToDomain(revision.formDataProto) - if (subData instanceof Error) { - throw subData - } - - // When not using tables, the protobuf ID is used to as the HPP id when inserting a new HPP in the tables. - // So HPP id and proto id are the same. - // Now that our form data is in postgres contract revision table, the ids are not the same. So this expect is - // removed when flag is on. - expect(subData.id).toEqual(createdID) - expect(subData.programIDs).toEqual([ - '5c10fe9f-bec9-416f-a20c-718b152ad633', - ]) - expect(subData.submissionDescription).toBe('An updated submission') - expect(subData.documents).toEqual([]) - expect(subData.contractDocuments).toEqual([ - { - name: 'contractDocument.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT'], - }, - ]) - }) +describe(`fetchHealthPlanPackage`, () => { + const testUserCMS = testCMSUser() - it('returns error if the ID doesnt exist', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const testUserState = testStateUser() - // then see if we can fetch that same submission - const input = { - pkgID: 'BOGUS-ID', - } + it('returns package with one revision', async () => { + const server = await constructTestPostgresServer() - const result = await server.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) + // First, create a new submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(server) - expect(result.errors).toBeDefined() - if (result.errors === undefined) { - throw new Error('annoying jest typing behavior') - } - expect(result.errors).toHaveLength(1) - const resultErr = result.errors[0] + const createdID = stateSubmission.id - const contractText = `Issue finding a contract with history with id ${input.pkgID}. Message: PRISMA ERROR: Cannot find contract with id: BOGUS-ID` - const pkgText = `Issue finding a package with id ${input.pkgID}. Message: Result was undefined.` + // then see if we can fetch that same submission + const input = { + pkgID: createdID, + } - const testString = flagValue ? contractText : pkgText + const result = await server.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, + }) - expect(resultErr?.message).toBe(testString) - expect(resultErr?.extensions?.code).toBe('NOT_FOUND') + expect(result.errors).toBeUndefined() + + const resultSub = result.data?.fetchHealthPlanPackage.pkg + expect(resultSub.id).toEqual(createdID) + expect(resultSub.revisions).toHaveLength(1) + + const revision = resultSub.revisions[0].node + + const subData = base64ToDomain(revision.formDataProto) + if (subData instanceof Error) { + throw subData + } + + // When not using tables, the protobuf ID is used to as the HPP id when inserting a new HPP in the tables. + // So HPP id and proto id are the same. + // Now that our form data is in postgres contract revision table, the ids are not the same. So this expect is + // removed when flag is on. + expect(subData.id).toEqual(createdID) + expect(subData.programIDs).toEqual([ + '5c10fe9f-bec9-416f-a20c-718b152ad633', + ]) + expect(subData.submissionDescription).toBe('An updated submission') + expect(subData.documents).toEqual([]) + expect(subData.contractDocuments).toEqual([ + { + name: 'contractDocument.pdf', + s3URL: 'fakeS3URL', + sha256: 'fakesha', + }, + ]) + }) + + it('returns error if the ID doesnt exist', async () => { + const server = await constructTestPostgresServer() + + // then see if we can fetch that same submission + const input = { + pkgID: 'BOGUS-ID', + } + + const result = await server.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, }) - it('returns multiple submissions payload with multiple revisions', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: testUserCMS, - }, - ldService: mockLDService, - }) - - // First, create a new submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - server - ) - const createdID = stateSubmission.id + expect(result.errors).toBeDefined() + if (result.errors === undefined) { + throw new Error('annoying jest typing behavior') + } + expect(result.errors).toHaveLength(1) + const resultErr = result.errors[0] - // unlock it - await unlockTestHealthPlanPackage( - cmsServer, - createdID, - 'Super duper good reason.' - ) + const testString = `Issue finding a contract with history with id ${input.pkgID}. Message: PRISMA ERROR: Cannot find contract with id: BOGUS-ID` - // then see if we can fetch that same submission - const input = { - pkgID: createdID, - } + expect(resultErr?.message).toBe(testString) + expect(resultErr?.extensions?.code).toBe('NOT_FOUND') + }) - const result = await server.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) + it('returns multiple submissions payload with multiple revisions', async () => { + const server = await constructTestPostgresServer() - expect(result.errors).toBeUndefined() + const cmsServer = await constructTestPostgresServer({ + context: { + user: testUserCMS, + }, + }) - const resultSub = result.data?.fetchHealthPlanPackage.pkg - expect(resultSub.id).toEqual(createdID) - expect(resultSub.revisions).toHaveLength(2) + // First, create a new submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(server) + const createdID = stateSubmission.id + + // unlock it + await unlockTestHealthPlanPackage( + cmsServer, + createdID, + 'Super duper good reason.' + ) + + // then see if we can fetch that same submission + const input = { + pkgID: createdID, + } + + const result = await server.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, }) - it('synthesizes the right statuses as a submission is submitted/unlocked/etc', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: testUserCMS, - }, - ldService: mockLDService, - }) - - // First, create a new submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - server - ) - const createdID = stateSubmission.id + expect(result.errors).toBeUndefined() - // DRAFT - const fetchInput = { - pkgID: createdID, - } + const resultSub = result.data?.fetchHealthPlanPackage.pkg + expect(resultSub.id).toEqual(createdID) + expect(resultSub.revisions).toHaveLength(2) + }) - const draftResult = await server.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input: fetchInput }, - }) + it('synthesizes the right statuses as a submission is submitted/unlocked/etc', async () => { + const server = await constructTestPostgresServer() - expect(draftResult.errors).toBeUndefined() + const cmsServer = await constructTestPostgresServer({ + context: { + user: testUserCMS, + }, + }) - const resultSub = draftResult.data?.fetchHealthPlanPackage.pkg + // First, create a new submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(server) + const createdID = stateSubmission.id - const today = todaysDate() + // DRAFT + const fetchInput = { + pkgID: createdID, + } - expect(resultSub.status).toBe('SUBMITTED') - expect(resultSub.initiallySubmittedAt).toEqual(today) + const draftResult = await server.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input: fetchInput }, + }) - // unlock it - await unlockTestHealthPlanPackage( - cmsServer, - createdID, - 'Super duper good reason.' - ) + expect(draftResult.errors).toBeUndefined() - const unlockResult = await server.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input: fetchInput }, - }) + const resultSub = draftResult.data?.fetchHealthPlanPackage.pkg - expect(unlockResult.errors).toBeUndefined() + const today = todaysDate() - expect(unlockResult.data?.fetchHealthPlanPackage.pkg.status).toBe( - 'UNLOCKED' - ) - expect( - unlockResult.data?.fetchHealthPlanPackage.pkg - .initiallySubmittedAt - ).toEqual(today) - - // resubmit it - await resubmitTestHealthPlanPackage( - server, - createdID, - 'Test resubmission reason' - ) + expect(resultSub.status).toBe('SUBMITTED') + expect(resultSub.initiallySubmittedAt).toEqual(today) - const resubmitResult = await server.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input: fetchInput }, - }) + // unlock it + await unlockTestHealthPlanPackage( + cmsServer, + createdID, + 'Super duper good reason.' + ) - expect(resubmitResult.errors).toBeUndefined() + const unlockResult = await server.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input: fetchInput }, + }) - expect(resubmitResult.data?.fetchHealthPlanPackage.pkg.status).toBe( - 'RESUBMITTED' - ) - expect( - resubmitResult.data?.fetchHealthPlanPackage.pkg - .initiallySubmittedAt - ).toEqual(today) + expect(unlockResult.errors).toBeUndefined() + + expect(unlockResult.data?.fetchHealthPlanPackage.pkg.status).toBe( + 'UNLOCKED' + ) + expect( + unlockResult.data?.fetchHealthPlanPackage.pkg.initiallySubmittedAt + ).toEqual(today) + + // resubmit it + await resubmitTestHealthPlanPackage( + server, + createdID, + 'Test resubmission reason' + ) + + const resubmitResult = await server.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input: fetchInput }, }) - it('a different user from the same state can fetch the draft', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + expect(resubmitResult.errors).toBeUndefined() - // First, create a new submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - server - ) + expect(resubmitResult.data?.fetchHealthPlanPackage.pkg.status).toBe( + 'RESUBMITTED' + ) + expect( + resubmitResult.data?.fetchHealthPlanPackage.pkg.initiallySubmittedAt + ).toEqual(today) + }) - const createdID = stateSubmission.id + it('a different user from the same state can fetch the draft', async () => { + const server = await constructTestPostgresServer() - // then see if we can fetch that same submission - const input = { - pkgID: createdID, - } + // First, create a new submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(server) - // setup a server with a different user - const otherUserServer = await constructTestPostgresServer({ - context: { - user: testUserState, - }, - ldService: mockLDService, - }) + const createdID = stateSubmission.id - const result = await otherUserServer.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) + // then see if we can fetch that same submission + const input = { + pkgID: createdID, + } - expect(result.errors).toBeUndefined() + // setup a server with a different user + const otherUserServer = await constructTestPostgresServer({ + context: { + user: testUserState, + }, + }) - expect(result.data?.fetchHealthPlanPackage.pkg).toBeDefined() - expect(result.data?.fetchHealthPlanPackage.pkg).not.toBeNull() + const result = await otherUserServer.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, }) - it('returns an error if you are requesting for a different state (403)', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + expect(result.errors).toBeUndefined() - // First, create a new submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - server - ) + expect(result.data?.fetchHealthPlanPackage.pkg).toBeDefined() + expect(result.data?.fetchHealthPlanPackage.pkg).not.toBeNull() + }) - const createdID = stateSubmission.id - - // then see if we can fetch that same submission - const input = { - pkgID: createdID, - } - - // setup a server with a different user - const otherUserServer = await constructTestPostgresServer({ - context: { - user: testStateUser({ - stateCode: 'VA', - email: 'aang@va.gov', - }), - }, - ldService: mockLDService, - }) - - const result = await otherUserServer.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) - - expect(result.errors).toBeDefined() - if (result.errors === undefined) { - throw new Error('annoying jest typing behavior') - } - expect(result.errors).toHaveLength(1) - const resultErr = result.errors[0] - - expect(resultErr?.message).toBe( - 'user not authorized to fetch data from a different state' - ) - expect(resultErr?.extensions?.code).toBe('FORBIDDEN') + it('returns an error if you are requesting for a different state (403)', async () => { + const server = await constructTestPostgresServer() + + // First, create a new submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(server) + + const createdID = stateSubmission.id + + // then see if we can fetch that same submission + const input = { + pkgID: createdID, + } + + // setup a server with a different user + const otherUserServer = await constructTestPostgresServer({ + context: { + user: testStateUser({ + stateCode: 'VA', + email: 'aang@va.gov', + }), + }, }) - it('returns an error if you are a CMS user requesting a draft submission', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const cmsServer = await constructTestPostgresServer({ - context: { - user: testUserCMS, - }, - ldService: mockLDService, - }) - - // First, create a new submission - const stateSubmission = await createTestHealthPlanPackage(server) - - const createdID = stateSubmission.id - - // then see if we can fetch that same submission - const input = { - pkgID: createdID, - } - - const result = await cmsServer.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) - - expect(result.errors).toBeDefined() - if (result.errors === undefined) { - throw new Error('annoying jest typing behavior') - } - expect(result.errors).toHaveLength(1) - const resultErr = result.errors[0] - - expect(resultErr?.message).toBe( - 'user not authorized to fetch a draft' - ) - expect(resultErr?.extensions?.code).toBe('FORBIDDEN') + const result = await otherUserServer.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, }) - it('returns the revisions in the correct order', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + expect(result.errors).toBeDefined() + if (result.errors === undefined) { + throw new Error('annoying jest typing behavior') + } + expect(result.errors).toHaveLength(1) + const resultErr = result.errors[0] + + expect(resultErr?.message).toBe( + 'user not authorized to fetch data from a different state' + ) + expect(resultErr?.extensions?.code).toBe('FORBIDDEN') + }) + + it('returns an error if you are a CMS user requesting a draft submission', async () => { + const server = await constructTestPostgresServer() + const cmsServer = await constructTestPostgresServer({ + context: { + user: testUserCMS, + }, + }) - // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + // First, create a new submission + const stateSubmission = await createTestHealthPlanPackage(server) - const cmsServer = await constructTestPostgresServer({ - context: { - user: testUserCMS, - }, - ldService: mockLDService, - }) - - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Super duper good reason.' - ) + const createdID = stateSubmission.id - await resubmitTestHealthPlanPackage( - stateServer, - stateSubmission.id, - 'Test first resubmission' - ) + // then see if we can fetch that same submission + const input = { + pkgID: createdID, + } - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Super duper good reason.' - ) + const result = await cmsServer.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, + }) - await resubmitTestHealthPlanPackage( - stateServer, - stateSubmission.id, - 'Test second resubmission' - ) + expect(result.errors).toBeDefined() + if (result.errors === undefined) { + throw new Error('annoying jest typing behavior') + } + expect(result.errors).toHaveLength(1) + const resultErr = result.errors[0] - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Super duper good reason.' - ) + expect(resultErr?.message).toBe('user not authorized to fetch a draft') + expect(resultErr?.extensions?.code).toBe('FORBIDDEN') + }) + + it('returns the revisions in the correct order', async () => { + const stateServer = await constructTestPostgresServer() + + // First, create a new submitted submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) - const input = { - pkgID: stateSubmission.id, - } - - const result = await cmsServer.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) - - expect(result.errors).toBeUndefined() - - const maxDate = new Date(8640000000000000) - let mostRecentDate = maxDate - const revs = result?.data?.fetchHealthPlanPackage.pkg.revisions - if (!revs) { - throw new Error('No revisions returned!') - } - for (const rev of revs) { - expect(rev.node.createdAt.getTime()).toBeLessThanOrEqual( - mostRecentDate.getTime() - ) - mostRecentDate = rev.node.createdAt - } + const cmsServer = await constructTestPostgresServer({ + context: { + user: testUserCMS, + }, }) - it('returns package with one revision again', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Super duper good reason.' + ) + + await resubmitTestHealthPlanPackage( + stateServer, + stateSubmission.id, + 'Test first resubmission' + ) + + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Super duper good reason.' + ) + + await resubmitTestHealthPlanPackage( + stateServer, + stateSubmission.id, + 'Test second resubmission' + ) + + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Super duper good reason.' + ) + + const input = { + pkgID: stateSubmission.id, + } + + const result = await cmsServer.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, + }) - // First, create a new submission - const stateSubmission = await createTestHealthPlanPackage(server) + expect(result.errors).toBeUndefined() + + const maxDate = new Date(8640000000000000) + let mostRecentDate = maxDate + const revs = result?.data?.fetchHealthPlanPackage.pkg.revisions + if (!revs) { + throw new Error('No revisions returned!') + } + for (const rev of revs) { + expect(rev.node.createdAt.getTime()).toBeLessThanOrEqual( + mostRecentDate.getTime() + ) + mostRecentDate = rev.node.createdAt + } + }) - const createdID = stateSubmission.id + it('returns package with one revision again', async () => { + const server = await constructTestPostgresServer() - // then see if we can fetch that same submission - const input = { - pkgID: createdID, - } + // First, create a new submission + const stateSubmission = await createTestHealthPlanPackage(server) - const result = await server.executeOperation({ - query: FETCH_HEALTH_PLAN_PACKAGE, - variables: { input }, - }) + const createdID = stateSubmission.id - expect(result.errors).toBeUndefined() + // then see if we can fetch that same submission + const input = { + pkgID: createdID, + } - const resultSub = result.data?.fetchHealthPlanPackage.pkg - expect(resultSub.id).toEqual(createdID) - expect(resultSub.revisions).toHaveLength(1) + const result = await server.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, + }) - const revision = resultSub.revisions[0].node + expect(result.errors).toBeUndefined() - const subData = base64ToDomain(revision.formDataProto) - if (subData instanceof Error) { - throw subData - } + const resultSub = result.data?.fetchHealthPlanPackage.pkg + expect(resultSub.id).toEqual(createdID) + expect(resultSub.revisions).toHaveLength(1) - // Expect the created revision and the fetchHPP revision are the same. - expect(subData.id).toEqual(stateSubmission.id) + const revision = resultSub.revisions[0].node - expect(subData.programIDs).toEqual([ - '5c10fe9f-bec9-416f-a20c-718b152ad633', - ]) - expect(subData.submissionDescription).toBe('A created submission') - expect(subData.documents).toEqual([]) - }) - } -) + const subData = base64ToDomain(revision.formDataProto) + if (subData instanceof Error) { + throw subData + } + + // Expect the created revision and the fetchHPP revision are the same. + expect(subData.id).toEqual(stateSubmission.id) + + expect(subData.programIDs).toEqual([ + '5c10fe9f-bec9-416f-a20c-718b152ad633', + ]) + expect(subData.submissionDescription).toBe('A created submission') + expect(subData.documents).toEqual([]) + }) +}) diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index c9ee1b4446..4f02fe477b 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -1,5 +1,4 @@ import { ForbiddenError } from 'apollo-server-lambda' -import type { HealthPlanPackageType } from '../../domain-models' import { isCMSUser, isStateUser, @@ -12,101 +11,65 @@ import { isHelpdeskUser } from '../../domain-models/user' import type { QueryResolvers, State } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' import type { Store } from '../../postgres' -import { isStoreError } from '../../postgres' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, setSuccessAttributesOnActiveSpan, } from '../attributeHelper' -import type { LDService } from '../../launchDarkly/launchDarkly' import { GraphQLError } from 'graphql/index' import { NotFoundError } from '../../postgres' export function fetchHealthPlanPackageResolver( - store: Store, - launchDarkly: LDService + store: Store ): QueryResolvers['fetchHealthPlanPackage'] { return async (_parent, { input }, context) => { const { user, span } = context setResolverDetailsOnActiveSpan('fetchHealthPlanPackage', user, span) - const ratesDatabaseRefactor = await launchDarkly.getFeatureFlag( - context, - 'rates-db-refactor' + // Fetch the full contract + const contractWithHistory = await store.findContractWithHistory( + input.pkgID ) - let pkg: HealthPlanPackageType - - // Here is where we flag finding health plan - if (ratesDatabaseRefactor) { - // Fetch the full contract - const contractWithHistory = await store.findContractWithHistory( - input.pkgID - ) - - if (contractWithHistory instanceof Error) { - const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - - if (contractWithHistory instanceof NotFoundError) { - throw new GraphQLError(errMessage, { - extensions: { - code: 'NOT_FOUND', - cause: 'DB_ERROR', - }, - }) - } + if (contractWithHistory instanceof Error) { + const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + if (contractWithHistory instanceof NotFoundError) { throw new GraphQLError(errMessage, { extensions: { - code: 'INTERNAL_SERVER_ERROR', + code: 'NOT_FOUND', cause: 'DB_ERROR', }, }) } - const convertedPkg = - convertContractWithRatesToUnlockedHPP(contractWithHistory) - - if (convertedPkg instanceof Error) { - const errMessage = `Issue converting contract. Message: ${convertedPkg.message}` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } - - pkg = convertedPkg - } else { - const result = await store.findHealthPlanPackage(input.pkgID) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - if (isStoreError(result)) { - const errMessage = `Issue finding a package of type ${result.code}. Message: ${result.message}` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new Error(errMessage) - } + const convertedPkg = + convertContractWithRatesToUnlockedHPP(contractWithHistory) - if (result === undefined) { - const errMessage = `Issue finding a package with id ${input.pkgID}. Message: Result was undefined.` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'NOT_FOUND', - cause: 'DB_ERROR', - }, - }) - } - - pkg = result + if (convertedPkg instanceof Error) { + const errMessage = `Issue converting contract. Message: ${convertedPkg.message}` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) } + const pkg = convertedPkg + // Authorization CMS users can view, state users can only view if the state matches if (isStateUser(user)) { const stateFromCurrentUser: State['code'] = user.stateCode diff --git a/services/app-api/src/resolvers/healthPlanPackage/indexHealthPlanPackages.test.ts b/services/app-api/src/resolvers/healthPlanPackage/indexHealthPlanPackages.test.ts index 14bf88f6ea..acd1dbf6b9 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/indexHealthPlanPackages.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/indexHealthPlanPackages.test.ts @@ -21,420 +21,356 @@ import { sharedTestPrismaClient } from '../../testHelpers/storeHelpers' import type { PrismaTransactionType } from '../../postgres/prismaTypes' import { createContractData, createDraftContractData } from '../../testHelpers' import { parseContractWithHistory } from '../../postgres/contractAndRates/parseContractWithHistory' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' import type { ContractOrErrorArrayType } from '../../postgres/contractAndRates/findAllContractsWithHistoryByState' -import type { - FeatureFlagLDConstant, - FlagValue, -} from '../../../../app-web/src/common-code/featureFlags' - -const flagValueTestParameters: { - flagName: FeatureFlagLDConstant - flagValue: FlagValue - testName: string -}[] = [ - { - flagName: 'rates-db-refactor', - flagValue: false, - testName: 'indexHealthPlanPackages with all feature flags off', - }, - { - flagName: 'rates-db-refactor', - flagValue: true, - testName: 'indexHealthPlanPackages with rates-db-refactor on', - }, -] - -describe.each(flagValueTestParameters)( - `indexHealthPlanPackages $testName`, - ({ flagName, flagValue }) => { - const mockLDService = testLDService({ [flagName]: flagValue }) - const cmsUser = testCMSUser() - describe('isStateUser', () => { - it('returns a list of submissions that includes newly created entries', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - // First, create a new submission - const draftPkg = await createTestHealthPlanPackage(server) - const draftFormData = latestFormData(draftPkg) - - const submittedPkg = await createAndSubmitTestHealthPlanPackage( - server - ) - const submittedFormData = latestFormData(submittedPkg) - - // then see if we can get that same submission back from the index - const result = await server.executeOperation({ - query: INDEX_HEALTH_PLAN_PACKAGES, - }) - - expect(result.errors).toBeUndefined() - - const submissionsIndex = result.data?.indexHealthPlanPackages - - expect(submissionsIndex.totalCount).toBeGreaterThan(1) - - // Since we don't wipe the DB between tests,filter out extraneous submissions and grab new submissions by ID to confirm they are returned - const theseSubmissions: HealthPlanPackage[] = - submissionsIndex.edges - .map((edge: HealthPlanPackageEdge) => edge.node) - .filter((sub: HealthPlanPackage) => - [draftPkg.id, submittedPkg.id].includes(sub.id) - ) - // specific submissions by id exist - expect(theseSubmissions).toHaveLength(2) - - // confirm some submission data is correct too, first in list will be draft, second is the submitted - expect(theseSubmissions[0].initiallySubmittedAt).toBeNull() - expect(theseSubmissions[0].status).toBe('DRAFT') - expect( - latestFormData(theseSubmissions[0]).submissionDescription - ).toBe(draftFormData.submissionDescription) - expect(theseSubmissions[1].initiallySubmittedAt).toBe( - todaysDate() - ) - expect(theseSubmissions[1].status).toBe('SUBMITTED') - expect( - latestFormData(theseSubmissions[1]).submissionDescription - ).toBe(submittedFormData.submissionDescription) + +describe(`indexHealthPlanPackages`, () => { + const cmsUser = testCMSUser() + describe('isStateUser', () => { + it('returns a list of submissions that includes newly created entries', async () => { + const server = await constructTestPostgresServer() + + // First, create a new submission + const draftPkg = await createTestHealthPlanPackage(server) + const draftFormData = latestFormData(draftPkg) + + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(server) + const submittedFormData = latestFormData(submittedPkg) + + // then see if we can get that same submission back from the index + const result = await server.executeOperation({ + query: INDEX_HEALTH_PLAN_PACKAGES, }) - it('synthesizes the right statuses as a submission is submitted/unlocked/etc', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) - - // First, create new submissions - const draftSubmission = await createTestHealthPlanPackage( - server - ) - const submittedSubmission = - await createAndSubmitTestHealthPlanPackage(server) - const unlockedSubmission = - await createAndSubmitTestHealthPlanPackage(server) - const relockedSubmission = - await createAndSubmitTestHealthPlanPackage(server) - - // unlock two - await unlockTestHealthPlanPackage( - cmsServer, - unlockedSubmission.id, - 'Test reason' - ) - await unlockTestHealthPlanPackage( - cmsServer, - relockedSubmission.id, - 'Test reason' + expect(result.errors).toBeUndefined() + + const submissionsIndex = result.data?.indexHealthPlanPackages + + expect(submissionsIndex.totalCount).toBeGreaterThan(1) + + // Since we don't wipe the DB between tests,filter out extraneous submissions and grab new submissions by ID to confirm they are returned + const theseSubmissions: HealthPlanPackage[] = submissionsIndex.edges + .map((edge: HealthPlanPackageEdge) => edge.node) + .filter((sub: HealthPlanPackage) => + [draftPkg.id, submittedPkg.id].includes(sub.id) ) + // specific submissions by id exist + expect(theseSubmissions).toHaveLength(2) + + // confirm some submission data is correct too, first in list will be draft, second is the submitted + expect(theseSubmissions[0].initiallySubmittedAt).toBeNull() + expect(theseSubmissions[0].status).toBe('DRAFT') + expect( + latestFormData(theseSubmissions[0]).submissionDescription + ).toBe(draftFormData.submissionDescription) + expect(theseSubmissions[1].initiallySubmittedAt).toBe(todaysDate()) + expect(theseSubmissions[1].status).toBe('SUBMITTED') + expect( + latestFormData(theseSubmissions[1]).submissionDescription + ).toBe(submittedFormData.submissionDescription) + }) + + it('synthesizes the right statuses as a submission is submitted/unlocked/etc', async () => { + const server = await constructTestPostgresServer() + + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) - // resubmit one - await resubmitTestHealthPlanPackage( - server, - relockedSubmission.id, - 'Test first resubmission' + // First, create new submissions + const draftSubmission = await createTestHealthPlanPackage(server) + const submittedSubmission = + await createAndSubmitTestHealthPlanPackage(server) + const unlockedSubmission = + await createAndSubmitTestHealthPlanPackage(server) + const relockedSubmission = + await createAndSubmitTestHealthPlanPackage(server) + + // unlock two + await unlockTestHealthPlanPackage( + cmsServer, + unlockedSubmission.id, + 'Test reason' + ) + await unlockTestHealthPlanPackage( + cmsServer, + relockedSubmission.id, + 'Test reason' + ) + + // resubmit one + await resubmitTestHealthPlanPackage( + server, + relockedSubmission.id, + 'Test first resubmission' + ) + + // index submissions api request + const result = await server.executeOperation({ + query: INDEX_HEALTH_PLAN_PACKAGES, + }) + const submissionsIndex = result.data?.indexHealthPlanPackages + + // pull out test related submissions and order them + const testSubmissionIDs = [ + draftSubmission.id, + submittedSubmission.id, + unlockedSubmission.id, + relockedSubmission.id, + ] + const testSubmissions: HealthPlanPackage[] = submissionsIndex.edges + .map((edge: HealthPlanPackageEdge) => edge.node) + .filter((test: HealthPlanPackage) => + testSubmissionIDs.includes(test.id) ) - // index submissions api request - const result = await server.executeOperation({ - query: INDEX_HEALTH_PLAN_PACKAGES, - }) - const submissionsIndex = result.data?.indexHealthPlanPackages - - // pull out test related submissions and order them - const testSubmissionIDs = [ - draftSubmission.id, - submittedSubmission.id, - unlockedSubmission.id, - relockedSubmission.id, - ] - const testSubmissions: HealthPlanPackage[] = - submissionsIndex.edges - .map((edge: HealthPlanPackageEdge) => edge.node) - .filter((test: HealthPlanPackage) => - testSubmissionIDs.includes(test.id) - ) - - expect(testSubmissions).toHaveLength(4) - - // organize test submissions in a predictable order via testSubmissionsIds array - testSubmissions.sort((a, b) => { - if ( - testSubmissionIDs.indexOf(a.id) > - testSubmissionIDs.indexOf(b.id) - ) { - return 1 - } else { - return -1 - } - }) - - expect(testSubmissions[0].status).toBe('DRAFT') - expect(testSubmissions[0].status).toBe('DRAFT') - expect(testSubmissions[0].status).toBe('DRAFT') - expect(testSubmissions[0].status).toBe('DRAFT') + expect(testSubmissions).toHaveLength(4) + + // organize test submissions in a predictable order via testSubmissionsIds array + testSubmissions.sort((a, b) => { + if ( + testSubmissionIDs.indexOf(a.id) > + testSubmissionIDs.indexOf(b.id) + ) { + return 1 + } else { + return -1 + } }) - it('a different user from the same state can index submissions', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + expect(testSubmissions[0].status).toBe('DRAFT') + expect(testSubmissions[0].status).toBe('DRAFT') + expect(testSubmissions[0].status).toBe('DRAFT') + expect(testSubmissions[0].status).toBe('DRAFT') + }) + + it('a different user from the same state can index submissions', async () => { + const server = await constructTestPostgresServer() - // First, create a new submission - const stateSubmission = - await createAndSubmitTestHealthPlanPackage(server) + // First, create a new submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(server) - const createdID = stateSubmission.id + const createdID = stateSubmission.id - // then see if we can fetch that same submission - const input = { - pkgID: createdID, - } + // then see if we can fetch that same submission + const input = { + pkgID: createdID, + } - // setup a server with a different user - const otherUserServer = await constructTestPostgresServer({ - context: { - user: testStateUser(), - }, - ldService: mockLDService, - }) - - const result = await otherUserServer.executeOperation({ - query: INDEX_HEALTH_PLAN_PACKAGES, - variables: { input }, - }) - - expect(result.errors).toBeUndefined() - const submissions = - result.data?.indexHealthPlanPackages.edges.map( - (edge: HealthPlanPackageEdge) => edge.node - ) - expect(submissions).not.toBeNull() - expect(submissions.length).toBeGreaterThan(1) - - const testSubmission = submissions.filter( - (test: HealthPlanPackage) => test.id === createdID - )[0] - expect(testSubmission.initiallySubmittedAt).toBe(todaysDate()) + // setup a server with a different user + const otherUserServer = await constructTestPostgresServer({ + context: { + user: testStateUser(), + }, }) - it('returns no submissions for a different states user', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const result = await otherUserServer.executeOperation({ + query: INDEX_HEALTH_PLAN_PACKAGES, + variables: { input }, + }) - await createTestHealthPlanPackage(server) - await createAndSubmitTestHealthPlanPackage(server) + expect(result.errors).toBeUndefined() + const submissions = result.data?.indexHealthPlanPackages.edges.map( + (edge: HealthPlanPackageEdge) => edge.node + ) + expect(submissions).not.toBeNull() + expect(submissions.length).toBeGreaterThan(1) + + const testSubmission = submissions.filter( + (test: HealthPlanPackage) => test.id === createdID + )[0] + expect(testSubmission.initiallySubmittedAt).toBe(todaysDate()) + }) - const otherUserServer = await constructTestPostgresServer({ - context: { - user: testStateUser({ - stateCode: 'VA', - }), - }, - ldService: mockLDService, - }) - - const result = await otherUserServer.executeOperation({ - query: INDEX_HEALTH_PLAN_PACKAGES, - }) - - expect(result.errors).toBeUndefined() - - const indexHealthPlanPackages = - result.data?.indexHealthPlanPackages - const otherStatePackages = indexHealthPlanPackages.edges.filter( - (pkg: HealthPlanPackageEdge) => pkg.node.stateCode !== 'VA' - ) + it('returns no submissions for a different states user', async () => { + const server = await constructTestPostgresServer() + + await createTestHealthPlanPackage(server) + await createAndSubmitTestHealthPlanPackage(server) + + const otherUserServer = await constructTestPostgresServer({ + context: { + user: testStateUser({ + stateCode: 'VA', + }), + }, + }) - expect(otherStatePackages).toEqual([]) + const result = await otherUserServer.executeOperation({ + query: INDEX_HEALTH_PLAN_PACKAGES, }) + + expect(result.errors).toBeUndefined() + + const indexHealthPlanPackages = result.data?.indexHealthPlanPackages + const otherStatePackages = indexHealthPlanPackages.edges.filter( + (pkg: HealthPlanPackageEdge) => pkg.node.stateCode !== 'VA' + ) + + expect(otherStatePackages).toEqual([]) }) + }) - describe('isCMSUser', () => { - it('returns an empty list if only draft packages exist', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) - // First, create new submissions - const draft1 = await createTestHealthPlanPackage(stateServer) - const draft2 = await createTestHealthPlanPackage(stateServer) - - // index submissions api request - const result = await cmsServer.executeOperation({ - query: INDEX_HEALTH_PLAN_PACKAGES, - }) - const submissionsIndex = result.data?.indexHealthPlanPackages - - // pull out test related submissions and order them - const testSubmissionIDs = [draft1.id, draft2.id] - const testSubmissions: HealthPlanPackage[] = - submissionsIndex.edges - .map((edge: HealthPlanPackageEdge) => edge.node) - .filter((test: HealthPlanPackage) => - testSubmissionIDs.includes(test.id) - ) - - expect(testSubmissions).toHaveLength(0) + describe('isCMSUser', () => { + it('returns an empty list if only draft packages exist', async () => { + const stateServer = await constructTestPostgresServer() + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, }) + // First, create new submissions + const draft1 = await createTestHealthPlanPackage(stateServer) + const draft2 = await createTestHealthPlanPackage(stateServer) - it('synthesizes the right statuses as a submission is submitted/unlocked/etc', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) - - // First, create new submissions - const submittedSubmission = - await createAndSubmitTestHealthPlanPackage(server) - const unlockedSubmission = - await createAndSubmitTestHealthPlanPackage(server) - const relockedSubmission = - await createAndSubmitTestHealthPlanPackage(server) - - // unlock two - await unlockTestHealthPlanPackage( - cmsServer, - unlockedSubmission.id, - 'Test reason' - ) - await unlockTestHealthPlanPackage( - cmsServer, - relockedSubmission.id, - 'Test reason' + // index submissions api request + const result = await cmsServer.executeOperation({ + query: INDEX_HEALTH_PLAN_PACKAGES, + }) + const submissionsIndex = result.data?.indexHealthPlanPackages + + // pull out test related submissions and order them + const testSubmissionIDs = [draft1.id, draft2.id] + const testSubmissions: HealthPlanPackage[] = submissionsIndex.edges + .map((edge: HealthPlanPackageEdge) => edge.node) + .filter((test: HealthPlanPackage) => + testSubmissionIDs.includes(test.id) ) - // resubmit one - await resubmitTestHealthPlanPackage( - server, - relockedSubmission.id, - 'Test first resubmission' - ) + expect(testSubmissions).toHaveLength(0) + }) + + it('synthesizes the right statuses as a submission is submitted/unlocked/etc', async () => { + const server = await constructTestPostgresServer() + + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) + + // First, create new submissions + const submittedSubmission = + await createAndSubmitTestHealthPlanPackage(server) + const unlockedSubmission = + await createAndSubmitTestHealthPlanPackage(server) + const relockedSubmission = + await createAndSubmitTestHealthPlanPackage(server) - // index submissions api request - const result = await cmsServer.executeOperation({ - query: INDEX_HEALTH_PLAN_PACKAGES, - }) - const submissionsIndex = result.data?.indexHealthPlanPackages - - // pull out test related submissions and order them - const testSubmissionIDs = [ - submittedSubmission.id, - unlockedSubmission.id, - relockedSubmission.id, - ] - const testSubmissions: HealthPlanPackage[] = - submissionsIndex.edges - .map((edge: HealthPlanPackageEdge) => edge.node) - .filter((test: HealthPlanPackage) => - testSubmissionIDs.includes(test.id) - ) - - expect(testSubmissions).toHaveLength(3) - - // organize test submissions in a predictable order via testSubmissionsIds array - testSubmissions.sort((a, b) => { - if ( - testSubmissionIDs.indexOf(a.id) > - testSubmissionIDs.indexOf(b.id) - ) { - return 1 - } else { - return -1 - } - }) + // unlock two + await unlockTestHealthPlanPackage( + cmsServer, + unlockedSubmission.id, + 'Test reason' + ) + await unlockTestHealthPlanPackage( + cmsServer, + relockedSubmission.id, + 'Test reason' + ) + + // resubmit one + await resubmitTestHealthPlanPackage( + server, + relockedSubmission.id, + 'Test first resubmission' + ) + + // index submissions api request + const result = await cmsServer.executeOperation({ + query: INDEX_HEALTH_PLAN_PACKAGES, }) + const submissionsIndex = result.data?.indexHealthPlanPackages - it('return a list of submitted packages from multiple states', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) - const otherStateServer = await constructTestPostgresServer({ - context: { - user: testStateUser({ - stateCode: 'VA', - email: 'aang@mn.gov', - }), - }, - ldService: mockLDService, - }) - // submit packages from two different states - const defaultState1 = - await createAndSubmitTestHealthPlanPackage(stateServer) - const defaultState2 = - await createAndSubmitTestHealthPlanPackage(stateServer) - const draft = await createAndUpdateTestHealthPlanPackage( - otherStateServer, - undefined, - 'VA' as const + // pull out test related submissions and order them + const testSubmissionIDs = [ + submittedSubmission.id, + unlockedSubmission.id, + relockedSubmission.id, + ] + const testSubmissions: HealthPlanPackage[] = submissionsIndex.edges + .map((edge: HealthPlanPackageEdge) => edge.node) + .filter((test: HealthPlanPackage) => + testSubmissionIDs.includes(test.id) ) - const otherState1 = await submitTestHealthPlanPackage( - otherStateServer, - draft.id + + expect(testSubmissions).toHaveLength(3) + + // organize test submissions in a predictable order via testSubmissionsIds array + testSubmissions.sort((a, b) => { + if ( + testSubmissionIDs.indexOf(a.id) > + testSubmissionIDs.indexOf(b.id) + ) { + return 1 + } else { + return -1 + } + }) + }) + + it('return a list of submitted packages from multiple states', async () => { + const stateServer = await constructTestPostgresServer() + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) + const otherStateServer = await constructTestPostgresServer({ + context: { + user: testStateUser({ + stateCode: 'VA', + email: 'aang@mn.gov', + }), + }, + }) + // submit packages from two different states + const defaultState1 = + await createAndSubmitTestHealthPlanPackage(stateServer) + const defaultState2 = + await createAndSubmitTestHealthPlanPackage(stateServer) + const draft = await createAndUpdateTestHealthPlanPackage( + otherStateServer, + undefined, + 'VA' as const + ) + const otherState1 = await submitTestHealthPlanPackage( + otherStateServer, + draft.id + ) + + const result = await cmsServer.executeOperation({ + query: INDEX_HEALTH_PLAN_PACKAGES, + }) + + expect(result.errors).toBeUndefined() + + const allHealthPlanPackages: HealthPlanPackage[] = + result.data?.indexHealthPlanPackages.edges.map( + (edge: HealthPlanPackageEdge) => edge.node ) - const result = await cmsServer.executeOperation({ - query: INDEX_HEALTH_PLAN_PACKAGES, - }) - - expect(result.errors).toBeUndefined() - - const allHealthPlanPackages: HealthPlanPackage[] = - result.data?.indexHealthPlanPackages.edges.map( - (edge: HealthPlanPackageEdge) => edge.node - ) - - // Pull out only the results relevant to the test by using id of recently created test packages. - const defaultStatePackages: HealthPlanPackage[] = [] - const otherStatePackages: HealthPlanPackage[] = [] - allHealthPlanPackages.forEach((pkg) => { - if ([defaultState1.id, defaultState2.id].includes(pkg.id)) { - defaultStatePackages.push(pkg) - } else if ([otherState1.id].includes(pkg.id)) { - otherStatePackages.push(pkg) - } - return - }) - - expect(defaultStatePackages).toHaveLength(2) - expect(otherStatePackages).toHaveLength(1) + // Pull out only the results relevant to the test by using id of recently created test packages. + const defaultStatePackages: HealthPlanPackage[] = [] + const otherStatePackages: HealthPlanPackage[] = [] + allHealthPlanPackages.forEach((pkg) => { + if ([defaultState1.id, defaultState2.id].includes(pkg.id)) { + defaultStatePackages.push(pkg) + } else if ([otherState1.id].includes(pkg.id)) { + otherStatePackages.push(pkg) + } + return }) + + expect(defaultStatePackages).toHaveLength(2) + expect(otherStatePackages).toHaveLength(1) }) - } -) -describe('indexHealthPlanPackages test rates-db-refactor flag on only', () => { - afterEach(() => { - jest.restoreAllMocks() }) it('correctly filters and log contracts that failed parsing or converting', async () => { - const mockFeatureFlag = testLDService({ 'rates-db-refactor': true }) const client = await sharedTestPrismaClient() const errors = jest.spyOn(global.console, 'error').mockImplementation() @@ -495,7 +431,6 @@ describe('indexHealthPlanPackages test rates-db-refactor flag on only', () => { const server = await constructTestPostgresServer({ store: mockStore, - ldService: mockFeatureFlag, context: { user: stateUser, }, diff --git a/services/app-api/src/resolvers/healthPlanPackage/indexHealthPlanPackages.ts b/services/app-api/src/resolvers/healthPlanPackage/indexHealthPlanPackages.ts index fe220799ef..b0a09de6e9 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/indexHealthPlanPackages.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/indexHealthPlanPackages.ts @@ -10,28 +10,20 @@ import { import { isHelpdeskUser } from '../../domain-models/user' import type { QueryResolvers } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' -import type { Store, StoreError } from '../../postgres' -import { isStoreError, NotFoundError } from '../../postgres' +import type { Store } from '../../postgres' +import { NotFoundError } from '../../postgres' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, setSuccessAttributesOnActiveSpan, } from '../attributeHelper' -import type { LDService } from '../../launchDarkly/launchDarkly' import { GraphQLError } from 'graphql/index' import { validateContractsAndConvert } from './contractAndRates/resolverHelpers' const validateAndReturnHealthPlanPackages = ( - results: HealthPlanPackageType[] | StoreError, + results: HealthPlanPackageType[], span?: Span ) => { - if (isStoreError(results)) { - const errMessage = `Issue indexing packages of type ${results.code}. Message: ${results.message}` - logError('indexHealthPlanPackages', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new Error(errMessage) - } - const packages: HealthPlanPackageType[] = results const edges = packages.map((sub) => { @@ -48,56 +40,41 @@ const validateAndReturnHealthPlanPackages = ( } export function indexHealthPlanPackagesResolver( - store: Store, - launchDarkly: LDService + store: Store ): QueryResolvers['indexHealthPlanPackages'] { return async (_parent, _args, context) => { const { user, span } = context setResolverDetailsOnActiveSpan('indexHealthPlanPackages', user, span) - const ratesDatabaseRefactor = await launchDarkly.getFeatureFlag( - context, - 'rates-db-refactor' - ) - if (isStateUser(user)) { - let results: StoreError | HealthPlanPackageType[] = [] - if (ratesDatabaseRefactor) { - const contractsWithHistory = - await store.findAllContractsWithHistoryByState( - user.stateCode - ) - - if (contractsWithHistory instanceof Error) { - const errMessage = `Issue finding contracts with history by stateCode: ${user.stateCode}. Message: ${contractsWithHistory.message}` - logError('indexHealthPlanPackages', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - - if (contractsWithHistory instanceof NotFoundError) { - throw new GraphQLError(errMessage, { - extensions: { - code: 'NOT_FOUND', - cause: 'DB_ERROR', - }, - }) - } + const contractsWithHistory = + await store.findAllContractsWithHistoryByState(user.stateCode) + + if (contractsWithHistory instanceof Error) { + const errMessage = `Issue finding contracts with history by stateCode: ${user.stateCode}. Message: ${contractsWithHistory.message}` + logError('indexHealthPlanPackages', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + if (contractsWithHistory instanceof NotFoundError) { throw new GraphQLError(errMessage, { extensions: { - code: 'INTERNAL_SERVER_ERROR', + code: 'NOT_FOUND', cause: 'DB_ERROR', }, }) } - results = validateContractsAndConvert(contractsWithHistory) - } else { - results = await store.findAllHealthPlanPackagesByState( - user.stateCode - ) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) } + const results = validateContractsAndConvert(contractsWithHistory) + return validateAndReturnHealthPlanPackages(results, span) } else if ( isCMSUser(user) || @@ -105,38 +82,33 @@ export function indexHealthPlanPackagesResolver( isHelpdeskUser(user) || isBusinessOwnerUser(user) ) { - let results: StoreError | HealthPlanPackageType[] = [] - if (ratesDatabaseRefactor) { - const contractsWithHistory = - await store.findAllContractsWithHistoryBySubmitInfo() - - if (contractsWithHistory instanceof Error) { - const errMessage = `Issue finding contracts with history by submit info. Message: ${contractsWithHistory.message}` - logError('indexHealthPlanPackages', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - - if (contractsWithHistory instanceof NotFoundError) { - throw new GraphQLError(errMessage, { - extensions: { - code: 'NOT_FOUND', - cause: 'DB_ERROR', - }, - }) - } + const contractsWithHistory = + await store.findAllContractsWithHistoryBySubmitInfo() + + if (contractsWithHistory instanceof Error) { + const errMessage = `Issue finding contracts with history by submit info. Message: ${contractsWithHistory.message}` + logError('indexHealthPlanPackages', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + if (contractsWithHistory instanceof NotFoundError) { throw new GraphQLError(errMessage, { extensions: { - code: 'INTERNAL_SERVER_ERROR', + code: 'NOT_FOUND', cause: 'DB_ERROR', }, }) } - results = validateContractsAndConvert(contractsWithHistory) - } else { - results = await store.findAllHealthPlanPackagesBySubmittedAt() + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) } + const results = validateContractsAndConvert(contractsWithHistory) + return validateAndReturnHealthPlanPackages(results, span) } else { const errMsg = 'user not authorized to fetch state data' diff --git a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts index 647e6c0a8f..a241974999 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.test.ts @@ -30,611 +30,555 @@ import { } from '../../testHelpers/parameterStoreHelpers' import * as awsSESHelpers from '../../testHelpers/awsSESHelpers' import { testCMSUser, testStateUser } from '../../testHelpers/userHelpers' -import type { - FeatureFlagLDConstant, - FlagValue, -} from 'app-web/src/common-code/featureFlags' import { testLDService } from '../../testHelpers/launchDarklyHelpers' -const flagValueTestParameters: { - flagName: FeatureFlagLDConstant - flagValue: FlagValue - testName: string -}[] = [ - { - flagName: 'rates-db-refactor', - flagValue: false, - testName: 'submitHealthPlanPackage with all feature flags off', - }, - { - flagName: 'rates-db-refactor', - flagValue: true, - testName: 'submitHealthPlanPackage with rates-db-refactor on', - }, -] - -describe.each(flagValueTestParameters)( - `Tests $testName`, - ({ flagName, flagValue }) => { - const cmsUser = testCMSUser() - const mockLDService = testLDService({ - [flagName]: flagValue, - }) - - afterEach(() => { - jest.restoreAllMocks() +describe(`Tests $testName`, () => { + const cmsUser = testCMSUser() + + afterEach(() => { + jest.restoreAllMocks() + }) + it('returns a StateSubmission if complete', async () => { + const server = await constructTestPostgresServer() + + // setup + const initialPkg = await createAndUpdateTestHealthPlanPackage( + server, + {} + ) + const draft = latestFormData(initialPkg) + const draftID = draft.id + + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // submit + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, }) - it('returns a StateSubmission if complete', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - // setup - const initialPkg = await createAndUpdateTestHealthPlanPackage( - server, - {} - ) - const draft = latestFormData(initialPkg) - const draftID = draft.id + expect(submitResult.errors).toBeUndefined() + const createdID = submitResult?.data?.submitHealthPlanPackage.pkg.id + + // test result + const pkg = await fetchTestHealthPlanPackageById(server, createdID) + + const resultDraft = latestFormData(pkg) + + // The submission fields should still be set + expect(resultDraft.id).toEqual(createdID) + expect(resultDraft.submissionType).toBe('CONTRACT_AND_RATES') + expect(resultDraft.programIDs).toEqual([defaultFloridaProgram().id]) + // check that the stateNumber is being returned the same + expect(resultDraft.stateNumber).toEqual(draft.stateNumber) + expect(resultDraft.submissionDescription).toBe('An updated submission') + expect(resultDraft.documents).toEqual(draft.documents) + + // Contract details fields should still be set + expect(resultDraft.contractType).toEqual(draft.contractType) + expect(resultDraft.contractExecutionStatus).toEqual( + draft.contractExecutionStatus + ) + expect(resultDraft.contractDateStart).toEqual(draft.contractDateStart) + expect(resultDraft.contractDateEnd).toEqual(draft.contractDateEnd) + expect(resultDraft.managedCareEntities).toEqual( + draft.managedCareEntities + ) + expect(resultDraft.contractDocuments).toEqual(draft.contractDocuments) + + expect(resultDraft.federalAuthorities).toEqual(draft.federalAuthorities) + + if (resultDraft.status == 'DRAFT') { + throw new Error('Not a locked submission') + } + + // submittedAt should be set to today's date + const today = new Date() + const expectedDate = today.toISOString().split('T')[0] + expect(pkg.initiallySubmittedAt).toEqual(expectedDate) + + // UpdatedAt should be after the former updatedAt + const resultUpdated = new Date(resultDraft.updatedAt) + const createdUpdated = new Date(draft.updatedAt) + expect( + resultUpdated.getTime() - createdUpdated.getTime() + ).toBeGreaterThan(0) + }, 20000) + + it('returns a state submission with the correct rate data on resubmit', async () => { + const cmsUser = testCMSUser() + const server = await constructTestPostgresServer() - await new Promise((resolve) => setTimeout(resolve, 2000)) + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) - // submit - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + const initialRateInfos = () => ({ + id: uuidv4(), + rateType: 'NEW' as const, + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: new Date(Date.UTC(2025, 3, 15)), + rateDocuments: [ + { + name: 'rateDocument.pdf', + s3URL: 'fakeS3URL', + sha256: 'fakesha', }, - }) - - expect(submitResult.errors).toBeUndefined() - const createdID = submitResult?.data?.submitHealthPlanPackage.pkg.id - - // test result - const pkg = await fetchTestHealthPlanPackageById(server, createdID) - - const resultDraft = latestFormData(pkg) - - // The submission fields should still be set - expect(resultDraft.id).toEqual(createdID) - expect(resultDraft.submissionType).toBe('CONTRACT_AND_RATES') - expect(resultDraft.programIDs).toEqual([defaultFloridaProgram().id]) - // check that the stateNumber is being returned the same - expect(resultDraft.stateNumber).toEqual(draft.stateNumber) - expect(resultDraft.submissionDescription).toBe( - 'An updated submission' - ) - expect(resultDraft.documents).toEqual(draft.documents) - - // Contract details fields should still be set - expect(resultDraft.contractType).toEqual(draft.contractType) - expect(resultDraft.contractExecutionStatus).toEqual( - draft.contractExecutionStatus - ) - expect(resultDraft.contractDateStart).toEqual( - draft.contractDateStart - ) - expect(resultDraft.contractDateEnd).toEqual(draft.contractDateEnd) - expect(resultDraft.managedCareEntities).toEqual( - draft.managedCareEntities - ) - expect(resultDraft.contractDocuments).toEqual( - draft.contractDocuments - ) - - expect(resultDraft.federalAuthorities).toEqual( - draft.federalAuthorities - ) + ], + supportingDocuments: [], + rateProgramIDs: [defaultFloridaRateProgram().id], + actuaryContacts: [ + { + name: 'test name', + titleRole: 'test title', + email: 'email@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + ], + actuaryCommunicationPreference: 'OACT_TO_ACTUARY' as const, + packagesWithSharedRateCerts: [], + }) - if (resultDraft.status == 'DRAFT') { - throw new Error('Not a locked submission') + // First, create new submissions + const submittedEditedRates = await createAndSubmitTestHealthPlanPackage( + server, + { + rateInfos: [initialRateInfos()], } + ) + const submittedNewRates = await createAndSubmitTestHealthPlanPackage( + server, + { + rateInfos: [initialRateInfos()], + } + ) + + // Unlock both - one to be rate edited in place, the other to add new rate + const existingRate1 = await unlockTestHealthPlanPackage( + cmsServer, + submittedEditedRates.id, + 'Unlock to edit an existing rate' + ) + const existingRate2 = await unlockTestHealthPlanPackage( + cmsServer, + submittedNewRates.id, + 'Unlock to add a new rate' + ) + + // update one with a new rate start and end date + const existingFormData = latestFormData(existingRate1) + expect(existingFormData.rateInfos).toHaveLength(1) + await updateTestHealthPlanPackage(server, submittedEditedRates.id, { + rateInfos: [ + { + ...existingFormData.rateInfos[0], + rateDateStart: new Date(Date.UTC(2025, 1, 1)), + rateDateEnd: new Date(Date.UTC(2027, 1, 1)), + }, + ], + }) - // submittedAt should be set to today's date - const today = new Date() - const expectedDate = today.toISOString().split('T')[0] - expect(pkg.initiallySubmittedAt).toEqual(expectedDate) - - // UpdatedAt should be after the former updatedAt - const resultUpdated = new Date(resultDraft.updatedAt) - const createdUpdated = new Date(draft.updatedAt) - expect( - resultUpdated.getTime() - createdUpdated.getTime() - ).toBeGreaterThan(0) - }, 20000) - - it('returns a state submission with the correct rate data on resubmit', async () => { - const cmsUser = testCMSUser() - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + // update the other with additional new rate + const existingFormData2 = latestFormData(existingRate2) + expect(existingFormData2.rateInfos).toHaveLength(1) + await updateTestHealthPlanPackage(server, submittedNewRates.id, { + rateInfos: [ + existingFormData2.rateInfos[0], + { + ...initialRateInfos(), + id: uuidv4(), // this is a new rate + rateDateStart: new Date(Date.UTC(2030, 1, 1)), + rateDateEnd: new Date(Date.UTC(2030, 12, 1)), + }, + ], + }) + // resubmit both + await resubmitTestHealthPlanPackage( + server, + submittedEditedRates.id, + 'Resubmit with edited rate description' + ) + await resubmitTestHealthPlanPackage( + server, + submittedNewRates.id, + 'Resubmit with an additional rate added' + ) + + // fetch both packages and check that the latest data is correct + const editedRatesPackage = await fetchTestHealthPlanPackageById( + server, + submittedEditedRates.id + ) + expect(latestFormData(editedRatesPackage).rateInfos).toHaveLength(1) + expect( + latestFormData(editedRatesPackage).rateInfos[0].rateDateStart + ).toMatchObject(new Date(Date.UTC(2025, 1, 1))) + expect( + latestFormData(editedRatesPackage).rateInfos[0].rateDateEnd + ).toMatchObject(new Date(Date.UTC(2027, 1, 1))) + expect( + editedRatesPackage.revisions[0].node.submitInfo?.updatedReason + ).toBe('Resubmit with edited rate description') + + const newRatesPackage = await fetchTestHealthPlanPackageById( + server, + submittedNewRates.id + ) + expect(latestFormData(newRatesPackage).rateInfos).toHaveLength(2) + expect( + latestFormData(newRatesPackage).rateInfos[0].rateDateStart + ).toMatchObject(initialRateInfos().rateDateStart) + expect( + latestFormData(newRatesPackage).rateInfos[0].rateDateEnd + ).toMatchObject(initialRateInfos().rateDateEnd) + expect( + latestFormData(newRatesPackage).rateInfos[1].rateDateStart + ).toMatchObject(new Date(Date.UTC(2030, 1, 1))) + expect( + latestFormData(newRatesPackage).rateInfos[1].rateDateEnd + ).toMatchObject(new Date(Date.UTC(2030, 12, 1))) + expect( + newRatesPackage.revisions[0].node.submitInfo?.updatedReason + ).toBe('Resubmit with an additional rate added') + + // also check both packages to ensure previous revision data is unchanged + expect(previousFormData(editedRatesPackage).rateInfos).toHaveLength(1) + expect( + previousFormData(editedRatesPackage).rateInfos[0].rateDateStart + ).toMatchObject(initialRateInfos().rateDateStart) + expect( + previousFormData(editedRatesPackage).rateInfos[0].rateDateEnd + ).toMatchObject(initialRateInfos().rateDateEnd) + expect( + editedRatesPackage.revisions[1].node.submitInfo?.updatedReason + ).toBe('Initial submission') + + expect(previousFormData(newRatesPackage).rateInfos).toHaveLength(1) + expect( + previousFormData(newRatesPackage).rateInfos[0].rateDateStart + ).toMatchObject(initialRateInfos().rateDateStart) + expect( + previousFormData(newRatesPackage).rateInfos[0].rateDateEnd + ).toMatchObject(initialRateInfos().rateDateEnd) + expect( + newRatesPackage.revisions[1].node.submitInfo?.updatedReason + ).toBe('Initial submission') + }) + + it('returns an error if there are no contract documents attached', async () => { + const server = await constructTestPostgresServer() + + const draft = await createAndUpdateTestHealthPlanPackage(server, { + documents: [], + contractDocuments: [], + }) + const draftID = draft.id - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, - ldService: mockLDService, - }) + }, + }) - const initialRateInfos = () => ({ - id: uuidv4(), - rateType: 'NEW' as const, - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: new Date(Date.UTC(2025, 3, 15)), - rateDocuments: [ - { - name: 'rateDocument.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['RATES' as const], - }, - ], - supportingDocuments: [], - rateProgramIDs: [defaultFloridaRateProgram().id], - actuaryContacts: [ - { - name: 'test name', - titleRole: 'test title', - email: 'email@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', - }, - ], - actuaryCommunicationPreference: 'OACT_TO_ACTUARY' as const, - packagesWithSharedRateCerts: [], - }) + expect(submitResult.errors).toBeDefined() - // First, create new submissions - const submittedEditedRates = - await createAndSubmitTestHealthPlanPackage(server, { - rateInfos: [initialRateInfos()], - }) - const submittedNewRates = - await createAndSubmitTestHealthPlanPackage(server, { - rateInfos: [initialRateInfos()], - }) - - // Unlock both - one to be rate edited in place, the other to add new rate - const existingRate1 = await unlockTestHealthPlanPackage( - cmsServer, - submittedEditedRates.id, - 'Unlock to edit an existing rate' - ) - const existingRate2 = await unlockTestHealthPlanPackage( - cmsServer, - submittedNewRates.id, - 'Unlock to add a new rate' - ) + expect(submitResult.errors?.[0].extensions?.code).toBe('BAD_USER_INPUT') + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData must have valid documents' + ) + }) - // update one with a new rate start and end date - const existingFormData = latestFormData(existingRate1) - expect(existingFormData.rateInfos).toHaveLength(1) - await updateTestHealthPlanPackage(server, submittedEditedRates.id, { - rateInfos: [ - { - ...existingFormData.rateInfos[0], - rateDateStart: new Date(Date.UTC(2025, 1, 1)), - rateDateEnd: new Date(Date.UTC(2027, 1, 1)), - }, - ], - }) + it('returns an error if the package is already SUBMITTED', async () => { + const server = await constructTestPostgresServer() - // update the other with additional new rate - const existingFormData2 = latestFormData(existingRate2) - expect(existingFormData2.rateInfos).toHaveLength(1) - await updateTestHealthPlanPackage(server, submittedNewRates.id, { - rateInfos: [ - existingFormData2.rateInfos[0], - { - ...initialRateInfos(), - id: uuidv4(), // this is a new rate - rateDateStart: new Date(Date.UTC(2030, 1, 1)), - rateDateEnd: new Date(Date.UTC(2030, 12, 1)), - }, - ], - }) - // resubmit both - await resubmitTestHealthPlanPackage( - server, - submittedEditedRates.id, - 'Resubmit with edited rate description' - ) - await resubmitTestHealthPlanPackage( - server, - submittedNewRates.id, - 'Resubmit with an additional rate added' - ) + const draft = await createAndSubmitTestHealthPlanPackage(server) + const draftID = draft.id - // fetch both packages and check that the latest data is correct - const editedRatesPackage = await fetchTestHealthPlanPackageById( - server, - submittedEditedRates.id - ) - expect(latestFormData(editedRatesPackage).rateInfos).toHaveLength(1) - expect( - latestFormData(editedRatesPackage).rateInfos[0].rateDateStart - ).toMatchObject(new Date(Date.UTC(2025, 1, 1))) - expect( - latestFormData(editedRatesPackage).rateInfos[0].rateDateEnd - ).toMatchObject(new Date(Date.UTC(2027, 1, 1))) - expect( - editedRatesPackage.revisions[0].node.submitInfo?.updatedReason - ).toBe('Resubmit with edited rate description') - - const newRatesPackage = await fetchTestHealthPlanPackageById( - server, - submittedNewRates.id - ) - expect(latestFormData(newRatesPackage).rateInfos).toHaveLength(2) - expect( - latestFormData(newRatesPackage).rateInfos[0].rateDateStart - ).toMatchObject(initialRateInfos().rateDateStart) - expect( - latestFormData(newRatesPackage).rateInfos[0].rateDateEnd - ).toMatchObject(initialRateInfos().rateDateEnd) - expect( - latestFormData(newRatesPackage).rateInfos[1].rateDateStart - ).toMatchObject(new Date(Date.UTC(2030, 1, 1))) - expect( - latestFormData(newRatesPackage).rateInfos[1].rateDateEnd - ).toMatchObject(new Date(Date.UTC(2030, 12, 1))) - expect( - newRatesPackage.revisions[0].node.submitInfo?.updatedReason - ).toBe('Resubmit with an additional rate added') - - // also check both packages to ensure previous revision data is unchanged - expect(previousFormData(editedRatesPackage).rateInfos).toHaveLength( - 1 - ) - expect( - previousFormData(editedRatesPackage).rateInfos[0].rateDateStart - ).toMatchObject(initialRateInfos().rateDateStart) - expect( - previousFormData(editedRatesPackage).rateInfos[0].rateDateEnd - ).toMatchObject(initialRateInfos().rateDateEnd) - expect( - editedRatesPackage.revisions[1].node.submitInfo?.updatedReason - ).toBe('Initial submission') - - expect(previousFormData(newRatesPackage).rateInfos).toHaveLength(1) - expect( - previousFormData(newRatesPackage).rateInfos[0].rateDateStart - ).toMatchObject(initialRateInfos().rateDateStart) - expect( - previousFormData(newRatesPackage).rateInfos[0].rateDateEnd - ).toMatchObject(initialRateInfos().rateDateEnd) - expect( - newRatesPackage.revisions[1].node.submitInfo?.updatedReason - ).toBe('Initial submission') + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, }) - it('returns an error if there are no contract documents attached', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - const draft = await createAndUpdateTestHealthPlanPackage(server, { - documents: [], - contractDocuments: [], - }) - const draftID = draft.id - - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + expect(submitResult.errors).toBeDefined() + + expect(submitResult.errors?.[0].extensions).toEqual( + expect.objectContaining({ + code: 'INTERNAL_SERVER_ERROR', + cause: 'INVALID_PACKAGE_STATUS', + exception: { + locations: undefined, + message: + 'Attempted to submit an already submitted package.', + path: undefined, }, }) + ) - expect(submitResult.errors).toBeDefined() - - expect(submitResult.errors?.[0].extensions?.code).toBe( - 'BAD_USER_INPUT' - ) - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData must have valid documents' - ) - }) + expect(submitResult.errors?.[0].message).toBe( + 'Attempted to submit an already submitted package.' + ) + }) - it('returns an error if the package is already SUBMITTED', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + it('returns an error if there are no contract details fields', async () => { + const server = await constructTestPostgresServer() - const draft = await createAndSubmitTestHealthPlanPackage(server) - const draftID = draft.id + const draft = await createAndUpdateTestHealthPlanPackage(server, { + contractType: undefined, + contractExecutionStatus: undefined, + managedCareEntities: [], + federalAuthorities: [], + }) - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + const draftID = draft.id + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, - }) - - expect(submitResult.errors).toBeDefined() - - expect(submitResult.errors?.[0].extensions).toEqual( - expect.objectContaining({ - code: 'INTERNAL_SERVER_ERROR', - cause: 'INVALID_PACKAGE_STATUS', - exception: { - locations: undefined, - message: - 'Attempted to submit an already submitted package.', - path: undefined, - }, - }) - ) - - expect(submitResult.errors?.[0].message).toBe( - 'Attempted to submit an already submitted package.' - ) + }, }) - it('returns an error if there are no contract details fields', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + expect(submitResult.errors).toBeDefined() - const draft = await createAndUpdateTestHealthPlanPackage(server, { - contractType: undefined, - contractExecutionStatus: undefined, - managedCareEntities: [], - federalAuthorities: [], - }) + expect(submitResult.errors?.[0].extensions?.code).toBe('BAD_USER_INPUT') + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData is missing required contract fields' + ) + }) - const draftID = draft.id - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, - }, - }) + it('returns an error if there are missing rate details fields for submission type', async () => { + const server = await constructTestPostgresServer() - expect(submitResult.errors).toBeDefined() + const draft = await createAndUpdateTestHealthPlanPackage(server, { + submissionType: 'CONTRACT_AND_RATES', + rateInfos: [ + { + id: uuidv4(), + rateType: 'NEW' as const, + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: new Date(Date.UTC(2025, 3, 15)), + rateDocuments: [ + { + name: 'rateDocument.pdf', + s3URL: 'fakeS3URL', + sha256: 'fakesha', + }, + ], + supportingDocuments: [], + rateProgramIDs: ['3b8d8fa1-1fa6-4504-9c5b-ef522877fe1e'], + actuaryContacts: [], // This is supposed to have at least one contact. + actuaryCommunicationPreference: 'OACT_TO_ACTUARY' as const, + packagesWithSharedRateCerts: [], + }, + ], + }) - expect(submitResult.errors?.[0].extensions?.code).toBe( - 'BAD_USER_INPUT' - ) - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData is missing required contract fields' - ) + const draftID = draft.id + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, }) - it('returns an error if there are missing rate details fields for submission type', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + expect(submitResult.errors).toBeDefined() - const draft = await createAndUpdateTestHealthPlanPackage(server, { - submissionType: 'CONTRACT_AND_RATES', - rateInfos: [ - { - id: uuidv4(), - rateType: 'NEW' as const, - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: new Date(Date.UTC(2025, 3, 15)), - rateDocuments: [ - { - name: 'rateDocument.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['RATES' as const], - }, - ], - supportingDocuments: [], - rateProgramIDs: [ - '3b8d8fa1-1fa6-4504-9c5b-ef522877fe1e', - ], - actuaryContacts: [], // This is supposed to have at least one contact. - actuaryCommunicationPreference: - 'OACT_TO_ACTUARY' as const, - packagesWithSharedRateCerts: [], - }, - ], - }) + expect(submitResult.errors?.[0].extensions?.code).toBe('BAD_USER_INPUT') + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData is missing required rate fields' + ) + }) - const draftID = draft.id - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + it('does not remove any rate data from CONTRACT_AND_RATES submissionType and submits successfully', async () => { + const server = await constructTestPostgresServer() + + //Create and update a contract and rate submission to contract only with rate data + const draft = await createAndUpdateTestHealthPlanPackage(server, { + submissionType: 'CONTRACT_AND_RATES', + documents: [ + { + name: 'contract_supporting_that_applies_to_a_rate_also.pdf', + s3URL: 'fakeS3URL', + sha256: 'fakesha', }, - }) + { + name: 'rate_only_supporting_doc.pdf', + s3URL: 'fakeS3URL', + sha256: 'fakesha', + }, + ], + }) - expect(submitResult.errors).toBeDefined() + const draftCurrentRevision = draft.revisions[0].node + const draftPackageData = base64ToDomain( + draftCurrentRevision.formDataProto + ) - expect(submitResult.errors?.[0].extensions?.code).toBe( - 'BAD_USER_INPUT' - ) - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData is missing required rate fields' - ) - }) + if (draftPackageData instanceof Error) { + throw new Error(draftPackageData.message) + } - it('does not remove any rate data from CONTRACT_AND_RATES submissionType and submits successfully', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const submitResult = await submitTestHealthPlanPackage(server, draft.id) + const currentRevision = submitResult.revisions[0].node + const packageData = base64ToDomain(currentRevision.formDataProto) + + if (packageData instanceof Error) { + throw new Error(packageData.message) + } - //Create and update a contract and rate submission to contract only with rate data - const draft = await createAndUpdateTestHealthPlanPackage(server, { - submissionType: 'CONTRACT_AND_RATES', + expect(packageData).toEqual( + expect.objectContaining({ + addtlActuaryContacts: draftPackageData.addtlActuaryContacts, documents: [ { name: 'contract_supporting_that_applies_to_a_rate_also.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { name: 'rate_only_supporting_doc.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, ], }) + ) + }) - const draftCurrentRevision = draft.revisions[0].node - const draftPackageData = base64ToDomain( - draftCurrentRevision.formDataProto - ) + it('removes any rate data from CONTRACT_ONLY submissionType and submits successfully', async () => { + const server = await constructTestPostgresServer() - if (draftPackageData instanceof Error) { - throw new Error(draftPackageData.message) - } + //Create and update a contract and rate submission to contract only with rate data + const draft = await createAndUpdateTestHealthPlanPackage(server, { + submissionType: 'CONTRACT_ONLY', + documents: [ + { + name: 'contract_supporting_that_applies_to_a_rate_also.pdf', + s3URL: 'fakeS3URL', + sha256: 'fakesha', + }, + { + name: 'rate_only_supporting_doc.pdf', + s3URL: 'fakeS3URL', + sha256: 'fakesha', + }, + ], + }) - const submitResult = await submitTestHealthPlanPackage( - server, - draft.id - ) - const currentRevision = submitResult.revisions[0].node - const packageData = base64ToDomain(currentRevision.formDataProto) + const submitResult = await submitTestHealthPlanPackage(server, draft.id) - if (packageData instanceof Error) { - throw new Error(packageData.message) - } + const currentRevision = submitResult.revisions[0].node + const packageData = base64ToDomain(currentRevision.formDataProto) - expect(packageData).toEqual( - expect.objectContaining({ - addtlActuaryContacts: draftPackageData.addtlActuaryContacts, - documents: [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], - }, - ], - }) - ) - }) - - it('removes any rate data from CONTRACT_ONLY submissionType and submits successfully', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + if (packageData instanceof Error) { + throw new Error(packageData.message) + } - //Create and update a contract and rate submission to contract only with rate data - const draft = await createAndUpdateTestHealthPlanPackage(server, { - submissionType: 'CONTRACT_ONLY', + expect(packageData).toEqual( + expect.objectContaining({ + rateInfos: [], + addtlActuaryContacts: [], documents: [ { name: 'contract_supporting_that_applies_to_a_rate_also.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { name: 'rate_only_supporting_doc.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, ], }) + ) - const submitResult = await submitTestHealthPlanPackage( - server, - draft.id - ) - - const currentRevision = submitResult.revisions[0].node - const packageData = base64ToDomain(currentRevision.formDataProto) + // Check to make sure disconnected rates were not submitted. + const draftFormData = latestFormData(draft) + const draftRates = draftFormData.rateInfos - if (packageData instanceof Error) { - throw new Error(packageData.message) - } - - expect(packageData).toEqual( - expect.objectContaining({ - rateInfos: [], - addtlActuaryContacts: [], - documents: [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - ], - }) - ) - - //Flag on only tests - if (!flagValue) { - return - } - - // Check to make sure disconnected rates were not submitted. - const draftFormData = latestFormData(draft) - const draftRates = draftFormData.rateInfos - - for (const rate of draftRates) { - const rateWithHistory = await server.executeOperation({ - query: FETCH_RATE, - variables: { - input: { - rateID: rate.id, - }, + for (const rate of draftRates) { + const rateWithHistory = await server.executeOperation({ + query: FETCH_RATE, + variables: { + input: { + rateID: rate.id, }, - }) - - const rateStatus = rateWithHistory.data?.fetchRate.rate.status + }, + }) - expect(rateStatus).not.toBe('SUBMITTED') - } + const rateStatus = rateWithHistory.data?.fetchRate.rate.status + + expect(rateStatus).not.toBe('SUBMITTED') + } + }) + + it('removes any invalid modified provisions from CHIP submission and submits successfully', async () => { + const server = await constructTestPostgresServer() + + //Create and update a submission as if the user edited and changed population covered after filling out yes/nos + const draft = await createAndUpdateTestHealthPlanPackage(server, { + contractType: 'AMENDMENT', + populationCovered: 'CHIP', + federalAuthorities: ['TITLE_XXI'], + contractAmendmentInfo: { + modifiedProvisions: { + inLieuServicesAndSettings: true, + modifiedBenefitsProvided: true, + modifiedGeoAreaServed: false, + modifiedMedicaidBeneficiaries: false, + modifiedRiskSharingStrategy: true, + modifiedIncentiveArrangements: true, + modifiedWitholdAgreements: true, + modifiedStateDirectedPayments: true, + modifiedPassThroughPayments: true, + modifiedPaymentsForMentalDiseaseInstitutions: true, + modifiedMedicalLossRatioStandards: false, + modifiedOtherFinancialPaymentIncentive: false, + modifiedEnrollmentProcess: false, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: false, + modifiedLengthOfContract: false, + modifiedNonRiskPaymentArrangements: false, + }, + }, }) - it('removes any invalid modified provisions from CHIP submission and submits successfully', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const submitResult = await submitTestHealthPlanPackage(server, draft.id) + + const currentRevision = submitResult.revisions[0].node + const packageData = base64ToDomain(currentRevision.formDataProto) - //Create and update a submission as if the user edited and changed population covered after filling out yes/nos - const draft = await createAndUpdateTestHealthPlanPackage(server, { - contractType: 'AMENDMENT', - populationCovered: 'CHIP', - federalAuthorities: ['TITLE_XXI'], + if (packageData instanceof Error) { + throw new Error(packageData.message) + } + expect(packageData).toEqual( + expect.objectContaining({ contractAmendmentInfo: { modifiedProvisions: { - inLieuServicesAndSettings: true, modifiedBenefitsProvided: true, modifiedGeoAreaServed: false, modifiedMedicaidBeneficiaries: false, - modifiedRiskSharingStrategy: true, - modifiedIncentiveArrangements: true, - modifiedWitholdAgreements: true, - modifiedStateDirectedPayments: true, - modifiedPassThroughPayments: true, - modifiedPaymentsForMentalDiseaseInstitutions: true, modifiedMedicalLossRatioStandards: false, - modifiedOtherFinancialPaymentIncentive: false, modifiedEnrollmentProcess: false, modifiedGrevienceAndAppeal: false, modifiedNetworkAdequacyStandards: false, @@ -643,487 +587,537 @@ describe.each(flagValueTestParameters)( }, }, }) + ) + }) + + it('removes any invalid federal authorities from CHIP submission and submits successfully', async () => { + const server = await constructTestPostgresServer() + + //Create and update a submission as if the user edited and changed population covered after filling out yes/nos + const draft = await createAndUpdateTestHealthPlanPackage(server, { + populationCovered: 'CHIP', + federalAuthorities: [ + 'STATE_PLAN', + 'WAIVER_1915B', + 'WAIVER_1115', + 'VOLUNTARY', + 'BENCHMARK', + 'TITLE_XXI', + ], + }) - const submitResult = await submitTestHealthPlanPackage( - server, - draft.id - ) - - const currentRevision = submitResult.revisions[0].node - const packageData = base64ToDomain(currentRevision.formDataProto) + const submitResult = await submitTestHealthPlanPackage(server, draft.id) - if (packageData instanceof Error) { - throw new Error(packageData.message) - } - expect(packageData).toEqual( - expect.objectContaining({ - contractAmendmentInfo: { - modifiedProvisions: { - modifiedBenefitsProvided: true, - modifiedGeoAreaServed: false, - modifiedMedicaidBeneficiaries: false, - modifiedMedicalLossRatioStandards: false, - modifiedEnrollmentProcess: false, - modifiedGrevienceAndAppeal: false, - modifiedNetworkAdequacyStandards: false, - modifiedLengthOfContract: false, - modifiedNonRiskPaymentArrangements: false, - }, - }, - }) - ) - }) + const currentRevision = submitResult.revisions[0].node + const packageData = base64ToDomain(currentRevision.formDataProto) - it('removes any invalid federal authorities from CHIP submission and submits successfully', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, + if (packageData instanceof Error) { + throw new Error(packageData.message) + } + expect(packageData).toEqual( + expect.objectContaining({ + federalAuthorities: ['WAIVER_1115', 'TITLE_XXI'], }) + ) + }) - //Create and update a submission as if the user edited and changed population covered after filling out yes/nos - const draft = await createAndUpdateTestHealthPlanPackage(server, { - populationCovered: 'CHIP', - federalAuthorities: [ - 'STATE_PLAN', - 'WAIVER_1915B', - 'WAIVER_1115', - 'VOLUNTARY', - 'BENCHMARK', - 'TITLE_XXI', - ], - }) + it('sends two emails', async () => { + const mockEmailer = testEmailer() - const submitResult = await submitTestHealthPlanPackage( - server, - draft.id - ) + //mock invoke email submit lambda + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, + }) - const currentRevision = submitResult.revisions[0].node - const packageData = base64ToDomain(currentRevision.formDataProto) + expect(submitResult.errors).toBeUndefined() + expect(mockEmailer.sendEmail).toHaveBeenCalledTimes(2) + }) - if (packageData instanceof Error) { - throw new Error(packageData.message) - } - expect(packageData).toEqual( - expect.objectContaining({ - federalAuthorities: ['WAIVER_1115', 'TITLE_XXI'], - }) - ) + it('send CMS email to CMS if submission is valid', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, }) - it('sends two emails', async () => { - const mockEmailer = testEmailer() - - //mock invoke email submit lambda - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - ldService: mockLDService, + const currentRevision = + submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0].node + + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + const stateAnalystsEmails = getTestStateAnalystsEmails(sub.stateCode) + + const cmsEmails = [ + ...config.devReviewTeamEmails, + ...stateAnalystsEmails, + ] + + // email subject line is correct for CMS email + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining( + `New Managed Care Submission: ${name}` + ), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining(Array.from(cmsEmails)), }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id - - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + ) + }) + + it('does send email when request for state analysts emails fails', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const mockEmailParameterStore = mockEmailParameterStoreError() + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + emailParameterStore: mockEmailParameterStore, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, - }) - - expect(submitResult.errors).toBeUndefined() - expect(mockEmailer.sendEmail).toHaveBeenCalledTimes(2) + }, }) - it('send CMS email to CMS if submission is valid', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - ldService: mockLDService, + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + toAddresses: expect.arrayContaining( + Array.from(config.devReviewTeamEmails) + ), }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id - - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + ) + }) + + it('does log error when request for state specific analysts emails failed', async () => { + const mockEmailParameterStore = mockEmailParameterStoreError() + const consoleErrorSpy = jest.spyOn(console, 'error') + const error = { + error: 'No store found', + message: 'getStateAnalystsEmails failed', + operation: 'getStateAnalystsEmails', + status: 'ERROR', + } + + const server = await constructTestPostgresServer({ + emailParameterStore: mockEmailParameterStore, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, - }) - - const currentRevision = - submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0] - .node - - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub - } + }, + }) - const programs = [defaultFloridaProgram()] - const name = packageName( - sub.stateCode, - sub.stateNumber, - sub.programIDs, - programs - ) - const stateAnalystsEmails = getTestStateAnalystsEmails( - sub.stateCode - ) + expect(consoleErrorSpy).toHaveBeenCalledWith(error) + }) - const cmsEmails = [ - ...config.devReviewTeamEmails, - ...stateAnalystsEmails, - ] - - // email subject line is correct for CMS email - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining( - `New Managed Care Submission: ${name}` - ), - sourceEmail: config.emailSource, - toAddresses: expect.arrayContaining(Array.from(cmsEmails)), - }) - ) + it('send state email to logged in user if submission is valid', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + const server = await constructTestPostgresServer({ + emailer: mockEmailer, }) - it('does send email when request for state analysts emails fails', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const mockEmailParameterStore = mockEmailParameterStoreError() - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - emailParameterStore: mockEmailParameterStore, - ldService: mockLDService, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id + const currentUser = defaultContext().user // need this to reach into gql tests and understand who current user is + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id - await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, - }) - - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - toAddresses: expect.arrayContaining( - Array.from(config.devReviewTeamEmails) - ), - }) - ) + }, }) - it('does log error when request for state specific analysts emails failed', async () => { - const mockEmailParameterStore = mockEmailParameterStoreError() - const consoleErrorSpy = jest.spyOn(console, 'error') - const error = { - error: 'No store found', - message: 'getStateAnalystsEmails failed', - operation: 'getStateAnalystsEmails', - status: 'ERROR', - } - - const server = await constructTestPostgresServer({ - emailParameterStore: mockEmailParameterStore, - ldService: mockLDService, + expect(submitResult.errors).toBeUndefined() + + const currentRevision = + submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0].node + + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const ratePrograms = [defaultFloridaRateProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + const rateName = generateRateName(sub, sub.rateInfos[0], ratePrograms) + + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining(`${name} was sent to CMS`), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining([currentUser.email]), + bodyHTML: expect.stringContaining(rateName), }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id - - await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + ) + }) + + it('send state email to submitter if submission is valid', async () => { + const mockEmailer = testEmailer() + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + context: { + user: testStateUser({ + email: 'notspiderman@example.com', + }), + }, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, - }) - - expect(consoleErrorSpy).toHaveBeenCalledWith(error) + }, }) - it('send state email to logged in user if submission is valid', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - ldService: mockLDService, + expect(submitResult.errors).toBeUndefined() + + const currentRevision = + submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0].node + + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining(`${name} was sent to CMS`), + toAddresses: expect.arrayContaining([ + 'notspiderman@example.com', + ]), }) + ) + }) + + it('send CMS email to CMS on valid resubmission', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const stateServer = await constructTestPostgresServer({ + emailer: mockEmailer, + }) - const currentUser = defaultContext().user // need this to reach into gql tests and understand who current user is - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Test unlock reason.' + ) + + const submitResult = await stateServer.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + submittedReason: 'Test resubmitted reason', }, + }, + }) + + const currentRevision = + submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0].node + + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + + // email subject line is correct for CMS email and contains correct email body text + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining(`${name} was resubmitted`), + sourceEmail: config.emailSource, + bodyText: expect.stringContaining( + `The state completed their edits on submission ${name}` + ), + toAddresses: expect.arrayContaining( + Array.from(config.devReviewTeamEmails) + ), }) + ) + }) + + it('send state email to state contacts and all submitters on valid resubmission', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const stateServer = await constructTestPostgresServer({ + context: { + user: testStateUser({ + email: 'alsonotspiderman@example.com', + }), + }, + }) - expect(submitResult.errors).toBeUndefined() + const stateServerTwo = await constructTestPostgresServer({ + emailer: mockEmailer, + context: { + user: testStateUser({ + email: 'notspiderman@example.com', + }), + }, + }) - const currentRevision = - submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0] - .node + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub - } + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) - const programs = [defaultFloridaProgram()] - const ratePrograms = [defaultFloridaRateProgram()] - const name = packageName( - sub.stateCode, - sub.stateNumber, - sub.programIDs, - programs - ) - const rateName = generateRateName( - sub, - sub.rateInfos[0], - ratePrograms - ) + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Test unlock reason.' + ) + + const submitResult = await resubmitTestHealthPlanPackage( + stateServerTwo, + stateSubmission.id, + 'Test resubmission reason' + ) + + const currentRevision = submitResult?.revisions[0].node + + const sub = base64ToDomain(currentRevision.formDataProto) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + + // email subject line is correct for CMS email and contains correct email body text + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining(`${name} was resubmitted`), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining([ + 'alsonotspiderman@example.com', + 'notspiderman@example.com', + sub.stateContacts[0].email, + ]), + }) + ) + }) - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining(`${name} was sent to CMS`), - sourceEmail: config.emailSource, - toAddresses: expect.arrayContaining([currentUser.email]), - bodyHTML: expect.stringContaining(rateName), - }) - ) + it('does not send any emails if submission fails', async () => { + const mockEmailer = testEmailer() + const server = await constructTestPostgresServer({ + emailer: mockEmailer, }) - - it('send state email to submitter if submission is valid', async () => { - const mockEmailer = testEmailer() - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - context: { - user: testStateUser({ - email: 'notspiderman@example.com', - }), + const draft = await createAndUpdateTestHealthPlanPackage(server, { + submissionType: 'CONTRACT_AND_RATES', + rateInfos: [ + { + id: uuidv4(), + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: undefined, + rateDocuments: [], + supportingDocuments: [], + actuaryContacts: [], + packagesWithSharedRateCerts: [], }, - ldService: mockLDService, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) - const draftID = draft.id + ], + }) + const draftID = draft.id - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, - }) + }, + }) - expect(submitResult.errors).toBeUndefined() + expect(submitResult.errors).toBeDefined() + expect(mockEmailer.sendEmail).not.toHaveBeenCalled() + }) - const currentRevision = - submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0] - .node + it('errors when SES email has failed.', async () => { + const mockEmailer = testEmailer() - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub + jest.spyOn(awsSESHelpers, 'testSendSESEmail').mockImplementation( + async () => { + throw new Error('Network error occurred') } + ) - const programs = [defaultFloridaProgram()] - const name = packageName( - sub.stateCode, - sub.stateNumber, - sub.programIDs, - programs - ) - - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining(`${name} was sent to CMS`), - toAddresses: expect.arrayContaining([ - 'notspiderman@example.com', - ]), - }) - ) + //mock invoke email submit lambda + const server = await constructTestPostgresServer({ + emailer: mockEmailer, + }) + const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draftID = draft.id + + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, + }, + }, }) - it('send CMS email to CMS on valid resubmission', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const stateServer = await constructTestPostgresServer({ - emailer: mockEmailer, - ldService: mockLDService, - }) + // expect errors from submission + // expect(submitResult.errors).toBeDefined() - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) + // expect sendEmail to have been called, so we know it did not error earlier + expect(mockEmailer.sendEmail).toHaveBeenCalled() - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Test unlock reason.' - ) + jest.resetAllMocks() - const submitResult = await stateServer.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - submittedReason: 'Test resubmitted reason', + // expect correct graphql error. + expect(submitResult.errors?.[0]).toEqual( + expect.objectContaining({ + message: 'Email failed', + path: ['submitHealthPlanPackage'], + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'EMAIL_ERROR', + exception: { + message: 'Email failed', }, }, }) + ) + }) - const currentRevision = - submitResult?.data?.submitHealthPlanPackage?.pkg.revisions[0] - .node + it('errors when risk based question is undefined', async () => { + const server = await constructTestPostgresServer() - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub - } - - const programs = [defaultFloridaProgram()] - const name = packageName( - sub.stateCode, - sub.stateNumber, - sub.programIDs, - programs - ) - - // email subject line is correct for CMS email and contains correct email body text - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining(`${name} was resubmitted`), - sourceEmail: config.emailSource, - bodyText: expect.stringContaining( - `The state completed their edits on submission ${name}` - ), - toAddresses: expect.arrayContaining( - Array.from(config.devReviewTeamEmails) - ), - }) - ) + // setup + const initialPkg = await createAndUpdateTestHealthPlanPackage(server, { + riskBasedContract: undefined, }) + const draft = latestFormData(initialPkg) + const draftID = draft.id - it('send state email to state contacts and all submitters on valid resubmission', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const stateServer = await constructTestPostgresServer({ - context: { - user: testStateUser({ - email: 'alsonotspiderman@example.com', - }), - }, - ldService: mockLDService, - }) - - const stateServerTwo = await constructTestPostgresServer({ - emailer: mockEmailer, - context: { - user: testStateUser({ - email: 'notspiderman@example.com', - }), - }, - ldService: mockLDService, - }) - - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + await new Promise((resolve) => setTimeout(resolve, 2000)) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, + // submit + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, - ldService: mockLDService, - }) - - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Test unlock reason.' - ) - - const submitResult = await resubmitTestHealthPlanPackage( - stateServerTwo, - stateSubmission.id, - 'Test resubmission reason' - ) - - const currentRevision = submitResult?.revisions[0].node + }, + }) - const sub = base64ToDomain(currentRevision.formDataProto) - if (sub instanceof Error) { - throw sub - } + expect(submitResult.errors).toBeDefined() + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData is missing required contract fields' + ) + }, 20000) - const programs = [defaultFloridaProgram()] - const name = packageName( - sub.stateCode, - sub.stateNumber, - sub.programIDs, - programs - ) + describe('Feature flagged population coverage question test', () => { + it('errors when population coverage question is undefined', async () => { + const server = await constructTestPostgresServer() - // email subject line is correct for CMS email and contains correct email body text - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining(`${name} was resubmitted`), - sourceEmail: config.emailSource, - toAddresses: expect.arrayContaining([ - 'alsonotspiderman@example.com', - 'notspiderman@example.com', - sub.stateContacts[0].email, - ]), - }) + // setup + const initialPkg = await createAndUpdateTestHealthPlanPackage( + server, + { + populationCovered: undefined, + } ) - }) - - it('does not send any emails if submission fails', async () => { - const mockEmailer = testEmailer() - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - ldService: mockLDService, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, { - submissionType: 'CONTRACT_AND_RATES', - rateInfos: [ - { - id: uuidv4(), - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: undefined, - rateDocuments: [], - supportingDocuments: [], - actuaryContacts: [], - packagesWithSharedRateCerts: [], - }, - ], - }) + const draft = latestFormData(initialPkg) const draftID = draft.id + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // submit const submitResult = await server.executeOperation({ query: SUBMIT_HEALTH_PLAN_PACKAGE, variables: { @@ -1134,26 +1128,36 @@ describe.each(flagValueTestParameters)( }) expect(submitResult.errors).toBeDefined() - expect(mockEmailer.sendEmail).not.toHaveBeenCalled() + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData is missing required contract fields' + ) + }, 20000) + }) + + describe('Feature flagged 4348 attestation question test', () => { + const ldService = testLDService({ + '438-attestation': true, }) - it('errors when SES email has failed.', async () => { - const mockEmailer = testEmailer() + it('errors when contract 4348 attestation question is undefined', async () => { + const server = await constructTestPostgresServer({ + ldService: ldService, + }) - jest.spyOn(awsSESHelpers, 'testSendSESEmail').mockImplementation( - async () => { - throw new Error('Network error occurred') + // setup + const initialPkg = await createAndUpdateTestHealthPlanPackage( + server, + { + statutoryRegulatoryAttestation: undefined, + statutoryRegulatoryAttestationDescription: undefined, } ) - - //mock invoke email submit lambda - const server = await constructTestPostgresServer({ - emailer: mockEmailer, - ldService: mockLDService, - }) - const draft = await createAndUpdateTestHealthPlanPackage(server, {}) + const draft = latestFormData(initialPkg) const draftID = draft.id + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // submit const submitResult = await server.executeOperation({ query: SUBMIT_HEALTH_PLAN_PACKAGE, variables: { @@ -1163,40 +1167,22 @@ describe.each(flagValueTestParameters)( }, }) - // expect errors from submission expect(submitResult.errors).toBeDefined() - - // expect sendEmail to have been called, so we know it did not error earlier - expect(mockEmailer.sendEmail).toHaveBeenCalled() - - jest.resetAllMocks() - - // expect correct graphql error. - expect(submitResult.errors?.[0]).toEqual( - expect.objectContaining({ - message: 'Email failed', - path: ['submitHealthPlanPackage'], - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'EMAIL_ERROR', - exception: { - message: 'Email failed', - }, - }, - }) + expect(submitResult.errors?.[0].extensions?.message).toBe( + 'formData is missing required contract fields' ) - }) - - it('errors when risk based question is undefined', async () => { + }, 20000) + it('errors when contract 4348 attestation question is false without a description', async () => { const server = await constructTestPostgresServer({ - ldService: mockLDService, + ldService: ldService, }) // setup const initialPkg = await createAndUpdateTestHealthPlanPackage( server, { - riskBasedContract: undefined, + statutoryRegulatoryAttestation: false, + statutoryRegulatoryAttestationDescription: undefined, } ) const draft = latestFormData(initialPkg) @@ -1219,145 +1205,35 @@ describe.each(flagValueTestParameters)( 'formData is missing required contract fields' ) }, 20000) - - describe('Feature flagged population coverage question test', () => { - it('errors when population coverage question is undefined', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - // setup - const initialPkg = await createAndUpdateTestHealthPlanPackage( - server, - { - populationCovered: undefined, - } - ) - const draft = latestFormData(initialPkg) - const draftID = draft.id - - await new Promise((resolve) => setTimeout(resolve, 2000)) - - // submit - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, - }, - }) - - expect(submitResult.errors).toBeDefined() - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData is missing required contract fields' - ) - }, 20000) - }) - - describe('Feature flagged 4348 attestation question test', () => { - const ldService = testLDService({ - [flagName]: flagValue, - '438-attestation': true, + it('successfully submits when contract 4348 attestation question is valid', async () => { + const server = await constructTestPostgresServer({ + ldService: ldService, }) - it('errors when contract 4348 attestation question is undefined', async () => { - const server = await constructTestPostgresServer({ - ldService: ldService, - }) + // setup + const initialPkg = await createAndUpdateTestHealthPlanPackage( + server, + { + statutoryRegulatoryAttestation: false, + statutoryRegulatoryAttestationDescription: 'No compliance', + } + ) + const draft = latestFormData(initialPkg) + const draftID = draft.id - // setup - const initialPkg = await createAndUpdateTestHealthPlanPackage( - server, - { - statutoryRegulatoryAttestation: undefined, - statutoryRegulatoryAttestationDescription: undefined, - } - ) - const draft = latestFormData(initialPkg) - const draftID = draft.id - - await new Promise((resolve) => setTimeout(resolve, 2000)) - - // submit - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, - }, - }) - - expect(submitResult.errors).toBeDefined() - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData is missing required contract fields' - ) - }, 20000) - it('errors when contract 4348 attestation question is false without a description', async () => { - const server = await constructTestPostgresServer({ - ldService: ldService, - }) - - // setup - const initialPkg = await createAndUpdateTestHealthPlanPackage( - server, - { - statutoryRegulatoryAttestation: false, - statutoryRegulatoryAttestationDescription: undefined, - } - ) - const draft = latestFormData(initialPkg) - const draftID = draft.id - - await new Promise((resolve) => setTimeout(resolve, 2000)) - - // submit - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, - }, - }) - - expect(submitResult.errors).toBeDefined() - expect(submitResult.errors?.[0].extensions?.message).toBe( - 'formData is missing required contract fields' - ) - }, 20000) - it('successfully submits when contract 4348 attestation question is valid', async () => { - const server = await constructTestPostgresServer({ - ldService: ldService, - }) - - // setup - const initialPkg = await createAndUpdateTestHealthPlanPackage( - server, - { - statutoryRegulatoryAttestation: false, - statutoryRegulatoryAttestationDescription: - 'No compliance', - } - ) - const draft = latestFormData(initialPkg) - const draftID = draft.id - - await new Promise((resolve) => setTimeout(resolve, 2000)) - - // submit - const submitResult = await server.executeOperation({ - query: SUBMIT_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: draftID, - }, + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // submit + const submitResult = await server.executeOperation({ + query: SUBMIT_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: draftID, }, - }) + }, + }) - expect(submitResult.errors).toBeUndefined() - }, 20000) - }) - } -) + expect(submitResult.errors).toBeUndefined() + }, 20000) + }) +}) diff --git a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts index f2039d9965..2f611afc4f 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/submitHealthPlanPackage.ts @@ -8,11 +8,10 @@ import { removeRatesData, removeInvalidProvisionsAndAuthorities, isValidAndCurrentLockedHealthPlanFormData, - hasValidSupportingDocumentCategories, isContractOnly, isCHIPOnly, } from '../../../../app-web/src/common-code/healthPlanFormDataType/healthPlanFormData' -import type { UpdateInfoType, HealthPlanPackageType } from '../../domain-models' +import type { UpdateInfoType } from '../../domain-models' import { isStateUser, packageStatus, @@ -22,13 +21,12 @@ import type { Emailer } from '../../emailer' import type { MutationResolvers, State } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' import type { Store } from '../../postgres' -import { NotFoundError, isStoreError } from '../../postgres' +import { NotFoundError } from '../../postgres' import { setResolverDetailsOnActiveSpan, setErrorAttributesOnActiveSpan, setSuccessAttributesOnActiveSpan, } from '../attributeHelper' -import { toDomain } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' import type { EmailParameterStore } from '../../parameterStore' import { GraphQLError } from 'graphql' @@ -170,16 +168,6 @@ export function parseAndSubmit( code: 'INCOMPLETE', message: 'formData must have valid documents', } - } else if ( - !hasValidSupportingDocumentCategories( - maybeStateSubmission as LockedHealthPlanFormDataType - ) - ) { - return { - code: 'INCOMPLETE', - message: - 'formData must have valid categories for supporting documents', - } } else return { code: 'INCOMPLETE', @@ -204,12 +192,6 @@ export function submitHealthPlanPackageResolver( setResolverDetailsOnActiveSpan('submitHealthPlanPackage', user, span) span?.setAttribute('mcreview.package_id', pkgID) - // Set up variables that are used across the feature flag boundary - let initialFormData: HealthPlanFormDataType // data from revision sent to resolver - let contractRevisionID: string // id for latest contract revision to reference later - let lockedFormData: LockedHealthPlanFormDataType // updated data (parsed and cleaned) passed to submit - let updatedPackage: HealthPlanPackageType // updated package returned from submit - //Set updateInfo default to initial submission const updateInfo: UpdateInfoType = { updatedAt: new Date(), @@ -231,260 +213,178 @@ export function submitHealthPlanPackageResolver( } const stateFromCurrentUser: State['code'] = user.stateCode - if (featureFlags?.['rates-db-refactor']) { - // fetch contract and related reates - convert to HealthPlanPackage and proto-ize to match the pattern for flag off\ - // this could be replaced with parsing to locked versus unlocked contracts and rates when types are available - const contractWithHistory = await store.findContractWithHistory( - input.pkgID - ) - - if (contractWithHistory instanceof Error) { - const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) + // fetch contract and related reates - convert to HealthPlanPackage and proto-ize to match the pattern for flag off\ + // this could be replaced with parsing to locked versus unlocked contracts and rates when types are available + const contractWithHistory = await store.findContractWithHistory( + input.pkgID + ) - if (contractWithHistory instanceof NotFoundError) { - throw new GraphQLError(errMessage, { - extensions: { - code: 'NOT_FOUND', - cause: 'DB_ERROR', - }, - }) - } + if (contractWithHistory instanceof Error) { + const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + if (contractWithHistory instanceof NotFoundError) { throw new GraphQLError(errMessage, { extensions: { - code: 'INTERNAL_SERVER_ERROR', + code: 'NOT_FOUND', cause: 'DB_ERROR', }, }) } - const maybeHealthPlanPackage = - convertContractWithRatesToUnlockedHPP(contractWithHistory) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - if (maybeHealthPlanPackage instanceof Error) { - const errMessage = `Error convert to contractWithHistory health plan package. Message: ${maybeHealthPlanPackage.message}` - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } + const maybeHealthPlanPackage = + convertContractWithRatesToUnlockedHPP(contractWithHistory) - // Validate user authorized to fetch state - if (contractWithHistory.stateCode !== stateFromCurrentUser) { - logError( - 'submitHealthPlanPackage', - 'user not authorized to fetch data from a different state' - ) - setErrorAttributesOnActiveSpan( - 'user not authorized to fetch data from a different state', - span - ) - throw new ForbiddenError( - 'user not authorized to fetch data from a different state' - ) - } + if (maybeHealthPlanPackage instanceof Error) { + const errMessage = `Error convert to contractWithHistory health plan package. Message: ${maybeHealthPlanPackage.message}` + logError('submitHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) + } - validateStatusAndUpdateInfo( - contractWithHistory.status, - updateInfo, - span, - submittedReason || undefined + // Validate user authorized to fetch state + if (contractWithHistory.stateCode !== stateFromCurrentUser) { + logError( + 'submitHealthPlanPackage', + 'user not authorized to fetch data from a different state' + ) + setErrorAttributesOnActiveSpan( + 'user not authorized to fetch data from a different state', + span ) + throw new ForbiddenError( + 'user not authorized to fetch data from a different state' + ) + } - if (!contractWithHistory.draftRevision) { - throw new Error( - 'PROGRAMMING ERROR: Status should not be submittable without a draft revision' - ) - } + validateStatusAndUpdateInfo( + contractWithHistory.status, + updateInfo, + span, + submittedReason || undefined + ) - // reassign variable set up before rates feature flag - const conversionResult = convertContractWithRatesToFormData( - contractWithHistory.draftRevision, - contractWithHistory.id, - contractWithHistory.stateCode, - contractWithHistory.stateNumber + if (!contractWithHistory.draftRevision) { + throw new Error( + 'PROGRAMMING ERROR: Status should not be submittable without a draft revision' ) + } - if (conversionResult instanceof Error) { - const errMessage = conversionResult.message - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new Error(errMessage) - } - - initialFormData = conversionResult - contractRevisionID = contractWithHistory.draftRevision.id + // reassign variable set up before rates feature flag + const conversionResult = convertContractWithRatesToFormData( + contractWithHistory.draftRevision, + contractWithHistory.id, + contractWithHistory.stateCode, + contractWithHistory.stateNumber + ) - // Final clean + check of data before submit - parse to state submission - const maybeLocked = parseAndSubmit(initialFormData, featureFlags) + if (conversionResult instanceof Error) { + const errMessage = conversionResult.message + logError('submitHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new Error(errMessage) + } - if (isSubmissionError(maybeLocked)) { - const errMessage = maybeLocked.message - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - message: maybeLocked.message, - }) - } + const initialFormData = conversionResult + const contractRevisionID = contractWithHistory.draftRevision.id - // Since submit can change the form data, we have to save it again. - // if the rates were removed, we remove them. - let removeRateInfos: RateFormEditable[] | undefined = undefined - if (maybeLocked.rateInfos.length === 0) { - // undefined means ignore rates in updaterDraftContractWithRates, empty array means empty them. - removeRateInfos = [] - } + // Final clean + check of data before submit - parse to state submission + const maybeLocked = parseAndSubmit(initialFormData, featureFlags) - const updateResult = await store.updateDraftContractWithRates({ - contractID: input.pkgID, - formData: { - ...maybeLocked, - ...maybeLocked.contractAmendmentInfo?.modifiedProvisions, - managedCareEntities: maybeLocked.managedCareEntities, - stateContacts: maybeLocked.stateContacts, - supportingDocuments: maybeLocked.documents.map((doc) => { - return { - name: doc.name, - s3URL: doc.s3URL, - sha256: doc.sha256, - id: doc.id, - } - }), - contractDocuments: maybeLocked.contractDocuments.map( - (doc) => { - return { - name: doc.name, - s3URL: doc.s3URL, - sha256: doc.sha256, - id: doc.id, - } - } - ), - }, - rateFormDatas: removeRateInfos, + if (isSubmissionError(maybeLocked)) { + const errMessage = maybeLocked.message + logError('submitHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new UserInputError(errMessage, { + message: maybeLocked.message, }) - if (updateResult instanceof Error) { - const errMessage = `Failed to update submitted contract info with ID: ${contractRevisionID}; ${updateResult.message}` - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - - if (!updateResult.draftRevision) { - throw new Error( - 'PROGRAMMING ERROR: draft contract does not contain a draft revision' - ) - } - - // From this point forward we use updateResult instead of contractWithHistory because it is now old data. - - // If there are rates, submit those first - if (updateResult.draftRevision.rateRevisions.length > 0) { - const ratePromises: Promise[] = [] - updateResult.draftRevision.rateRevisions.forEach((rateRev) => { - ratePromises.push( - store.submitRate({ - rateRevisionID: rateRev.id, - submittedByUserID: user.id, - submitReason: updateInfo.updatedReason, - }) - ) - }) + } - const submitRatesResult = await Promise.all(ratePromises) - // if any of the promises reject, which shouldn't happen b/c we don't throw... - if (submitRatesResult instanceof Error) { - const errMessage = `Failed to submit contract revision's rates with ID: ${contractRevisionID}; ${submitRatesResult.message}` - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - const submitRateErrors: Error[] = submitRatesResult.filter( - (res) => res instanceof Error - ) as Error[] - if (submitRateErrors.length > 0) { - console.error('Errors submitting Rates: ', submitRateErrors) - const errMessage = `Failed to submit contract revision's rates with ID: ${contractRevisionID}; ${submitRateErrors.map( - (err) => err.message - )}` - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - } + // Since submit can change the form data, we have to save it again. + // if the rates were removed, we remove them. + let removeRateInfos: RateFormEditable[] | undefined = undefined + if (maybeLocked.rateInfos.length === 0) { + // undefined means ignore rates in updaterDraftContractWithRates, empty array means empty them. + removeRateInfos = [] + } - // then submit the contract! - const submitContractResult = await store.submitContract({ - contractID: updateResult.id, - submittedByUserID: user.id, - submitReason: updateInfo.updatedReason, + const updateResult = await store.updateDraftContractWithRates({ + contractID: input.pkgID, + formData: { + ...maybeLocked, + ...maybeLocked.contractAmendmentInfo?.modifiedProvisions, + managedCareEntities: maybeLocked.managedCareEntities, + stateContacts: maybeLocked.stateContacts, + supportingDocuments: maybeLocked.documents.map((doc) => { + return { + name: doc.name, + s3URL: doc.s3URL, + sha256: doc.sha256, + id: doc.id, + } + }), + contractDocuments: maybeLocked.contractDocuments.map((doc) => { + return { + name: doc.name, + s3URL: doc.s3URL, + sha256: doc.sha256, + id: doc.id, + } + }), + }, + rateFormDatas: removeRateInfos, + }) + if (updateResult instanceof Error) { + const errMessage = `Failed to update submitted contract info with ID: ${contractRevisionID}; ${updateResult.message}` + logError('submitHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, }) - if (submitContractResult instanceof Error) { - const errMessage = `Failed to submit contract revision with ID: ${contractRevisionID}; ${submitContractResult.message}` - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - const maybeSubmittedPkg = - convertContractWithRatesToUnlockedHPP(submitContractResult) - - if (maybeSubmittedPkg instanceof Error) { - const errMessage = `Error converting draft contract. Message: ${maybeSubmittedPkg.message}` - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } + } - // set variables used across feature flag boundary - lockedFormData = maybeLocked - updatedPackage = maybeSubmittedPkg - } else { - // fetch from package flag off - returns HealthPlanPackage - const initialPackage = await store.findHealthPlanPackage( - input.pkgID + if (!updateResult.draftRevision) { + throw new Error( + 'PROGRAMMING ERROR: draft contract does not contain a draft revision' ) + } - if (isStoreError(initialPackage) || !initialPackage) { - if (!initialPackage) { - throw new GraphQLError('Issue finding package.', { - extensions: { - code: 'NOT_FOUND', - cause: 'DB_ERROR', - }, + // From this point forward we use updateResult instead of contractWithHistory because it is now old data. + + // If there are rates, submit those first + if (updateResult.draftRevision.rateRevisions.length > 0) { + const ratePromises: Promise[] = [] + updateResult.draftRevision.rateRevisions.forEach((rateRev) => { + ratePromises.push( + store.submitRate({ + rateRevisionID: rateRev.id, + submittedByUserID: user.id, + submitReason: updateInfo.updatedReason, }) - } - const errMessage = `Issue finding a package of type ${initialPackage.code}. Message: ${initialPackage.message}` + ) + }) + + const submitRatesResult = await Promise.all(ratePromises) + // if any of the promises reject, which shouldn't happen b/c we don't throw... + if (submitRatesResult instanceof Error) { + const errMessage = `Failed to submit contract revision's rates with ID: ${contractRevisionID}; ${submitRatesResult.message}` logError('submitHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new GraphQLError(errMessage, { @@ -494,80 +394,14 @@ export function submitHealthPlanPackageResolver( }, }) } - - // unwrap HealthPlanPackage again to make further edits to data - const maybeFormData = toDomain( - initialPackage.revisions[0].formDataProto - ) - if (maybeFormData instanceof Error) { - const errMessage = `Failed to decode draft proto ${maybeFormData}.` - logError('submitHealthPlanPackage', errMessage) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } - - // Validate user authorized to fetch state - if (initialPackage.stateCode !== stateFromCurrentUser) { - logError( - 'submitHealthPlanPackage', - 'user not authorized to fetch data from a different state' - ) - setErrorAttributesOnActiveSpan( - 'user not authorized to fetch data from a different state', - span - ) - throw new ForbiddenError( - 'user not authorized to fetch data from a different state' - ) - } - const status = packageStatus(initialPackage) - if (status instanceof Error) { - throw new GraphQLError(status.message, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'INVALID_PACKAGE_STATUS', - }, - }) - } - - validateStatusAndUpdateInfo( - status, - updateInfo, - span, - submittedReason || undefined - ) - // reassign variable set up before rates feature flagx - initialFormData = maybeFormData - contractRevisionID = initialPackage.revisions[0].id - - // Final clean + check of data before submit - parse to state submission - const maybeLocked = parseAndSubmit(initialFormData, featureFlags) - - if (isSubmissionError(maybeLocked)) { - const errMessage = maybeLocked.message - logError('submitHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - message: maybeLocked.message, - }) - } - - // We're getting weird tests b/c of the difference between updateInfo and submittedAt - maybeLocked.submittedAt = updateInfo.updatedAt - - // Save the package! - const updateResult = await store.updateHealthPlanRevision( - input.pkgID, - contractRevisionID, - maybeLocked, - updateInfo - ) - if (isStoreError(updateResult)) { - const errMessage = `Issue updating a package of type ${updateResult.code}. Message: ${updateResult.message}` + const submitRateErrors: Error[] = submitRatesResult.filter( + (res) => res instanceof Error + ) as Error[] + if (submitRateErrors.length > 0) { + console.error('Errors submitting Rates: ', submitRateErrors) + const errMessage = `Failed to submit contract revision's rates with ID: ${contractRevisionID}; ${submitRateErrors.map( + (err) => err.message + )}` logError('submitHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new GraphQLError(errMessage, { @@ -577,12 +411,44 @@ export function submitHealthPlanPackageResolver( }, }) } + } - // set variables used across feature flag boundary - lockedFormData = maybeLocked - updatedPackage = updateResult + // then submit the contract! + const submitContractResult = await store.submitContract({ + contractID: updateResult.id, + submittedByUserID: user.id, + submitReason: updateInfo.updatedReason, + }) + if (submitContractResult instanceof Error) { + const errMessage = `Failed to submit contract revision with ID: ${contractRevisionID}; ${submitContractResult.message}` + logError('submitHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } + const maybeSubmittedPkg = + convertContractWithRatesToUnlockedHPP(submitContractResult) + + if (maybeSubmittedPkg instanceof Error) { + const errMessage = `Error converting draft contract. Message: ${maybeSubmittedPkg.message}` + logError('submitHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) } + // set variables used across feature flag boundary + const lockedFormData = maybeLocked + const updatedPackage = maybeSubmittedPkg + // Send emails! const status = packageStatus(updatedPackage) // Get state analysts emails from parameter store @@ -617,7 +483,6 @@ export function submitHealthPlanPackageResolver( let statePackageEmailResult if (status === 'RESUBMITTED') { - logSuccess('It was resubmitted') cmsPackageEmailResult = await emailer.sendResubmittedCMSEmail( lockedFormData, updateInfo, diff --git a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts index 8b30e08320..4e3bf7500a 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.test.ts @@ -32,974 +32,883 @@ import { mockEmailParameterStoreError, } from '../../testHelpers/parameterStoreHelpers' import { testCMSUser, testStateUser } from '../../testHelpers/userHelpers' -import type { - FeatureFlagLDConstant, - FlagValue, -} from 'app-web/src/common-code/featureFlags' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' - -const flagValueTestParameters: { - flagName: FeatureFlagLDConstant - flagValue: FlagValue - testName: string -}[] = [ - { - flagName: 'rates-db-refactor', - flagValue: false, - testName: 'unlockHealthPlanPackage with all feature flags off', - }, - { - flagName: 'rates-db-refactor', - flagValue: true, - testName: 'unlockHealthPlanPackage with rates-db-refactor on', - }, -] - -describe.each(flagValueTestParameters)( - `Tests $testName`, - ({ flagName, flagValue }) => { - const cmsUser = testCMSUser() - const mockLDService = testLDService({ [flagName]: flagValue }) - - it('returns a HealthPlanPackage with all revisions', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) - - // Unlock - const unlockResult = await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - unlockedReason: 'Super duper good reason.', - }, - }, - }) - - expect(unlockResult.errors).toBeUndefined() - - if (!unlockResult?.data) { - throw new Error('this should never happen') - } - - const unlockedSub: HealthPlanPackage = - unlockResult.data.unlockHealthPlanPackage.pkg - // After unlock, we should get a draft submission back - expect(unlockedSub.status).toBe('UNLOCKED') +describe(`Tests unlockHealthPlanPackage`, () => { + const cmsUser = testCMSUser() - expect(unlockedSub.revisions).toHaveLength(2) + it('returns a HealthPlanPackage with all revisions', async () => { + const stateServer = await constructTestPostgresServer() - expect(unlockedSub.revisions[0].node.submitInfo).toBeNull() - expect(unlockedSub.revisions[1].node.submitInfo).toBeDefined() - expect( - unlockedSub.revisions[1].node.submitInfo?.updatedAt.toISOString() - ).toContain(todaysDate()) - // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z - expect( - unlockedSub.revisions[1].node.submitInfo?.updatedAt.toISOString() - ).toContain('Z') - - expect(unlockedSub.revisions[0].node.unlockInfo).toBeDefined() - expect(unlockedSub.revisions[0].node.unlockInfo?.updatedBy).toBe( - 'zuko@example.com' - ) - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedReason - ).toBe('Super duper good reason.') - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain(todaysDate()) - // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain('Z') - }, 20000) - - it('returns a package that can be updated without errors', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + // First, create a new submitted submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) - - // Unlock - const unlockResult = await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - unlockedReason: 'Super duper good reason.', - }, - }, - }) - - expect(unlockResult.errors).toBeUndefined() - const unlockedSub = unlockResult?.data?.unlockHealthPlanPackage.pkg - - // After unlock, we should get a draft submission back - expect(unlockedSub.status).toBe('UNLOCKED') - expect(unlockedSub.revisions[0].node.unlockInfo).toBeDefined() - expect(unlockedSub.revisions[0].node.unlockInfo?.updatedBy).toBe( - 'zuko@example.com' - ) - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedReason - ).toBe('Super duper good reason.') - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain(todaysDate()) - // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain('Z') - - const formData = latestFormData(unlockedSub) - - // after unlock we should be able to update that draft submission and get the results - formData.programIDs = [defaultFloridaProgram().id] - formData.submissionType = 'CONTRACT_AND_RATES' as const - formData.submissionDescription = 'UPDATED_AFTER_UNLOCK' - formData.documents = [] - formData.contractType = 'BASE' as const - formData.contractDocuments = [] - formData.managedCareEntities = ['MCO'] - formData.federalAuthorities = ['VOLUNTARY' as const] - formData.stateContacts = [] - formData.addtlActuaryContacts = [] - - await updateTestHealthPlanFormData(stateServer, formData) - - const refetched = await fetchTestHealthPlanPackageById( - stateServer, - stateSubmission.id - ) - - const refetchedFormData = latestFormData(refetched) - - expect(refetchedFormData.submissionDescription).toBe( - 'UPDATED_AFTER_UNLOCK' - ) - }, 20000) - - // this test is currently failing for valid reasons - it('allows for multiple edits, editing the set of revisions correctly', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - // First, create a new submitted submission // SUBMISSION 1 - const submittedOnce = await createAndSubmitTestHealthPlanPackage( - stateServer - ) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) - - // Unlock - const unlockedOnce = await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: submittedOnce.id, - unlockedReason: 'Super duper good reason.', - }, - }, - }) - - expect(unlockedOnce.errors).toBeUndefined() - const unlockedSub = unlockedOnce?.data?.unlockHealthPlanPackage.pkg - - // After unlock, we should get a draft submission back - expect(unlockedSub.status).toBe('UNLOCKED') - expect(unlockedSub.revisions[0].node.unlockInfo).toBeDefined() - expect(unlockedSub.revisions[0].node.unlockInfo?.updatedBy).toBe( - 'zuko@example.com' - ) - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedReason - ).toBe('Super duper good reason.') - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain(todaysDate()) - // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z - expect( - unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain('Z') - - const formData = latestFormData(unlockedSub) - - // after unlock we should be able to update that draft submission and get the results - formData.submissionDescription = 'UPDATED_AFTER_UNLOCK' - - formData.rateInfos.push( - { - id: uuidv4(), - rateDateStart: new Date(), - rateDateEnd: new Date(), - rateProgramIDs: ['5c10fe9f-bec9-416f-a20c-718b152ad633'], - rateType: 'NEW', - rateDateCertified: new Date(), - rateDocuments: [ - { - name: 'fake doc', - s3URL: 'foo://bar', - sha256: 'fakesha', - documentCategories: ['RATES'], - }, - { - name: 'fake doc 2', - s3URL: 'foo://bar', - sha256: 'fakesha', - documentCategories: ['RATES'], - }, - { - name: 'fake doc 3', - s3URL: 'foo://bar', - sha256: 'fakesha', - documentCategories: ['RATES'], - }, - { - name: 'fake doc 4', - s3URL: 'foo://bar', - sha256: 'fakesha', - documentCategories: ['RATES'], - }, - ], - supportingDocuments: [], - actuaryContacts: [ - { - name: 'Enrico Soletzo 1', - titleRole: 'person', - email: 'en@example.com', - actuarialFirm: 'MERCER', - }, - { - name: 'Enrico Soletzo 2', - titleRole: 'person', - email: 'en@example.com', - actuarialFirm: 'MERCER', - }, - { - name: 'Enrico Soletzo 3', - titleRole: 'person', - email: 'en@example.com', - actuarialFirm: 'MERCER', - }, - ], - }, - { - id: uuidv4(), - rateDateStart: new Date(), - rateDateEnd: new Date(), - rateProgramIDs: ['08d114c2-0c01-4a1a-b8ff-e2b79336672d'], - rateType: 'NEW', - rateDateCertified: new Date(), - rateDocuments: [ - { - name: 'fake doc number two', - s3URL: 'foo://bar', - sha256: 'fakesha', - documentCategories: ['RATES'], - }, - ], - supportingDocuments: [], - actuaryContacts: [ - { - name: 'Enrico Soletzo', - titleRole: 'person', - email: 'en@example.com', - actuarialFirm: 'MERCER', - }, - ], - } - ) - - await updateTestHealthPlanFormData(stateServer, formData) - - const refetched = await fetchTestHealthPlanPackageById( - stateServer, - submittedOnce.id - ) - - const refetchedFormData = latestFormData(refetched) - - expect(refetchedFormData.submissionDescription).toBe( - 'UPDATED_AFTER_UNLOCK' - ) - - expect(refetchedFormData.rateInfos).toHaveLength(3) - - const rateDocs = refetchedFormData.rateInfos.map( - (r) => r.rateDocuments[0].name - ) - expect(rateDocs).toEqual([ - 'rateDocument.pdf', - 'fake doc', - 'fake doc number two', - ]) - - await resubmitTestHealthPlanPackage( - // SUBMISSION 2 - stateServer, - submittedOnce.id, - 'Test first resubmission reason' - ) - - const unlockedTwice = await unlockTestHealthPlanPackage( - cmsServer, - submittedOnce.id, - 'unlock to remove rate' - ) - - const unlockedFormData = latestFormData(unlockedTwice) - const unlockedRateDocs = unlockedFormData.rateInfos.map( - (r) => r.rateDocuments[0].name - ) - expect(unlockedRateDocs).toEqual([ - 'rateDocument.pdf', - 'fake doc', - 'fake doc number two', - ]) - - // remove the first rate - unlockedFormData.rateInfos = unlockedFormData.rateInfos.slice(1) - - await updateTestHealthPlanFormData(stateServer, unlockedFormData) - - const submittedThrice = await resubmitTestHealthPlanPackage( - // SUBMISSION 3 - stateServer, - submittedOnce.id, - 'Test second resubmission reason' - ) - - const finallySubmittedFormData = latestFormData(submittedThrice) - - expect(finallySubmittedFormData.rateInfos).toHaveLength(2) - const finalRateDocs = finallySubmittedFormData.rateInfos.map( - (r) => r.rateDocuments[0].name - ) - expect(finalRateDocs).toEqual(['fake doc', 'fake doc number two']) - - // check document order - const docsInOrder = - finallySubmittedFormData.rateInfos[0].rateDocuments.map( - (d) => d.name - ) - expect(docsInOrder).toEqual([ - 'fake doc', - 'fake doc 2', - 'fake doc 3', - 'fake doc 4', - ]) - - // check contacts order - const actuariesInOrder = - finallySubmittedFormData.rateInfos[0].actuaryContacts.map( - (c) => c.name - ) - expect(actuariesInOrder).toEqual([ - 'Enrico Soletzo 1', - 'Enrico Soletzo 2', - 'Enrico Soletzo 3', - ]) - - const returnedRevisionIDs = submittedThrice.revisions.map( - (r: HealthPlanRevisionEdge) => r.node.id - ) - - expect(returnedRevisionIDs).toHaveLength(3) - - const formDatas: HealthPlanFormDataType[] = - submittedThrice.revisions.map((r: HealthPlanRevisionEdge) => - base64ToDomain(r.node.formDataProto) - ) - - expect(formDatas).toHaveLength(3) - - // expect(formDatas[0].rateInfos).toHaveLength(2) - // expect(formDatas[1].rateInfos).toHaveLength(3) - // expect(formDatas[2].rateInfos).toHaveLength(1) - - // TODO: The below section tests rate revision history, enable this when that feature is reimplemented - // if (flagValue) { - // // POST REFACTOR. also assert that the correct Rate table entries have been created. - // const prismaClient = await sharedTestPrismaClient() - // - // const rates = [] - // const rateIDs = new Set() - // for (const formData of formDatas) { - // for (const rateInfo of formData.rateInfos) { - // rateIDs.add(rateInfo.id!) - // } - // } - // - // expect(formDatas).toHaveLength(6) - // - // expect(rateIDs.size).toBe(3) - // - // for (const rateID of rateIDs.values()) { - // - // const rateTable = await prismaClient.rateTable.findFirstOrThrow({ - // where: { - // id: rateID - // }, - // include: { - // revisions: true, - // }, - // }) - // rates.push(rateTable) - // } - // - // rates.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) - // - // expect(rates[0].revisions).toHaveLength(3) // this first rate was unlocked twice so should have 3 revisions even though only 2 of them end up associated with our contract. - // expect(rates[1].revisions).toHaveLength(2) - // expect(rates[2].revisions).toHaveLength(2) - // - // } - // throw new Error('Not done with this test yet') - }, 20000) - - it('can be unlocked repeatedly', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, + // Unlock + const unlockResult = await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + unlockedReason: 'Super duper good reason.', }, - ldService: mockLDService, - }) - - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Super duper good reason.' - ) - - await resubmitTestHealthPlanPackage( - stateServer, - stateSubmission.id, - 'Test second resubmission reason' - ) - - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Super duper duper good reason.' - ) - - await resubmitTestHealthPlanPackage( - stateServer, - stateSubmission.id, - 'Test second resubmission reason' - ) - - const draft = await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Very super duper good reason.' - ) - expect(draft.status).toBe('UNLOCKED') - expect(draft.revisions[0].node.unlockInfo?.updatedBy).toBe( - 'zuko@example.com' - ) - expect(draft.revisions[0].node.unlockInfo?.updatedReason).toBe( - 'Very super duper good reason.' - ) - expect( - draft.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain(todaysDate()) - // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z - expect( - draft.revisions[0].node.unlockInfo?.updatedAt.toISOString() - ).toContain('Z') - }, 20000) - - it.todo( - 'returns package where previously linked documents and contacts can be deleted without breaking old revisions' - ) // this can be completed after unlock - want to create, submit, unlock, then re-edit - - it('returns errors if a state user tries to unlock', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + }, + }) - // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + expect(unlockResult.errors).toBeUndefined() + + if (!unlockResult?.data) { + throw new Error('this should never happen') + } + + const unlockedSub: HealthPlanPackage = + unlockResult.data.unlockHealthPlanPackage.pkg + + // After unlock, we should get a draft submission back + expect(unlockedSub.status).toBe('UNLOCKED') + + expect(unlockedSub.revisions).toHaveLength(2) + + expect(unlockedSub.revisions[0].node.submitInfo).toBeNull() + expect(unlockedSub.revisions[1].node.submitInfo).toBeDefined() + expect( + unlockedSub.revisions[1].node.submitInfo?.updatedAt.toISOString() + ).toContain(todaysDate()) + // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z + expect( + unlockedSub.revisions[1].node.submitInfo?.updatedAt.toISOString() + ).toContain('Z') + + expect(unlockedSub.revisions[0].node.unlockInfo).toBeDefined() + expect(unlockedSub.revisions[0].node.unlockInfo?.updatedBy).toBe( + 'zuko@example.com' + ) + expect(unlockedSub.revisions[0].node.unlockInfo?.updatedReason).toBe( + 'Super duper good reason.' + ) + expect( + unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() + ).toContain(todaysDate()) + // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z + expect( + unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() + ).toContain('Z') + }, 20000) + + it('returns a package that can be updated without errors', async () => { + const stateServer = await constructTestPostgresServer() + + // First, create a new submitted submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) - // Unlock - const unlockResult = await stateServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - unlockedReason: 'Super duper good reason.', - }, + // Unlock + const unlockResult = await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + unlockedReason: 'Super duper good reason.', }, - }) - - expect(unlockResult.errors).toBeDefined() - const err = (unlockResult.errors as GraphQLError[])[0] + }, + }) - expect(err.extensions['code']).toBe('FORBIDDEN') - expect(err.message).toBe('user not authorized to unlock package') + expect(unlockResult.errors).toBeUndefined() + const unlockedSub = unlockResult?.data?.unlockHealthPlanPackage.pkg + + // After unlock, we should get a draft submission back + expect(unlockedSub.status).toBe('UNLOCKED') + expect(unlockedSub.revisions[0].node.unlockInfo).toBeDefined() + expect(unlockedSub.revisions[0].node.unlockInfo?.updatedBy).toBe( + 'zuko@example.com' + ) + expect(unlockedSub.revisions[0].node.unlockInfo?.updatedReason).toBe( + 'Super duper good reason.' + ) + expect( + unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() + ).toContain(todaysDate()) + // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z + expect( + unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() + ).toContain('Z') + + const formData = latestFormData(unlockedSub) + + // after unlock we should be able to update that draft submission and get the results + formData.programIDs = [defaultFloridaProgram().id] + formData.submissionType = 'CONTRACT_AND_RATES' as const + formData.submissionDescription = 'UPDATED_AFTER_UNLOCK' + formData.documents = [] + formData.contractType = 'BASE' as const + formData.contractDocuments = [] + formData.managedCareEntities = ['MCO'] + formData.federalAuthorities = ['VOLUNTARY' as const] + formData.stateContacts = [] + formData.addtlActuaryContacts = [] + + await updateTestHealthPlanFormData(stateServer, formData) + + const refetched = await fetchTestHealthPlanPackageById( + stateServer, + stateSubmission.id + ) + + const refetchedFormData = latestFormData(refetched) + + expect(refetchedFormData.submissionDescription).toBe( + 'UPDATED_AFTER_UNLOCK' + ) + }, 20000) + + // this test is currently failing for valid reasons + it('allows for multiple edits, editing the set of revisions correctly', async () => { + const stateServer = await constructTestPostgresServer() + // First, create a new submitted submission // SUBMISSION 1 + const submittedOnce = + await createAndSubmitTestHealthPlanPackage(stateServer) + + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, }) - it('returns errors if trying to unlock package with wrong package status', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, + // Unlock + const unlockedOnce = await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: submittedOnce.id, + unlockedReason: 'Super duper good reason.', }, - ldService: mockLDService, - }) - - // First, create a new draft submission - const stateSubmission = await createAndUpdateTestHealthPlanPackage( - stateServer - ) + }, + }) - // Attempt Unlock Draft - const unlockDraftResult = await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - unlockedReason: 'Super duper good reason.', + expect(unlockedOnce.errors).toBeUndefined() + const unlockedSub = unlockedOnce?.data?.unlockHealthPlanPackage.pkg + + // After unlock, we should get a draft submission back + expect(unlockedSub.status).toBe('UNLOCKED') + expect(unlockedSub.revisions[0].node.unlockInfo).toBeDefined() + expect(unlockedSub.revisions[0].node.unlockInfo?.updatedBy).toBe( + 'zuko@example.com' + ) + expect(unlockedSub.revisions[0].node.unlockInfo?.updatedReason).toBe( + 'Super duper good reason.' + ) + expect( + unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() + ).toContain(todaysDate()) + // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z + expect( + unlockedSub.revisions[0].node.unlockInfo?.updatedAt.toISOString() + ).toContain('Z') + + const formData = latestFormData(unlockedSub) + + // after unlock we should be able to update that draft submission and get the results + formData.submissionDescription = 'UPDATED_AFTER_UNLOCK' + + formData.rateInfos.push( + { + id: uuidv4(), + rateDateStart: new Date(), + rateDateEnd: new Date(), + rateProgramIDs: ['5c10fe9f-bec9-416f-a20c-718b152ad633'], + rateType: 'NEW', + rateDateCertified: new Date(), + rateDocuments: [ + { + name: 'fake doc', + s3URL: 'foo://bar', + sha256: 'fakesha', }, - }, - }) - - expect(unlockDraftResult.errors).toBeDefined() - const err = (unlockDraftResult.errors as GraphQLError[])[0] - - expect(err.extensions).toEqual( - expect.objectContaining({ - code: 'BAD_USER_INPUT', - cause: 'INVALID_PACKAGE_STATUS', - argumentName: 'pkgID', - }) - ) - expect(err.message).toBe( - 'Attempted to unlock package with wrong status' - ) - - await submitTestHealthPlanPackage(stateServer, stateSubmission.id) - - // Unlock Submission - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Super duper good reason.' - ) - - // Attempt Unlock Unlocked - const unlockUnlockedResult = await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - unlockedReason: 'Super duper good reason.', + { + name: 'fake doc 2', + s3URL: 'foo://bar', + sha256: 'fakesha', }, - }, - }) - - expect(unlockUnlockedResult.errors).toBeDefined() - const unlockErr = (unlockUnlockedResult.errors as GraphQLError[])[0] - - expect(unlockErr.extensions).toEqual( - expect.objectContaining({ - code: 'BAD_USER_INPUT', - cause: 'INVALID_PACKAGE_STATUS', - argumentName: 'pkgID', - }) - ) - expect(unlockErr.message).toBe( - 'Attempted to unlock package with wrong status' - ) - }) - - it('returns an error if the submission does not exit', async () => { - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) - - // First, create a new submitted submission - // const stateSubmission = await createAndSubmitTestHealthPlanPackage(stateServer) - - // Unlock - const unlockResult = await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: 'foo-bar', - unlockedReason: 'Super duper good reason.', + { + name: 'fake doc 3', + s3URL: 'foo://bar', + sha256: 'fakesha', }, - }, - }) - - expect(unlockResult.errors).toBeDefined() - const err = (unlockResult.errors as GraphQLError[])[0] - - expect(err.extensions['code']).toBe('BAD_USER_INPUT') - expect(err.message).toBe( - 'A package must exist to be unlocked: foo-bar' - ) + { + name: 'fake doc 4', + s3URL: 'foo://bar', + sha256: 'fakesha', + }, + ], + supportingDocuments: [], + actuaryContacts: [ + { + name: 'Enrico Soletzo 1', + titleRole: 'person', + email: 'en@example.com', + actuarialFirm: 'MERCER', + }, + { + name: 'Enrico Soletzo 2', + titleRole: 'person', + email: 'en@example.com', + actuarialFirm: 'MERCER', + }, + { + name: 'Enrico Soletzo 3', + titleRole: 'person', + email: 'en@example.com', + actuarialFirm: 'MERCER', + }, + ], + }, + { + id: uuidv4(), + rateDateStart: new Date(), + rateDateEnd: new Date(), + rateProgramIDs: ['08d114c2-0c01-4a1a-b8ff-e2b79336672d'], + rateType: 'NEW', + rateDateCertified: new Date(), + rateDocuments: [ + { + name: 'fake doc number two', + s3URL: 'foo://bar', + sha256: 'fakesha', + }, + ], + supportingDocuments: [], + actuaryContacts: [ + { + name: 'Enrico Soletzo', + titleRole: 'person', + email: 'en@example.com', + actuarialFirm: 'MERCER', + }, + ], + } + ) + + await updateTestHealthPlanFormData(stateServer, formData) + + const refetched = await fetchTestHealthPlanPackageById( + stateServer, + submittedOnce.id + ) + + const refetchedFormData = latestFormData(refetched) + + expect(refetchedFormData.submissionDescription).toBe( + 'UPDATED_AFTER_UNLOCK' + ) + + expect(refetchedFormData.rateInfos).toHaveLength(3) + + const rateDocs = refetchedFormData.rateInfos.map( + (r) => r.rateDocuments[0].name + ) + expect(rateDocs).toEqual([ + 'rateDocument.pdf', + 'fake doc', + 'fake doc number two', + ]) + + await resubmitTestHealthPlanPackage( + // SUBMISSION 2 + stateServer, + submittedOnce.id, + 'Test first resubmission reason' + ) + + const unlockedTwice = await unlockTestHealthPlanPackage( + cmsServer, + submittedOnce.id, + 'unlock to remove rate' + ) + + const unlockedFormData = latestFormData(unlockedTwice) + const unlockedRateDocs = unlockedFormData.rateInfos.map( + (r) => r.rateDocuments[0].name + ) + expect(unlockedRateDocs).toEqual([ + 'rateDocument.pdf', + 'fake doc', + 'fake doc number two', + ]) + + // remove the first rate + unlockedFormData.rateInfos = unlockedFormData.rateInfos.slice(1) + + await updateTestHealthPlanFormData(stateServer, unlockedFormData) + + const submittedThrice = await resubmitTestHealthPlanPackage( + // SUBMISSION 3 + stateServer, + submittedOnce.id, + 'Test second resubmission reason' + ) + + const finallySubmittedFormData = latestFormData(submittedThrice) + + expect(finallySubmittedFormData.rateInfos).toHaveLength(2) + const finalRateDocs = finallySubmittedFormData.rateInfos.map( + (r) => r.rateDocuments[0].name + ) + expect(finalRateDocs).toEqual(['fake doc', 'fake doc number two']) + + // check document order + const docsInOrder = + finallySubmittedFormData.rateInfos[0].rateDocuments.map( + (d) => d.name + ) + expect(docsInOrder).toEqual([ + 'fake doc', + 'fake doc 2', + 'fake doc 3', + 'fake doc 4', + ]) + + // check contacts order + const actuariesInOrder = + finallySubmittedFormData.rateInfos[0].actuaryContacts.map( + (c) => c.name + ) + expect(actuariesInOrder).toEqual([ + 'Enrico Soletzo 1', + 'Enrico Soletzo 2', + 'Enrico Soletzo 3', + ]) + + const returnedRevisionIDs = submittedThrice.revisions.map( + (r: HealthPlanRevisionEdge) => r.node.id + ) + + expect(returnedRevisionIDs).toHaveLength(3) + + const formDatas: HealthPlanFormDataType[] = + submittedThrice.revisions.map((r: HealthPlanRevisionEdge) => + base64ToDomain(r.node.formDataProto) + ) + + expect(formDatas).toHaveLength(3) + + // expect(formDatas[0].rateInfos).toHaveLength(2) + // expect(formDatas[1].rateInfos).toHaveLength(3) + // expect(formDatas[2].rateInfos).toHaveLength(1) + + // TODO: The below section tests rate revision history, enable this when that feature is reimplemented + // if (flagValue) { + // // POST REFACTOR. also assert that the correct Rate table entries have been created. + // const prismaClient = await sharedTestPrismaClient() + // + // const rates = [] + // const rateIDs = new Set() + // for (const formData of formDatas) { + // for (const rateInfo of formData.rateInfos) { + // rateIDs.add(rateInfo.id!) + // } + // } + // + // expect(formDatas).toHaveLength(6) + // + // expect(rateIDs.size).toBe(3) + // + // for (const rateID of rateIDs.values()) { + // + // const rateTable = await prismaClient.rateTable.findFirstOrThrow({ + // where: { + // id: rateID + // }, + // include: { + // revisions: true, + // }, + // }) + // rates.push(rateTable) + // } + // + // rates.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) + // + // expect(rates[0].revisions).toHaveLength(3) // this first rate was unlocked twice so should have 3 revisions even though only 2 of them end up associated with our contract. + // expect(rates[1].revisions).toHaveLength(2) + // expect(rates[2].revisions).toHaveLength(2) + // + // } + // throw new Error('Not done with this test yet') + }, 20000) + + it('can be unlocked repeatedly', async () => { + const stateServer = await constructTestPostgresServer() + + // First, create a new submitted submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, }) - it('returns an error if the DB errors', async () => { - const errorStore = mockStoreThatErrors() - - const cmsServer = await constructTestPostgresServer({ - store: errorStore, - context: { - user: cmsUser, + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Super duper good reason.' + ) + + await resubmitTestHealthPlanPackage( + stateServer, + stateSubmission.id, + 'Test second resubmission reason' + ) + + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Super duper duper good reason.' + ) + + await resubmitTestHealthPlanPackage( + stateServer, + stateSubmission.id, + 'Test second resubmission reason' + ) + + const draft = await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Very super duper good reason.' + ) + expect(draft.status).toBe('UNLOCKED') + expect(draft.revisions[0].node.unlockInfo?.updatedBy).toBe( + 'zuko@example.com' + ) + expect(draft.revisions[0].node.unlockInfo?.updatedReason).toBe( + 'Very super duper good reason.' + ) + expect( + draft.revisions[0].node.unlockInfo?.updatedAt.toISOString() + ).toContain(todaysDate()) + // check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z + expect( + draft.revisions[0].node.unlockInfo?.updatedAt.toISOString() + ).toContain('Z') + }, 20000) + + it.todo( + 'returns package where previously linked documents and contacts can be deleted without breaking old revisions' + ) // this can be completed after unlock - want to create, submit, unlock, then re-edit + + it('returns errors if a state user tries to unlock', async () => { + const stateServer = await constructTestPostgresServer() + + // First, create a new submitted submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + + // Unlock + const unlockResult = await stateServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + unlockedReason: 'Super duper good reason.', }, - ldService: mockLDService, - }) + }, + }) - // Unlock - const unlockResult = await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: 'foo-bar', - unlockedReason: 'Super duper good reason.', - }, - }, - }) + expect(unlockResult.errors).toBeDefined() + const err = (unlockResult.errors as GraphQLError[])[0] - expect(unlockResult.errors).toBeDefined() - const err = (unlockResult.errors as GraphQLError[])[0] + expect(err.extensions['code']).toBe('FORBIDDEN') + expect(err.message).toBe('user not authorized to unlock package') + }) - expect(err.extensions['code']).toBe('INTERNAL_SERVER_ERROR') - expect(err.message).toContain( - 'error came from the generic store with errors mock' - ) + it('returns errors if trying to unlock package with wrong package status', async () => { + const stateServer = await constructTestPostgresServer() + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, }) - it('returns errors if unlocked reason is undefined', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, + // First, create a new draft submission + const stateSubmission = + await createAndUpdateTestHealthPlanPackage(stateServer) + + // Attempt Unlock Draft + const unlockDraftResult = await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + unlockedReason: 'Super duper good reason.', }, - ldService: mockLDService, - }) + }, + }) - // Attempt Unlock Draft - const unlockedResult = await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - unlockedReason: undefined, - }, + expect(unlockDraftResult.errors).toBeDefined() + const err = (unlockDraftResult.errors as GraphQLError[])[0] + + expect(err.extensions).toEqual( + expect.objectContaining({ + code: 'BAD_USER_INPUT', + cause: 'INVALID_PACKAGE_STATUS', + argumentName: 'pkgID', + }) + ) + expect(err.message).toBe( + 'Attempted to unlock package with wrong status' + ) + + await submitTestHealthPlanPackage(stateServer, stateSubmission.id) + + // Unlock Submission + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Super duper good reason.' + ) + + // Attempt Unlock Unlocked + const unlockUnlockedResult = await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + unlockedReason: 'Super duper good reason.', }, - }) - - expect(unlockedResult.errors).toBeDefined() - const err = (unlockedResult.errors as GraphQLError[])[0] + }, + }) - expect(err.extensions['code']).toBe('BAD_USER_INPUT') - expect(err.message).toContain( - 'Field "unlockedReason" of required type "String!" was not provided.' - ) + expect(unlockUnlockedResult.errors).toBeDefined() + const unlockErr = (unlockUnlockedResult.errors as GraphQLError[])[0] + + expect(unlockErr.extensions).toEqual( + expect.objectContaining({ + code: 'BAD_USER_INPUT', + cause: 'INVALID_PACKAGE_STATUS', + argumentName: 'pkgID', + }) + ) + expect(unlockErr.message).toBe( + 'Attempted to unlock package with wrong status' + ) + }) + + it('returns an error if the submission does not exit', async () => { + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, }) - it('send email to CMS when unlocking submission succeeds', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + // First, create a new submitted submission + // const stateSubmission = await createAndSubmitTestHealthPlanPackage(stateServer) - // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, + // Unlock + const unlockResult = await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: 'foo-bar', + unlockedReason: 'Super duper good reason.', }, - ldService: mockLDService, - emailer: mockEmailer, - }) + }, + }) - // Unlock - const unlockResult = await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Super duper good reason.' - ) + expect(unlockResult.errors).toBeDefined() + const err = (unlockResult.errors as GraphQLError[])[0] - const currentRevision = unlockResult.revisions[0].node.formDataProto + expect(err.extensions['code']).toBe('BAD_USER_INPUT') + expect(err.message).toBe('A package must exist to be unlocked: foo-bar') + }) - const sub = base64ToDomain(currentRevision) - if (sub instanceof Error) { - throw sub - } + it('returns an error if the DB errors', async () => { + const errorStore = mockStoreThatErrors() - const programs = [defaultFloridaProgram()] - const ratePrograms = [defaultFloridaRateProgram()] - const name = packageName( - sub.stateCode, - sub.stateNumber, - sub.programIDs, - programs - ) - const rateName = generateRateName( - sub, - sub.rateInfos[0], - ratePrograms - ) - const stateAnalystsEmails = getTestStateAnalystsEmails( - sub.stateCode - ) - - const cmsEmails = [ - ...config.devReviewTeamEmails, - ...stateAnalystsEmails, - ...config.oactEmails, - ] - - // email subject line is correct for CMS email - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: expect.stringContaining(`${name} was unlocked`), - sourceEmail: config.emailSource, - toAddresses: expect.arrayContaining(Array.from(cmsEmails)), - bodyHTML: expect.stringContaining(rateName), - }) - ) + const cmsServer = await constructTestPostgresServer({ + store: errorStore, + context: { + user: cmsUser, + }, }) - it('send state email to state contacts and all submitters when unlocking submission succeeds', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const stateServerTwo = await constructTestPostgresServer({ - context: { - user: testStateUser({ - email: 'notspiderman@example.com', - }), + // Unlock + const unlockResult = await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: 'foo-bar', + unlockedReason: 'Super duper good reason.', }, - ldService: mockLDService, - }) - - // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + }, + }) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - emailer: mockEmailer, - }) + expect(unlockResult.errors).toBeDefined() + const err = (unlockResult.errors as GraphQLError[])[0] - // First unlock - await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Super duper good reason.' - ) + expect(err.extensions['code']).toBe('INTERNAL_SERVER_ERROR') + expect(err.message).toContain( + 'error came from the generic store with errors mock' + ) + }) - // Resubmission to get multiple submitters - await resubmitTestHealthPlanPackage( - stateServerTwo, - stateSubmission.id, - 'Test resubmission reason' - ) + it('returns errors if unlocked reason is undefined', async () => { + const stateServer = await constructTestPostgresServer() - // Final unlock to test against - const unlockResult = await unlockTestHealthPlanPackage( - cmsServer, - stateSubmission.id, - 'Super duper good reason.' - ) + // First, create a new submitted submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) - const currentRevision = unlockResult.revisions[0].node.formDataProto + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) - const sub = base64ToDomain(currentRevision) - if (sub instanceof Error) { - throw sub - } + // Attempt Unlock Draft + const unlockedResult = await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + unlockedReason: undefined, + }, + }, + }) - const programs = [defaultFloridaProgram()] - const ratePrograms = [defaultFloridaRateProgram()] - const name = packageName( - sub.stateCode, - sub.stateNumber, - sub.programIDs, - programs - ) - const rateName = generateRateName( - sub, - sub.rateInfos[0], - ratePrograms - ) + expect(unlockedResult.errors).toBeDefined() + const err = (unlockedResult.errors as GraphQLError[])[0] + + expect(err.extensions['code']).toBe('BAD_USER_INPUT') + expect(err.message).toContain( + 'Field "unlockedReason" of required type "String!" was not provided.' + ) + }) + + it('send email to CMS when unlocking submission succeeds', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const stateServer = await constructTestPostgresServer() + + // First, create a new submitted submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + emailer: mockEmailer, + }) - const stateReceiverEmails = [ - 'james@example.com', - 'notspiderman@example.com', - ...sub.stateContacts.map((contact) => contact.email), - ] - - // email subject line is correct for CMS email. - // Mock emailer is called 4 times, 2 for the first unlock, 2 for the second unlock. - // From the pair of emails, the second one is the state email. - expect(mockEmailer.sendEmail).toHaveBeenNthCalledWith( - 4, - expect.objectContaining({ - subject: expect.stringContaining( - `${name} was unlocked by CMS` - ), - sourceEmail: config.emailSource, - toAddresses: expect.arrayContaining( - Array.from(stateReceiverEmails) - ), - bodyHTML: expect.stringContaining(rateName), - }) - ) + // Unlock + const unlockResult = await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Super duper good reason.' + ) + + const currentRevision = unlockResult.revisions[0].node.formDataProto + + const sub = base64ToDomain(currentRevision) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const ratePrograms = [defaultFloridaRateProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + const rateName = generateRateName(sub, sub.rateInfos[0], ratePrograms) + const stateAnalystsEmails = getTestStateAnalystsEmails(sub.stateCode) + + const cmsEmails = [ + ...config.devReviewTeamEmails, + ...stateAnalystsEmails, + ...config.oactEmails, + ] + + // email subject line is correct for CMS email + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: expect.stringContaining(`${name} was unlocked`), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining(Array.from(cmsEmails)), + bodyHTML: expect.stringContaining(rateName), + }) + ) + }) + + it('send state email to state contacts and all submitters when unlocking submission succeeds', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const stateServer = await constructTestPostgresServer() + const stateServerTwo = await constructTestPostgresServer({ + context: { + user: testStateUser({ + email: 'notspiderman@example.com', + }), + }, }) - it('does send unlock email when request for state analysts emails fails', async () => { - const config = testEmailConfig() - const mockEmailer = testEmailer(config) - //mock invoke email submit lambda - const mockEmailParameterStore = mockEmailParameterStoreError() - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + // First, create a new submitted submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) - // First, create a new submitted submission - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + emailer: mockEmailer, + }) - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - emailer: mockEmailer, - emailParameterStore: mockEmailParameterStore, - }) + // First unlock + await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Super duper good reason.' + ) + + // Resubmission to get multiple submitters + await resubmitTestHealthPlanPackage( + stateServerTwo, + stateSubmission.id, + 'Test resubmission reason' + ) + + // Final unlock to test against + const unlockResult = await unlockTestHealthPlanPackage( + cmsServer, + stateSubmission.id, + 'Super duper good reason.' + ) + + const currentRevision = unlockResult.revisions[0].node.formDataProto + + const sub = base64ToDomain(currentRevision) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const ratePrograms = [defaultFloridaRateProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + const rateName = generateRateName(sub, sub.rateInfos[0], ratePrograms) + + const stateReceiverEmails = [ + 'james@example.com', + 'notspiderman@example.com', + ...sub.stateContacts.map((contact) => contact.email), + ] + + // email subject line is correct for CMS email. + // Mock emailer is called 4 times, 2 for the first unlock, 2 for the second unlock. + // From the pair of emails, the second one is the state email. + expect(mockEmailer.sendEmail).toHaveBeenNthCalledWith( + 4, + expect.objectContaining({ + subject: expect.stringContaining(`${name} was unlocked by CMS`), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining( + Array.from(stateReceiverEmails) + ), + bodyHTML: expect.stringContaining(rateName), + }) + ) + }) + + it('does send unlock email when request for state analysts emails fails', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const mockEmailParameterStore = mockEmailParameterStoreError() + const stateServer = await constructTestPostgresServer() + + // First, create a new submitted submission + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + emailer: mockEmailer, + emailParameterStore: mockEmailParameterStore, + }) - // Unlock - await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - unlockedReason: 'Super duper good reason.', - }, + // Unlock + await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + unlockedReason: 'Super duper good reason.', }, - }) - - expect(mockEmailer.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - toAddresses: expect.arrayContaining( - Array.from(config.devReviewTeamEmails) - ), - }) - ) + }, }) - it('does log error when request for state specific analysts emails failed', async () => { - const mockEmailParameterStore = mockEmailParameterStoreError() - const consoleErrorSpy = jest.spyOn(console, 'error') - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const error = { - error: 'No store found', - message: 'getStateAnalystsEmails failed', - operation: 'getStateAnalystsEmails', - status: 'ERROR', - } - - const stateSubmission = await createAndSubmitTestHealthPlanPackage( - stateServer - ) - - const cmsServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - emailParameterStore: mockEmailParameterStore, - }) + expect(mockEmailer.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + toAddresses: expect.arrayContaining( + Array.from(config.devReviewTeamEmails) + ), + }) + ) + }) + + it('does log error when request for state specific analysts emails failed', async () => { + const mockEmailParameterStore = mockEmailParameterStoreError() + const consoleErrorSpy = jest.spyOn(console, 'error') + const stateServer = await constructTestPostgresServer() + const error = { + error: 'No store found', + message: 'getStateAnalystsEmails failed', + operation: 'getStateAnalystsEmails', + status: 'ERROR', + } + + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + emailParameterStore: mockEmailParameterStore, + }) - await cmsServer.executeOperation({ - query: UNLOCK_HEALTH_PLAN_PACKAGE, - variables: { - input: { - pkgID: stateSubmission.id, - unlockedReason: 'Super duper good reason.', - }, + await cmsServer.executeOperation({ + query: UNLOCK_HEALTH_PLAN_PACKAGE, + variables: { + input: { + pkgID: stateSubmission.id, + unlockedReason: 'Super duper good reason.', }, - }) - - expect(consoleErrorSpy).toHaveBeenCalledWith(error) + }, }) - } -) + + expect(consoleErrorSpy).toHaveBeenCalledWith(error) + }) +}) diff --git a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.ts index 03fd65a08e..9a972f3339 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/unlockHealthPlanPackage.ts @@ -1,18 +1,10 @@ import { ForbiddenError, UserInputError } from 'apollo-server-lambda' -import type { - UnlockedHealthPlanFormDataType, - LockedHealthPlanFormDataType, -} from '../../../../app-web/src/common-code/healthPlanFormDataType' +import type { UnlockedHealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' import { toDomain } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import type { - UpdateInfoType, - HealthPlanPackageType, - ContractType, -} from '../../domain-models' +import type { UpdateInfoType, ContractType } from '../../domain-models' import { isCMSUser, convertContractWithRatesToUnlockedHPP, - packageStatus, packageSubmitters, } from '../../domain-models' import type { Emailer } from '../../emailer' @@ -20,7 +12,6 @@ import type { MutationResolvers } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' import { NotFoundError } from '../../postgres' import type { Store } from '../../postgres' -import { isStoreError } from '../../postgres' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, @@ -28,36 +19,14 @@ import { } from '../attributeHelper' import type { EmailParameterStore } from '../../parameterStore' import { GraphQLError } from 'graphql' -import type { LDService } from '../../launchDarkly/launchDarkly' - -// unlock is a state machine transforming a LockedFormData and turning it into UnlockedFormData -// Since Unlocked is a strict subset of Locked, this can't error today. -function unlock( - submission: LockedHealthPlanFormDataType -): UnlockedHealthPlanFormDataType { - const draft: UnlockedHealthPlanFormDataType = { - ...submission, - status: 'DRAFT', - } - // this method does persist the submittedAt field onto the draft, but typescript won't let - // us access it so that's fine. - - return draft -} // unlockHealthPlanPackageResolver is a state machine transition for HealthPlanPackage export function unlockHealthPlanPackageResolver( store: Store, emailer: Emailer, - emailParameterStore: EmailParameterStore, - launchDarkly: LDService + emailParameterStore: EmailParameterStore ): MutationResolvers['unlockHealthPlanPackage'] { return async (_parent, { input }, context) => { - const ratesDatabaseRefactor = await launchDarkly.getFeatureFlag( - context, - 'rates-db-refactor' - ) - const { user, span } = context const { unlockedReason, pkgID } = input setResolverDetailsOnActiveSpan('unlockHealthPlanPackage', user, span) @@ -76,223 +45,124 @@ export function unlockHealthPlanPackageResolver( throw new ForbiddenError('user not authorized to unlock package') } - let unlockedPackage: HealthPlanPackageType | undefined = undefined - - if (ratesDatabaseRefactor) { - const contractResult = await store.findContractWithHistory(pkgID) + const contractResult = await store.findContractWithHistory(pkgID) - if (contractResult instanceof Error) { - if (contractResult instanceof NotFoundError) { - const errMessage = `A package must exist to be unlocked: ${pkgID}` - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - argumentName: 'pkgID', - }) - } - - const errMessage = `Issue finding a package. Message: ${contractResult.message}` - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - - const contract: ContractType = contractResult - - if (contract.draftRevision) { - const errMessage = `Attempted to unlock package with wrong status` + if (contractResult instanceof Error) { + if (contractResult instanceof NotFoundError) { + const errMessage = `A package must exist to be unlocked: ${pkgID}` logError('unlockHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new UserInputError(errMessage, { argumentName: 'pkgID', - cause: 'INVALID_PACKAGE_STATUS', }) } - // unlock all the revisions, then unlock the contract, in a transaction. - const currentRateRevIDs = contract.revisions[0].rateRevisions.map( - (rr) => rr.id - ) - const unlockRatePromises = [] - for (const rateRevisionID of currentRateRevIDs) { - const resPromise = store.unlockRate({ - rateRevisionID, - unlockReason: unlockedReason, - unlockedByUserID: user.id, - }) - - unlockRatePromises.push(resPromise) - } + const errMessage = `Issue finding a package. Message: ${contractResult.message}` + logError('unlockHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - const unlockRateResults = await Promise.all(unlockRatePromises) - // if any of the promises reject, which shouldn't happen b/c we don't throw... - if (unlockRateResults instanceof Error) { - const errMessage = `Failed to unlock contract rates with ID: ${contract.id}; ${unlockRateResults.message}` - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } + const contract: ContractType = contractResult - const unlockRateErrors: Error[] = unlockRateResults.filter( - (res) => res instanceof Error - ) as Error[] - if (unlockRateErrors.length > 0) { - console.error('Errors unlocking Rates: ', unlockRateErrors) - const errMessage = `Failed to submit contract revision's rates with ID: ${ - contract.id - }; ${unlockRateErrors.map((err) => err.message)}` - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } + if (contract.draftRevision) { + const errMessage = `Attempted to unlock package with wrong status` + logError('unlockHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new UserInputError(errMessage, { + argumentName: 'pkgID', + cause: 'INVALID_PACKAGE_STATUS', + }) + } - // Now, unlock the contract! - const unlockContractResult = await store.unlockContract({ - contractID: contract.id, + // unlock all the revisions, then unlock the contract, in a transaction. + const currentRateRevIDs = contract.revisions[0].rateRevisions.map( + (rr) => rr.id + ) + const unlockRatePromises = [] + for (const rateRevisionID of currentRateRevIDs) { + const resPromise = store.unlockRate({ + rateRevisionID, unlockReason: unlockedReason, unlockedByUserID: user.id, }) - if (unlockContractResult instanceof Error) { - const errMessage = `Failed to unlock contract revision with ID: ${contract.id}; ${unlockContractResult.message}` - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - - const unlockedPKGResult = - convertContractWithRatesToUnlockedHPP(unlockContractResult) - - if (unlockedPKGResult instanceof Error) { - const errMessage = `Error converting draft contract. Message: ${unlockedPKGResult.message}` - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } - - // set variables used across feature flag boundary - unlockedPackage = unlockedPKGResult - } else { - // pre-rates refactor code path - // fetch from the store - const result = await store.findHealthPlanPackage(pkgID) - - if (isStoreError(result)) { - const errMessage = `Issue finding a package of type ${result.code}. Message: ${result.message}` - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - - if (result === undefined) { - const errMessage = `A package must exist to be unlocked: ${pkgID}` - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - argumentName: 'pkgID', - }) - } - - const pkg: HealthPlanPackageType = result - const pkgStatus = packageStatus(pkg) - const currentRevision = pkg.revisions[0] - - // Check that the package is in an unlockable state - if (pkgStatus === 'UNLOCKED' || pkgStatus === 'DRAFT') { - const errMessage = - 'Attempted to unlock package with wrong status' - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - argumentName: 'pkgID', - cause: 'INVALID_PACKAGE_STATUS', - }) - } - - // pull the current revision out to unlock it. - const formDataResult = toDomain(currentRevision.formDataProto) - if (formDataResult instanceof Error) { - const errMessage = `Failed to decode proto ${formDataResult}.` - logError('unlockHealthPlanPackage', errMessage) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } - if (formDataResult.status !== 'SUBMITTED') { - const errMessage = `A locked package had unlocked formData.` - logError('unlockHealthPlanPackage', errMessage) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'INVALID_PACKAGE_STATUS', - }, - }) - } + unlockRatePromises.push(resPromise) + } - const draftformData: UnlockedHealthPlanFormDataType = - unlock(formDataResult) + const unlockRateResults = await Promise.all(unlockRatePromises) + // if any of the promises reject, which shouldn't happen b/c we don't throw... + if (unlockRateResults instanceof Error) { + const errMessage = `Failed to unlock contract rates with ID: ${contract.id}; ${unlockRateResults.message}` + logError('unlockHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - // Create a new revision with this draft in it - const updateInfo: UpdateInfoType = { - updatedAt: new Date(), - updatedBy: context.user.email, - updatedReason: unlockedReason, - } + const unlockRateErrors: Error[] = unlockRateResults.filter( + (res) => res instanceof Error + ) as Error[] + if (unlockRateErrors.length > 0) { + console.error('Errors unlocking Rates: ', unlockRateErrors) + const errMessage = `Failed to submit contract revision's rates with ID: ${ + contract.id + }; ${unlockRateErrors.map((err) => err.message)}` + logError('unlockHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - const unlockedPkg = await store.insertHealthPlanRevision( - pkgID, - updateInfo, - draftformData - ) + // Now, unlock the contract! + const unlockContractResult = await store.unlockContract({ + contractID: contract.id, + unlockReason: unlockedReason, + unlockedByUserID: user.id, + }) + if (unlockContractResult instanceof Error) { + const errMessage = `Failed to unlock contract revision with ID: ${contract.id}; ${unlockContractResult.message}` + logError('unlockHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - if (isStoreError(unlockedPkg)) { - const errMessage = `Issue unlocking a package of type ${unlockedPkg.code}. Message: ${unlockedPkg.message}` - logError('unlockHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } + const unlockedPKGResult = + convertContractWithRatesToUnlockedHPP(unlockContractResult) - unlockedPackage = unlockedPkg + if (unlockedPKGResult instanceof Error) { + const errMessage = `Error converting draft contract. Message: ${unlockedPKGResult.message}` + logError('unlockHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) } + // set variables used across feature flag boundary + const unlockedPackage = unlockedPKGResult + // Send emails! const formDataResult = toDomain( diff --git a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts index 572b7a1c8e..299258bb99 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.test.ts @@ -1,6 +1,5 @@ import { v4 as uuidv4 } from 'uuid' import { findStatePrograms, NewPostgresStore } from '../../postgres' -import * as add_sha from '../../handlers/add_sha' import { submitContract } from '../../postgres/contractAndRates/submitContract' import UPDATE_HEALTH_PLAN_FORM_DATA from '../../../../app-graphql/src/mutations/updateHealthPlanFormData.graphql' import { domainToBase64 } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' @@ -16,985 +15,891 @@ import { } from '../../testHelpers/storeHelpers' import { constructTestPostgresServer, - createAndSubmitTestHealthPlanPackage, createTestHealthPlanPackage, } from '../../testHelpers/gqlHelpers' import { testCMSUser, testStateUser } from '../../testHelpers/userHelpers' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' import { must } from '../../testHelpers' -import type { - FeatureFlagLDConstant, - FlagValue, -} from '../../../../app-web/src/common-code/featureFlags' - -const flagValueTestParameters: { - flagName: FeatureFlagLDConstant - flagValue: FlagValue - testName: string -}[] = [ - { - flagName: 'rates-db-refactor', - flagValue: false, - testName: 'updateHealthPlanFormData with all feature flags off', - }, - { - flagName: 'rates-db-refactor', - flagValue: true, - testName: 'updateHealthPlanFormData with rates-db-refactor on', - }, -] - -describe.each(flagValueTestParameters)( - `Tests $testName`, - ({ flagName, flagValue }) => { - const cmsUser = testCMSUser() - const mockLDService = testLDService({ [flagName]: flagValue }) - - beforeEach(() => { - jest.resetAllMocks() - jest.spyOn(add_sha, 'calculateSHA256').mockImplementation(() => { - return Promise.resolve('mockSHA256') - }) - }) - - it('updates valid scalar fields in the formData', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const createdDraft = await createTestHealthPlanPackage(server) - - // update that draft. - const formData = Object.assign(latestFormData(createdDraft), { - programIDs: [], - populationCovered: 'MEDICAID', - submissionType: 'CONTRACT_ONLY', - riskBasedContract: true, - submissionDescription: 'Updated submission', - stateContacts: [], - documents: [], - contractType: 'BASE', - contractExecutionStatus: 'EXECUTED', - contractDocuments: [], - contractDateStart: new Date(Date.UTC(2025, 5, 1)), - contractDateEnd: new Date(Date.UTC(2026, 5, 1)), - managedCareEntities: ['MCO'], - federalAuthorities: [], - contractAmendmentInfo: { - modifiedProvisions: { - inLieuServicesAndSettings: true, - modifiedBenefitsProvided: true, - modifiedGeoAreaServed: true, - modifiedMedicaidBeneficiaries: true, - modifiedRiskSharingStrategy: true, - modifiedIncentiveArrangements: true, - modifiedWitholdAgreements: true, - modifiedStateDirectedPayments: true, - modifiedPassThroughPayments: false, - modifiedPaymentsForMentalDiseaseInstitutions: false, - modifiedMedicalLossRatioStandards: false, - modifiedOtherFinancialPaymentIncentive: false, - modifiedEnrollmentProcess: false, - modifiedGrevienceAndAppeal: false, - modifiedNetworkAdequacyStandards: undefined, - modifiedLengthOfContract: undefined, - modifiedNonRiskPaymentArrangements: undefined, - }, +describe(`Tests UpdateHealthPlanFormData`, () => { + const cmsUser = testCMSUser() + + beforeEach(() => { + jest.resetAllMocks() + }) + + it('updates valid scalar fields in the formData', async () => { + const server = await constructTestPostgresServer() + + const createdDraft = await createTestHealthPlanPackage(server) + + // update that draft. + const formData = Object.assign(latestFormData(createdDraft), { + programIDs: [], + populationCovered: 'MEDICAID', + submissionType: 'CONTRACT_ONLY', + riskBasedContract: true, + submissionDescription: 'Updated submission', + stateContacts: [], + documents: [], + contractType: 'BASE', + contractExecutionStatus: 'EXECUTED', + contractDocuments: [], + contractDateStart: new Date(Date.UTC(2025, 5, 1)), + contractDateEnd: new Date(Date.UTC(2026, 5, 1)), + managedCareEntities: ['MCO'], + federalAuthorities: [], + contractAmendmentInfo: { + modifiedProvisions: { + inLieuServicesAndSettings: true, + modifiedBenefitsProvided: true, + modifiedGeoAreaServed: true, + modifiedMedicaidBeneficiaries: true, + modifiedRiskSharingStrategy: true, + modifiedIncentiveArrangements: true, + modifiedWitholdAgreements: true, + modifiedStateDirectedPayments: true, + modifiedPassThroughPayments: false, + modifiedPaymentsForMentalDiseaseInstitutions: false, + modifiedMedicalLossRatioStandards: false, + modifiedOtherFinancialPaymentIncentive: false, + modifiedEnrollmentProcess: false, + modifiedGrevienceAndAppeal: false, + modifiedNetworkAdequacyStandards: undefined, + modifiedLengthOfContract: undefined, + modifiedNonRiskPaymentArrangements: undefined, }, - statutoryRegulatoryAttestation: true, - statutoryRegulatoryAttestationDescription: undefined, - rateInfos: [], - }) + }, + statutoryRegulatoryAttestation: true, + statutoryRegulatoryAttestationDescription: undefined, + rateInfos: [], + }) - // convert to base64 proto - const updatedB64 = domainToBase64(formData) + // convert to base64 proto + const updatedB64 = domainToBase64(formData) - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: updatedB64, - }, + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: updatedB64, }, - }) - - expect(updateResult.errors).toBeUndefined() - - const healthPlanPackage = - updateResult.data?.updateHealthPlanFormData.pkg + }, + }) - const updatedFormData = latestFormData(healthPlanPackage) - expect(updatedFormData).toEqual( - expect.objectContaining({ - ...formData, - updatedAt: expect.any(Date), - }) - ) + expect(updateResult.errors).toBeUndefined() + + const healthPlanPackage = + updateResult.data?.updateHealthPlanFormData.pkg + + const updatedFormData = latestFormData(healthPlanPackage) + expect(updatedFormData).toEqual( + expect.objectContaining({ + ...formData, + updatedAt: expect.any(Date), + }) + ) + }) + + it('creates, updates, and deletes rates in the contract', async () => { + const stateUser = { + id: uuidv4(), + givenName: 'Aang', + familyName: 'Avatar', + email: 'aang@example.com', + role: 'STATE_USER' as const, + stateCode: 'MN', + } + const server = await constructTestPostgresServer({ + context: { + user: stateUser, + }, }) - it('creates, updates, and deletes rates in the contract', async () => { - const stateUser = { - id: uuidv4(), - givenName: 'Aang', - familyName: 'Avatar', - email: 'aang@example.com', - role: 'STATE_USER' as const, - stateCode: 'MN', - } - const server = await constructTestPostgresServer({ - ldService: mockLDService, - context: { - user: stateUser, + const stateCode = 'MN' + const createdDraft = await createTestHealthPlanPackage( + server, + stateCode + ) + const statePrograms = must(findStatePrograms(createdDraft.stateCode)) + + // Create 2 valid contracts to attached to packagesWithSharedRateCerts + const createdDraftTwo = await createTestHealthPlanPackage( + server, + stateCode + ) + const createdDraftThree = await createTestHealthPlanPackage( + server, + stateCode + ) + + const createdDraftTwoFormData = latestFormData(createdDraftTwo) + const createdDraftThreeFormData = latestFormData(createdDraftThree) + + const packageWithSharedRate1 = { + packageId: createdDraftTwo.id, + packageName: packageName( + createdDraftTwo.stateCode, + createdDraftTwoFormData.stateNumber, + createdDraftTwoFormData.programIDs, + statePrograms + ), + } as const + + const packageWithSharedRate2 = { + packageId: createdDraftThree.id, + packageName: packageName( + createdDraftThree.stateCode, + createdDraftThreeFormData.stateNumber, + createdDraftThreeFormData.programIDs, + statePrograms + ), + } as const + + // Create 2 rate data for insertion + const rate1 = { + id: uuidv4(), + rateType: 'NEW' as const, + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: new Date(Date.UTC(2025, 3, 15)), + rateDocuments: [ + { + name: 'rateDocument.pdf', + s3URL: 's3://bucketname/key/supporting-documents', + sha256: 'rate1-sha', }, - }) + ], + rateAmendmentInfo: undefined, + rateCapitationType: undefined, + rateCertificationName: undefined, + supportingDocuments: [], + //We only want one rate ID and use last program in list to differentiate from programID if possible. + rateProgramIDs: [statePrograms.reverse()[0].id], + actuaryContacts: [ + { + name: 'test name', + titleRole: 'test title', + email: 'email@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + ], + packagesWithSharedRateCerts: [ + packageWithSharedRate1, + packageWithSharedRate2, + ], + } + + const rate2 = { + id: uuidv4(), + rateType: 'NEW' as const, + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: new Date(Date.UTC(2025, 3, 15)), + rateDocuments: [ + { + name: 'rateDocument.pdf', + s3URL: 's3://bucketname/key/supporting-documents', + sha256: 'rate2-sha', + }, + ], + rateAmendmentInfo: undefined, + rateCapitationType: undefined, + rateCertificationName: undefined, + supportingDocuments: [], + //We only want one rate ID and use last program in list to differentiate from programID if possible. + rateProgramIDs: [statePrograms.reverse()[0].id], + actuaryContacts: [ + { + name: 'test name', + titleRole: 'test title', + email: 'email@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + ], + packagesWithSharedRateCerts: [], + } - const stateCode = 'MN' - const createdDraft = await createTestHealthPlanPackage( - server, - stateCode - ) - const statePrograms = must( - findStatePrograms(createdDraft.stateCode) - ) + // update that draft form data. + const formData = Object.assign(latestFormData(createdDraft), { + addtlActuaryContacts: [ + { + name: 'additional actuary 1', + titleRole: 'additional actuary title 1', + email: 'additionalactuary1@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + { + name: 'additional actuary 2', + titleRole: 'additional actuary title 2', + email: 'additionalactuary1@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + ], + rateInfos: [rate1, rate2], + }) - // Create 2 valid contracts to attached to packagesWithSharedRateCerts - const createdDraftTwo = await createTestHealthPlanPackage( - server, - stateCode - ) - const createdDraftThree = await createTestHealthPlanPackage( - server, - stateCode - ) + // convert to base64 proto + const updatedB64 = domainToBase64(formData) - const createdDraftTwoFormData = latestFormData(createdDraftTwo) - const createdDraftThreeFormData = latestFormData(createdDraftThree) - - const packageWithSharedRate1 = { - packageId: createdDraftTwo.id, - packageName: packageName( - createdDraftTwo.stateCode, - createdDraftTwoFormData.stateNumber, - createdDraftTwoFormData.programIDs, - statePrograms - ), - } as const - - const packageWithSharedRate2 = { - packageId: createdDraftThree.id, - packageName: packageName( - createdDraftThree.stateCode, - createdDraftThreeFormData.stateNumber, - createdDraftThreeFormData.programIDs, - statePrograms - ), - } as const - - // Create 2 rate data for insertion - const rate1 = { - id: uuidv4(), - rateType: 'NEW' as const, - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: new Date(Date.UTC(2025, 3, 15)), - rateDocuments: [ - { - name: 'rateDocument.pdf', - s3URL: 's3://bucketname/key/supporting-documents', - documentCategories: ['RATES' as const], - sha256: 'rate1-sha', - }, - ], - rateAmendmentInfo: undefined, - rateCapitationType: undefined, - rateCertificationName: undefined, - supportingDocuments: [], - //We only want one rate ID and use last program in list to differentiate from programID if possible. - rateProgramIDs: [statePrograms.reverse()[0].id], - actuaryContacts: [ - { - name: 'test name', - titleRole: 'test title', - email: 'email@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', - }, - ], - packagesWithSharedRateCerts: [ - packageWithSharedRate1, - packageWithSharedRate2, - ], - } + // update the DB contract + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: updatedB64, + }, + }, + }) - const rate2 = { - id: uuidv4(), - rateType: 'NEW' as const, - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: new Date(Date.UTC(2025, 3, 15)), - rateDocuments: [ + expect(updateResult.errors).toBeUndefined() + + const updatedHealthPlanPackage = + updateResult.data?.updateHealthPlanFormData.pkg + + const updatedFormData = latestFormData(updatedHealthPlanPackage) + + // Expect our rates to be in the contract from our database + expect(updatedFormData).toEqual( + expect.objectContaining({ + ...formData, + updatedAt: expect.any(Date), + rateInfos: expect.arrayContaining([ + expect.objectContaining({ + ...rate1, + id: expect.any(String), + rateCertificationName: expect.any(String), + packagesWithSharedRateCerts: expect.arrayContaining([ + expect.objectContaining({ + packageId: packageWithSharedRate1.packageId, + packageName: packageWithSharedRate1.packageName, + }), + expect.objectContaining({ + packageId: packageWithSharedRate2.packageId, + packageName: packageWithSharedRate2.packageName, + }), + ]), + }), + expect.objectContaining({ + ...rate2, + id: expect.any(String), + rateCertificationName: expect.any(String), + }), + ]), + }) + ) + + const rate3 = { + id: uuidv4(), + rateType: 'AMENDMENT' as const, + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: new Date(Date.UTC(2025, 3, 15)), + rateDocuments: [], + rateAmendmentInfo: undefined, + rateCapitationType: undefined, + rateCertificationName: undefined, + supportingDocuments: [], + //We only want one rate ID and use last program in list to differentiate from programID if possible. + rateProgramIDs: [statePrograms.reverse()[0].id], + actuaryContacts: [], + packagesWithSharedRateCerts: [], + } + + // Update first rate and remove second from contract and add a new rate. + const formData2 = Object.assign( + latestFormData(updatedHealthPlanPackage), + { + rateInfos: [ { - name: 'rateDocument.pdf', - s3URL: 's3://bucketname/key/supporting-documents', - documentCategories: ['RATES' as const], - sha256: 'rate2-sha', + ...updatedFormData.rateInfos[0], + // updating the actuary on the first rate + actuaryContacts: [ + { + name: 'New actuary', + titleRole: 'Better title', + email: 'actuary@example.com', + actuarialFirm: 'OPTUMAS' as const, + actuarialFirmOther: '', + }, + ], + // remove second package with shared rate, by only passing in the first + packagesWithSharedRateCerts: [], }, - ], - rateAmendmentInfo: undefined, - rateCapitationType: undefined, - rateCertificationName: undefined, - supportingDocuments: [], - //We only want one rate ID and use last program in list to differentiate from programID if possible. - rateProgramIDs: [statePrograms.reverse()[0].id], - actuaryContacts: [ { - name: 'test name', - titleRole: 'test title', - email: 'email@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', + ...rate3, }, ], - packagesWithSharedRateCerts: [], } + ) - // update that draft form data. - const formData = Object.assign(latestFormData(createdDraft), { - addtlActuaryContacts: [ - { - name: 'additional actuary 1', - titleRole: 'additional actuary title 1', - email: 'additionalactuary1@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', - }, - { - name: 'additional actuary 2', - titleRole: 'additional actuary title 2', - email: 'additionalactuary1@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', - }, - ], - rateInfos: [rate1, rate2], - }) + const secondUpdatedB64 = domainToBase64(formData2) - // convert to base64 proto - const updatedB64 = domainToBase64(formData) - - // update the DB contract - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: updatedB64, - }, + // update the DB contract again + const updateResult2 = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: secondUpdatedB64, }, - }) - - expect(updateResult.errors).toBeUndefined() - - const updatedHealthPlanPackage = - updateResult.data?.updateHealthPlanFormData.pkg - - const updatedFormData = latestFormData(updatedHealthPlanPackage) - - // Expect our rates to be in the contract from our database - expect(updatedFormData).toEqual( - expect.objectContaining({ - ...formData, - updatedAt: expect.any(Date), - rateInfos: expect.arrayContaining([ - expect.objectContaining({ - ...rate1, - id: expect.any(String), - rateCertificationName: expect.any(String), - packagesWithSharedRateCerts: expect.arrayContaining( - [ - expect.objectContaining({ - packageId: - packageWithSharedRate1.packageId, - packageName: - packageWithSharedRate1.packageName, - }), - expect.objectContaining({ - packageId: - packageWithSharedRate2.packageId, - packageName: - packageWithSharedRate2.packageName, - }), - ] - ), - }), - expect.objectContaining({ - ...rate2, - id: expect.any(String), - rateCertificationName: expect.any(String), - }), - ]), - }) - ) + }, + }) - const rate3 = { - id: uuidv4(), - rateType: 'AMENDMENT' as const, - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: new Date(Date.UTC(2025, 3, 15)), - rateDocuments: [], - rateAmendmentInfo: undefined, - rateCapitationType: undefined, - rateCertificationName: undefined, - supportingDocuments: [], - //We only want one rate ID and use last program in list to differentiate from programID if possible. - rateProgramIDs: [statePrograms.reverse()[0].id], - actuaryContacts: [], - packagesWithSharedRateCerts: [], - } + expect(updateResult2.errors).toBeUndefined() + + const updatedHealthPlanPackage2 = + updateResult2.data?.updateHealthPlanFormData.pkg + + const updatedFormData2 = latestFormData(updatedHealthPlanPackage2) + + // Expect our rates to be updated + expect(updatedFormData2).toEqual( + expect.objectContaining({ + ...formData2, + updatedAt: expect.any(Date), + rateInfos: expect.arrayContaining([ + expect.objectContaining({ + ...formData2.rateInfos[0], + id: expect.any(String), + rateCertificationName: expect.any(String), + packagesWithSharedRateCerts: [], + }), + expect.objectContaining({ + ...formData2.rateInfos[1], + id: expect.any(String), + rateCertificationName: expect.any(String), + }), + ]), + }) + ) + }) + + it('errors on a rate with no ID.', async () => { + const stateUser = { + id: uuidv4(), + givenName: 'Aang', + familyName: 'Avatar', + email: 'aang@example.com', + role: 'STATE_USER' as const, + stateCode: 'MN', + } + const server = await constructTestPostgresServer({ + context: { + user: stateUser, + }, + }) - // Update first rate and remove second from contract and add a new rate. - const formData2 = Object.assign( - latestFormData(updatedHealthPlanPackage), + const stateCode = 'MN' + const createdDraft = await createTestHealthPlanPackage( + server, + stateCode + ) + const statePrograms = must(findStatePrograms(createdDraft.stateCode)) + + // Create 2 valid contracts to attached to packagesWithSharedRateCerts + const createdDraftTwo = await createTestHealthPlanPackage( + server, + stateCode + ) + const createdDraftThree = await createTestHealthPlanPackage( + server, + stateCode + ) + + const createdDraftTwoFormData = latestFormData(createdDraftTwo) + const createdDraftThreeFormData = latestFormData(createdDraftThree) + + const packageWithSharedRate1 = { + packageId: createdDraftTwo.id, + packageName: packageName( + createdDraftTwo.stateCode, + createdDraftTwoFormData.stateNumber, + createdDraftTwoFormData.programIDs, + statePrograms + ), + } as const + + const packageWithSharedRate2 = { + packageId: createdDraftThree.id, + packageName: packageName( + createdDraftThree.stateCode, + createdDraftThreeFormData.stateNumber, + createdDraftThreeFormData.programIDs, + statePrograms + ), + } as const + + // Create 2 rate data for insertion + const rate1 = { + rateType: 'NEW' as const, + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: new Date(Date.UTC(2025, 3, 15)), + rateDocuments: [ { - rateInfos: [ - { - ...updatedFormData.rateInfos[0], - // updating the actuary on the first rate - actuaryContacts: [ - { - name: 'New actuary', - titleRole: 'Better title', - email: 'actuary@example.com', - actuarialFirm: 'OPTUMAS' as const, - actuarialFirmOther: '', - }, - ], - // remove second package with shared rate, by only passing in the first - packagesWithSharedRateCerts: [], - }, - { - ...rate3, - }, - ], - } - ) - - const secondUpdatedB64 = domainToBase64(formData2) - - // update the DB contract again - const updateResult2 = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: secondUpdatedB64, - }, + name: 'rateDocument.pdf', + s3URL: 's3://bucketname/key/supporting-documents', + sha256: 'rate1-sha', }, - }) - - expect(updateResult2.errors).toBeUndefined() - - const updatedHealthPlanPackage2 = - updateResult2.data?.updateHealthPlanFormData.pkg - - const updatedFormData2 = latestFormData(updatedHealthPlanPackage2) - - // Expect our rates to be updated - expect(updatedFormData2).toEqual( - expect.objectContaining({ - ...formData2, - updatedAt: expect.any(Date), - rateInfos: expect.arrayContaining([ - expect.objectContaining({ - ...formData2.rateInfos[0], - id: expect.any(String), - rateCertificationName: expect.any(String), - packagesWithSharedRateCerts: [], - }), - expect.objectContaining({ - ...formData2.rateInfos[1], - id: expect.any(String), - rateCertificationName: expect.any(String), - }), - ]), - }) - ) - }) - - it('errors on a rate with no ID.', async () => { - const stateUser = { - id: uuidv4(), - givenName: 'Aang', - familyName: 'Avatar', - email: 'aang@example.com', - role: 'STATE_USER' as const, - stateCode: 'MN', - } - const server = await constructTestPostgresServer({ - ldService: mockLDService, - context: { - user: stateUser, + ], + rateAmendmentInfo: undefined, + rateCapitationType: undefined, + rateCertificationName: undefined, + supportingDocuments: [], + //We only want one rate ID and use last program in list to differentiate from programID if possible. + rateProgramIDs: [statePrograms.reverse()[0].id], + actuaryContacts: [ + { + name: 'test name', + titleRole: 'test title', + email: 'email@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', }, - }) + ], + packagesWithSharedRateCerts: [ + packageWithSharedRate1, + packageWithSharedRate2, + ], + } + + const rate2 = { + id: uuidv4(), + rateType: 'NEW' as const, + rateDateStart: new Date(Date.UTC(2025, 5, 1)), + rateDateEnd: new Date(Date.UTC(2026, 4, 30)), + rateDateCertified: new Date(Date.UTC(2025, 3, 15)), + rateDocuments: [ + { + name: 'rateDocument.pdf', + s3URL: 's3://bucketname/key/supporting-documents', + sha256: 'rate2-sha', + }, + ], + rateAmendmentInfo: undefined, + rateCapitationType: undefined, + rateCertificationName: undefined, + supportingDocuments: [], + //We only want one rate ID and use last program in list to differentiate from programID if possible. + rateProgramIDs: [statePrograms.reverse()[0].id], + actuaryContacts: [ + { + name: 'test name', + titleRole: 'test title', + email: 'email@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + ], + packagesWithSharedRateCerts: [], + } - const stateCode = 'MN' - const createdDraft = await createTestHealthPlanPackage( - server, - stateCode - ) - const statePrograms = must( - findStatePrograms(createdDraft.stateCode) - ) + // update that draft form data. + const formData = Object.assign(latestFormData(createdDraft), { + addtlActuaryContacts: [ + { + name: 'additional actuary 1', + titleRole: 'additional actuary title 1', + email: 'additionalactuary1@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + { + name: 'additional actuary 2', + titleRole: 'additional actuary title 2', + email: 'additionalactuary1@example.com', + actuarialFirm: 'MERCER' as const, + actuarialFirmOther: '', + }, + ], + rateInfos: [rate1, rate2], + }) - // Create 2 valid contracts to attached to packagesWithSharedRateCerts - const createdDraftTwo = await createTestHealthPlanPackage( - server, - stateCode - ) - const createdDraftThree = await createTestHealthPlanPackage( - server, - stateCode - ) + // convert to base64 proto + const updatedB64 = domainToBase64(formData) - const createdDraftTwoFormData = latestFormData(createdDraftTwo) - const createdDraftThreeFormData = latestFormData(createdDraftThree) - - const packageWithSharedRate1 = { - packageId: createdDraftTwo.id, - packageName: packageName( - createdDraftTwo.stateCode, - createdDraftTwoFormData.stateNumber, - createdDraftTwoFormData.programIDs, - statePrograms - ), - } as const - - const packageWithSharedRate2 = { - packageId: createdDraftThree.id, - packageName: packageName( - createdDraftThree.stateCode, - createdDraftThreeFormData.stateNumber, - createdDraftThreeFormData.programIDs, - statePrograms - ), - } as const - - // Create 2 rate data for insertion - const rate1 = { - rateType: 'NEW' as const, - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: new Date(Date.UTC(2025, 3, 15)), - rateDocuments: [ - { - name: 'rateDocument.pdf', - s3URL: 's3://bucketname/key/supporting-documents', - documentCategories: ['RATES' as const], - sha256: 'rate1-sha', - }, - ], - rateAmendmentInfo: undefined, - rateCapitationType: undefined, - rateCertificationName: undefined, - supportingDocuments: [], - //We only want one rate ID and use last program in list to differentiate from programID if possible. - rateProgramIDs: [statePrograms.reverse()[0].id], - actuaryContacts: [ - { - name: 'test name', - titleRole: 'test title', - email: 'email@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', - }, - ], - packagesWithSharedRateCerts: [ - packageWithSharedRate1, - packageWithSharedRate2, - ], - } + // update the DB contract + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: updatedB64, + }, + }, + }) - const rate2 = { - id: uuidv4(), - rateType: 'NEW' as const, - rateDateStart: new Date(Date.UTC(2025, 5, 1)), - rateDateEnd: new Date(Date.UTC(2026, 4, 30)), - rateDateCertified: new Date(Date.UTC(2025, 3, 15)), - rateDocuments: [ - { - name: 'rateDocument.pdf', - s3URL: 's3://bucketname/key/supporting-documents', - documentCategories: ['RATES' as const], - sha256: 'rate2-sha', - }, - ], - rateAmendmentInfo: undefined, - rateCapitationType: undefined, - rateCertificationName: undefined, - supportingDocuments: [], - //We only want one rate ID and use last program in list to differentiate from programID if possible. - rateProgramIDs: [statePrograms.reverse()[0].id], - actuaryContacts: [ - { - name: 'test name', - titleRole: 'test title', - email: 'email@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', - }, - ], - packagesWithSharedRateCerts: [], - } + expect(updateResult.errors).toBeDefined() + }) - // update that draft form data. - const formData = Object.assign(latestFormData(createdDraft), { - addtlActuaryContacts: [ - { - name: 'additional actuary 1', - titleRole: 'additional actuary title 1', - email: 'additionalactuary1@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', - }, - { - name: 'additional actuary 2', - titleRole: 'additional actuary title 2', - email: 'additionalactuary1@example.com', - actuarialFirm: 'MERCER' as const, - actuarialFirmOther: '', - }, - ], - rateInfos: [rate1, rate2], - }) + it('updates relational fields such as documents and contacts', async () => { + const server = await constructTestPostgresServer() - // convert to base64 proto - const updatedB64 = domainToBase64(formData) + const createdDraft = await createTestHealthPlanPackage(server) - // update the DB contract - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: updatedB64, - }, + // update that draft. + const formData = Object.assign(latestFormData(createdDraft), { + programIDs: [], + populationCovered: 'MEDICAID', + submissionType: 'CONTRACT_ONLY', + riskBasedContract: true, + submissionDescription: 'Updated submission', + stateContacts: [ + { + name: 'statecontact', + titleRole: 'thestatestofcontacts', + email: 'statemcstate@examepl.com', }, - }) - - expect(updateResult.errors).toBeDefined() + ], + documents: [ + { + name: 'supportingDocument11.pdf', + s3URL: 'fakeS3URL', + sha256: 'needs-to-be-there', + }, + ], + adsfdas: 'sdfsdf', + rateInfos: [], }) - it('updates relational fields such as documents and contacts', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - - const createdDraft = await createTestHealthPlanPackage(server) - - // update that draft. - const formData = Object.assign(latestFormData(createdDraft), { - programIDs: [], - populationCovered: 'MEDICAID', - submissionType: 'CONTRACT_ONLY', - riskBasedContract: true, - submissionDescription: 'Updated submission', - stateContacts: [ - { - name: 'statecontact', - titleRole: 'thestatestofcontacts', - email: 'statemcstate@examepl.com', - }, - ], - documents: [ - { - name: 'supportingDocument11.pdf', - s3URL: 'fakeS3URL', - documentCategories: ['CONTRACT_RELATED' as const], - sha256: 'needs-to-be-there', - }, - ], - adsfdas: 'sdfsdf', - rateInfos: [], - }) - - // convert to base64 proto - const updatedB64 = domainToBase64(formData) + // convert to base64 proto + const updatedB64 = domainToBase64(formData) - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: updatedB64, - }, + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: updatedB64, }, - }) + }, + }) - expect(updateResult.errors).toBeUndefined() + expect(updateResult.errors).toBeUndefined() - const healthPlanPackage = - updateResult.data?.updateHealthPlanFormData.pkg + const healthPlanPackage = + updateResult.data?.updateHealthPlanFormData.pkg - const updatedFormData = latestFormData(healthPlanPackage) - expect(updatedFormData.documents).toEqual( - expect.arrayContaining(formData.documents) - ) - expect(updatedFormData.contractDocuments).toEqual( - expect.arrayContaining(formData.contractDocuments) - ) + const updatedFormData = latestFormData(healthPlanPackage) + expect(updatedFormData.documents).toEqual( + expect.arrayContaining(formData.documents) + ) + expect(updatedFormData.contractDocuments).toEqual( + expect.arrayContaining(formData.contractDocuments) + ) - expect(updatedFormData.stateContacts).toEqual( - expect.arrayContaining(formData.stateContacts) - ) - }) + expect(updatedFormData.stateContacts).toEqual( + expect.arrayContaining(formData.stateContacts) + ) + }) - it('errors if a CMS user calls it', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + it('errors if a CMS user calls it', async () => { + const server = await constructTestPostgresServer() - const createdDraft = await createTestHealthPlanPackage(server) + const createdDraft = await createTestHealthPlanPackage(server) - const formData = latestFormData(createdDraft) + const formData = latestFormData(createdDraft) - // update that draft. - formData.submissionDescription = 'UPDATED BY REVISION' + // update that draft. + formData.submissionDescription = 'UPDATED BY REVISION' - // convert to base64 proto - const updatedB64 = domainToBase64(formData) + // convert to base64 proto + const updatedB64 = domainToBase64(formData) - const cmsUserServer = await constructTestPostgresServer({ - context: { - user: cmsUser, - }, - ldService: mockLDService, - }) + const cmsUserServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + }) - const updateResult = await cmsUserServer.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: updatedB64, - }, + const updateResult = await cmsUserServer.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: updatedB64, }, - }) - - expect(updateResult.errors).toBeDefined() - if (updateResult.errors === undefined) { - throw new Error('type narrow') - } - - expect(updateResult.errors[0].extensions?.code).toBe('FORBIDDEN') - expect(updateResult.errors[0].message).toBe( - 'user not authorized to modify state data' - ) + }, }) - it('errors if a state user from a different state calls it', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const createdDraft = await createTestHealthPlanPackage(server) - const formData = latestFormData(createdDraft) - - // update that draft. - formData.submissionDescription = 'UPDATED BY REVISION' - - // convert to base64 proto - const updatedB64 = domainToBase64(formData) - - // setup a server with a different user - const otherUserServer = await constructTestPostgresServer({ - context: { - user: testStateUser({ stateCode: 'VA' }), - }, - ldService: mockLDService, - }) + expect(updateResult.errors).toBeDefined() + if (updateResult.errors === undefined) { + throw new Error('type narrow') + } + + expect(updateResult.errors[0].extensions?.code).toBe('FORBIDDEN') + expect(updateResult.errors[0].message).toBe( + 'user not authorized to modify state data' + ) + }) + + it('errors if a state user from a different state calls it', async () => { + const server = await constructTestPostgresServer() + const createdDraft = await createTestHealthPlanPackage(server) + const formData = latestFormData(createdDraft) + + // update that draft. + formData.submissionDescription = 'UPDATED BY REVISION' + + // convert to base64 proto + const updatedB64 = domainToBase64(formData) + + // setup a server with a different user + const otherUserServer = await constructTestPostgresServer({ + context: { + user: testStateUser({ stateCode: 'VA' }), + }, + }) - const updateResult = await otherUserServer.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: updatedB64, - }, + const updateResult = await otherUserServer.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: updatedB64, }, - }) - - expect(updateResult.errors).toBeDefined() - if (updateResult.errors === undefined) { - throw new Error('type narrow') - } - - expect(updateResult.errors[0].extensions?.code).toBe('FORBIDDEN') - expect(updateResult.errors[0].message).toBe( - 'user not authorized to fetch data from a different state' - ) + }, }) - it('errors if the payload isnt valid', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + expect(updateResult.errors).toBeDefined() + if (updateResult.errors === undefined) { + throw new Error('type narrow') + } - const createdDraft = await createTestHealthPlanPackage(server) + expect(updateResult.errors[0].extensions?.code).toBe('FORBIDDEN') + expect(updateResult.errors[0].message).toBe( + 'user not authorized to fetch data from a different state' + ) + }) - const formData = 'not-valid-proto' + it('errors if the payload isnt valid', async () => { + const server = await constructTestPostgresServer() - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: formData, - }, - }, - }) + const createdDraft = await createTestHealthPlanPackage(server) - expect(updateResult.errors).toBeDefined() - if (updateResult.errors === undefined) { - throw new Error('type narrow') - } + const formData = 'not-valid-proto' - expect(updateResult.errors[0].extensions?.code).toBe( - 'BAD_USER_INPUT' - ) - expect(updateResult.errors[0].message).toContain( - 'Failed to parse out form data in request' - ) + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: formData, + }, + }, }) - it('errors if the payload is submitted', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + expect(updateResult.errors).toBeDefined() + if (updateResult.errors === undefined) { + throw new Error('type narrow') + } - const createdDraft = await createTestHealthPlanPackage(server) + expect(updateResult.errors[0].extensions?.code).toBe('BAD_USER_INPUT') + expect(updateResult.errors[0].message).toContain( + 'Failed to parse out form data in request' + ) + }) - const stateSubmission = basicLockedHealthPlanFormData() + it('errors if the payload is submitted', async () => { + const server = await constructTestPostgresServer() - const formData = domainToBase64(stateSubmission) + const createdDraft = await createTestHealthPlanPackage(server) - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: formData, - }, - }, - }) + const stateSubmission = basicLockedHealthPlanFormData() - expect(updateResult.errors).toBeDefined() - if (updateResult.errors === undefined) { - throw new Error('type narrow') - } + const formData = domainToBase64(stateSubmission) - expect(updateResult.errors[0].extensions?.code).toBe( - 'BAD_USER_INPUT' - ) - expect(updateResult.errors[0].message).toContain( - 'Attempted to update with a StateSubmission' - ) + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: formData, + }, + }, }) - it('errors if the Package is already submitted', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, + expect(updateResult.errors).toBeDefined() + if (updateResult.errors === undefined) { + throw new Error('type narrow') + } + + expect(updateResult.errors[0].extensions?.code).toBe('BAD_USER_INPUT') + expect(updateResult.errors[0].message).toContain( + 'Attempted to update with a StateSubmission' + ) + }) + + it('errors if the Package is already submitted', async () => { + const server = await constructTestPostgresServer() + const createdDraft = await createTestHealthPlanPackage(server) + const submitPackage = async () => { + const client = await sharedTestPrismaClient() + const stateUser = await client.user.create({ + data: { + id: uuidv4(), + givenName: 'Aang', + familyName: 'Avatar', + email: 'aang@example.com', + role: 'STATE_USER', + stateCode: 'NM', + }, }) - const createdDraft = await createTestHealthPlanPackage(server) - const submitPackage = async () => { - // Manually submit package when flag is on. - // TODO: remove the conditional after submit resolver has been modified. - if (flagValue) { - const client = await sharedTestPrismaClient() - const stateUser = await client.user.create({ - data: { - id: uuidv4(), - givenName: 'Aang', - familyName: 'Avatar', - email: 'aang@example.com', - role: 'STATE_USER', - stateCode: 'NM', - }, - }) - return must( - await submitContract(client, { - contractID: createdDraft.id, - submittedByUserID: stateUser.id, - submitReason: 'Submission', - }) - ) - } else { - return await createAndSubmitTestHealthPlanPackage(server) - } - } + return must( + await submitContract(client, { + contractID: createdDraft.id, + submittedByUserID: stateUser.id, + submitReason: 'Submission', + }) + ) + } - const createdSubmitted = await submitPackage() + const createdSubmitted = await submitPackage() - const draft = basicHealthPlanFormData() - const b64 = domainToBase64(draft) + const draft = basicHealthPlanFormData() + const b64 = domainToBase64(draft) - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdSubmitted.id, - healthPlanFormData: b64, - }, + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdSubmitted.id, + healthPlanFormData: b64, }, - }) + }, + }) - expect(updateResult.errors).toBeDefined() - if (updateResult.errors === undefined) { - throw new Error('type narrow') - } + expect(updateResult.errors).toBeDefined() + if (updateResult.errors === undefined) { + throw new Error('type narrow') + } - expect(updateResult.errors[0].extensions?.code).toBe( - 'BAD_USER_INPUT' - ) - expect(updateResult.errors[0].message).toContain( - 'Package is not in editable state:' - ) - expect(updateResult.errors[0].message).toContain( - 'status: SUBMITTED' - ) - }) + expect(updateResult.errors[0].extensions?.code).toBe('BAD_USER_INPUT') + expect(updateResult.errors[0].message).toContain( + 'Package is not in editable state:' + ) + expect(updateResult.errors[0].message).toContain('status: SUBMITTED') + }) - it('errors if the id doesnt match the db', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const createdDraft = await createTestHealthPlanPackage(server) + it('errors if the id doesnt match the db', async () => { + const server = await constructTestPostgresServer() + const createdDraft = await createTestHealthPlanPackage(server) - const formData = latestFormData(createdDraft) + const formData = latestFormData(createdDraft) - formData.updatedAt = new Date(Date.UTC(2025, 5, 1)) + formData.updatedAt = new Date(Date.UTC(2025, 5, 1)) - const b64 = domainToBase64(formData) + const b64 = domainToBase64(formData) - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: b64, - }, + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: b64, }, - }) + }, + }) - expect(updateResult.errors).toBeDefined() - if (updateResult.errors === undefined) { - throw new Error('type narrow') - } + expect(updateResult.errors).toBeDefined() + if (updateResult.errors === undefined) { + throw new Error('type narrow') + } - expect(updateResult.errors[0].extensions?.code).toBe( - 'BAD_USER_INPUT' - ) + expect(updateResult.errors[0].extensions?.code).toBe('BAD_USER_INPUT') - const expectedErrorMsg = flagValue - ? 'Concurrent update error: The data you are trying to modify has changed since you last retrieved it. Please refresh the page to continue.' - : 'Transient server error: attempted to modify un-modifiable field(s): updatedAt. Please refresh the page to continue.' + const expectedErrorMsg = + 'Concurrent update error: The data you are trying to modify has changed since you last retrieved it. Please refresh the page to continue.' - expect(updateResult.errors[0].message).toBe(expectedErrorMsg) - }) + expect(updateResult.errors[0].message).toBe(expectedErrorMsg) + }) - it('errors if the other payload values dont match the db', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const createdDraft = await createTestHealthPlanPackage(server) + it('errors if the other payload values dont match the db', async () => { + const server = await constructTestPostgresServer() + const createdDraft = await createTestHealthPlanPackage(server) - const formData = latestFormData(createdDraft) + const formData = latestFormData(createdDraft) - formData.stateCode = 'CA' - formData.stateNumber = 9999999 - formData.createdAt = new Date(2021) - formData.updatedAt = new Date(2021) + formData.stateCode = 'CA' + formData.stateNumber = 9999999 + formData.createdAt = new Date(2021) + formData.updatedAt = new Date(2021) - const b64 = domainToBase64(formData) + const b64 = domainToBase64(formData) - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: b64, - }, + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: b64, }, - }) - - expect(updateResult.errors).toBeDefined() - if (updateResult.errors === undefined) { - throw new Error('type narrow') - } + }, + }) - expect(updateResult.errors[0].extensions?.code).toBe( - 'BAD_USER_INPUT' - ) + expect(updateResult.errors).toBeDefined() + if (updateResult.errors === undefined) { + throw new Error('type narrow') + } - const expectedErrorMsg = flagValue - ? 'Concurrent update error: The data you are trying to modify has changed since you last retrieved it. Please refresh the page to continue.' - : 'Transient server error: attempted to modify un-modifiable field(s): stateCode,stateNumber,createdAt,updatedAt. Please refresh the page to continue.' + expect(updateResult.errors[0].extensions?.code).toBe('BAD_USER_INPUT') - expect(updateResult.errors[0].message).toBe(expectedErrorMsg) - }) + const expectedErrorMsg = + 'Concurrent update error: The data you are trying to modify has changed since you last retrieved it. Please refresh the page to continue.' - it('errors if the update call to the db fails', async () => { - const prismaClient = await sharedTestPrismaClient() - const postgresStore = NewPostgresStore(prismaClient) - const failStore = mockStoreThatErrors() + expect(updateResult.errors[0].message).toBe(expectedErrorMsg) + }) - // set store error for flag off - postgresStore.updateHealthPlanRevision = - failStore.updateHealthPlanRevision + it('errors if the update call to the db fails', async () => { + const prismaClient = await sharedTestPrismaClient() + const postgresStore = NewPostgresStore(prismaClient) + const failStore = mockStoreThatErrors() - // set store error for flag on. - postgresStore.updateDraftContractWithRates = - failStore.updateDraftContractWithRates + // set store error for flag on. + postgresStore.updateDraftContractWithRates = + failStore.updateDraftContractWithRates - const server = await constructTestPostgresServer({ - store: postgresStore, - ldService: mockLDService, - }) + const server = await constructTestPostgresServer({ + store: postgresStore, + }) - const createdDraft = await createTestHealthPlanPackage(server) + const createdDraft = await createTestHealthPlanPackage(server) - const formData = latestFormData(createdDraft) + const formData = latestFormData(createdDraft) - // update that draft. - formData.submissionDescription = 'UPDATED BY REVISION' + // update that draft. + formData.submissionDescription = 'UPDATED BY REVISION' - // convert to base64 proto - const updatedB64 = domainToBase64(formData) + // convert to base64 proto + const updatedB64 = domainToBase64(formData) - const updateResult = await server.executeOperation({ - query: UPDATE_HEALTH_PLAN_FORM_DATA, - variables: { - input: { - pkgID: createdDraft.id, - healthPlanFormData: updatedB64, - }, + const updateResult = await server.executeOperation({ + query: UPDATE_HEALTH_PLAN_FORM_DATA, + variables: { + input: { + pkgID: createdDraft.id, + healthPlanFormData: updatedB64, }, - }) - - expect(updateResult.errors).toBeDefined() - if (updateResult.errors === undefined) { - throw new Error('type narrow') - } - - expect(updateResult.errors[0].extensions?.code).toBe( - 'INTERNAL_SERVER_ERROR' - ) - expect(updateResult.errors[0].message).toContain( - 'UNEXPECTED_EXCEPTION' - ) - expect(updateResult.errors[0].message).toContain( - 'Error updating form data' - ) + }, }) - } -) + + expect(updateResult.errors).toBeDefined() + if (updateResult.errors === undefined) { + throw new Error('type narrow') + } + + expect(updateResult.errors[0].extensions?.code).toBe( + 'INTERNAL_SERVER_ERROR' + ) + expect(updateResult.errors[0].message).toContain('UNEXPECTED_EXCEPTION') + expect(updateResult.errors[0].message).toContain( + 'Error updating form data' + ) + }) +}) diff --git a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts index a4d2982df1..2a5bae9978 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/updateHealthPlanFormData.ts @@ -1,19 +1,14 @@ import { ForbiddenError, UserInputError } from 'apollo-server-lambda' import type { UnlockedHealthPlanFormDataType } from '../../../../app-web/src/common-code/healthPlanFormDataType' -import { - base64ToDomain, - toDomain, -} from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' -import type { HealthPlanPackageType } from '../../domain-models' +import { base64ToDomain } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' import { convertContractWithRatesToUnlockedHPP, isStateUser, - packageStatus, } from '../../domain-models' import type { MutationResolvers } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' import type { Store } from '../../postgres' -import { isStoreError, NotFoundError } from '../../postgres' +import { NotFoundError } from '../../postgres' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, @@ -23,49 +18,6 @@ import type { LDService } from '../../launchDarkly/launchDarkly' import { GraphQLError } from 'graphql/index' import { convertHealthPlanPackageRatesToDomain } from './contractAndRates/resolverHelpers' -type ProtectedFieldType = Pick< - UnlockedHealthPlanFormDataType, - 'id' | 'stateCode' | 'stateNumber' | 'createdAt' | 'updatedAt' -> - -// Validate that none of these protected fields have been modified. -// These fields should only be modified by the server, never by an update. -function validateProtectedFields( - previousFormData: ProtectedFieldType, - unlockedFormData: ProtectedFieldType -): string[] { - const fixedFields: (keyof ProtectedFieldType)[] = [ - 'id', - 'stateCode', - 'stateNumber', - 'createdAt', - 'updatedAt', - ] - const unfixedFields = [] - for (const fixedField of fixedFields) { - const prevVal = previousFormData[fixedField] - const newVal = unlockedFormData[fixedField] - - if (prevVal instanceof Date && newVal instanceof Date) { - if (prevVal.getTime() !== newVal.getTime()) { - console.info( - `ERRMOD ${fixedField}: old: ${previousFormData[fixedField]} new: ${unlockedFormData[fixedField]}` - ) - unfixedFields.push(fixedField) - } - } else { - if (previousFormData[fixedField] !== unlockedFormData[fixedField]) { - console.info( - `ERRMOD ${fixedField}: old: ${previousFormData[fixedField]} new: ${unlockedFormData[fixedField]}` - ) - unfixedFields.push(fixedField) - } - } - } - - return unfixedFields -} - export function updateHealthPlanFormDataResolver( store: Store, launchDarkly: LDService @@ -134,272 +86,145 @@ export function updateHealthPlanFormDataResolver( formDataResult.statutoryRegulatoryAttestationDescription = undefined } - // Uses new DB if flag is on - if (featureFlags?.['rates-db-refactor']) { - // Find contract from DB - const contractWithHistory = await store.findContractWithHistory( - input.pkgID - ) - - if (contractWithHistory instanceof Error) { - const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) + // Find contract from DB + const contractWithHistory = await store.findContractWithHistory( + input.pkgID + ) - if (contractWithHistory instanceof NotFoundError) { - throw new GraphQLError(errMessage, { - extensions: { - code: 'NOT_FOUND', - cause: 'DB_ERROR', - }, - }) - } + if (contractWithHistory instanceof Error) { + const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + if (contractWithHistory instanceof NotFoundError) { throw new GraphQLError(errMessage, { extensions: { - code: 'INTERNAL_SERVER_ERROR', + code: 'NOT_FOUND', cause: 'DB_ERROR', }, }) } - // Authorize the update - const stateFromCurrentUser = context.user.stateCode - if (contractWithHistory.stateCode !== stateFromCurrentUser) { - logError( - 'updateHealthPlanFormData', - 'user not authorized to fetch data from a different state' - ) - setErrorAttributesOnActiveSpan( - 'user not authorized to fetch data from a different state', - span - ) - throw new ForbiddenError( - 'user not authorized to fetch data from a different state' - ) - } - - // Can't update a submission that is locked or resubmitted - if (!contractWithHistory.draftRevision) { - const errMessage = `Package is not in editable state: ${input.pkgID} status: ${contractWithHistory.status}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - argumentName: 'pkg', - }) - } - - // If updatedAt does not match concurrent editing occurred. - if ( - contractWithHistory.draftRevision.updatedAt.getTime() !== - unlockedFormData.updatedAt.getTime() - ) { - const errMessage = `Concurrent update error: The data you are trying to modify has changed since you last retrieved it. Please refresh the page to continue.` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage) - } - - // Check for any rate updates - const updateRateFormDatas = - await convertHealthPlanPackageRatesToDomain(unlockedFormData) - - if (updateRateFormDatas instanceof Error) { - const errMessage = `Error converting rate. Message: ${updateRateFormDatas.message}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - }, - }) - } - - // Update contract draft revision - const updateResult = await store.updateDraftContractWithRates({ - contractID: input.pkgID, - formData: { - ...unlockedFormData, - ...unlockedFormData.contractAmendmentInfo - ?.modifiedProvisions, - managedCareEntities: unlockedFormData.managedCareEntities, - stateContacts: unlockedFormData.stateContacts, - supportingDocuments: unlockedFormData.documents.map( - (doc) => { - return { - name: doc.name, - s3URL: doc.s3URL, - sha256: doc.sha256, - id: doc.id, - } - } - ), - contractDocuments: unlockedFormData.contractDocuments.map( - (doc) => { - return { - name: doc.name, - s3URL: doc.s3URL, - sha256: doc.sha256, - id: doc.id, - } - } - ), + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', }, - rateFormDatas: updateRateFormDatas, }) + } - if (updateResult instanceof Error) { - const errMessage = `Error updating form data: ${input.pkgID}:: ${updateResult.message}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - - // Convert back to health plan package - const pkg = convertContractWithRatesToUnlockedHPP(updateResult) - - if (pkg instanceof Error) { - const errMessage = `Error converting draft contract. Message: ${pkg.message}` - logError('createHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'PROTO_DECODE_ERROR', - }, - }) - } - - return { pkg } - } else { - const result = await store.findHealthPlanPackage(input.pkgID) - - if (isStoreError(result)) { - console.info('Error finding a package', result) - const errMessage = `Issue finding a package of type ${result.code}. Message: ${result.message}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new Error(errMessage) - } - - if (result === undefined) { - const errMessage = `No package found to update with that ID: ${input.pkgID}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - argumentName: 'pgkID', - }) - } - - const planPackage: HealthPlanPackageType = result - - // Authorize the update - const stateFromCurrentUser = context.user.stateCode - if (planPackage.stateCode !== stateFromCurrentUser) { - logError( - 'updateHealthPlanFormData', - 'user not authorized to fetch data from a different state' - ) - setErrorAttributesOnActiveSpan( - 'user not authorized to fetch data from a different state', - span - ) - throw new ForbiddenError( - 'user not authorized to fetch data from a different state' - ) - } - - // Check the package is in an update-able state - const planPackageStatus = packageStatus(planPackage) - if (planPackageStatus instanceof Error) { - const errMessage = `No revisions found on package: ${input.pkgID}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new Error(errMessage) - } - - // Can't update a submission that is locked or resubmitted - if (!['DRAFT', 'UNLOCKED'].includes(planPackageStatus)) { - const errMessage = `Package is not in editable state: ${input.pkgID} status: ${planPackageStatus}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - argumentName: 'pkg', - }) - } - - // Validate input against the db. - // Having to crack this open to check on this is probably an indication that some of this info - // really belongs on the HealthPlanPackage itself instead of being inside form data, but this is where we are now. - - const previousFormDataResult = toDomain( - planPackage.revisions[0].formDataProto + // Authorize the update + const stateFromCurrentUser = context.user.stateCode + if (contractWithHistory.stateCode !== stateFromCurrentUser) { + logError( + 'updateHealthPlanFormData', + 'user not authorized to fetch data from a different state' ) - if (previousFormDataResult instanceof Error) { - const errMessage = `Issue deserializing old formData ${previousFormDataResult.message}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new Error(errMessage) - } + setErrorAttributesOnActiveSpan( + 'user not authorized to fetch data from a different state', + span + ) + throw new ForbiddenError( + 'user not authorized to fetch data from a different state' + ) + } - // Sanity check, Can't update a package that is locked or resubmitted in the form data either - if (previousFormDataResult.status === 'SUBMITTED') { - const errMessage = `Package form data is not in editable state: ${input.pkgID}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - argumentName: 'pgkID', - }) - } + // Can't update a submission that is locked or resubmitted + if (!contractWithHistory.draftRevision) { + const errMessage = `Package is not in editable state: ${input.pkgID} status: ${contractWithHistory.status}` + logError('updateHealthPlanFormData', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new UserInputError(errMessage, { + argumentName: 'pkg', + }) + } - const previousFormData: UnlockedHealthPlanFormDataType = - previousFormDataResult + // If updatedAt does not match concurrent editing occurred. + if ( + contractWithHistory.draftRevision.updatedAt.getTime() !== + unlockedFormData.updatedAt.getTime() + ) { + const errMessage = `Concurrent update error: The data you are trying to modify has changed since you last retrieved it. Please refresh the page to continue.` + logError('updateHealthPlanFormData', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new UserInputError(errMessage) + } - // Validate that none of these protected fields have been modified. - // These fields should only be modified by the server, never by an update. - const unfixedFields = validateProtectedFields( - previousFormData, - unlockedFormData - ) + // Check for any rate updates + const updateRateFormDatas = + await convertHealthPlanPackageRatesToDomain(unlockedFormData) - if (unfixedFields.length !== 0) { - const errMessage = `Transient server error: attempted to modify un-modifiable field(s): ${unfixedFields.join( - ',' - )}. Please refresh the page to continue.` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage, { - argumentName: unfixedFields.join(','), - }) - } + if (updateRateFormDatas instanceof Error) { + const errMessage = `Error converting rate. Message: ${updateRateFormDatas.message}` + logError('updateHealthPlanFormData', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + }, + }) + } - const editableRevision = planPackage.revisions[0] + // Update contract draft revision + const updateResult = await store.updateDraftContractWithRates({ + contractID: input.pkgID, + formData: { + ...unlockedFormData, + ...unlockedFormData.contractAmendmentInfo?.modifiedProvisions, + managedCareEntities: unlockedFormData.managedCareEntities, + stateContacts: unlockedFormData.stateContacts, + supportingDocuments: unlockedFormData.documents.map((doc) => { + return { + name: doc.name, + s3URL: doc.s3URL, + sha256: doc.sha256, + id: doc.id, + } + }), + contractDocuments: unlockedFormData.contractDocuments.map( + (doc) => { + return { + name: doc.name, + s3URL: doc.s3URL, + sha256: doc.sha256, + id: doc.id, + } + } + ), + }, + rateFormDatas: updateRateFormDatas, + }) + + if (updateResult instanceof Error) { + const errMessage = `Error updating form data: ${input.pkgID}:: ${updateResult.message}` + logError('updateHealthPlanFormData', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } - // save the new form data to the db - const updateResult = await store.updateHealthPlanRevision( - planPackage.id, - editableRevision.id, - unlockedFormData - ) + // Convert back to health plan package + const pkg = convertContractWithRatesToUnlockedHPP(updateResult) - if (isStoreError(updateResult)) { - const errMessage = `Error updating form data: ${input.pkgID}:: ${updateResult.code}: ${updateResult.message}` - logError('updateHealthPlanFormData', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new Error(errMessage) - } + if (pkg instanceof Error) { + const errMessage = `Error converting draft contract. Message: ${pkg.message}` + logError('updateHealthPlanFormData', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) + } - logSuccess('updateHealthPlanFormData') - setSuccessAttributesOnActiveSpan(span) + logSuccess('updateHealthPlanFormData') + setSuccessAttributesOnActiveSpan(span) - return { - pkg: updateResult, - } - } + return { pkg } } } diff --git a/services/app-api/src/resolvers/questionResponse/createQuestion.test.ts b/services/app-api/src/resolvers/questionResponse/createQuestion.test.ts index a661e8f327..e2f4d54f4d 100644 --- a/services/app-api/src/resolvers/questionResponse/createQuestion.test.ts +++ b/services/app-api/src/resolvers/questionResponse/createQuestion.test.ts @@ -7,16 +7,19 @@ import { unlockTestHealthPlanPackage, createTestQuestion, indexTestQuestions, + defaultFloridaProgram, } from '../../testHelpers/gqlHelpers' +import { getTestStateAnalystsEmails } from '../../testHelpers/parameterStoreHelpers' +import { packageName } from '../../../../app-web/src/common-code/healthPlanFormDataType' import { assertAnError, assertAnErrorCode } from '../../testHelpers' import { createDBUsersWithFullData, testCMSUser, } from '../../testHelpers/userHelpers' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' +import { base64ToDomain } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' +import { testEmailConfig, testEmailer } from '../../testHelpers/emailerHelpers' describe('createQuestion', () => { - const mockLDService = testLDService({ ['rates-db-refactor']: true }) const cmsUser = testCMSUser() beforeAll(async () => { //Inserting a new CMS user, with division assigned, in postgres in order to create the question to user relationship. @@ -24,19 +27,15 @@ describe('createQuestion', () => { }) it('returns question data after creation', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) - const submittedPkg = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(stateServer) const createdQuestion = await createTestQuestion( cmsServer, @@ -59,19 +58,15 @@ describe('createQuestion', () => { ) }) it('allows question creation on UNLOCKED and RESUBMITTED package', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) - const submittedPkg = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(stateServer) const unlockedPkg = await unlockTestHealthPlanPackage( cmsServer, @@ -151,15 +146,12 @@ describe('createQuestion', () => { }) ) }) - it('returns an error package status is DRAFT', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + it('returns an error if package status is DRAFT', async () => { + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) const draftPkg = await createTestHealthPlanPackage(stateServer) @@ -186,12 +178,9 @@ describe('createQuestion', () => { ) }) it('returns an error if a state user attempts to create a question for a package', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) - const submittedPkg = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const stateServer = await constructTestPostgresServer() + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(stateServer) const createdQuestion = await stateServer.executeOperation({ query: CREATE_QUESTION, @@ -215,14 +204,11 @@ describe('createQuestion', () => { ) }) it('returns error on invalid package id', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) await createAndSubmitTestHealthPlanPackage(stateServer) @@ -253,14 +239,11 @@ describe('createQuestion', () => { divisionAssignment: undefined, }) await createDBUsersWithFullData([cmsUserWithNoDivision]) - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUserWithNoDivision, }, - ldService: mockLDService, }) await createAndSubmitTestHealthPlanPackage(stateServer) @@ -286,4 +269,215 @@ describe('createQuestion', () => { `users without an assigned division are not authorized to create a question` ) }) + it('send state email to state contacts and all submitters when submitting a question succeeds', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const stateServer = await constructTestPostgresServer() + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + emailer: mockEmailer, + }) + + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + + await createTestQuestion(cmsServer, stateSubmission.id) + + const currentRevision = stateSubmission.revisions[0].node.formDataProto + + const sub = base64ToDomain(currentRevision) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + + const stateReceiverEmails = [ + 'james@example.com', + ...sub.stateContacts.map((contact) => contact.email), + ] + + // email subject line is correct for state email + // Mock emailer is called 1 time + expect(mockEmailer.sendEmail).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + subject: expect.stringContaining( + `[LOCAL] New questions about ${name}` + ), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining( + Array.from(stateReceiverEmails) + ), + bodyText: expect.stringContaining( + `CMS asked questions about ${name}` + ), + bodyHTML: expect.stringContaining( + `http://localhost/submissions/${sub.id}/question-and-answers` + ), + }) + ) + }) + + it('send CMS email to state analysts if question is successfully submitted', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const stateServer = await constructTestPostgresServer() + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + emailer: mockEmailer, + }) + + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + + await createTestQuestion(cmsServer, stateSubmission.id) + + const currentRevision = stateSubmission.revisions[0].node.formDataProto + + const sub = base64ToDomain(currentRevision) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + const stateAnalystsEmails = getTestStateAnalystsEmails(sub.stateCode) + + const cmsEmails = [ + ...config.devReviewTeamEmails, + ...stateAnalystsEmails, + ] + + // email subject line is correct for CMS email + // email is sent to the state anaylsts since it + // was submitted by a DCMO user + // Mock emailer is called 2 times, + // first called to send the state email, then to CMS + expect(mockEmailer.sendEmail).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + subject: expect.stringContaining( + `[LOCAL] Questions sent for ${name}` + ), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining(Array.from(cmsEmails)), + bodyText: expect.stringContaining( + `DMCO sent questions to the state for submission ${name}` + ), + bodyHTML: expect.stringContaining( + `http://localhost/submissions/${sub.id}/question-and-answers` + ), + }) + ) + }) + + it('send CMS email to state analysts with correct round number if multiple questions have been asked', async () => { + const config = testEmailConfig() + const mockEmailer = testEmailer(config) + //mock invoke email submit lambda + const stateServer = await constructTestPostgresServer() + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + emailer: mockEmailer, + }) + const cmsDMCPUser = testCMSUser({ divisionAssignment: 'DMCP' }) + const cmsDMCPServer = await constructTestPostgresServer({ + context: { + user: cmsDMCPUser, + }, + emailer: mockEmailer, + }) + const stateSubmission = + await createAndSubmitTestHealthPlanPackage(stateServer) + + await createTestQuestion(cmsDMCPServer, stateSubmission.id) + await createTestQuestion(cmsServer, stateSubmission.id) + await createTestQuestion(cmsServer, stateSubmission.id) + + const currentRevision = stateSubmission.revisions[0].node.formDataProto + + const sub = base64ToDomain(currentRevision) + if (sub instanceof Error) { + throw sub + } + + const programs = [defaultFloridaProgram()] + const name = packageName( + sub.stateCode, + sub.stateNumber, + sub.programIDs, + programs + ) + const stateAnalystsEmails = getTestStateAnalystsEmails(sub.stateCode) + + const cmsEmails = [ + ...config.devReviewTeamEmails, + ...stateAnalystsEmails, + ] + + // email subject line is correct for CMS email + // email is sent to the state anaylsts since it + // was submitted by a DCMO user + // Mock emailer is called 4 times, + // first called to send the state email, then to CMS, two times each + expect(mockEmailer.sendEmail).toHaveBeenNthCalledWith( + 6, + expect.objectContaining({ + subject: expect.stringContaining( + `[LOCAL] Questions sent for ${name}` + ), + sourceEmail: config.emailSource, + toAddresses: expect.arrayContaining(Array.from(cmsEmails)), + bodyText: expect.stringContaining('Round: 2'), + }) + ) + }) + + it('does not send any emails if submission fails', async () => { + const mockEmailer = testEmailer() + const cmsServer = await constructTestPostgresServer({ + context: { + user: cmsUser, + }, + emailer: mockEmailer, + }) + + const submitResult = await cmsServer.executeOperation({ + query: CREATE_QUESTION, + variables: { + input: { + contractID: '1234', + documents: [ + { + name: 'Test Question', + s3URL: 'testS3Url', + }, + ], + }, + }, + }) + + expect(submitResult.errors).toBeDefined() + expect(mockEmailer.sendEmail).not.toHaveBeenCalled() + }) }) diff --git a/services/app-api/src/resolvers/questionResponse/createQuestion.ts b/services/app-api/src/resolvers/questionResponse/createQuestion.ts index aee2a351bb..85e5390341 100644 --- a/services/app-api/src/resolvers/questionResponse/createQuestion.ts +++ b/services/app-api/src/resolvers/questionResponse/createQuestion.ts @@ -1,5 +1,5 @@ import type { MutationResolvers } from '../../gen/gqlServer' -import { isCMSUser } from '../../domain-models' +import { isCMSUser, contractSubmitters } from '../../domain-models' import { logError, logSuccess } from '../../logger' import { setErrorAttributesOnActiveSpan, @@ -8,12 +8,15 @@ import { import { ForbiddenError, UserInputError } from 'apollo-server-lambda' import { NotFoundError } from '../../postgres' import type { Store } from '../../postgres' -import { isStoreError } from '../../postgres' import { GraphQLError } from 'graphql' import { isValidCmsDivison } from '../../domain-models' +import type { Emailer } from '../../emailer' +import type { EmailParameterStore } from '../../parameterStore' export function createQuestionResolver( - store: Store + store: Store, + emailParameterStore: EmailParameterStore, + emailer: Emailer ): MutationResolvers['createQuestion'] { return async (_parent, { input }, context) => { const { user, span } = context @@ -77,15 +80,98 @@ export function createQuestionResolver( throw new UserInputError(errMessage) } + const statePrograms = store.findStatePrograms(contractResult.stateCode) + const submitterEmails = contractSubmitters(contractResult) + + if (statePrograms instanceof Error) { + logError('findStatePrograms', statePrograms.message) + setErrorAttributesOnActiveSpan(statePrograms.message, span) + throw new GraphQLError(statePrograms.message, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } + + const allQuestions = await store.findAllQuestionsByContract( + contractResult.id + ) + if (allQuestions instanceof Error) { + const errMessage = `Issue finding all questions associated with the contract: ${contractResult.id}` + logError('createQuestion', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new Error(errMessage) + } + const questionResult = await store.insertQuestion(input, user) - if (isStoreError(questionResult)) { - const errMessage = `Issue creating question for package of type ${questionResult.code}. Message: ${questionResult.message}` + if (questionResult instanceof Error) { + const errMessage = `Issue creating question for package. Message: ${questionResult.message}` logError('createQuestion', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new Error(errMessage) } + allQuestions.push(questionResult) + + const sendQuestionsStateEmailResult = + await emailer.sendQuestionsStateEmail( + contractResult.revisions[0], + submitterEmails, + statePrograms, + questionResult + ) + + if (sendQuestionsStateEmailResult instanceof Error) { + logError( + 'sendQuestionsStateEmail - state email failed', + sendQuestionsStateEmailResult + ) + setErrorAttributesOnActiveSpan('state email failed', span) + const errMessage = `Error sending a state email for + questionID: ${questionResult.id} and contractID: ${contractResult.id}` + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'EMAIL_ERROR', + }, + }) + } + + let stateAnalystsEmails = + await emailParameterStore.getStateAnalystsEmails( + contractResult.stateCode + ) + //If error log it and set stateAnalystsEmails to empty string as to not interrupt the emails. + if (stateAnalystsEmails instanceof Error) { + logError('getStateAnalystsEmails', stateAnalystsEmails.message) + setErrorAttributesOnActiveSpan(stateAnalystsEmails.message, span) + stateAnalystsEmails = [] + } + + const sendQuestionsCMSEmailResult = await emailer.sendQuestionsCMSEmail( + contractResult.revisions[0], + stateAnalystsEmails, + statePrograms, + allQuestions + ) + + if (sendQuestionsCMSEmailResult instanceof Error) { + logError( + 'sendQuestionsCMSEmail - CMS email failed', + sendQuestionsCMSEmailResult + ) + setErrorAttributesOnActiveSpan('CMS email failed', span) + const errMessage = `Error sending a CMS email for + questionID: ${questionResult.id} and contractID: ${contractResult.id}` + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'EMAIL_ERROR', + }, + }) + } logSuccess('createQuestion') setSuccessAttributesOnActiveSpan(span) diff --git a/services/app-api/src/resolvers/questionResponse/createQuestionResponse.test.ts b/services/app-api/src/resolvers/questionResponse/createQuestionResponse.test.ts index dbfbed1364..6d1acaefc3 100644 --- a/services/app-api/src/resolvers/questionResponse/createQuestionResponse.test.ts +++ b/services/app-api/src/resolvers/questionResponse/createQuestionResponse.test.ts @@ -5,15 +5,19 @@ import { createTestQuestion, createTestQuestionResponse, } from '../../testHelpers/gqlHelpers' +import { base64ToDomain } from '../../../../app-web/src/common-code/proto/healthPlanFormDataProto' import { assertAnError, assertAnErrorCode } from '../../testHelpers' import { createDBUsersWithFullData, testCMSUser, } from '../../testHelpers/userHelpers' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' +import { testEmailConfig, testEmailer } from '../../testHelpers/emailerHelpers' +import { latestFormData } from '../../testHelpers/healthPlanPackageHelpers' +import { findStatePrograms } from '../../postgres' +import { packageName } from '../../../../app-web/src/common-code/healthPlanFormDataType' +import { getTestStateAnalystsEmails } from '../../testHelpers/parameterStoreHelpers' describe('createQuestionResponse', () => { - const mockLDService = testLDService({ ['rates-db-refactor']: true }) const cmsUser = testCMSUser() beforeAll(async () => { //Inserting a new CMS user, with division assigned, in postgres in order to create the question to user relationship. @@ -21,54 +25,53 @@ describe('createQuestionResponse', () => { }) it('returns question response data', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) - const submittedPkg = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(stateServer) const createdQuestion = await createTestQuestion( cmsServer, submittedPkg.id ) - const createdResponse = await createTestQuestionResponse( + const createResponseResult = await createTestQuestionResponse( stateServer, - createdQuestion?.question.id + createdQuestion.question.id ) - expect(createdResponse).toEqual({ - response: expect.objectContaining({ - id: expect.any(String), - questionID: createdQuestion?.question.id, - documents: [ - { - name: 'Test Question', - s3URL: 'testS3Url', - }, - ], - addedBy: expect.objectContaining({ - role: 'STATE_USER', - }), - }), - }) + expect(createResponseResult.question).toEqual( + expect.objectContaining({ + ...createdQuestion.question, + responses: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + questionID: createdQuestion.question.id, + documents: [ + { + name: 'Test Question Response', + s3URL: 'testS3Url', + }, + ], + addedBy: expect.objectContaining({ + role: 'STATE_USER', + }), + }), + ]), + }) + ) }) it('returns an error when attempting to create response for a question that does not exist', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const fakeID = 'abc-123' - const createdResponse = await stateServer.executeOperation({ + const createResponseResult = await stateServer.executeOperation({ query: CREATE_QUESTION_RESPONSE, variables: { input: { @@ -83,32 +86,28 @@ describe('createQuestionResponse', () => { }, }) - expect(createdResponse.errors).toBeDefined() - expect(assertAnErrorCode(createdResponse)).toBe('BAD_USER_INPUT') - expect(assertAnError(createdResponse).message).toBe( - `Issue creating question response for question ${fakeID} of type NOT_FOUND_ERROR. Message: An operation failed because it depends on one or more records that were required but not found.` + expect(createResponseResult).toBeDefined() + expect(assertAnErrorCode(createResponseResult)).toBe('BAD_USER_INPUT') + expect(assertAnError(createResponseResult).message).toBe( + `Question with ID: ${fakeID} not found to attach response to` ) }) it('returns an error if a cms user attempts to create a question response for a package', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const cmsServer = await constructTestPostgresServer({ context: { user: cmsUser, }, - ldService: mockLDService, }) - const submittedPkg = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(stateServer) const createdQuestion = await createTestQuestion( cmsServer, submittedPkg.id ) - const createdResponse = await cmsServer.executeOperation({ + const createResponseResult = await cmsServer.executeOperation({ query: CREATE_QUESTION_RESPONSE, variables: { input: { @@ -123,10 +122,154 @@ describe('createQuestionResponse', () => { }, }) - expect(createdResponse.errors).toBeDefined() - expect(assertAnErrorCode(createdResponse)).toBe('FORBIDDEN') - expect(assertAnError(createdResponse).message).toBe( + expect(createResponseResult.errors).toBeDefined() + expect(assertAnErrorCode(createResponseResult)).toBe('FORBIDDEN') + expect(assertAnError(createResponseResult).message).toBe( 'user not authorized to create a question response' ) }) + + it('sends CMS email', async () => { + const emailConfig = testEmailConfig() + const mockEmailer = testEmailer(emailConfig) + const oactCMS = testCMSUser({ + divisionAssignment: 'OACT' as const, + }) + const stateServer = await constructTestPostgresServer({ + emailer: mockEmailer, + }) + const cmsServer = await constructTestPostgresServer({ + context: { + user: oactCMS, + }, + emailer: mockEmailer, + }) + + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(stateServer) + + const formData = latestFormData(submittedPkg) + + const createdQuestion = await createTestQuestion(cmsServer, formData.id) + + await createTestQuestionResponse( + stateServer, + createdQuestion?.question.id + ) + + const statePrograms = findStatePrograms(formData.stateCode) + if (statePrograms instanceof Error) { + throw new Error( + `Unexpected error: No state programs found for stateCode ${formData.stateCode}` + ) + } + + const pkgName = packageName( + formData.stateCode, + formData.stateNumber, + formData.programIDs, + statePrograms + ) + + const stateAnalystsEmails = getTestStateAnalystsEmails( + formData.stateCode + ) + const cmsRecipientEmails = [ + ...stateAnalystsEmails, + ...emailConfig.devReviewTeamEmails, + ...emailConfig.oactEmails, + ] + + expect(mockEmailer.sendEmail).toHaveBeenNthCalledWith( + 5, // New response CMS email notification is the fifth email + expect.objectContaining({ + subject: expect.stringContaining( + `[LOCAL] New Responses for ${pkgName}` + ), + sourceEmail: emailConfig.emailSource, + toAddresses: expect.arrayContaining( + Array.from(cmsRecipientEmails) + ), + bodyText: expect.stringContaining( + `The state submitted responses to OACT's questions about ${pkgName}` + ), + bodyHTML: expect.stringContaining( + `View submission Q&A` + ), + }) + ) + }) + + it('sends State email', async () => { + const emailConfig = testEmailConfig() + const mockEmailer = testEmailer(emailConfig) + const oactCMS = testCMSUser({ + divisionAssignment: 'OACT' as const, + }) + const stateServer = await constructTestPostgresServer({ + emailer: mockEmailer, + }) + const cmsServer = await constructTestPostgresServer({ + context: { + user: oactCMS, + }, + emailer: mockEmailer, + }) + + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(stateServer) + + const formData = latestFormData(submittedPkg) + + const createdQuestion = await createTestQuestion(cmsServer, formData.id) + + await createTestQuestionResponse( + stateServer, + createdQuestion?.question.id + ) + + const statePrograms = findStatePrograms(formData.stateCode) + if (statePrograms instanceof Error) { + throw new Error( + `Unexpected error: No state programs found for stateCode ${formData.stateCode}` + ) + } + + const pkgName = packageName( + formData.stateCode, + formData.stateNumber, + formData.programIDs, + statePrograms + ) + const currentRevision = submittedPkg.revisions[0].node.formDataProto + + const sub = base64ToDomain(currentRevision) + if (sub instanceof Error) { + throw sub + } + + const stateReceiverEmails = [ + 'james@example.com', + ...sub.stateContacts.map((contact) => contact.email), + ] + + expect(mockEmailer.sendEmail).toHaveBeenNthCalledWith( + 6, // New response CMS email notification is the fifth email + expect.objectContaining({ + subject: expect.stringContaining( + `[LOCAL] Response submitted to CMS for ${pkgName}` + ), + sourceEmail: emailConfig.emailSource, + toAddresses: expect.arrayContaining( + Array.from(stateReceiverEmails) + ), + bodyText: expect.stringContaining( + `${oactCMS.divisionAssignment} round 1 response was successfully submitted` + ), + bodyHTML: expect.stringContaining( + `View response` + ), + }) + ) + }) }) diff --git a/services/app-api/src/resolvers/questionResponse/createQuestionResponse.ts b/services/app-api/src/resolvers/questionResponse/createQuestionResponse.ts index 8a3ffe7511..d43665e983 100644 --- a/services/app-api/src/resolvers/questionResponse/createQuestionResponse.ts +++ b/services/app-api/src/resolvers/questionResponse/createQuestionResponse.ts @@ -1,16 +1,21 @@ import type { MutationResolvers } from '../../gen/gqlServer' -import { isStateUser } from '../../domain-models' +import { isStateUser, contractSubmitters } from '../../domain-models' import { logError, logSuccess } from '../../logger' import { setErrorAttributesOnActiveSpan, setSuccessAttributesOnActiveSpan, } from '../attributeHelper' import { ForbiddenError, UserInputError } from 'apollo-server-lambda' +import { NotFoundError } from '../../postgres' import type { Store } from '../../postgres' -import { isStoreError } from '../../postgres' +import { GraphQLError } from 'graphql/index' +import type { Emailer } from '../../emailer' +import type { EmailParameterStore } from '../../parameterStore' export function createQuestionResponseResolver( - store: Store + store: Store, + emailer: Emailer, + emailParameterStore: EmailParameterStore ): MutationResolvers['createQuestionResponse'] { return async (_parent, { input }, context) => { const { user, span } = context @@ -29,20 +34,143 @@ export function createQuestionResponseResolver( throw new UserInputError(msg) } - const responseResult = await store.insertQuestionResponse(input, user) + const createResponseResult = await store.insertQuestionResponse( + input, + user + ) - if (isStoreError(responseResult)) { - const errMessage = `Issue creating question response for question ${input.questionID} of type ${responseResult.code}. Message: ${responseResult.message}` + if (createResponseResult instanceof Error) { + if (createResponseResult instanceof NotFoundError) { + const errMessage = `Question with ID: ${input.questionID} not found to attach response to` + logError('createQuestionResponse', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new UserInputError(errMessage) + } + + const errMessage = `Issue creating question response for question ${input.questionID}. Message: ${createResponseResult.message}` + logError('createQuestionResponse', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new Error(errMessage) + } + + const questions = await store.findAllQuestionsByContract( + createResponseResult.contractID + ) + if (questions instanceof Error) { + const errMessage = `Issue finding all questions for contract with ID ${createResponseResult.contractID}. Message: ${questions.message}` + logError('createQuestionResponse', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } + + const contract = await store.findContractWithHistory( + createResponseResult.contractID + ) + if (contract instanceof Error) { + if (contract instanceof NotFoundError) { + const errMessage = `Package with id ${createResponseResult.contractID} does not exist` + logError('createQuestionResponse', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { code: 'NOT_FOUND' }, + }) + } + + const errMessage = `Issue finding a package. Message: ${contract.message}` logError('createQuestionResponse', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) - throw new UserInputError(errMessage) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } + + const statePrograms = store.findStatePrograms(contract.stateCode) + if (statePrograms instanceof Error) { + logError('createQuestionResponse', statePrograms.message) + setErrorAttributesOnActiveSpan(statePrograms.message, span) + throw new GraphQLError(statePrograms.message, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } + const submitterEmails = contractSubmitters(contract) + + let stateAnalystsEmails = + await emailParameterStore.getStateAnalystsEmails(contract.stateCode) + //If error log it and set stateAnalystsEmails to empty string as to not interrupt the emails. + if (stateAnalystsEmails instanceof Error) { + logError('createQuestionResponse', stateAnalystsEmails.message) + setErrorAttributesOnActiveSpan(stateAnalystsEmails.message, span) + stateAnalystsEmails = [] + } + + const sendQuestionResponseCMSEmailResult = + await emailer.sendQuestionResponseCMSEmail( + contract.revisions[0], + statePrograms, + stateAnalystsEmails, + createResponseResult, + questions + ) + + if (sendQuestionResponseCMSEmailResult instanceof Error) { + logError( + 'sendQuestionResponseCMSEmail - Send CMS email', + sendQuestionResponseCMSEmailResult.message + ) + setErrorAttributesOnActiveSpan( + `Send CMS email failed: ${sendQuestionResponseCMSEmailResult.message}`, + span + ) + throw new GraphQLError('Email failed', { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'EMAIL_ERROR', + }, + }) + } + + const sendQuestionResponseStateEmailResult = + await emailer.sendQuestionResponseStateEmail( + contract.revisions[0], + statePrograms, + submitterEmails, + createResponseResult, + questions + ) + + if (sendQuestionResponseStateEmailResult instanceof Error) { + logError( + 'sendQuestionResponseStateEmail - Send State email', + sendQuestionResponseStateEmailResult.message + ) + setErrorAttributesOnActiveSpan( + `Send State email failed: ${sendQuestionResponseStateEmailResult.message}`, + span + ) + throw new GraphQLError('Email failed', { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'EMAIL_ERROR', + }, + }) } logSuccess('createQuestionResponse') setSuccessAttributesOnActiveSpan(span) return { - response: responseResult, + question: createResponseResult, } } } diff --git a/services/app-api/src/resolvers/questionResponse/indexQuestions.test.ts b/services/app-api/src/resolvers/questionResponse/indexQuestions.test.ts index 23ff062804..a9ffbb5403 100644 --- a/services/app-api/src/resolvers/questionResponse/indexQuestions.test.ts +++ b/services/app-api/src/resolvers/questionResponse/indexQuestions.test.ts @@ -11,11 +11,8 @@ import { createDBUsersWithFullData, testCMSUser, } from '../../testHelpers/userHelpers' -import { testLDService } from '../../testHelpers/launchDarklyHelpers' describe('indexQuestions', () => { - const mockLDService = testLDService({ ['rates-db-refactor']: true }) - const dmcoCMSUser = testCMSUser({ divisionAssignment: 'DMCO', }) @@ -31,31 +28,25 @@ describe('indexQuestions', () => { }) it('returns package with questions and responses for each division', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const dmcoCMSServer = await constructTestPostgresServer({ context: { user: dmcoCMSUser, }, - ldService: mockLDService, }) const dmcpCMSServer = await constructTestPostgresServer({ context: { user: dmcpCMSUser, }, - ldService: mockLDService, }) const oactCMServer = await constructTestPostgresServer({ context: { user: oactCMSUser, }, - ldService: mockLDService, }) - const submittedPkg = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(stateServer) const createdDMCOQuestion = await createTestQuestion( dmcoCMSServer, @@ -122,20 +113,9 @@ describe('indexQuestions', () => { totalCount: 1, edges: expect.arrayContaining([ { - node: expect.objectContaining({ - id: expect.any(String), - createdAt: expect.any(Date), - contractID: submittedPkg.id, - division: 'DMCO', - documents: [ - { - name: 'Test Question 1', - s3URL: 'testS3Url1', - }, - ], - addedBy: dmcoCMSUser, - responses: [responseToDMCO.response], - }), + node: expect.objectContaining( + responseToDMCO.question + ), }, ]), }), @@ -143,20 +123,9 @@ describe('indexQuestions', () => { totalCount: 1, edges: [ { - node: expect.objectContaining({ - id: expect.any(String), - createdAt: expect.any(Date), - contractID: submittedPkg.id, - division: 'DMCP', - documents: [ - { - name: 'Test Question 2', - s3URL: 'testS3Url2', - }, - ], - addedBy: dmcpCMSUser, - responses: [responseToDMCP.response], - }), + node: expect.objectContaining( + responseToDMCP.question + ), }, ], }), @@ -164,20 +133,9 @@ describe('indexQuestions', () => { totalCount: 1, edges: [ { - node: expect.objectContaining({ - id: expect.any(String), - createdAt: expect.any(Date), - contractID: submittedPkg.id, - division: 'OACT', - documents: [ - { - name: 'Test Question 3', - s3URL: 'testS3Url3', - }, - ], - addedBy: oactCMSUser, - responses: [responseToOACT.response], - }), + node: expect.objectContaining( + responseToOACT.question + ), }, ], }), @@ -185,9 +143,7 @@ describe('indexQuestions', () => { ) }) it('returns an error if you are requesting for a different state (403)', async () => { - const stateServer = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const stateServer = await constructTestPostgresServer() const otherStateServer = await constructTestPostgresServer({ context: { user: { @@ -199,18 +155,15 @@ describe('indexQuestions', () => { givenName: 'Aang', }, }, - ldService: mockLDService, }) const cmsServer = await constructTestPostgresServer({ context: { user: dmcoCMSUser, }, - ldService: mockLDService, }) - const submittedPkg = await createAndSubmitTestHealthPlanPackage( - stateServer - ) + const submittedPkg = + await createAndSubmitTestHealthPlanPackage(stateServer) await createTestQuestion(cmsServer, submittedPkg.id) @@ -230,9 +183,7 @@ describe('indexQuestions', () => { ) }) it('returns an error if health plan package does not exist', async () => { - const server = await constructTestPostgresServer({ - ldService: mockLDService, - }) + const server = await constructTestPostgresServer() await createAndSubmitTestHealthPlanPackage(server) diff --git a/services/app-api/src/resolvers/questionResponse/indexQuestions.ts b/services/app-api/src/resolvers/questionResponse/indexQuestions.ts index c157073bbc..6f6ad47980 100644 --- a/services/app-api/src/resolvers/questionResponse/indexQuestions.ts +++ b/services/app-api/src/resolvers/questionResponse/indexQuestions.ts @@ -20,14 +20,14 @@ export function indexQuestionsResolver( if (contractResult instanceof Error) { if (contractResult instanceof NotFoundError) { const errMessage = `Issue finding a contract with id ${input.contractID}. Message: Contract with id ${input.contractID} does not exist` - logError('createQuestion', errMessage) + logError('indexQuestion', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new GraphQLError(errMessage, { extensions: { code: 'NOT_FOUND' }, }) } const errMessage = `Issue finding a package. Message: ${contractResult.message}` - logError('createQuestion', errMessage) + logError('indexQuestion', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new UserInputError(errMessage) } diff --git a/services/app-api/src/resolvers/user/indexUsers.test.ts b/services/app-api/src/resolvers/user/indexUsers.test.ts index 04738814fb..553a3903bd 100644 --- a/services/app-api/src/resolvers/user/indexUsers.test.ts +++ b/services/app-api/src/resolvers/user/indexUsers.test.ts @@ -1,5 +1,5 @@ import type { InsertUserArgsType } from '../../postgres' -import { isStoreError, NewPostgresStore } from '../../postgres' +import { NewPostgresStore } from '../../postgres' import INDEX_USERS from '../../../../app-graphql/src/queries/indexUsers.graphql' import { v4 as uuidv4 } from 'uuid' import { constructTestPostgresServer } from '../../testHelpers/gqlHelpers' @@ -62,8 +62,8 @@ describe('indexUsers', () => { const newUsers = await postgresStore.insertManyUsers(usersToInsert) - if (isStoreError(newUsers)) { - throw new Error(newUsers.code) + if (newUsers instanceof Error) { + throw newUsers } const updateRes = await server.executeOperation({ diff --git a/services/app-api/src/resolvers/user/indexUsers.ts b/services/app-api/src/resolvers/user/indexUsers.ts index d3c93631c2..25b0745b01 100644 --- a/services/app-api/src/resolvers/user/indexUsers.ts +++ b/services/app-api/src/resolvers/user/indexUsers.ts @@ -4,7 +4,6 @@ import { hasAdminPermissions } from '../../domain-models' import type { QueryResolvers } from '../../gen/gqlServer' import { logError } from '../../logger' import type { Store } from '../../postgres' -import { isStoreError } from '../../postgres' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, @@ -23,7 +22,7 @@ export function indexUsersResolver(store: Store): QueryResolvers['indexUsers'] { } const findResult = await store.findAllUsers() - if (isStoreError(findResult)) { + if (findResult instanceof Error) { logError('indexUsers', findResult.message) setErrorAttributesOnActiveSpan(findResult.message, span) throw new Error('Unexpected Error Querying Users') diff --git a/services/app-api/src/resolvers/user/updateCMSUser.test.ts b/services/app-api/src/resolvers/user/updateCMSUser.test.ts index 4589357d0c..b0356c7f6b 100644 --- a/services/app-api/src/resolvers/user/updateCMSUser.test.ts +++ b/services/app-api/src/resolvers/user/updateCMSUser.test.ts @@ -1,7 +1,7 @@ import { constructTestPostgresServer } from '../../testHelpers/gqlHelpers' import UPDATE_CMS_USER from '../../../../app-graphql/src/mutations/updateCMSUser.graphql' import type { InsertUserArgsType } from '../../postgres' -import { isStoreError, NewPostgresStore } from '../../postgres' +import { NewPostgresStore } from '../../postgres' import { v4 as uuidv4 } from 'uuid' import { sharedTestPrismaClient } from '../../testHelpers/storeHelpers' import { @@ -36,8 +36,8 @@ describe('updateCMSUser', () => { } const newUser = await postgresStore.insertUser(userToInsert) - if (isStoreError(newUser)) { - throw new Error(newUser.code) + if (newUser instanceof Error) { + throw newUser } const updateRes = await server.executeOperation({ @@ -112,8 +112,8 @@ describe('updateCMSUser', () => { } const newUser = await postgresStore.insertUser(userToInsert) - if (isStoreError(newUser)) { - throw new Error(newUser.code) + if (newUser instanceof Error) { + throw newUser } // make the first update to the division assignment @@ -209,8 +209,8 @@ describe('updateCMSUser', () => { } const newUser = await postgresStore.insertUser(userToInsert) - if (isStoreError(newUser)) { - throw new Error(newUser.code) + if (newUser instanceof Error) { + throw newUser } const updateRes = await server.executeOperation({ @@ -333,8 +333,8 @@ describe('updateCMSUser', () => { email: 'zuko@example.com', } const newUser = await postgresStore.insertUser(userToInsert) - if (isStoreError(newUser)) { - throw new Error(newUser.code) + if (newUser instanceof Error) { + throw newUser } const updateRes = await server.executeOperation({ query: UPDATE_CMS_USER, diff --git a/services/app-api/src/resolvers/user/updateCMSUser.ts b/services/app-api/src/resolvers/user/updateCMSUser.ts index f6facea6c6..e0f58bc34a 100644 --- a/services/app-api/src/resolvers/user/updateCMSUser.ts +++ b/services/app-api/src/resolvers/user/updateCMSUser.ts @@ -5,8 +5,8 @@ import type { StateCodeType } from '../../../../app-web/src/common-code/healthPl import { isValidStateCode } from '../../../../app-web/src/common-code/healthPlanFormDataType' import type { MutationResolvers } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' +import { NotFoundError } from '../../postgres' import type { Store } from '../../postgres' -import { isStoreError } from '../../postgres' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, @@ -103,8 +103,8 @@ export function updateCMSUserResolver( divisionAssignment, 'Updated user assignments' // someday might have a note field and make this a param ) - if (isStoreError(result)) { - if (result.code === 'NOT_FOUND_ERROR') { + if (result instanceof Error) { + if (result instanceof NotFoundError) { const errMsg = 'cmsUserID does not exist' logError('updateCmsUser', errMsg) setErrorAttributesOnActiveSpan(errMsg, span) diff --git a/services/app-api/src/testHelpers/emailerHelpers.ts b/services/app-api/src/testHelpers/emailerHelpers.ts index 118070fcf9..23dea8e94e 100644 --- a/services/app-api/src/testHelpers/emailerHelpers.ts +++ b/services/app-api/src/testHelpers/emailerHelpers.ts @@ -1,20 +1,15 @@ import type { EmailConfiguration, EmailData, Emailer } from '../emailer' -import { - newPackageCMSEmail, - newPackageStateEmail, - unlockPackageCMSEmail, - unlockPackageStateEmail, - resubmitPackageStateEmail, - resubmitPackageCMSEmail, -} from '../emailer' +import { emailer } from '../emailer' import type { LockedHealthPlanFormDataType, ProgramArgType, UnlockedHealthPlanFormDataType, } from '../../../app-web/src/common-code/healthPlanFormDataType' -import type { StateUserType } from '../domain-models' +import type { ContractRevisionWithRatesType, Question } from '../domain-models' import { SESServiceException } from '@aws-sdk/client-ses' import { testSendSESEmail } from './awsSESHelpers' +import { testCMSUser, testStateUser } from './userHelpers' +import { v4 as uuidv4 } from 'uuid' const testEmailConfig = (): EmailConfiguration => ({ stage: 'LOCAL', @@ -24,7 +19,8 @@ const testEmailConfig = (): EmailConfiguration => ({ cmsReviewHelpEmailAddress: '"MCOG Example" ', cmsRateHelpEmailAddress: '"Rates Example" ', oactEmails: ['ratesreview@example.com'], - dmcpEmails: ['policyreview1@example.com'], + dmcpReviewEmails: ['policyreview1@example.com'], + dmcpSubmissionEmails: ['policyreviewsubmission1@example.com'], dmcoEmails: ['overallreview@example.com'], helpDeskEmail: '"MC-Review Help Desk" ', }) @@ -41,7 +37,8 @@ const testDuplicateEmailConfig: EmailConfiguration = { cmsReviewHelpEmailAddress: 'duplicate@example.com', cmsRateHelpEmailAddress: 'duplicate@example.com', oactEmails: ['duplicate@example.com', 'duplicate@example.com'], - dmcpEmails: ['duplicate@example.com', 'duplicate@example.com'], + dmcpReviewEmails: ['duplicate@example.com', 'duplicate@example.com'], + dmcpSubmissionEmails: ['duplicate@example.com', 'duplicate@example.com'], dmcoEmails: ['duplicate@example.com', 'duplicate@example.com'], helpDeskEmail: 'duplicate@example.com', } @@ -56,148 +53,23 @@ const testDuplicateStateAnalystsEmails: string[] = [ 'duplicate@example.com', ] -function testEmailer(customConfig?: EmailConfiguration): Emailer { - const config = customConfig || testEmailConfig() - return { - config, - sendEmail: jest.fn( - async (emailData: EmailData): Promise => { - try { - await testSendSESEmail(emailData) - } catch (err) { - if (err instanceof SESServiceException) { - return new Error( - 'SES email send failed. Error is from Amazon SES. Error: ' + - JSON.stringify(err) - ) - } - return new Error('SES email send failed. Error: ' + err) - } - } - ), - sendCMSNewPackage: async function ( - formData, - stateAnalystsEmails, - statePrograms - ): Promise { - const emailData = await newPackageCMSEmail( - formData, - config, - stateAnalystsEmails, - statePrograms - ) - if (emailData instanceof Error) { - return emailData - } else { - return await this.sendEmail(emailData) - } - }, - sendStateNewPackage: async function ( - formData, - submitterEmails, - statePrograms - ): Promise { - const emailData = await newPackageStateEmail( - formData, - submitterEmails, - config, - statePrograms - ) - if (emailData instanceof Error) { - return emailData - } else { - return await this.sendEmail(emailData) - } - }, - sendUnlockPackageCMSEmail: async function ( - formData, - updateInfo, - stateAnalystsEmails, - statePrograms - ): Promise { - const emailData = await unlockPackageCMSEmail( - formData, - updateInfo, - config, - stateAnalystsEmails, - statePrograms - ) - - if (emailData instanceof Error) { - return emailData - } else { - return this.sendEmail(emailData) - } - }, - sendUnlockPackageStateEmail: async function ( - formData, - updateInfo, - statePrograms, - submitterEmails - ): Promise { - const emailData = await unlockPackageStateEmail( - formData, - updateInfo, - config, - statePrograms, - submitterEmails - ) - if (emailData instanceof Error) { - return emailData - } else { - return this.sendEmail(emailData) - } - }, - sendResubmittedStateEmail: async function ( - formData, - updateInfo, - submitterEmails, - statePrograms - ): Promise { - const emailData = await resubmitPackageStateEmail( - formData, - submitterEmails, - updateInfo, - config, - statePrograms - ) - if (emailData instanceof Error) { - return emailData - } else { - return this.sendEmail(emailData) - } - }, - sendResubmittedCMSEmail: async function ( - formData, - updateInfo, - stateAnalystsEmails, - statePrograms - ): Promise { - const emailData = await resubmitPackageCMSEmail( - formData, - updateInfo, - config, - stateAnalystsEmails, - statePrograms +const sendTestEmails = async (emailData: EmailData): Promise => { + try { + await testSendSESEmail(emailData) + } catch (err) { + if (err instanceof SESServiceException) { + return new Error( + 'SES email send failed. Error is from Amazon SES. Error: ' + + JSON.stringify(err) ) - if (emailData instanceof Error) { - return emailData - } else { - return this.sendEmail(emailData) - } - }, + } + return new Error('SES email send failed. Error: ' + err) } } -const mockUser = (): StateUserType => { - return { - id: '6ec0e9a7-b5fc-44c2-a049-2d60ac37c6ee', - role: 'STATE_USER', - email: 'test+state+user@example.com', - stateCode: 'MN', - familyName: 'State', - givenName: 'User', - } +function testEmailer(customConfig?: EmailConfiguration): Emailer { + const config = customConfig || testEmailConfig() + return emailer(config, jest.fn(sendTestEmails)) } type State = { @@ -253,6 +125,127 @@ export function mockMSState(): State { code: 'MS', } } +const mockContractRev = ( + submissionPartial?: Partial +): ContractRevisionWithRatesType => { + return { + createdAt: new Date('01/01/2021'), + updatedAt: new Date('02/01/2021'), + contract: { + stateCode: 'MN', + stateNumber: 3, + id: '12345', + }, + id: 'test-abc-125', + formData: { + programIDs: ['abbdf9b0-c49e-4c4c-bb6f-040cb7b51cce'], + populationCovered: 'CHIP', + submissionType: 'CONTRACT_AND_RATES', + riskBasedContract: false, + submissionDescription: 'A submitted submission', + stateContacts: [ + { + name: 'Test Person', + titleRole: 'A Role', + email: 'test+state+contact@example.com', + }, + ], + supportingDocuments: [ + { + s3URL: 'bar', + name: 'foo', + sha256: 'fakesha', + }, + ], + contractType: 'BASE', + contractExecutionStatus: undefined, + contractDocuments: [ + { + s3URL: 'bar', + name: 'foo', + sha256: 'fakesha', + }, + ], + contractDateStart: new Date('01/01/2024'), + contractDateEnd: new Date('01/01/2025'), + managedCareEntities: ['MCO'], + federalAuthorities: ['VOLUNTARY', 'BENCHMARK'], + inLieuServicesAndSettings: undefined, + modifiedBenefitsProvided: undefined, + modifiedGeoAreaServed: undefined, + modifiedMedicaidBeneficiaries: undefined, + modifiedRiskSharingStrategy: undefined, + modifiedIncentiveArrangements: undefined, + modifiedWitholdAgreements: undefined, + modifiedStateDirectedPayments: undefined, + modifiedPassThroughPayments: undefined, + modifiedPaymentsForMentalDiseaseInstitutions: undefined, + modifiedMedicalLossRatioStandards: undefined, + modifiedOtherFinancialPaymentIncentive: undefined, + modifiedEnrollmentProcess: undefined, + modifiedGrevienceAndAppeal: undefined, + modifiedNetworkAdequacyStandards: undefined, + modifiedLengthOfContract: undefined, + modifiedNonRiskPaymentArrangements: undefined, + statutoryRegulatoryAttestation: undefined, + statutoryRegulatoryAttestationDescription: undefined, + }, + rateRevisions: [ + { + id: '12345', + rate: { + id: 'rate-id', + stateCode: 'MN', + stateNumber: 3, + createdAt: new Date(11 / 27 / 2023), + }, + submitInfo: undefined, + unlockInfo: undefined, + createdAt: new Date(11 / 27 / 2023), + updatedAt: new Date(11 / 27 / 2023), + formData: { + id: 'test-id-1234', + rateID: 'test-id-1234', + rateType: 'NEW', + rateCapitationType: 'RATE_CELL', + rateDocuments: [ + { + s3URL: 'bar', + name: 'foo', + sha256: 'fakesha', + }, + ], + supportingDocuments: [], + rateDateStart: new Date('01/01/2024'), + rateDateEnd: new Date('01/01/2025'), + rateDateCertified: new Date('01/01/2024'), + amendmentEffectiveDateStart: new Date('01/01/2024'), + amendmentEffectiveDateEnd: new Date('01/01/2025'), + rateProgramIDs: ['3fd36500-bf2c-47bc-80e8-e7aa417184c5'], + rateCertificationName: 'Rate Cert Name', + certifyingActuaryContacts: [ + { + actuarialFirm: 'DELOITTE', + name: 'Actuary Contact 1', + titleRole: 'Test Actuary Contact 1', + email: 'actuarycontact1@example.com', + }, + ], + addtlActuaryContacts: [], + actuaryCommunicationPreference: 'OACT_TO_ACTUARY', + packagesWithSharedRateCerts: [ + { + packageName: 'pkgName', + packageId: '12345', + packageStatus: 'SUBMITTED', + }, + ], + }, + }, + ], + ...submissionPartial, + } +} const mockContractAndRatesFormData = ( submissionPartial?: Partial @@ -274,7 +267,6 @@ const mockContractAndRatesFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], }, ], contractType: 'BASE', @@ -284,7 +276,6 @@ const mockContractAndRatesFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['CONTRACT' as const], }, ], contractDateStart: new Date('01/01/2021'), @@ -299,7 +290,6 @@ const mockContractAndRatesFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -361,7 +351,6 @@ const mockUnlockedContractAndRatesFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], }, ], contractType: 'BASE', @@ -371,7 +360,6 @@ const mockUnlockedContractAndRatesFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['CONTRACT' as const], }, ], contractDateStart: new Date('01/01/2021'), @@ -386,7 +374,6 @@ const mockUnlockedContractAndRatesFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -448,7 +435,6 @@ const mockUnlockedContractOnlyFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, ], contractType: 'BASE', @@ -458,7 +444,6 @@ const mockUnlockedContractOnlyFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['CONTRACT' as const], }, ], contractDateStart: new Date('01/01/2021'), @@ -500,7 +485,6 @@ const mockContractOnlyFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], }, ], contractType: 'BASE', @@ -510,7 +494,6 @@ const mockContractOnlyFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['CONTRACT' as const], }, ], contractDateStart: new Date('01/01/2021'), @@ -552,7 +535,6 @@ const mockContractAmendmentFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], }, ], contractType: 'AMENDMENT', @@ -562,7 +544,6 @@ const mockContractAmendmentFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['CONTRACT' as const], }, ], contractDateStart: new Date('01/01/2021'), @@ -577,7 +558,6 @@ const mockContractAmendmentFormData = ( s3URL: 'bar', name: 'foo', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -620,6 +600,57 @@ const mockContractAmendmentFormData = ( } } +const mockQuestionAndResponses = ( + questionData?: Partial +): Question => { + const question: Question = { + id: `test-question-id-1`, + contractID: 'contract-id-test', + createdAt: new Date('01/01/2024'), + addedBy: testCMSUser(), + documents: [ + { + name: 'Test Question', + s3URL: 'testS3Url', + }, + ], + division: 'DMCO', + responses: [], + ...questionData, + } + + const defaultResponses = [ + { + id: uuidv4(), + questionID: question.id, + //Add 1 day to date, to make sure this date is always after question.createdAt + createdAt: ((): Date => { + const responseDate = new Date(question.createdAt) + return new Date( + responseDate.setDate(responseDate.getDate() + 1) + ) + })(), + addedBy: testStateUser(), + documents: [ + { + name: 'Test Question Response', + s3URL: 'testS3Url', + }, + ], + }, + ] + + // If responses are passed in, use that and replace questionIDs, so they match the question. + question.responses = questionData?.responses + ? questionData.responses.map((response) => ({ + ...response, + questionID: question.id, + })) + : defaultResponses + + return question +} + export { testEmailConfig, testStateAnalystsEmails, @@ -627,9 +658,10 @@ export { testDuplicateStateAnalystsEmails, mockContractAmendmentFormData, mockContractOnlyFormData, + mockContractRev, mockContractAndRatesFormData, mockUnlockedContractAndRatesFormData, mockUnlockedContractOnlyFormData, - mockUser, testEmailer, + mockQuestionAndResponses, } diff --git a/services/app-api/src/testHelpers/errorHelpers.ts b/services/app-api/src/testHelpers/errorHelpers.ts index e35a75b04f..30275a9b56 100644 --- a/services/app-api/src/testHelpers/errorHelpers.ts +++ b/services/app-api/src/testHelpers/errorHelpers.ts @@ -1,15 +1,8 @@ // For use in TESTS only. Throws a returned error -import type { StoreError } from '../postgres' -import { isStoreError } from '../postgres' - -function must(maybeErr: T | Error | StoreError): T { +function must(maybeErr: T | Error): T { if (maybeErr instanceof Error) { throw maybeErr } - - if (isStoreError(maybeErr)) { - throw maybeErr - } return maybeErr } diff --git a/services/app-api/src/testHelpers/gqlHelpers.ts b/services/app-api/src/testHelpers/gqlHelpers.ts index baaedc3648..0cabd82817 100644 --- a/services/app-api/src/testHelpers/gqlHelpers.ts +++ b/services/app-api/src/testHelpers/gqlHelpers.ts @@ -110,7 +110,8 @@ const constructTestEmailer = (): Emailer => { cmsReviewHelpEmailAddress: 'mcog@example.com', cmsRateHelpEmailAddress: 'rates@example.com', oactEmails: ['testRate@example.com'], - dmcpEmails: ['testPolicy@example.com'], + dmcpReviewEmails: ['testPolicy@example.com'], + dmcpSubmissionEmails: ['testPolicySubmission@example.com'], dmcoEmails: ['testDmco@example.com'], helpDeskEmail: 'MC_Review_HelpDesk@example.com>', } @@ -243,7 +244,6 @@ const createAndUpdateTestHealthPlanPackage = async ( name: 'rateDocument.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -280,7 +280,6 @@ const createAndUpdateTestHealthPlanPackage = async ( { name: 'contractDocument.pdf', s3URL: 'fakeS3URL', - documentCategories: ['CONTRACT' as const], sha256: 'fakesha', }, ] @@ -495,7 +494,7 @@ const createTestQuestionResponse = async ( const response = responseData || { documents: [ { - name: 'Test Question', + name: 'Test Question Response', s3URL: 'testS3Url', }, ], @@ -504,8 +503,8 @@ const createTestQuestionResponse = async ( query: CREATE_QUESTION_RESPONSE, variables: { input: { - questionID, ...response, + questionID, }, }, }) diff --git a/services/app-api/src/testHelpers/parameterStoreHelpers.ts b/services/app-api/src/testHelpers/parameterStoreHelpers.ts index c5fafeac35..4e70bd1017 100644 --- a/services/app-api/src/testHelpers/parameterStoreHelpers.ts +++ b/services/app-api/src/testHelpers/parameterStoreHelpers.ts @@ -26,7 +26,10 @@ function mockEmailParameterStoreError(error?: string): EmailParameterStore { getDMCOEmails: async (): Promise => { return new Error(message) }, - getDMCPEmails: async (): Promise => { + getDMCPReviewEmails: async (): Promise => { + return new Error(message) + }, + getDMCPSubmissionEmails: async (): Promise => { return new Error(message) }, getSourceEmail: async (): Promise => { diff --git a/services/app-api/src/testHelpers/storeHelpers.ts b/services/app-api/src/testHelpers/storeHelpers.ts index bff94b49fa..6d621b8b9a 100644 --- a/services/app-api/src/testHelpers/storeHelpers.ts +++ b/services/app-api/src/testHelpers/storeHelpers.ts @@ -1,5 +1,5 @@ import type { PrismaClient } from '@prisma/client' -import type { Store, StoreError } from '../postgres' +import type { Store } from '../postgres' import { NewPrismaClient } from '../postgres' async function configurePrismaClient(): Promise { @@ -33,34 +33,11 @@ async function sharedTestPrismaClient(): Promise { } function mockStoreThatErrors(): Store { - const genericStoreError: StoreError = { - code: 'UNEXPECTED_EXCEPTION', - message: 'this error came from the generic store with errors mock', - } - const genericError: Error = new Error( 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' ) return { - findAllHealthPlanPackagesByState: async (_stateCode) => { - return genericStoreError - }, - findAllHealthPlanPackagesBySubmittedAt: async () => { - return genericStoreError - }, - insertHealthPlanPackage: async (_args) => { - return genericStoreError - }, - findHealthPlanPackage: async (_draftUUID) => { - return genericStoreError - }, - insertHealthPlanRevision: async (_pkgID, _draft) => { - return genericStoreError - }, - updateHealthPlanRevision: async (_pkgID, _formData) => { - return genericStoreError - }, findPrograms: () => { return new Error( 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' @@ -72,34 +49,31 @@ function mockStoreThatErrors(): Store { ) }, findAllSupportedStates: async () => { - return genericStoreError - }, - findAllRevisions: async () => { - return genericStoreError + return genericError }, findAllUsers: async () => { - return genericStoreError + return genericError }, findUser: async (_ID) => { - return genericStoreError + return genericError }, insertUser: async (_args) => { - return genericStoreError + return genericError }, insertManyUsers: async (_args) => { - return genericStoreError + return genericError }, updateCmsUserProperties: async (_ID, _State) => { - return genericStoreError + return genericError }, insertQuestion: async (_ID) => { - return genericStoreError + return genericError }, findAllQuestionsByContract: async (_pkgID) => { return genericError }, insertQuestionResponse: async (_ID) => { - return genericStoreError + return genericError }, insertDraftContract: async (_ID) => { return genericError diff --git a/services/app-api/src/testHelpers/userHelpers.ts b/services/app-api/src/testHelpers/userHelpers.ts index c523d749b4..cc6671a9ae 100644 --- a/services/app-api/src/testHelpers/userHelpers.ts +++ b/services/app-api/src/testHelpers/userHelpers.ts @@ -1,5 +1,5 @@ import type { InsertUserArgsType } from '../postgres' -import { isStoreError, NewPostgresStore } from '../postgres' +import { NewPostgresStore } from '../postgres' import type { AdminUserType, CMSUserType, @@ -58,8 +58,8 @@ const createDBUsersWithFullData = async ( const result = await postgresStore.insertManyUsers(usersSeed) - if (isStoreError(result)) { - throw new Error(result.message) + if (result instanceof Error) { + throw result } return result diff --git a/services/app-graphql/src/mutations/createQuestionResponse.graphql b/services/app-graphql/src/mutations/createQuestionResponse.graphql index 0c3b15bc94..3b12555e21 100644 --- a/services/app-graphql/src/mutations/createQuestionResponse.graphql +++ b/services/app-graphql/src/mutations/createQuestionResponse.graphql @@ -1,8 +1,8 @@ mutation createQuestionResponse($input: CreateQuestionResponseInput!) { createQuestionResponse(input: $input) { - response { + question { id - questionID + contractID createdAt addedBy { id @@ -10,20 +10,42 @@ mutation createQuestionResponse($input: CreateQuestionResponseInput!) { email givenName familyName - state { + divisionAssignment + stateAssignments { code name - programs { - id - name - fullName - } } } + division documents { name s3URL } + responses { + id + questionID + createdAt + addedBy { + id + role + email + givenName + familyName + state { + code + name + programs { + id + name + fullName + } + } + } + documents { + name + s3URL + } + } } } } diff --git a/services/app-graphql/src/queries/fetchEmailSettings.graphql b/services/app-graphql/src/queries/fetchEmailSettings.graphql index f271219d53..0bf9d52ca5 100644 --- a/services/app-graphql/src/queries/fetchEmailSettings.graphql +++ b/services/app-graphql/src/queries/fetchEmailSettings.graphql @@ -6,7 +6,8 @@ query fetchEmailSettings { emailSource devReviewTeamEmails oactEmails - dmcpEmails + dmcpReviewEmails + dmcpSubmissionEmails dmcoEmails cmsReviewHelpEmailAddress cmsRateHelpEmailAddress diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index c46bce7078..0a0c85897b 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -62,20 +62,23 @@ type Query { """ indexRates returns an array of rates with their revisions and related contracts - - It can be called by CMS users + Only rates with at least one submitted revision are returned + indexRates can be called by CMS and admin users Errors: - - ForbiddenError: This API is not available due to feature flags. + - ForbiddenError: User must be a CMS or Admin type user + - NotFoundError: No rates with at least one submitted revision were found """ indexRates: IndexRatesPayload! """ - fetchRate returns an array of rates with their revisions and related contracts + fetchRate returns a rate with its revisions, including contract revisions + for a given rate's ID It can be called by CMS or State users Errors: - ForbiddenError: This API is not available due to feature flags. + - NotFoundError: rate for rate.ID not found in database """ fetchRate( input: FetchRateInput! @@ -269,16 +272,26 @@ type Mutation { } input CreateHealthPlanPackageInput { - "Population that the contract covers" + """ + The large overarching population of people that the program covers. + Options are MEDICAID, CHIP, MEDICAID_AND_CHIP + """ populationCovered: PopulationCoveredType! "An array of managed care program IDs this package covers" programIDs: [ID!]! "Whether or not this contract is risk based" riskBasedContract: Boolean - "The submission type of this package" + """ + The submission type of this package + Options are CONTRACT_ONLY and CONTRACT_AND_RATES + """ submissionType: SubmissionType! - "User description of the package" + "User description of the reason for the submission" submissionDescription: String! + """ + Type of contract the state is submitting + Options are: BASE, AMENDMENT + """ contractType: ContractType! } @@ -376,8 +389,8 @@ input CreateQuestionResponseInput { } type CreateQuestionResponsePayload { - "The newly created QuestionResponse" - response: QuestionResponse! + "Question with newly created response" + question: Question! } type UserEdge { @@ -695,7 +708,8 @@ type EmailConfiguration { emailSource: String! devReviewTeamEmails: [String!]! oactEmails: [String!]! - dmcpEmails: [String!]! + dmcpReviewEmails: [String!]! + dmcpSubmissionEmails: [String!]! dmcoEmails: [String!]! cmsReviewHelpEmailAddress: String! cmsRateHelpEmailAddress: String! @@ -726,32 +740,22 @@ type GenericDocument { sha256: String! } -""" -Rates are rate certifications and their associated actuary contacts and documents -State users may create, update, and submit several rates at a time -""" - +"The large overarching population of people that the program covers." enum PopulationCovered { MEDICAID CHIP MEDICAID_AND_CHIP } - -enum SubmissionType { - CONTRACT_ONLY - CONTRACT_AND_RATES -} - -enum ContractType { - BASE - AMENDMENT -} - +"Whether contract has been fully executed by all parties or not" enum ContractExecutionStatus { EXECUTED UNEXECUTED } +""" +The type of organization the state is contracting with in order to deliver +managed care services +""" enum ManagedCareEntity { MCO PIHP @@ -759,6 +763,10 @@ enum ManagedCareEntity { PCCM } +""" +The state plan and/or waiver authorities that allow the +state to run its managed care programs +""" enum FederalAuthority { STATE_PLAN WAIVER_1915B @@ -767,63 +775,156 @@ enum FederalAuthority { BENCHMARK TITLE_XXI } - +"Contact information for contacting states regarding their submission" type StateContact { name: String title: String email: String } +""" +ContractFormData represents the form data that was inputted by the state +This type is used for the form data field found on a contract revision +""" type ContractFormData { + """ + An array of IDs representing state programs that the contract covers + """ programIDs: [String!]! + """ + The large overarching population of people that the program covers. + Options are MEDICAID, CHIP, MEDICAID_AND_CHIP + """ populationCovered: PopulationCovered + """ + The submission type of this package + Options are CONTRACT_ONLY and CONTRACT_AND_RATES + """ submissionType: SubmissionType! + """ + Whether or not this contract is risk based + Risk-based contracts have specific requirements that + non-risk based contracts do not have + """ riskBasedContract: Boolean + "State provided summary of the contract being submitted" submissionDescription: String! + """ + Array of state contacts of state representatives who should be + contacted about updates to the contract + Each state contact contains string fields for: name, title, and email + """ stateContacts: [StateContact!]! + """ + Additional documents the state uploads to support a contract + Files can be PDF, DOC, DOCX, XLSX, CSV format + """ supportingDocuments: [GenericDocument!]! + """ + Type of contract the state is submitting + Options are: BASE, AMENDMENT + """ contractType: ContractType + """ + Execution status for a contract. + Contracts are fully executed or unexecuted by some or all parties + Status can be either EXECUTED or UNEXECUTED + """ contractExecutionStatus: ContractExecutionStatus + """ + State upload of the submitted contract + """ contractDocuments: [GenericDocument!]! + "Start date of the contract" contractDateStart: Date + "End date of the contract" contractDateEnd: Date + """ + The type of organization the state is contracting with + in order to deliver managed care services + Options are MCO, PIHP, PAHP, and PCCM + """ managedCareEntities: [ManagedCareEntity!]! + """ + The state plan and/or waiver authorities that allow the state + to run its managed care programs + """ federalAuthorities: [FederalAuthority!]! + """ + If contract is in Lieu-of Services and Settings (ILOSs) + in accordance with 42 CFR § 438.3(e)(2) + """ inLieuServicesAndSettings: Boolean, + "If contract includes modifications to benefits provided by the managed care plans" modifiedBenefitsProvided: Boolean, + "If contract includes modifications to the geographic areas served by the managed care plans" modifiedGeoAreaServed: Boolean, + """ + If contract includes modifications to the Medicaid beneficiaries served by the managed care + plans (e.g. eligibility or enrollment criteria) + """ modifiedMedicaidBeneficiaries: Boolean, + "If contract includes modifications to the risk sharing strategy" modifiedRiskSharingStrategy: Boolean, + "If contract includes modifications to incentive arrangements" modifiedIncentiveArrangements: Boolean, + "If contract includes modifications to the withold agreements" modifiedWitholdAgreements: Boolean, + "If contract includes modifications to the state directed payments" modifiedStateDirectedPayments: Boolean, + "If contract includes modifications to the pass-through payments" modifiedPassThroughPayments: Boolean, + """ + If contract includes modifications to payments to MCOs and PIHPs for enrollees that + are a patient in an institution for mental disease + """ modifiedPaymentsForMentalDiseaseInstitutions: Boolean, + "If contract includes modifications to the medical loss ratio standards" modifiedMedicalLossRatioStandards: Boolean, + """ + If contract includes modifications to + other financial, payment, incentive or related contractual provisions + """ modifiedOtherFinancialPaymentIncentive: Boolean, + "If contract includes modifications to the enrollment/disenrollment process" modifiedEnrollmentProcess: Boolean, + "If contract includes modifications to the grevience and appeal system" modifiedGrevienceAndAppeal: Boolean, + "If contract includes modifications to the network adequacy standards" modifiedNetworkAdequacyStandards: Boolean, + "If contract includes modifications to the length of the contract period" modifiedLengthOfContract: Boolean, + "If contract includes modifications to the non-risk payment arrangements" modifiedNonRiskPaymentArrangements: Boolean, } +"Either new capitation rates (NEW) or updates to previously certified capitation rates (AMENDMENT)" enum RateType { NEW AMENDMENT } +""" +Determines on what basis the capitation rate is actuarially sound. +With RATE_RANGE the state certifies a range of rates +from the low to high end of the range as actuarially sound +""" enum RateCapitationType { RATE_CELL RATE_RANGE } - +""" +State's communication preference for contacting their actuaries. Either: +- wants CMS to reach out to their actuaries directly or +- go through them +""" enum ActuaryCommunication { OACT_TO_ACTUARY OACT_TO_STATE } +"The firm that the certifying actuary works for" enum ActuarialFirm { MERCER MILLIMAN @@ -834,6 +935,7 @@ enum ActuarialFirm { OTHER } +"Contact information for the certifying or additional state actuary" type ActuaryContact { name: String titleRole: String @@ -842,68 +944,167 @@ type ActuaryContact { actuarialFirmOther: String } +""" +A package in the system +that shares a rate with another package. +It's used as a part of RateFormData +""" type PackageWithSameRate { packageName: String! packageId: String! packageStatus: String } +""" +RateFormData represents the form data that was inputted by the state +This type is used for the form data field found on a rate revision +""" type RateFormData { + """ + Can be 'NEW' or 'AMENDMENT' + Refers to whether the state is submitting a brand new rate certification + or an amendment to an existing rate certification + """ rateType: RateType + """ + Can be 'RATE_CELL' or 'RATE_RANGE' + These values represent on what basis the capitation rate is actuarially sound + """ rateCapitationType: RateCapitationType + """ + Signed certification documents the state uploads + Files can be PDF, DOC, or DOCX format + """ rateDocuments: [GenericDocument!]! + """ + Additional documents the state uploads to support a rate cert + Files can be PDF, DOC, DOCX, XLSX, CSV format + """ supportingDocuments: [GenericDocument!]! + """ + If the rateType is NEW this is the start date of the + rating period for a new certification. + If the rateType is AMENDMENT this is the start date of the + rating period for the original rate certification + """ rateDateStart: Date + """ + If the rateType is NEW this is the end date of the + rating period for a new certification. + If the rateType is AMENDMENT this is the end date of the + rating period for the original rate certification + """ rateDateEnd: Date + """ + The date the rate certification was + certified/signed by the state's actuary + """ rateDateCertified: Date + """ + The start date of the rate amendment + Only relevant if rate type is AMENDMENT + """ amendmentEffectiveDateStart: Date + """ + The end date of the rate amendment + Only relevant if rate type is AMENDMENT + """ amendmentEffectiveDateEnd: Date + """ + An array of IDs representing state programs that the rate covers + """ rateProgramIDs: [String!]! + """ + Represents the name of the rate. + This value is auto generated based on rate, package and state program details + """ rateCertificationName: String + """ + An array of ActuaryContacts + Each element includes the the name, title/role and email + of the actuaries who certified the rate + """ certifyingActuaryContacts: [ActuaryContact!]! + """ + An array of additional ActuaryContacts + Each element includes the the name, title/role and email + """ addtlActuaryContacts: [ActuaryContact!]! + """ + Is either OACT_TO_ACTUARY or OACT_TO_STATE + It specifies whether the state wants CMS to reach out to their actuaries + directly or go through them + """ actuaryCommunicationPreference: ActuaryCommunication + """ + An array of PackageWithSameRate elements + which contain the packageName, packageId, and packageStatus + These elements represent other packages in the system + that are using this rate + """ packagesWithSharedRateCerts: [PackageWithSameRate!]! } type ContractOnRevisionType { id: String! + "The two letter abbreviation for the state the contract covers" stateCode: String! + """ + Maps a MC-Review contract to the record number + used in the MC-CRS system + """ mccrsID: String + "Number of contracts a state has" stateNumber: Int! } type RelatedContractRevisions { id: String! contract: ContractOnRevisionType! + """ + Information about who, when, and why this revision was unlocked. + Will be blank on the initial revision. + """ submitInfo: UpdateInformation + "Information on who, when, and why this revision was submitted." unlockInfo: UpdateInformation createdAt: DateTime! updatedAt: DateTime! formData: ContractFormData! } +""" +A rate revision represents a single submission +for the rate and contains the full data from when the rate cert was submitted +""" type RateRevision { id: ID! createdAt: DateTime! updatedAt: DateTime! """ - Information about who, when, and why this revision was unlocked. - Will be blank on the initial revision. - """ + Information about who, when, and why this revision was unlocked. + Will be blank on the initial revision. + """ unlockInfo: UpdateInformation - "Information on who, when, and why this revision was submitted." + "Information on who, when, and why this revision was submitted." submitInfo: UpdateInformation + "The rate related form data that was inputed by the state" formData: RateFormData! + "Contract revisions related to the rate" contractRevisions: [RelatedContractRevisions!]! } +""" +Rates are rate certifications and their associated actuary contacts and documents +State users may create, update, and submit several rates at a time +""" type Rate { id: ID! createdAt: DateTime! updatedAt: DateTime! """ Where the package is in the submission flow. + Options are DRAFT, SUBMITTED, RESUBMITTED and UNLOCKED SUBMITTED and RESUBMITTED packages cannot be modified """ status: HealthPlanPackageStatus! @@ -913,6 +1114,10 @@ type Rate { stateCode: String! "Fuller state data for the submitting state" state: State! + """ + The number of contracts this rate relates to. + This value is used to generate the rateName + """ stateNumber: Int! "The currently modifiable revision if the rate is DRAFT or UNLOCKED" draftRevision: RateRevision @@ -928,11 +1133,18 @@ type RateEdge { } type IndexRatesPayload { + "Total number of submitted rates returned on request" totalCount: Int + """ + Rates that include rate and contract revisions + """ edges: [RateEdge!]! } type FetchRatePayload { + """ + A rate that include contract and rate revisions + """ rate: Rate! } diff --git a/services/app-proto/README.md b/services/app-proto/README.md index 1f8a299d3c..c44229b329 100644 --- a/services/app-proto/README.md +++ b/services/app-proto/README.md @@ -13,47 +13,3 @@ In short: our submission form data is complex nested data that is slowly changin We use [protobuf.js](https://github.com/protobufjs/protobuf.js) to generate javascript code for reading and writing protobufs based on the schema defined in /src/state_submission.proto To read about the code we wrote that converts our domain models to and from protobuf, check out the [stateSubmission package](https://github.com/Enterprise-CMCS/managed-care-review/tree/main/services/app-web/src/common-code/proto/healthPlanFormDataProto) - -# HealthPlanFormData Protobuf Migrations - -This module contains a script migrate_protos.ts that runs the migrations in `./protoMigrations/healthPlanFormDataMigrations` on all the protos in our db or all the protos saved as files in a directory. - -Usage: - -``` -# build -../node_modules/.bin/tsc - -# run against db running at DATABASE_URL -npx node ./protoMigrations/build/migrate_protos.js db - -# run against a directory of .proto files (npx runs as if from the app-proto directory) -npx node ./protoMigrations/build/migrate_protos.js files ../app-web/src/common-code/proto/healthPlanFormDataProto/testData - -``` - -## Adding a new migration - -Migrations are typescript files that export a single function called "migrateProto" with the signature: - -``` -export function migrateProto( - oldProto: mcreviewproto.IHealthPlanFormData -): mcreviewproto.IHealthPlanFormData -``` - -To create a new migration, add a new file to the `healthPlanFormDataMigrations` directory with that exported function in it. The function will be called by the migration script for every single proto that is being migrated. The migration is called with a decoded protobuf data in the _generated proto format_, _not_ our domain models. This is to provide us with the maximum flexibility for updating proto data. We interact with this format directly in toProto and toDomain, whereas the rest of our app uses our domain models. - -The migration function should return a transformed protobuf which will be written out by the migrator. - -Please document the reason for the migration thoroughly in the file. - -## Testing migrations - -In the proto package, we have a set of old protos saved as files with tests that confirm that they are still decoded correctly with our current code. Those files will all need to be run through your new migration in order for those tests to continue to be accurate, since we will migrate all the protos in our db as well. - -The saved protos are located at `/services/app-web/src/common-code/proto/healthPlanFormDataProto/testData` and tests are in the directory one level up. - -These .proto files are generated /automatically/ by the writeNewProtos.test.ts test. If our handful of monitored test _domain models_ change how they are written out as a protobuf, this test will write a new proto with the date on it into the testData directory. - -Tests, like the unlockedWithALittleBitOfEverything.test.ts file decode these saved protos and make assertions about them. Any new migration can be tested by writing a test that fails against some of the old .proto files, then running the migration against our testData directory and ensuring that the test now passes. This is the best way to confirm that a migration is going to behave as expected against our live data. diff --git a/services/app-proto/package.json b/services/app-proto/package.json index b0ed598229..f58c1d013e 100644 --- a/services/app-proto/package.json +++ b/services/app-proto/package.json @@ -11,8 +11,7 @@ "proto:copy-web": "rsync -av ./gen/ ../app-web/src/gen", "proto:copy-cypress": "rsync -av ./gen/ ../cypress/gen", "precommit": "lint-staged", - "lint": "protolint lint .", - "build": "tsc" + "lint": "protolint lint ." }, "lint-staged": { "*.proto": [ diff --git a/services/app-proto/protoMigrations/healthPlanFormDataMigrations/0000_initial_migration.ts b/services/app-proto/protoMigrations/healthPlanFormDataMigrations/0000_initial_migration.ts deleted file mode 100644 index 98150db805..0000000000 --- a/services/app-proto/protoMigrations/healthPlanFormDataMigrations/0000_initial_migration.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { mcreviewproto } from '../../gen/healthPlanFormDataProto' - -/* - * This is our first pass at a migration, the intention is to run this migration through prod - * first of all so that there are no important code changes that are associated with it. - * This migration just bumps the version number of our protos, nothing more. - */ - -export function migrateProto( - oldProto: mcreviewproto.IHealthPlanFormData -): mcreviewproto.IHealthPlanFormData { - if (!oldProto.protoVersion || oldProto.protoVersion <= 1) { - oldProto.protoVersion = 2 - } - - return oldProto -} diff --git a/services/app-proto/protoMigrations/healthPlanFormDataMigrations/0001_rate_id_migration.ts b/services/app-proto/protoMigrations/healthPlanFormDataMigrations/0001_rate_id_migration.ts deleted file mode 100644 index 3c6a0a6515..0000000000 --- a/services/app-proto/protoMigrations/healthPlanFormDataMigrations/0001_rate_id_migration.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { mcreviewproto } from '../../gen/healthPlanFormDataProto' - -/** - * In preparation for rates across submissions. This migration is to generate an uuid for on existing submissions - * with rate certifications. New submissions will have uuids generated when form data gets encoded into protobuffer - * in toProtoBuffer. - */ - -export function migrateProto( - oldProto: mcreviewproto.IHealthPlanFormData -): mcreviewproto.IHealthPlanFormData { - const { v4: uuidv4 } = require('uuid'); - //Only perform migration on contract and submission packages that contain a rate certification. - if (oldProto.submissionType === 3 && oldProto.rateInfos && oldProto.rateInfos?.length > 0) { - oldProto.rateInfos = oldProto.rateInfos.map(rateInfo => ( - { - ...rateInfo, - id: rateInfo.id ?? uuidv4() - } - )) - } - - return oldProto -} diff --git a/services/app-proto/protoMigrations/healthPlanFormDataMigrations/0002_rate_programs_migration.ts b/services/app-proto/protoMigrations/healthPlanFormDataMigrations/0002_rate_programs_migration.ts deleted file mode 100644 index 8dbb32247e..0000000000 --- a/services/app-proto/protoMigrations/healthPlanFormDataMigrations/0002_rate_programs_migration.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { mcreviewproto } from '../../gen/healthPlanFormDataProto' - -/** - * There are old submitted contract and rate submissions in prod without `rateProgramsIDs` from before this field was - * added to the proto schema and domain model. This migration is to add `rateProgramIDs` to those packages with the - * package programs 'programsIDs`. This will be done in conjunction with the api fix, in `isValidRates`, that will require - * contract and rate submissions to have a rate program on submission/resubmission. - */ - -export function migrateProto( - oldProto: mcreviewproto.IHealthPlanFormData -): mcreviewproto.IHealthPlanFormData { - //Only perform migration on packages that: - // - submitted contract and submission packages. - // - contain at least one rate certification. - // - package has programs - const isSubmittedContractAndRates = oldProto.submissionType === 3 && oldProto.status === 'SUBMITTED' - const hasPackagePrograms = oldProto.programIds && oldProto.programIds.length > 0 - const hasRates = oldProto.rateInfos && oldProto.rateInfos.length > 0 - if (isSubmittedContractAndRates && hasPackagePrograms && hasRates) { - oldProto.rateInfos = oldProto?.rateInfos?.map(rateInfo => ( - { - ...rateInfo, - //If rate programs do not exist, then use package programs - rateProgramIds: rateInfo.rateProgramIds && rateInfo.rateProgramIds.length > 0 ? rateInfo.rateProgramIds : oldProto.programIds - } - )) - } - - return oldProto -} diff --git a/services/app-proto/protoMigrations/migrate_protos.ts b/services/app-proto/protoMigrations/migrate_protos.ts deleted file mode 100644 index 3853feee9e..0000000000 --- a/services/app-proto/protoMigrations/migrate_protos.ts +++ /dev/null @@ -1,258 +0,0 @@ -import fs from 'fs' -import path from 'path' - -import * as genproto from '../gen/healthPlanFormDataProto' - -import { PrismaClient } from '@prisma/client' - -// decodes the proto -function decodeOrError( - buff: Uint8Array -): genproto.mcreviewproto.HealthPlanFormData | Error { - try { - const message = genproto.mcreviewproto.HealthPlanFormData.decode(buff) - return message - } catch (e) { - return new Error(`${e}`) - } -} - -// MigrationType describes a single migration with a name and a callable function called migrateProto -interface MigrationType { - name: string - module: { - migrateProto: ( - oldProto: genproto.mcreviewproto.IHealthPlanFormData - ) => genproto.mcreviewproto.IHealthPlanFormData - } -} - -// MigratorType is a type covering our two different migrators -export interface MigratorType { - listMigrationsThatHaveRun(): Promise - runMigrations(migrations: MigrationType[]): Promise -} - -export function newDBMigrator(dbConnString: string): MigratorType { - const prismaClient = new PrismaClient({ - datasources: { - db: { - url: dbConnString, - }, - }, - }) - - return { - async listMigrationsThatHaveRun(): Promise { - const listMigrationsThatHaveRunTable = - await prismaClient.protoMigrationsTable.findMany() - const migrations = listMigrationsThatHaveRunTable.map( - (m) => m.migrationName - ) - - return migrations - }, - - async runMigrations(migrations: MigrationType[]) { - const revs = await prismaClient.healthPlanRevisionTable.findMany() - - for (const revision of revs) { - const protoBytes = revision.formDataProto - - // decode proto files into generated types - const proto = decodeOrError(protoBytes) - if (proto instanceof Error) { - throw proto - } - - // migrate proto - for (const migration of migrations) { - migration.module.migrateProto(proto) - } - - const newProtoBytes = - genproto.mcreviewproto.HealthPlanFormData.encode( - proto - ).finish() - const newProtoBuffer = Buffer.from(newProtoBytes) - - await prismaClient.healthPlanRevisionTable.update({ - where: { - id: revision.id, - }, - data: { - formDataProto: newProtoBuffer, - }, - }) - } - - const appliedMigrationNames = migrations.map((m) => m.name) - const appliedMigrationsRows = appliedMigrationNames.map((n) => { - return { migrationName: n } - }) - await prismaClient.protoMigrationsTable.createMany({ - data: appliedMigrationsRows, - }) - - console.info('Done with DB') - }, - } -} - -export function newFileMigrator(protoPath: string): MigratorType { - return { - async listMigrationsThatHaveRun() { - // determine migrations to run - const listMigrationsThatHaveRunList: string[] = [] - const listMigrationsThatHaveRunPath = path.join( - protoPath, - '_ran_migrations' - ) - try { - const listMigrationsThatHaveRunListBytes = fs.readFileSync( - listMigrationsThatHaveRunPath, - { - encoding: 'utf8', - } - ) - - listMigrationsThatHaveRunListBytes - .trim() - .split('\n') - .forEach((filename) => - listMigrationsThatHaveRunList.push(filename) - ) - } catch (e) { - // if there is no file, treat it like there are no ran migrations. - if (e.code != 'ENOENT') { - throw e - } - } - return listMigrationsThatHaveRunList - }, - - async runMigrations(migrations) { - const testFiles = fs - .readdirSync(protoPath) - .filter((filename) => filename.endsWith('.proto')) - - for (const testFile of testFiles) { - const tPath = path.join(protoPath, testFile) - const protoBytes = fs.readFileSync(tPath) - - // decode proto files into generated types - const proto = decodeOrError(protoBytes) - if (proto instanceof Error) { - throw proto - } - - // migrate proto - for (const migration of migrations) { - migration.module.migrateProto(proto) - } - - //write Proto - const newProtoBytes = - genproto.mcreviewproto.HealthPlanFormData.encode( - proto - ).finish() - - fs.writeFileSync(tPath, newProtoBytes) - } - - // write run migrations to file - const ranMigrationNames = - migrations.map((m) => m.name).join('\n') + '\n' - - const listMigrationsThatHaveRunPath = path.join( - protoPath, - '_ran_migrations' - ) - fs.writeFileSync(listMigrationsThatHaveRunPath, ranMigrationNames, { - encoding: 'utf8', - flag: 'a', - }) - }, - } -} - -export async function migrate(migrator: MigratorType, path?: string) { - const migrationPath = path ?? './healthPlanFormDataMigrations' - - const migrationFiles = fs - .readdirSync(migrationPath) - .filter((m) => m.endsWith('.js') && !m.endsWith('.test.js')) - - const migrations: MigrationType[] = [] - for (const migrationFile of migrationFiles) { - const fullPath = `${migrationPath}/${migrationFile}` - - const migrationName = migrationFile.substring( - 0, - migrationFile.lastIndexOf('.') - ) - - const migration = await import(fullPath) - - migrations.push({ - name: migrationName, - module: migration, - }) - } - - const previouslyAppliedMigrationNames = - await migrator.listMigrationsThatHaveRun() - - const migrationsToRun = migrations.filter((migration) => { - return !previouslyAppliedMigrationNames.includes(migration.name) - }) - - console.info( - 'New Migrations To Run: ', - migrationsToRun.map((m) => m.name) - ) - - if (migrationsToRun.length > 0) { - await migrator.runMigrations(migrationsToRun) - } -} - -async function main() { - const args = process.argv.slice(2) - - const usage = `USAGE: -./migrate_protos.js db [PATH TO PROTOS] :: run migrations against all protos in the db -./migrate_protos.js files [PATH TO .PROTOS] :: run migrations against all protos in given directory` - - const connectionType = - args.length > 0 && args[0] === 'db' ? 'DATABASE' : 'FILES' - - const pathToProtos = args[1] - if (pathToProtos === undefined) { - console.info(usage) - process.exit(1) - } - - let migrator: MigratorType | undefined = undefined - if (connectionType === 'DATABASE') { - const dbConn = process.env.DATABASE_URL - if (!dbConn) { - throw new Error('DATABASE_URL must be defined in env') - } - - migrator = newDBMigrator(dbConn) - } else if (connectionType === 'FILES') { - if (args.length !== 2 || args[0] !== 'files') { - console.info(usage) - process.exit(1) - } - migrator = newFileMigrator(pathToProtos) - } else { - console.info(usage) - throw new Error('unimplemented migrator') - } - - await migrate(migrator, pathToProtos) -} - -void main() diff --git a/services/app-proto/protoMigrations/test_migrate b/services/app-proto/protoMigrations/test_migrate deleted file mode 100755 index 851abb1b14..0000000000 --- a/services/app-proto/protoMigrations/test_migrate +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -e - -echo "hello" - -git checkout tests/protos/ - -../node_modules/.bin/tsc -npx node ./protoMigrations/build/migrate_protos.js diff --git a/services/app-web/package.json b/services/app-web/package.json index a237889b4c..f6632bf171 100644 --- a/services/app-web/package.json +++ b/services/app-web/package.json @@ -129,7 +129,7 @@ "@storybook/manager-webpack5": "^6.5.15", "@storybook/preset-create-react-app": "7.3.2", "@storybook/react": "^6.5.15", - "@testing-library/cypress": "^9.0.0", + "@testing-library/cypress": "^10.0.1", "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", @@ -144,7 +144,7 @@ "@typescript-eslint/parser": "^6.5.0", "add": "^2.0.6", "classnames": "^2.2.6", - "eslint-config-prettier": "^8.7.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^27.0.1", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^4.0.0", @@ -153,7 +153,7 @@ "graphql.macro": "^1.4.2", "jest-launchdarkly-mock": "^2.1.0", "lint-staged": "^14.0.1", - "prettier": "^2.3.2", + "prettier": "^3.1.0", "react-scripts": "5.0.1", "react-select-event": "^5.5.0", "react-test-renderer": "^18.2.0", diff --git a/services/app-web/src/common-code/data/statePrograms.json b/services/app-web/src/common-code/data/statePrograms.json index 21341c8ad0..531ff56547 100644 --- a/services/app-web/src/common-code/data/statePrograms.json +++ b/services/app-web/src/common-code/data/statePrograms.json @@ -594,9 +594,9 @@ "name": "Montana", "programs": [ { - "id": "9e79aa85-64cc-4751-9c63-3492d49af68f", - "fullName": "Passport to Health", - "name": "PCCM" + "id": "c757ddee-19a2-4aaa-aa70-3aaeee970f91", + "fullName": "Tribal Health Improvement Program", + "name": "T-HIP" } ], "code": "MT" @@ -843,11 +843,6 @@ "id": "46246196-3b63-40be-8db1-c09cec281dae", "fullName": "Coordinated Care Organizations", "name": "CCO" - }, - { - "id": "7f44b186-2a82-4200-a595-a0b8c0de6318", - "fullName": "Dental Care Organizations", - "name": "DCO" } ], "code": "OR" diff --git a/services/app-web/src/common-code/featureFlags/flags.ts b/services/app-web/src/common-code/featureFlags/flags.ts index e9f1f25051..9263939513 100644 --- a/services/app-web/src/common-code/featureFlags/flags.ts +++ b/services/app-web/src/common-code/featureFlags/flags.ts @@ -21,19 +21,12 @@ const featureFlags = { defaultValue: true, }, /** - The number of minutes before the session expires + The number of minutes before the session expires and countdown modal appears */ MINUTES_UNTIL_SESSION_EXPIRES: { flag: 'session-expiration-minutes', defaultValue: 30, }, - /** - The number of minutes before session expiration that the warning modal appears - */ - MODAL_COUNTDOWN_DURATION: { - flag: 'modal-countdown-duration', - defaultValue: 2, - }, /** * Enables state and CMS Q&A features */ @@ -41,41 +34,6 @@ const featureFlags = { flag: 'cms-questions', defaultValue: false, }, - /** - * Enables packages with shared rates dropdown on rate details page. This was an early version of rates across subs functionality. - */ - PACKAGES_WITH_SHARED_RATES: { - flag: 'packages-with-shared-rates', - defaultValue: false, - }, - /** - * Enables supporting documents to be associated with a specific rate certification on the Rate Details page - */ - SUPPORTING_DOCS_BY_RATE: { - flag: 'supporting-docs-by-rate', - defaultValue: false, - }, - /** - * Rates refactor database handlers live behind this flag. We will use this to switchover to the new database tables when we migrate. - */ - RATES_DATABASE_REFACTOR: { - flag: 'rates-db-refactor', - defaultValue: false, - }, - /** - * Controls the rates review dashboard UI. This flag should not be turned on without rates-db-refactor also on. - */ - RATE_REVIEWS_DASHBOARD: { - flag: 'rate-reviews-dashboard', - defaultValue: false, - }, - /** - * Controls the rates filter UI/ This flag should not be turned on without rate-reviews-dashboard also on. - */ - RATE_FILTERS: { - flag: 'rate-filters', - defaultValue: false, - }, CONTRACT_438_ATTESTATION: { flag: '438-attestation', defaultValue: false, @@ -88,13 +46,6 @@ const featureFlags = { flag: 'test-error-fetching-flag', defaultValue: undefined, }, - /* - Temporary flag for the cutover to a new support email address - */ - HELPDESK_EMAIL: { - flag: 'helpdesk-email', - defaultValue: false, - }, } as const /** diff --git a/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts b/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts index 4ecb1c1147..0c5d2fd120 100644 --- a/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts +++ b/services/app-web/src/common-code/healthPlanFormDataMocks/healthPlanFormData.ts @@ -276,13 +276,11 @@ function unlockedWithDocuments(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'contract doc', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/key/foo.png', name: 'rates and contract addendum doc', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED', 'RATES_RELATED'], }, ], contractType: 'BASE', @@ -294,7 +292,6 @@ function unlockedWithDocuments(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'contract doc', sha256: 'fakesha', - documentCategories: ['CONTRACT'], }, ], rateInfos: [ @@ -317,7 +314,6 @@ function unlockedWithDocuments(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'Rates certification', sha256: 'fakesha', - documentCategories: ['RATES'], }, ], supportingDocuments: [], @@ -393,13 +389,11 @@ function unlockedWithFullRates(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'contract doc', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/key/foo.png', name: 'rates and contract addendum doc', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED', 'RATES_RELATED'], }, ], contractType: 'BASE', @@ -429,7 +423,6 @@ function unlockedWithFullRates(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'Rates certification', sha256: 'fakesha', - documentCategories: ['RATES'], }, ], supportingDocuments: [], @@ -503,13 +496,11 @@ function unlockedWithFullContracts(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'contract doc', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/key/foo.png', name: 'rates and contract addendum doc', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED', 'RATES_RELATED'], }, ], contractType: 'AMENDMENT', @@ -521,7 +512,6 @@ function unlockedWithFullContracts(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'contract doc', sha256: 'fakesha', - documentCategories: ['CONTRACT'], }, ], contractAmendmentInfo: { @@ -567,7 +557,6 @@ function unlockedWithFullContracts(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'Rates certification', sha256: 'fakesha', - documentCategories: ['RATES'], }, ], supportingDocuments: [], @@ -645,7 +634,6 @@ function unlockedWithALittleBitOfEverything(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'contract doc', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], }, ], contractType: 'AMENDMENT', @@ -657,7 +645,6 @@ function unlockedWithALittleBitOfEverything(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'contract doc', sha256: 'fakesha', - documentCategories: ['CONTRACT'], }, ], contractAmendmentInfo: { @@ -693,13 +680,11 @@ function unlockedWithALittleBitOfEverything(): UnlockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'rates cert 1', sha256: 'fakesha', - documentCategories: ['RATES_RELATED'], }, { s3URL: 's3://bucketname/key/foo.png', name: 'rates cert 2', sha256: 'fakesha', - documentCategories: ['RATES_RELATED'], }, ], supportingDocuments: [], @@ -789,7 +774,6 @@ function basicLockedHealthPlanFormData(): LockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/foo.png', name: 'contract doc', sha256: 'fakesha', - documentCategories: ['CONTRACT'], }, ], managedCareEntities: ['PIHP'], diff --git a/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts b/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts index 2b2bf25ff2..67902cd997 100644 --- a/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts +++ b/services/app-web/src/common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType.ts @@ -7,18 +7,11 @@ type SubmissionType = 'CONTRACT_ONLY' | 'CONTRACT_AND_RATES' type PopulationCoveredType = 'MEDICAID' | 'CHIP' | 'MEDICAID_AND_CHIP' -type DocumentCategoryType = - | 'CONTRACT' - | 'RATES' - | 'CONTRACT_RELATED' - | 'RATES_RELATED' - type SubmissionDocument = { id?: string name: string s3URL: string sha256: string - documentCategories: DocumentCategoryType[] } type ContractAmendmentInfo = { @@ -123,7 +116,6 @@ type UnlockedHealthPlanFormDataType = { } export type { - DocumentCategoryType, SubmissionType, SubmissionDocument, RateType, diff --git a/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.test.ts b/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.test.ts index 8e619b4d3a..6aca621adf 100644 --- a/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.test.ts +++ b/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.test.ts @@ -6,15 +6,11 @@ import { mockStateSubmissionContractAmendment, } from '../../testHelpers/apolloMocks' import { - convertRateSupportingDocs, generateRateName, - hasValidSupportingDocumentCategories, HealthPlanFormDataType, isValidAndCurrentLockedHealthPlanFormData, LockedHealthPlanFormDataType, packageName, - removeRatesData, - UnlockedHealthPlanFormDataType, } from '.' import { hasValidContract, @@ -59,113 +55,6 @@ describe('submission type assertions', () => { } ) - test.each([ - [mockStateSubmission(), true], - [ - { - ...mockStateSubmission(), - documents: [ - { - name: 'A.pdf', - s3URL: 's3://local-uploads/1644167870842-A.pdf/A.pdf', - documentCategories: [], - }, - ], - }, - false, - ], - [ - { - ...mockStateSubmission(), - documents: [ - { - name: 'A.pdf', - s3URL: 's3://local-uploads/1644167870842-A.pdf/A.pdf', - documentCategories: ['RATES_RELATED'], - }, - ], - submissionType: 'CONTRACT_ONLY', - }, - false, - ], - [ - { - ...mockStateSubmission(), - documents: [ - { - name: 'A.pdf', - s3URL: 's3://local-uploads/1644167870842-A.pdf/A.pdf', - documentCategories: ['CONTRACT_RELATED'], - }, - ], - submissionType: 'CONTRACT_ONLY', - }, - true, - ], - [ - { - ...mockStateSubmission(), - documents: [ - { - name: 'A.pdf', - s3URL: 's3://local-uploads/1644167870842-A.pdf/A.pdf', - documentCategories: ['RATES_RELATED'], - }, - ], - submissionType: 'CONTRACT_AND_RATES', - }, - true, - ], - [ - { - ...mockStateSubmission(), - documents: [ - { - name: 'A.pdf', - s3URL: 's3://local-uploads/1644167870842-A.pdf/A.pdf', - documentCategories: ['RATES_RELATED'], - }, - { - name: 'B.pdf', - s3URL: 's3://local-uploads/1644167870842-B.pdf/B.pdf', - documentCategories: ['CONTRACT_RELATED'], - }, - ], - submissionType: 'CONTRACT_ONLY', - }, - false, - ], - [ - { - ...mockStateSubmission(), - documents: [ - { - name: 'A.pdf', - s3URL: 's3://local-uploads/1644167870842-A.pdf/A.pdf', - documentCategories: ['CONTRACT_RELATED'], - }, - { - name: 'B.pdf', - s3URL: 's3://local-uploads/1644167870842-B.pdf/B.pdf', - documentCategories: ['CONTRACT_RELATED'], - }, - ], - submissionType: 'CONTRACT_ONLY', - }, - true, - ], - ])( - 'hasValidSupportingDocumentCategories evaluates as expected', - (submission, expectedResponse) => { - // type coercion to allow us to test - expect( - hasValidSupportingDocumentCategories( - submission as unknown as LockedHealthPlanFormDataType - ) - ).toEqual(expectedResponse) - } - ) - test.each([ [mockStateSubmission(), true], [{ ...mockStateSubmission(), documents: [] }, true], @@ -181,7 +70,6 @@ describe('submission type assertions', () => { { s3URL: 's3://bucketname/key/rate', name: 'rate', - documentCategories: ['RATES' as const], }, ], rateDateStart: new Date(), @@ -797,261 +685,4 @@ describe('submission type assertions', () => { ).toMatch(expectedName) } ) - - const contractOnlyWithValidRateData: { - submission: UnlockedHealthPlanFormDataType - testDescription: string - expectedResult: Partial | Error - }[] = [ - { - submission: { - ...mockContractAndRateSub, - documents: [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], - }, - { - name: 'contract_supporting_that_applies_to_a_rate_also_2.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: [ - 'RATES_RELATED' as const, - 'CONTRACT_RELATED' as const, - ], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], - }, - ], - }, - testDescription: 'With all valid rate data ', - expectedResult: { - addtlActuaryContacts: [], - documents: [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - { - name: 'contract_supporting_that_applies_to_a_rate_also_2.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - ], - rateInfos: [], - }, - }, - { - submission: { - ...mockContractAndRateSub, - documents: [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: [ - 'RATES_RELATED' as const, - 'CONTRACT_RELATED' as const, - ], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], - }, - ], - }, - testDescription: 'With valid contract and rate related documents', - expectedResult: { - addtlActuaryContacts: [], - documents: [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], - }, - ], - rateInfos: [], - }, - }, - { - submission: { - ...mockContractAndRateSub, - documents: [ - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], - }, - ], - }, - testDescription: 'With only valid rate related documents', - expectedResult: { - addtlActuaryContacts: [], - documents: [ - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - ], - rateInfos: [], - }, - }, - ] - test.each(contractOnlyWithValidRateData)( - 'Remove rates data on CONTRACT_ONLY submission: $testDescription', - ({ submission, expectedResult }) => { - expect(removeRatesData(submission)).toEqual( - expect.objectContaining(expectedResult) - ) - } - ) - - test('convertRateSupportingDocs does convert rate supporting documents to contract supporting', () => { - const documents = [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], - }, - { - name: 'contract_supporting_that_applies_to_a_rate_also_2.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: [ - 'RATES_RELATED' as const, - 'CONTRACT_RELATED' as const, - ], - }, - { - name: 'contract_supporting_that_applies_to_a_rate_also_3.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], - }, - ] - - expect(convertRateSupportingDocs(documents)).toEqual([ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - { - name: 'contract_supporting_that_applies_to_a_rate_also_2.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - { - name: 'contract_supporting_that_applies_to_a_rate_also_3.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - { - name: 'rate_only_supporting_doc.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], - }, - ]) - }) - - test('convertRateSupportingDocs throws error with CONTRACT or RATE documents', () => { - const contractDocument = [ - { - name: 'contract_certification.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['CONTRACT' as const], - }, - ] - - const rateDocument = [ - { - name: 'rates_certification.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: ['RATES' as const], - }, - ] - - const mixedDocuments = [ - { - name: 'contract_supporting_that_applies_to_a_rate_also.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], - }, - { - name: 'rates_certification.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: [ - 'RATES' as const, - 'RATES_RELATED' as const, - ], - }, - { - name: 'contract_certification.pdf', - s3URL: 'fakeS3URL', - sha256: 'fakesha', - documentCategories: [ - 'CONTRACT' as const, - 'CONTRACT_RELATED' as const, - ], - }, - ] - // eslint-disable-next-line @typescript-eslint/no-empty-function - jest.spyOn(console, 'error').mockImplementation(() => {}) - - expect(() => convertRateSupportingDocs(contractDocument)).toThrow() - expect(() => convertRateSupportingDocs(rateDocument)).toThrow() - expect(() => convertRateSupportingDocs(mixedDocuments)).toThrow() - jest.clearAllMocks() - }) }) diff --git a/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.ts b/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.ts index 2a87abe4dd..1a5a832674 100644 --- a/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.ts +++ b/services/app-web/src/common-code/healthPlanFormDataType/healthPlanFormData.ts @@ -1,6 +1,5 @@ import { RateInfoType, - SubmissionDocument, UnlockedHealthPlanFormDataType, ActuaryContact, } from './UnlockedHealthPlanFormDataType' @@ -59,12 +58,12 @@ const hasValidModifiedProvisions = ( (provision) => provisions[provision] !== undefined ) : isBaseContract(sub) - ? modifiedProvisionMedicaidBaseKeys.every( - (provision) => provisions[provision] !== undefined - ) - : modifiedProvisionMedicaidAmendmentKeys.every( - (provision) => provisions[provision] !== undefined - ) + ? modifiedProvisionMedicaidBaseKeys.every( + (provision) => provisions[provision] !== undefined + ) + : modifiedProvisionMedicaidAmendmentKeys.every( + (provision) => provisions[provision] !== undefined + ) } const hasValidContract = (sub: LockedHealthPlanFormDataType): boolean => sub.contractType !== undefined && @@ -137,10 +136,7 @@ const hasAnyValidRateData = ( ): boolean => { return ( //Any rate inside array of rateInfo would mean there is rate data. - Boolean(sub.rateInfos.length) || - sub.documents.some((document) => - document.documentCategories.includes('CONTRACT_RELATED') - ) + sub.rateInfos.length > 0 ) } @@ -156,26 +152,6 @@ const hasValidDocuments = (sub: LockedHealthPlanFormDataType): boolean => { return validRateDocuments && validContractDocuments } -const hasValidSupportingDocumentCategories = ( - sub: LockedHealthPlanFormDataType -): boolean => { - // every document must have a category - if (!sub.documents.every((doc) => doc.documentCategories.length > 0)) { - return false - } - // if the submission is contract-only, all supporting docs must be 'CONTRACT-RELATED - if ( - sub.submissionType === 'CONTRACT_ONLY' && - sub.documents.length > 0 && - !sub.documents.every((doc) => - doc.documentCategories.includes('CONTRACT_RELATED') - ) - ) { - return false - } - return true -} - const isLockedHealthPlanFormData = ( sub: unknown ): sub is LockedHealthPlanFormDataType => { @@ -314,35 +290,12 @@ const generateRateName = ( return rateName } -// This logic is no longer needed once SUPPORTING_DOCS_BY_RATE flag is on in production -const convertRateSupportingDocs = ( - documents: SubmissionDocument[] -): SubmissionDocument[] => { - if ( - documents.some( - (document) => - document.documentCategories.includes('CONTRACT') || - document.documentCategories.includes('RATES') - ) - ) { - const errorMessage = - 'convertRateSupportingDocs does not support CONTRACT or RATES documents.' - console.error(errorMessage) - throw new Error(errorMessage) - } - return documents.map((document) => ({ - ...document, - documentCategories: ['CONTRACT_RELATED'], - })) -} - const removeRatesData = ( pkg: HealthPlanFormDataType ): HealthPlanFormDataType => { pkg.rateInfos = [] pkg.addtlActuaryContacts = [] pkg.addtlActuaryCommunicationPreference = undefined - pkg.documents = convertRateSupportingDocs(pkg.documents) return pkg } @@ -380,7 +333,6 @@ export { hasValidModifiedProvisions, hasValidContract, hasValidDocuments, - hasValidSupportingDocumentCategories, hasValidRates, hasAnyValidRateData, isBaseContract, @@ -395,7 +347,6 @@ export { programNames, packageName, generateRateName, - convertRateSupportingDocs, removeRatesData, removeInvalidProvisionsAndAuthorities, hasValidPopulationCoverage, diff --git a/services/app-web/src/common-code/healthPlanFormDataType/index.ts b/services/app-web/src/common-code/healthPlanFormDataType/index.ts index 34d028daf1..9521e5d11d 100644 --- a/services/app-web/src/common-code/healthPlanFormDataType/index.ts +++ b/services/app-web/src/common-code/healthPlanFormDataType/index.ts @@ -9,7 +9,6 @@ export type { ContractAmendmentInfo, ContractExecutionStatus, ContractType, - DocumentCategoryType, UnlockedHealthPlanFormDataType, ManagedCareEntity, RateType, @@ -55,7 +54,6 @@ export { hasValidDocuments, hasValidRates, hasAnyValidRateData, - hasValidSupportingDocumentCategories, isContractAndRates, isContractOnly, isUnlockedHealthPlanFormData, @@ -64,7 +62,6 @@ export { programNames, packageName, generateRateName, - convertRateSupportingDocs, removeInvalidProvisionsAndAuthorities, removeRatesData, hasValidPopulationCoverage, diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/testData/unlockedWithALittleBitOfEverything-2023-12-09.proto b/services/app-web/src/common-code/proto/healthPlanFormDataProto/testData/unlockedWithALittleBitOfEverything-2023-12-09.proto new file mode 100644 index 0000000000..c928ea66cd Binary files /dev/null and b/services/app-web/src/common-code/proto/healthPlanFormDataProto/testData/unlockedWithALittleBitOfEverything-2023-12-09.proto differ diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts index a629c3c4d0..ffaf3755f1 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toDomain.ts @@ -6,7 +6,6 @@ import { import { UnlockedHealthPlanFormDataType, LockedHealthPlanFormDataType, - DocumentCategoryType, ActuarialFirmType, FederalAuthority, ManagedCareEntity, @@ -27,12 +26,12 @@ import { findStatePrograms } from '../../healthPlanFormDataType/findStateProgram type RecursivelyReplaceNullWithUndefined = T extends null ? undefined : T extends Date - ? T - : { - [K in keyof T]: T[K] extends (infer U)[] - ? RecursivelyReplaceNullWithUndefined[] - : RecursivelyReplaceNullWithUndefined - } + ? T + : { + [K in keyof T]: T[K] extends (infer U)[] + ? RecursivelyReplaceNullWithUndefined[] + : RecursivelyReplaceNullWithUndefined + } export function replaceNullsWithUndefineds( obj: T @@ -46,9 +45,11 @@ export function replaceNullsWithUndefineds( v === null ? undefined : // eslint-disable-next-line no-proto - v && typeof v === 'object' && v.__proto__.constructor === Object - ? replaceNullsWithUndefineds(v) - : v + v && + typeof v === 'object' && + v.__proto__.constructor === Object + ? replaceNullsWithUndefineds(v) + : v }) return newObj } @@ -187,10 +188,6 @@ function parseProtoDocuments( return replaceNullsWithUndefineds(docs).map((doc) => ({ s3URL: doc.s3Url, name: doc.name, - documentCategories: protoEnumArrayToDomain( - mcreviewproto.DocumentCategory, - doc.documentCategories - ) as DocumentCategoryType[], sha256: doc.sha256 || 'sha_undefined_in_proto', })) } diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts index 5fd04e7f72..394f2d4715 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/toProtoBuffer.ts @@ -71,10 +71,6 @@ const domainDocsToProtoDocs = ( return domainDocs.map((doc) => ({ s3Url: doc.s3URL, name: doc.name, - documentCategories: domainEnumArrayToProto( - mcreviewproto.DocumentCategory, - doc.documentCategories - ), sha256: doc.sha256, })) } @@ -296,10 +292,6 @@ const toProtoBuffer = ( documents: domainData.documents.map((doc) => ({ s3Url: doc.s3URL, name: doc.name, - documentCategories: domainEnumArrayToProto( - mcreviewproto.DocumentCategory, - doc.documentCategories - ), sha256: doc.sha256, })), } diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/unlockedWithALittleBitOfEverything.test.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/unlockedWithALittleBitOfEverything.test.ts deleted file mode 100644 index 11d37acf85..0000000000 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/unlockedWithALittleBitOfEverything.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import fs from 'fs' -import { - toDomain, - toProtoBuffer, -} from 'app-web/src/common-code/proto/healthPlanFormDataProto' -import { migrateProto as initialMigration } from '../../../../../app-proto/protoMigrations/healthPlanFormDataMigrations/0000_initial_migration' -import { migrateProto as rateIDMigration } from '../../../../../app-proto/protoMigrations/healthPlanFormDataMigrations/0001_rate_id_migration' -import { migrateProto as rateProgramsMigration } from '../../../../../app-proto/protoMigrations/healthPlanFormDataMigrations/0002_rate_programs_migration' -import { mcreviewproto } from '../../../gen/healthPlanFormDataProto' -import * as genproto from '../../../gen/healthPlanFormDataProto' - -const decodeOrError = ( - buff: Uint8Array -): mcreviewproto.HealthPlanFormData | Error => { - try { - const message = mcreviewproto.HealthPlanFormData.decode(buff) - return message - } catch (e) { - return new Error(`${e}`) - } -} - -describe('0000_initial_migration', () => { - it('version 2022-08-19 matches the expected values', async () => { - // read the file from the filesystem - console.info('directory: ', fs.readdirSync('.')) - const oldProtoBytes = fs.readFileSync( - 'src/common-code/proto/healthPlanFormDataProto/testData/unlockedWithALittleBitOfEverything-2022-08-19.proto' - ) - - // Decode proto - const oldProto = decodeOrError(oldProtoBytes) - - if (oldProto instanceof Error) { - throw oldProto - } - - // initial_migration - // There is no change to our domain model here, but a warning will be printed by toDomain - // if we load a proto that has not had its version updated or if the version is above 1 - - //Run Migration - const migratedProto = initialMigration(oldProto) - const migratedProtoBytes = - genproto.mcreviewproto.HealthPlanFormData.encode( - migratedProto - ).finish() - - // turn into domain model - const migratedFormData = toDomain(migratedProtoBytes) - - if (migratedFormData instanceof Error) { - throw migratedFormData - } - - // add_one_month - expect( - migratedFormData.contractDateStart?.toISOString().split('T')[0] - ).toBe('2021-05-22') - expect( - migratedFormData.contractDateEnd?.toISOString().split('T')[0] - ).toBe('2022-05-21') - expect( - migratedFormData.rateInfos[0]?.rateDateStart - ?.toISOString() - .split('T')[0] - ).toBe('2021-05-22') - expect( - migratedFormData.rateInfos[0]?.rateDateEnd - ?.toISOString() - .split('T')[0] - ).toBe('2022-04-29') - expect( - migratedFormData.rateInfos[0]?.rateDateCertified - ?.toISOString() - .split('T')[0] - ).toBe('2021-05-23') - expect( - migratedFormData.rateInfos[0]?.rateAmendmentInfo?.effectiveDateStart - ?.toISOString() - .split('T')[0] - ).toBe('2022-06-21') - expect( - migratedFormData.rateInfos[0]?.rateAmendmentInfo?.effectiveDateEnd - ?.toISOString() - .split('T')[0] - ).toBe('2022-10-21') - expect(migratedFormData.rateInfos[0].rateCertificationName).toBe( - 'MCR-MN-0005-SNBC-RATE-20220621-20221021-AMENDMENT-20210523' - ) - }) -}) - -describe('0001_rate_id_migration', () => { - it('correctly generates rate ids for old contract and rates submissions', () => { - //Get old proto - const oldProtoBytes = fs.readFileSync( - 'src/common-code/proto/healthPlanFormDataProto/testData/unlockedWithALittleBitOfEverything-2022-08-19.proto' - ) - const oldFormData = toDomain(oldProtoBytes) - - if (oldFormData instanceof Error) { - throw oldFormData - } - - // Decode proto - const oldProto = decodeOrError(oldProtoBytes) - - if (oldProto instanceof Error) { - throw oldProto - } - - //Run Migration - const migratedProto = rateIDMigration(oldProto) - const migratedProtoBytes = - genproto.mcreviewproto.HealthPlanFormData.encode( - migratedProto - ).finish() - - const migratedFormData = toDomain(migratedProtoBytes) - - if (migratedFormData instanceof Error) { - throw migratedFormData - } - - //Encoding to proto and back to domain should still be symmetric - expect(toDomain(toProtoBuffer(migratedFormData))).toEqual( - migratedFormData - ) - - //Rate id assertions - expect(oldFormData.rateInfos[0].id).toBeUndefined() - expect(migratedFormData.rateInfos[0].id).toBeDefined() - }) - - it('does not override existing rate ids', () => { - //Get proto with rate ids - const oldProtoBytes = fs.readFileSync( - 'src/common-code/proto/healthPlanFormDataProto/testData/unlockedWithALittleBitOfEverything-2022-11-07.proto' - ) - - const oldFormData = toDomain(oldProtoBytes) - - if (oldFormData instanceof Error) { - throw oldFormData - } - - const rateID = oldFormData.rateInfos[0].id - - // Decode proto - const oldProto = decodeOrError(oldProtoBytes) - - if (oldProto instanceof Error) { - throw oldProto - } - - //Run Migration - const migratedProto = rateIDMigration(oldProto) - const migratedProtoBytes = - genproto.mcreviewproto.HealthPlanFormData.encode( - migratedProto - ).finish() - - const migratedFormData = toDomain(migratedProtoBytes) - - if (migratedFormData instanceof Error) { - throw migratedFormData - } - - //Encoding to proto and back to domain should still be symmetric - expect(toDomain(toProtoBuffer(migratedFormData))).toEqual(oldFormData) - - //Rate id should not have changed. - expect(migratedFormData.rateInfos[0].id).toEqual(rateID) - }) -}) - -describe('0002_rate_programs_migration', () => { - it('correctly copies package programs to rate programs for old contract and rates submissions', () => { - //Get old proto - const oldProtoBytes = fs.readFileSync( - 'src/common-code/proto/healthPlanFormDataProto/testData/unlockedWithALittleBitOfEverything-2022-08-19.proto' - ) - const oldProto = decodeOrError(oldProtoBytes) - - if (oldProto instanceof Error) { - throw oldProto - } - - // Turn draft package to submitted package, since we only have unlocked protos to work with. - oldProto.status = 'SUBMITTED' - // This proto version was also missing contractDocuments, required for a SUBMITTED package. - oldProto.contractInfo = { - ...oldProto.contractInfo, - contractDocuments: [ - { - s3Url: 's3://bucketname/key/foo.png', - name: 'contract doc', - documentCategories: [1], - }, - ], - } - - //We do not have ay old protos from before rate programs was introduced. So we have to manually modify the data - // by removing rate programs from rateInfos. - oldProto.rateInfos = oldProto.rateInfos.map((rateInfo) => ({ - ...rateInfo, - rateProgramIds: undefined, - })) - - // encode modified proto into proto bytes - const modifiedOldProtoBytes = - genproto.mcreviewproto.HealthPlanFormData.encode(oldProto).finish() - - // Decode modified proto - const modifiedOldProto = decodeOrError(modifiedOldProtoBytes) - - if (modifiedOldProto instanceof Error) { - throw modifiedOldProto - } - - //Run previous migrations then current migration on modified proto - const migratedProto = rateProgramsMigration( - rateIDMigration(initialMigration(modifiedOldProto)) - ) - const migratedProtoBytes = - genproto.mcreviewproto.HealthPlanFormData.encode( - migratedProto - ).finish() - - const migratedFormData = toDomain(migratedProtoBytes) - - if (migratedFormData instanceof Error) { - throw migratedFormData - } - - //Encoding to proto and back to domain should still be symmetric - expect(toDomain(toProtoBuffer(migratedFormData))).toEqual( - migratedFormData - ) - - //Rate id assertions - expect(migratedFormData.rateInfos[0].rateProgramIDs).toHaveLength(3) - }) - - it('does not override existing rate programs', () => { - //Get proto with rate ids - const oldProtoBytes = fs.readFileSync( - 'src/common-code/proto/healthPlanFormDataProto/testData/unlockedWithALittleBitOfEverything-2022-11-07.proto' - ) - - const oldProto = decodeOrError(oldProtoBytes) - - if (oldProto instanceof Error) { - throw oldProto - } - - // Turn draft package to submitted package, since we only have unlocked protos to work with. - oldProto.status = 'SUBMITTED' - - // encode modified proto into proto bytes - const modifiedOldProtoBytes = - genproto.mcreviewproto.HealthPlanFormData.encode(oldProto).finish() - - // Decode modified proto - const modifiedOldProto = decodeOrError(modifiedOldProtoBytes) - - if (modifiedOldProto instanceof Error) { - throw modifiedOldProto - } - - //Run Migration - const migratedProto = rateProgramsMigration( - rateIDMigration(initialMigration(modifiedOldProto)) - ) - const migratedProtoBytes = - genproto.mcreviewproto.HealthPlanFormData.encode( - migratedProto - ).finish() - - const migratedFormData = toDomain(migratedProtoBytes) - - if (migratedFormData instanceof Error) { - throw migratedFormData - } - - //Get old package data to compare against migrated. - const modifiedOldFormData = toDomain(modifiedOldProtoBytes) - if (modifiedOldFormData instanceof Error) { - throw modifiedOldFormData - } - - //Encoding to proto and back to domain should still be symmetric - expect(toDomain(toProtoBuffer(migratedFormData))).toEqual( - modifiedOldFormData - ) - - //Rate programs should not have changed - expect(migratedFormData.rateInfos[0].rateProgramIDs).toEqual( - modifiedOldFormData.rateInfos[0].rateProgramIDs - ) - }) -}) diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts index 3455639544..1540fec261 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts @@ -27,16 +27,6 @@ const capitationRatesAmendedReasonSchema = z.union([ const submissionDocumentSchema = z.object({ name: z.string(), s3URL: z.string(), - documentCategories: z.array( - z - .union([ - z.literal('CONTRACT'), - z.literal('RATES'), - z.literal('CONTRACT_RELATED'), - z.literal('RATES_RELATED'), - ]) - .optional() - ), sha256: z.string().optional(), id: z.string().optional(), // doesn't exist for newly created }) diff --git a/services/app-web/src/components/FileUpload/FileItemList/FileItemsList.test.tsx b/services/app-web/src/components/FileUpload/FileItemList/FileItemsList.test.tsx index 205387eadf..972e3c3f39 100644 --- a/services/app-web/src/components/FileUpload/FileItemList/FileItemsList.test.tsx +++ b/services/app-web/src/components/FileUpload/FileItemList/FileItemsList.test.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { render, screen } from '@testing-library/react' import renderer from 'react-test-renderer' import userEvent from '@testing-library/user-event' @@ -15,7 +14,6 @@ describe('FileItemList component', () => { key: undefined, s3URL: undefined, status: 'PENDING', - documentCategories: ['CONTRACT_RELATED'], } const scanning: FileItemT = { id: 'testFile1', @@ -24,7 +22,6 @@ describe('FileItemList component', () => { key: '4545454-testFile1', s3URL: 'tests3://uploaded-12313123213/4545454-testFile1', status: 'SCANNING', - documentCategories: [], } const uploadError: FileItemT = { id: 'testFile2', @@ -33,7 +30,6 @@ describe('FileItemList component', () => { key: undefined, s3URL: undefined, status: 'UPLOAD_ERROR', - documentCategories: ['CONTRACT_RELATED'], } const scanningError: FileItemT = { @@ -43,7 +39,6 @@ describe('FileItemList component', () => { key: '4545454-testFile3', s3URL: 'tests3://uploaded-12313123213/4545454-testFile3', status: 'SCANNING_ERROR', - documentCategories: [], } const complete: FileItemT = { @@ -53,7 +48,6 @@ describe('FileItemList component', () => { key: '4545454-testFile4', s3URL: 'tests3://uploaded-12313123213/4545454-testFile4', status: 'UPLOAD_COMPLETE', - documentCategories: ['CONTRACT_RELATED'], } const duplicateError: FileItemT = { @@ -63,28 +57,16 @@ describe('FileItemList component', () => { key: '1234545454-testFile4', s3URL: 'tests3://uploaded-12313123213/1234545454-testFile4', status: 'DUPLICATE_NAME_ERROR', - documentCategories: [], } const buttonActionProps = { deleteItem: jest.fn(), retryItem: jest.fn(), } - const categoryCheckboxProps = { - handleCheckboxClick: jest.fn(), - } - beforeEach(() => jest.clearAllMocks()) it('renders a list without errors', () => { const fileItems = [pending, uploadError] - render( - - ) + render() expect(screen.getAllByRole('listitem')).toHaveLength(fileItems.length) expect(screen.getByText(/testFile.pdf/)).toBeInTheDocument() @@ -93,16 +75,9 @@ describe('FileItemList component', () => { it('renders a table without errors', () => { const fileItems = [pending, uploadError] - render( - - ) + render() // the table has a header row so we need to add 1 to the length - expect(screen.getAllByRole('row')).toHaveLength(fileItems.length + 1) + expect(screen.getAllByRole('listitem')).toHaveLength(fileItems.length) expect(screen.getByText(/testFile.pdf/)).toBeInTheDocument() expect(screen.getByText('testFile2.pdf')).toBeInTheDocument() }) @@ -117,12 +92,7 @@ describe('FileItemList component', () => { ] const tree = renderer .create( - + ) .toJSON() expect(tree).toMatchSnapshot() @@ -130,14 +100,7 @@ describe('FileItemList component', () => { it('button actions in a list work as expected', async () => { const fileItems = [uploadError] - render( - - ) + render() await userEvent.click(screen.getByRole('button', { name: /Retry/ })) expect(buttonActionProps.retryItem).toHaveBeenCalled() @@ -148,14 +111,7 @@ describe('FileItemList component', () => { it('button actions in a table work as expected', async () => { const fileItems = [uploadError] - render( - - ) + render() await userEvent.click(screen.getByText('Retry')) expect(buttonActionProps.retryItem).toHaveBeenCalled() @@ -173,14 +129,7 @@ describe('FileItemList component', () => { duplicateError, scanning, ] - render( - - ) + render() const listItems = screen.getAllByRole('listitem') const loadingListItem = listItems[0] @@ -222,22 +171,15 @@ describe('FileItemList component', () => { duplicateError, scanning, ] - render( - - ) - - const rows = screen.getAllByRole('row') - const loadingRow = rows[1] - const uploadErrorRow = rows[2] - const scanningErrorRow = rows[3] - const completeRow = rows[4] - const duplicateErrorRow = rows[5] - const scanningRow = rows[6] + render() + + const rows = screen.getAllByRole('listitem') + const loadingRow = rows[0] + const uploadErrorRow = rows[1] + const scanningErrorRow = rows[2] + const completeRow = rows[3] + const duplicateErrorRow = rows[4] + const scanningRow = rows[5] // Items not in error state expect(loadingRow).not.toHaveClass('bg-error-lighter') diff --git a/services/app-web/src/components/FileUpload/FileItemList/FileItemsList.tsx b/services/app-web/src/components/FileUpload/FileItemList/FileItemsList.tsx index b59044bbf4..31f3f287fd 100644 --- a/services/app-web/src/components/FileUpload/FileItemList/FileItemsList.tsx +++ b/services/app-web/src/components/FileUpload/FileItemList/FileItemsList.tsx @@ -2,25 +2,16 @@ import React from 'react' import classnames from 'classnames' import { FileItemT, FileStatus } from '../FileProcessor/FileProcessor' import styles from '../FileUpload.module.scss' -import { TableWrapper } from '../TableWrapper/TableWrapper' import { ListWrapper } from '../ListWrapper/ListWrapper' export const FileItemsList = ({ fileItems, deleteItem, retryItem, - renderMode, - handleCheckboxClick, - isContractOnly, - shouldValidate, }: { fileItems: FileItemT[] deleteItem: (id: FileItemT) => void retryItem: (item: FileItemT) => void - renderMode: 'table' | 'list' - handleCheckboxClick: (event: React.ChangeEvent) => void - isContractOnly?: boolean - shouldValidate?: boolean }): React.ReactElement => { const liClasses = (status: FileStatus): string => { const hasError = @@ -33,22 +24,12 @@ export const FileItemsList = ({ }) } - return renderMode === 'table' ? ( - - ) : ( + return ( ) } diff --git a/services/app-web/src/components/FileUpload/FileListItem/FileListItem.tsx b/services/app-web/src/components/FileUpload/FileListItem/FileListItem.tsx index 2ea3c5ba1b..5de0428b03 100644 --- a/services/app-web/src/components/FileUpload/FileListItem/FileListItem.tsx +++ b/services/app-web/src/components/FileUpload/FileListItem/FileListItem.tsx @@ -16,7 +16,6 @@ type FileListItemProps = { hasRecoverableError: boolean handleDelete: (_e: React.MouseEvent) => void handleRetry: (_e: React.MouseEvent) => void - handleCheckboxClick: (event: React.ChangeEvent) => void } export const FileListItem = ({ @@ -29,7 +28,6 @@ export const FileListItem = ({ hasRecoverableError, handleDelete, handleRetry, - handleCheckboxClick, }: FileListItemProps): React.ReactElement => { const { name } = item return ( diff --git a/services/app-web/src/components/FileUpload/FileProcessor/FileProcessor.test.tsx b/services/app-web/src/components/FileUpload/FileProcessor/FileProcessor.test.tsx index fd7ed05213..ab4eae047e 100644 --- a/services/app-web/src/components/FileUpload/FileProcessor/FileProcessor.test.tsx +++ b/services/app-web/src/components/FileUpload/FileProcessor/FileProcessor.test.tsx @@ -12,7 +12,6 @@ describe('FileProcessor component', () => { key: undefined, s3URL: undefined, status: 'PENDING', - documentCategories: ['CONTRACT_RELATED'], } const scanning: FileItemT = { id: 'testFile1', @@ -21,7 +20,6 @@ describe('FileProcessor component', () => { key: '4545454-testFile1', s3URL: 'tests3://uploaded-12313123213/4545454-testFile1', status: 'SCANNING', - documentCategories: [], } const uploadError: FileItemT = { id: 'testFile2', @@ -30,7 +28,6 @@ describe('FileProcessor component', () => { key: undefined, s3URL: undefined, status: 'UPLOAD_ERROR', - documentCategories: [], } const scanningError: FileItemT = { @@ -40,7 +37,6 @@ describe('FileProcessor component', () => { key: '4545454-testFile3', s3URL: 'tests3://uploaded-12313123213/4545454-testFile3', status: 'SCANNING_ERROR', - documentCategories: [], } const uploadComplete: FileItemT = { @@ -50,7 +46,6 @@ describe('FileProcessor component', () => { key: '4545454-testFile4', s3URL: 'tests3://uploaded-12313123213/4545454-testFile4', status: 'UPLOAD_COMPLETE', - documentCategories: [], } const duplicateError: FileItemT = { @@ -60,7 +55,6 @@ describe('FileProcessor component', () => { key: '4545454-testFile4', s3URL: 'tests3://uploaded-12313123213/4545454-testFile4', status: 'DUPLICATE_NAME_ERROR', - documentCategories: [], } const buttonActionProps = { @@ -76,7 +70,6 @@ describe('FileProcessor component', () => { it('renders a list without errors', () => { render( { it('renders a table without errors', () => { render( { it('includes appropriate aria- attributes in the list', () => { render( { it('includes appropriate aria- attributes in the table', () => { render( { it('button actions work as expected in the list', async () => { render( { it('button actions work as expected in the table', async () => { render( { it('displays loading image, loading text, and remove button when status is LOADING in the list', () => { render( { it('displays loading image, loading text, and remove button when status is LOADING in the table', () => { render( { it('displays loading image, scanning text, and remove button when status is SCANNING in the list', () => { render( { it('displays loading image, scanning text, and remove button when status is SCANNING in the table', () => { render( { it('displays file image and remove button when status is UPLOAD_COMPLETE in a list', () => { render( { expect(screen.queryByRole('button', { name: /Retry/ })).toBeNull() }) - it('has clickable document category checkboxes', async () => { - render( - - ) - const contractCheckbox = screen.getByRole('checkbox', { - name: 'contract-supporting', - }) - const ratesCheckbox = screen.getByRole('checkbox', { - name: 'rate-supporting', - }) - await userEvent.click(contractCheckbox) - expect(categoryCheckboxProps.handleCheckboxClick).toHaveBeenCalledWith( - expect.objectContaining({ - target: expect.objectContaining({ - name: 'contract-supporting', - }), - }) - ) - await userEvent.click(ratesCheckbox) - expect(categoryCheckboxProps.handleCheckboxClick).toHaveBeenCalledWith( - expect.objectContaining({ - target: expect.objectContaining({ - name: 'rate-supporting', - }), - }) - ) - }) - - it('does not have clickable document category checkboxes for a contract-only submission', () => { - render( - - ) - const contractCheckbox = screen.queryByRole('checkbox', { - name: 'contract-supporting', - }) - const ratesCheckbox = screen.queryByRole('checkbox', { - name: 'rate-supporting', - }) - expect(contractCheckbox).not.toBeInTheDocument() - expect(ratesCheckbox).not.toBeInTheDocument() - }) - it('displays the remove button when status is UPLOAD_COMPLETE in a table', () => { render( { it('displays upload failed message and both retry and remove buttons when status is UPLOAD_ERROR in a list', async () => { render( { it('displays upload failed message, without checkboxes, and both retry and remove buttons when status is UPLOAD_ERROR in a table', async () => { render( { it('displays security scan failed message and both retry and remove buttons when status is SCANNING_ERROR in a list', () => { render( { it('displays security scan failed message, without checkboxes, and both retry and remove buttons when status is SCANNING_ERROR in a table', async () => { render( { it('displays duplicate name error message and remove button when status is DUPLICATE_NAME_ERROR in a list', () => { render( { it('displays duplicate name error message, without checkboxes , and remove button when status is DUPLICATE_NAME_ERROR in a table', () => { render( { expect(screen.queryByText('Retry')).not.toBeInTheDocument() }) - it('displays document categories error with checkboxes when expected for categories error (in table view)', () => { - render( - - ) - - const contractCheckbox = screen.queryByRole('checkbox', { - name: 'contract-supporting', - }) - const ratesCheckbox = screen.queryByRole('checkbox', { - name: 'rate-supporting', - }) - const itemTableRow = screen.getByRole('row') - - expect(contractCheckbox).toBeInTheDocument() - expect(ratesCheckbox).toBeInTheDocument() - expect(itemTableRow).toHaveClass('warningRow') - expect( - screen.getByText(/Must select at least one category checkbox/) - ).toBeInTheDocument() - expect(screen.queryByRole('button', { name: /Retry/ })).toBeNull() - }) - - it('does not display document categories error when expected (relevant in table view before validation)', () => { - render( - - ) - - const itemTableRow = screen.getByRole('row') - expect(itemTableRow).not.toHaveClass('bg-error-lighter warningRow') - - expect( - screen.queryByText(/Must select at least one category checkbox/) - ).not.toBeInTheDocument() - }) - it('displays unexpected error message and remove button when status is UPLOAD_ERROR but file reference is undefined (this is an unexpected state but it would mean the upload cannot be retried) in a list', () => { render( { it('displays unexpected error message and remove button when status is UPLOAD_ERROR but file reference is undefined (this is an unexpected state but it would mean the upload cannot be retried) in a table', () => { render( { if (hasDuplicateNameError) return ( @@ -80,14 +73,6 @@ const DocumentError = ({ ) - } else if (shouldValidate && hasMissingCategories) { - return ( - <> - - Must select at least one category checkbox - - - ) } else { return null } @@ -97,32 +82,19 @@ type FileProcessorProps = { item: FileItemT deleteItem: (item: FileItemT) => void retryItem: (item: FileItemT) => void - renderMode: 'table' | 'list' - handleCheckboxClick: (event: React.ChangeEvent) => void - isContractOnly?: boolean - shouldValidate?: boolean } export const FileProcessor = ({ item, deleteItem, retryItem, - renderMode, - handleCheckboxClick, - isContractOnly, - shouldValidate, }: FileProcessorProps): React.ReactElement => { const { name, status, file } = item - const isRateSupporting = item.documentCategories.includes('RATES_RELATED') - const isContractSupporting = - item.documentCategories.includes('CONTRACT_RELATED') const hasDuplicateNameError = status === 'DUPLICATE_NAME_ERROR' const hasScanningError = status === 'SCANNING_ERROR' const hasUploadError = status === 'UPLOAD_ERROR' const hasUnexpectedError = status === 'UPLOAD_ERROR' && file === undefined const hasRecoverableError = (hasUploadError || hasScanningError) && !hasUnexpectedError - const hasMissingCategories = - !isContractOnly && item.documentCategories.length === 0 const isLoading = status === 'PENDING' const isScanning = status === 'SCANNING' const isComplete = status === 'UPLOAD_COMPLETE' @@ -167,43 +139,11 @@ export const FileProcessor = ({ statusValue = 'error' } - const missingCategoryError = shouldValidate && hasMissingCategories - const errorRowClass = classnames({ - 'bg-error-lighter': statusValue === 'error' || missingCategoryError, + 'bg-error-lighter': statusValue === 'error', }) - const hasNonDocumentError = statusValue === 'error' - - return renderMode === 'table' ? ( - - } - hasRecoverableError={hasRecoverableError} - handleDelete={handleDelete} - handleRetry={handleRetry} - handleCheckboxClick={handleCheckboxClick} - isContractOnly={isContractOnly} - shouldValidate={shouldValidate} - hasNonDocumentError={hasNonDocumentError} - /> - ) : ( + return ( } hasRecoverableError={hasRecoverableError} handleDelete={handleDelete} handleRetry={handleRetry} - handleCheckboxClick={handleCheckboxClick} /> ) } diff --git a/services/app-web/src/components/FileUpload/FileUpload.stories.tsx b/services/app-web/src/components/FileUpload/FileUpload.stories.tsx index 80a2029de3..a6a869638c 100644 --- a/services/app-web/src/components/FileUpload/FileUpload.stories.tsx +++ b/services/app-web/src/components/FileUpload/FileUpload.stories.tsx @@ -14,20 +14,18 @@ export const DemoListUploadSuccess = (): React.ReactElement => { id="Default" name="Default Input" label="FileInput" - renderMode="list" - uploadFile={(file: File) => + uploadFile={(_file: File) => fakeRequest(true, resolveData) } - scanFile={async (key: string) => { + scanFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} - deleteFile={async (key: string) => { + deleteFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} onFileItemsUpdate={() => console.info('Async load complete')} - isContractOnly={false} /> ) } @@ -38,20 +36,18 @@ export const DemoTableUploadSuccess = (): React.ReactElement => { id="Default" name="Default Input" label="FileInput" - renderMode="table" - uploadFile={(file: File) => + uploadFile={(_file: File) => fakeRequest(true, resolveData) } - scanFile={async (key: string) => { + scanFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} - deleteFile={async (key: string) => { + deleteFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} onFileItemsUpdate={() => console.info('Async load complete')} - isContractOnly={false} /> ) } @@ -62,20 +58,18 @@ export const DemoListUploadFailure = (): React.ReactElement => { id="Default" name="Default Input" label="FileInput" - renderMode="list" - uploadFile={(file: File) => + uploadFile={(_file: File) => fakeRequest(false, resolveData) } - scanFile={async (key: string) => { + scanFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} - deleteFile={async (key: string) => { + deleteFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} onFileItemsUpdate={() => console.info('Async load complete')} - isContractOnly={false} /> ) } @@ -86,20 +80,18 @@ export const DemoTableUploadFailure = (): React.ReactElement => { id="Default" name="Default Input" label="FileInput" - renderMode="table" - uploadFile={(file: File) => + uploadFile={(_file: File) => fakeRequest(false, resolveData) } - scanFile={async (key: string) => { + scanFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} - deleteFile={async (key: string) => { + deleteFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} onFileItemsUpdate={() => console.info('Async load complete')} - isContractOnly={false} /> ) } @@ -110,20 +102,18 @@ export const DemoListScanFailure = (): React.ReactElement => { id="Default" name="Default Input" label="FileInput" - renderMode="list" - uploadFile={(file: File) => + uploadFile={(_file: File) => fakeRequest(true, resolveData) } - scanFile={async (key: string) => { + scanFile={async (_key: string) => { await fakeRequest(false, resolveData) return }} - deleteFile={async (key: string) => { + deleteFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} onFileItemsUpdate={() => console.info('Async load complete')} - isContractOnly={false} /> ) } @@ -134,20 +124,18 @@ export const DemoTableScanFailure = (): React.ReactElement => { id="Default" name="Default Input" label="FileInput" - renderMode="table" - uploadFile={(file: File) => + uploadFile={(_file: File) => fakeRequest(true, resolveData) } - scanFile={async (key: string) => { + scanFile={async (_key: string) => { await fakeRequest(false, resolveData) return }} - deleteFile={async (key: string) => { + deleteFile={async (_key: string) => { await fakeRequest(true, resolveData) return }} onFileItemsUpdate={() => console.info('Async load complete')} - isContractOnly={false} /> ) } diff --git a/services/app-web/src/components/FileUpload/FileUpload.test.tsx b/services/app-web/src/components/FileUpload/FileUpload.test.tsx index 9d55b4554c..169a98b14b 100644 --- a/services/app-web/src/components/FileUpload/FileUpload.test.tsx +++ b/services/app-web/src/components/FileUpload/FileUpload.test.tsx @@ -20,19 +20,19 @@ describe('FileUpload component', () => { id: 'Default', name: 'Default Input', label: 'File input label', - uploadFile: (file: File) => + uploadFile: (_file: File) => fakeRequest(true, { key: 'testtest', s3URL: 'fakeS3url', }), - deleteFile: async (key: string) => { + deleteFile: async (_key: string) => { await fakeRequest(true, { key: 'testtest', s3URL: 'fakeS3url', }) return }, - scanFile: async (key: string) => { + scanFile: async (_key: string) => { await fakeRequest(true, { key: 'testtest', s3URL: 'fakeS3url', @@ -42,12 +42,11 @@ describe('FileUpload component', () => { onFileItemsUpdate: () => { return }, - renderMode: 'list', } beforeEach(() => jest.clearAllMocks()) it('renders without errors', async () => { - await render() + render() expect(screen.getByTestId('file-input')).toBeInTheDocument() expect(screen.getByTestId('file-input')).toHaveClass('usa-file-input') expect(screen.getByText('File input label')).toBeInTheDocument() @@ -61,11 +60,10 @@ describe('FileUpload component', () => { name: 'Trussel Guide to Truss - trussels-guide.pdf', s3URL: "s3://local-uploads/1620164967212-Trussels' Guide to Truss - trussels-guide.pdf/Trussels' Guide to Truss - trussels-guide.pdf", status: 'UPLOAD_COMPLETE', - documentCategories: [], }, ] - await render() + render() // check for initial items const items = screen.getAllByRole('listitem') @@ -129,7 +127,7 @@ describe('FileUpload component', () => { }) it('accepts multiple files', async () => { - await render() + render() const inputEl = screen.getByTestId('file-input-input') @@ -146,7 +144,7 @@ describe('FileUpload component', () => { }) it('accepts an upload file of a valid type', async () => { - await render() + render() const inputEl = screen.getByTestId('file-input-input') expect(inputEl).toHaveAttribute('accept', '.pdf,.txt') @@ -163,7 +161,7 @@ describe('FileUpload component', () => { }) it('does not accept upload file of invalid type', async () => { - await render() + render() const inputEl = screen.getByTestId('file-input-input') expect(inputEl).toHaveAttribute('accept', '.pdf,.txt') @@ -176,7 +174,7 @@ describe('FileUpload component', () => { }) it('displays a duplicate file error when expected', async () => { - await render() + render() const input = screen.getByTestId('file-input-input') await userEvent.upload(input, [TEST_DOC_FILE]) @@ -204,7 +202,6 @@ describe('FileUpload component', () => { scanFile: jest.fn().mockResolvedValue(undefined), onFileItemsUpdate: jest.fn().mockResolvedValue(undefined), accept: '.pdf,.txt', - renderMode: 'list', } render() @@ -231,7 +228,6 @@ describe('FileUpload component', () => { scanFile: jest.fn().mockRejectedValue(new Error('failed')), onFileItemsUpdate: jest.fn().mockResolvedValue(undefined), accept: '.pdf,.txt', - renderMode: 'list', } render() @@ -258,7 +254,6 @@ describe('FileUpload component', () => { scanFile: jest.fn().mockRejectedValue(new Error('failed')), onFileItemsUpdate: jest.fn().mockResolvedValue(undefined), accept: '.pdf,.txt', - renderMode: 'list', } render() @@ -276,7 +271,7 @@ describe('FileUpload component', () => { describe('list summary heading', () => { it('display list count - X files added', async () => { - await render() + render() const input = screen.getByTestId('file-input-input') await userEvent.upload(input, [TEST_DOC_FILE]) @@ -287,7 +282,7 @@ describe('FileUpload component', () => { }) it('displays error count when scan error occurs', async () => { - await render( + render( { }) it('displays error count when duplicate name occurs', async () => { - await render() + render() const input = screen.getByTestId('file-input-input') await userEvent.upload(input, [TEST_DOC_FILE]) @@ -315,7 +310,7 @@ describe('FileUpload component', () => { }) it('displays complete count when file upload completes without issue', async () => { - await render() + render() const input = screen.getByTestId('file-input-input') await userEvent.upload(input, [TEST_DOC_FILE]) @@ -336,7 +331,7 @@ describe('FileUpload component', () => { }) it('displays pending count when file upload is still in progress', async () => { - await render() + render() const input = screen.getByTestId('file-input-input') await userEvent.upload(input, [TEST_DOC_FILE]) diff --git a/services/app-web/src/components/FileUpload/FileUpload.tsx b/services/app-web/src/components/FileUpload/FileUpload.tsx index 4da94d62fa..95d3bf18e6 100644 --- a/services/app-web/src/components/FileUpload/FileUpload.tsx +++ b/services/app-web/src/components/FileUpload/FileUpload.tsx @@ -28,7 +28,6 @@ export type FileUploadProps = { id: string name: string label: string - renderMode: 'list' | 'table' error?: string hint?: React.ReactNode initialItems?: FileItemT[] @@ -38,8 +37,6 @@ export type FileUploadProps = { scanFile?: (key: string) => Promise // optional function to be called after uploading (used for scanning) deleteFile: (key: string) => Promise onFileItemsUpdate: ({ fileItems }: { fileItems: FileItemT[] }) => void - isContractOnly?: boolean - shouldDisplayMissingCategoriesError?: boolean // by default, false. the parent component may read current files list and requirements of the form to determine otherwise. innerInputRef?: (el: HTMLInputElement) => void } & JSX.IntrinsicElements['input'] @@ -54,7 +51,6 @@ export const FileUpload = ({ id, name, label, - renderMode, hint, error, initialItems, @@ -63,8 +59,6 @@ export const FileUpload = ({ scanFile, deleteFile, onFileItemsUpdate, - isContractOnly, - shouldDisplayMissingCategoriesError = false, allowMultipleUploads = true, innerInputRef, ...inputProps @@ -74,31 +68,6 @@ export const FileUpload = ({ const summaryRef = useRef(null) // reference to the heading that we will focus const previousFileItems = usePrevious(fileItems) const isRequired = inputProps['aria-required'] - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const handleCheckboxClick = ( - event: React.ChangeEvent - ) => { - const changeType = - event.target.name === 'contract-supporting' - ? 'CONTRACT_RELATED' - : 'RATES_RELATED' - const id = event.target.id.substring(0, event.target.id.indexOf('--')) - const fileIndex = fileItems.findIndex((file) => file.id === id) - if (fileIndex === -1) return - if (fileItems[fileIndex].documentCategories.includes(changeType)) { - fileItems[fileIndex].documentCategories = fileItems[ - fileIndex - ].documentCategories.filter((category) => category !== changeType) - } else { - fileItems[fileIndex].documentCategories = [ - ...fileItems[fileIndex].documentCategories, - changeType, - ] - } - - setFileItems([...fileItems]) - } - const inputRequired = inputProps['aria-required'] || inputProps.required React.useEffect(() => { @@ -119,11 +88,6 @@ export const FileUpload = ({ currentItem: FileItemT ) => Boolean(existingList.some((item) => item.name === currentItem.name)) - const isMissingCategoriesItem = (fileItem: FileItemT) => { - if (!shouldDisplayMissingCategoriesError) return false // either no missing categories or else missing categories are not relevant - return fileItem.documentCategories.length === 0 - } - const isAcceptableFile = (file: File): boolean => { const acceptedTypes = inputProps?.accept?.split(',') || [] if (acceptedTypes.length === 0) return true @@ -153,7 +117,6 @@ export const FileUpload = ({ key: undefined, s3URL: undefined, status: 'PENDING', - documentCategories: isContractOnly ? ['CONTRACT_RELATED'] : [], } if (isDuplicateItem(fileItems, newItem)) { @@ -403,15 +366,13 @@ export const FileUpload = ({ addFilesAndUpdateList(files) } const uploadedCount = fileItems.filter( - (item) => - item.status === 'UPLOAD_COMPLETE' && !isMissingCategoriesItem(item) + (item) => item.status === 'UPLOAD_COMPLETE' ).length const errorCount = fileItems.filter( (item) => item.status === 'UPLOAD_ERROR' || item.status === 'SCANNING_ERROR' || - item.status === 'DUPLICATE_NAME_ERROR' || - isMissingCategoriesItem(item) + item.status === 'DUPLICATE_NAME_ERROR' ).length const pendingCount = fileItems.filter( (item) => item.status === 'PENDING' || item.status === 'SCANNING' @@ -471,10 +432,6 @@ export const FileUpload = ({ retryItem={retryFile} deleteItem={deleteItem} fileItems={fileItems} - renderMode={renderMode} - handleCheckboxClick={handleCheckboxClick} - isContractOnly={isContractOnly} - shouldValidate={shouldDisplayMissingCategoriesError} /> ) diff --git a/services/app-web/src/components/FileUpload/ListWrapper/ListWrapper.tsx b/services/app-web/src/components/FileUpload/ListWrapper/ListWrapper.tsx index 37d535f56b..f8632c2809 100644 --- a/services/app-web/src/components/FileUpload/ListWrapper/ListWrapper.tsx +++ b/services/app-web/src/components/FileUpload/ListWrapper/ListWrapper.tsx @@ -7,8 +7,6 @@ type ListWrapperProps = { liClasses: (status: FileItemT['status']) => string deleteItem: (id: FileItemT) => void retryItem: (item: FileItemT) => void - handleCheckboxClick: (event: React.ChangeEvent) => void - isContractOnly?: boolean } export const ListWrapper = ({ @@ -16,8 +14,6 @@ export const ListWrapper = ({ liClasses, deleteItem, retryItem, - handleCheckboxClick, - isContractOnly, }: ListWrapperProps): React.ReactElement => { return (
    ))} diff --git a/services/app-web/src/components/FileUpload/TableWrapper/TableWrapper.module.scss b/services/app-web/src/components/FileUpload/TableWrapper/TableWrapper.module.scss deleted file mode 100644 index cc14c825e4..0000000000 --- a/services/app-web/src/components/FileUpload/TableWrapper/TableWrapper.module.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import '../../../styles/uswdsImports.scss'; -@import '../../../styles/custom.scss'; - -.filesEmpty { - margin-top: units(8); - - h3 { - font-weight: font-weight('normal'); - color: color('base'); - text-align: center; - } -} diff --git a/services/app-web/src/components/FileUpload/TableWrapper/TableWrapper.tsx b/services/app-web/src/components/FileUpload/TableWrapper/TableWrapper.tsx deleted file mode 100644 index 25f75d7f1a..0000000000 --- a/services/app-web/src/components/FileUpload/TableWrapper/TableWrapper.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react' -import { FileProcessor, FileItemT } from '../FileProcessor/FileProcessor' -import { Table } from '@trussworks/react-uswds' -import styles from './TableWrapper.module.scss' - -type TableWrapperProps = { - fileItems: FileItemT[] - deleteItem: (id: FileItemT) => void - retryItem: (item: FileItemT) => void - handleCheckboxClick: (event: React.ChangeEvent) => void - isContractOnly?: boolean - shouldValidate?: boolean - hasMissingCategories?: boolean -} - -export const TableWrapper = ({ - fileItems, - deleteItem, - retryItem, - handleCheckboxClick, - isContractOnly, - shouldValidate, -}: TableWrapperProps): React.ReactElement => { - - const hasFiles = fileItems.length > 0 - - return ( - <> - {hasFiles ? ( - - - - - {!isContractOnly && } - {!isContractOnly && } - - - - - {fileItems.map((item) => ( - - ))} - -
    Document nameContract-supportingRate-supporting
    - ):( -
    -

    You have not uploaded any files

    -
    - )} - - ) -} diff --git a/services/app-web/src/components/Header/UserLoginInfo/UserLoginInfo.test.tsx b/services/app-web/src/components/Header/UserLoginInfo/UserLoginInfo.test.tsx index 8a104c30fb..3349e09c0b 100644 --- a/services/app-web/src/components/Header/UserLoginInfo/UserLoginInfo.test.tsx +++ b/services/app-web/src/components/Header/UserLoginInfo/UserLoginInfo.test.tsx @@ -1,8 +1,5 @@ import { screen } from '@testing-library/react' -import { - renderWithProviders, - ldUseClientSpy, -} from '../../../testHelpers/jestHelpers' +import { renderWithProviders } from '../../../testHelpers/jestHelpers' import { UserLoginInfo } from './UserLoginInfo' import { useStringConstants } from '../../../hooks/useStringConstants' @@ -56,7 +53,6 @@ describe('UserLoginInfo', () => { }) it('renders link to support email', () => { - ldUseClientSpy({ 'helpdesk-email': true }) const stringConstants = useStringConstants() const MAIL_TO_SUPPORT = stringConstants.MAIL_TO_SUPPORT const jestFn = jest.fn() diff --git a/services/app-web/src/components/SectionCard/SectionCard.module.scss b/services/app-web/src/components/SectionCard/SectionCard.module.scss new file mode 100644 index 0000000000..93c27bc726 --- /dev/null +++ b/services/app-web/src/components/SectionCard/SectionCard.module.scss @@ -0,0 +1,10 @@ +@import '../../styles/uswdsImports.scss'; +@import '../../styles/custom.scss'; +.section { + @include sectionCard; + + >h3, fieldset>h3{ + margin-top: 0; // adjust vertical space between section edge and the first heading + } + +} diff --git a/services/app-web/src/components/SectionCard/SectionCard.tsx b/services/app-web/src/components/SectionCard/SectionCard.tsx new file mode 100644 index 0000000000..643f91d780 --- /dev/null +++ b/services/app-web/src/components/SectionCard/SectionCard.tsx @@ -0,0 +1,26 @@ +import classNames from 'classnames' +import styles from './SectionCard.module.scss' +import React from 'react' + +/* + HTML section with card styling. + This type of section area stands out visually from the rest of the content. +*/ +type SectionCardProps = { + children: React.ReactNode +} & React.JSX.IntrinsicElements['section'] + +const SectionCard = ({ + children, + className, + ...restProps +}: SectionCardProps) => { + const classes = classNames(styles.section, className) + return ( +
    + {children} +
    + ) +} + +export { SectionCard } diff --git a/services/app-web/src/components/SectionCard/index.ts b/services/app-web/src/components/SectionCard/index.ts new file mode 100644 index 0000000000..fefa98aeaa --- /dev/null +++ b/services/app-web/src/components/SectionCard/index.ts @@ -0,0 +1 @@ +export { SectionCard } from './SectionCard' diff --git a/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx index 5822ed58a1..8a721a18bd 100644 --- a/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx @@ -8,6 +8,7 @@ import { import { HealthPlanFormDataType } from '../../../common-code/healthPlanFormDataType' import { ActuaryContact } from '../../../common-code/healthPlanFormDataType' import { DataDetail, DataDetailContactField } from '../../DataDetail' +import { SectionCard } from '../../SectionCard' export type ContactsSummarySectionProps = { submission: HealthPlanFormDataType @@ -37,7 +38,7 @@ export const ContactsSummarySection = ({ const isSubmitted = submission.status === 'SUBMITTED' return ( -
    + )} -
    + ) } diff --git a/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.test.tsx b/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.test.tsx index ecfa8b3d73..895e1c45d1 100644 --- a/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.test.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.test.tsx @@ -32,16 +32,11 @@ describe('ContractDetailsSummarySection', () => { s3URL: 's3://bucketname/key/test1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://bucketname/key/test3', name: 'supporting docs test 3', sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ], } @@ -242,7 +237,6 @@ describe('ContractDetailsSummarySection', () => { s3URL: 's3://foo/bar/contract', name: 'contract test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT' as const], }, ], documents: [ @@ -250,22 +244,16 @@ describe('ContractDetailsSummarySection', () => { s3URL: 's3://bucketname/key/test1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://bucketname/key/test2', name: 'supporting docs test 2', sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], }, { s3URL: 's3://bucketname/key/test3', name: 'supporting docs test 3', sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ], } @@ -302,6 +290,9 @@ describe('ContractDetailsSummarySection', () => { expect( within(supportingDocsTable).getByText('supporting docs test 1') ).toBeInTheDocument() + expect( + within(supportingDocsTable).getByText('supporting docs test 2') + ).toBeInTheDocument() expect( within(supportingDocsTable).getByText('supporting docs test 3') ).toBeInTheDocument() @@ -309,7 +300,7 @@ describe('ContractDetailsSummarySection', () => { // check correct category on supporting docs expect( within(supportingDocsTable).getAllByText('Contract-supporting') - ).toHaveLength(2) + ).toHaveLength(3) }) }) diff --git a/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.tsx index c11273b99b..b99214afcd 100644 --- a/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/ContractDetailsSummarySection/ContractDetailsSummarySection.tsx @@ -42,6 +42,7 @@ import { StatutoryRegulatoryAttestation, StatutoryRegulatoryAttestationQuestion, } from '../../../constants/statutoryRegulatoryAttestation' +import { SectionCard } from '../../SectionCard' export type ContractDetailsSummarySectionProps = { submission: HealthPlanFormDataType @@ -91,9 +92,7 @@ export const ContractDetailsSummarySection = ({ submission.statutoryRegulatoryAttestation ) - const contractSupportingDocuments = submission.documents.filter((doc) => - doc.documentCategories.includes('CONTRACT_RELATED' as const) - ) + const contractSupportingDocuments = submission.documents const isEditing = !isSubmitted(submission) && editNavigateTo !== undefined const applicableFederalAuthorities = isCHIPOnly(submission) ? submission.federalAuthorities.filter((authority) => @@ -152,7 +151,10 @@ export const ContractDetailsSummarySection = ({ ]) return ( -
    + -
    + ) } diff --git a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.test.tsx b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.test.tsx index e21237ab13..1576bc3ce6 100644 --- a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.test.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.test.tsx @@ -26,7 +26,6 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/rate', name: 'rate docs test 1', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -56,7 +55,6 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/rate2', name: 'rate docs test 2', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -300,7 +298,18 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/rate', name: 'rate docs test 1', sha256: 'fakesha', - documentCategories: ['RATES' as const], + }, + ], + supportingDocuments: [ + { + s3URL: 's3://foo/bar/test-2', + name: 'supporting docs test 2', + sha256: 'fakesha', + }, + { + s3URL: 's3://foo/bar/test-3', + name: 'supporting docs test 3', + sha256: 'fakesha', }, ], }, @@ -310,22 +319,6 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], - }, - { - s3URL: 's3://foo/bar/test-2', - name: 'supporting docs test 2', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], - }, - { - s3URL: 's3://foo/bar/test-3', - name: 'supporting docs test 3', - sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ], } @@ -353,11 +346,6 @@ describe('RateDetailsSummarySection', () => { expect(rateDocsTable).toBeInTheDocument() expect(supportingDocsTable).toBeInTheDocument() - expect( - screen.getByRole('link', { - name: /Edit Rate supporting documents/, - }) - ).toHaveAttribute('href', '/documents') const supportingDocsTableRows = within(supportingDocsTable).getAllByRole('rowgroup') @@ -654,7 +642,6 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/rate', name: 'rate docs test 1', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], }, @@ -664,22 +651,16 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], }, { s3URL: 's3://foo/bar/test-3', name: 'supporting docs test 3', sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ], } @@ -758,7 +739,6 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/rate', name: 'rate docs test 1', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], }, @@ -768,22 +748,16 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], }, { s3URL: 's3://foo/bar/test-3', name: 'supporting docs test 3', sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ], } @@ -838,7 +812,6 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/rate', name: 'rate docs test 1', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], }, @@ -848,22 +821,16 @@ describe('RateDetailsSummarySection', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], }, { s3URL: 's3://foo/bar/test-3', name: 'supporting docs test 3', sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ], } diff --git a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.tsx index 7fe0bd07de..78c338b91e 100644 --- a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/RateDetailsSummarySection.tsx @@ -20,12 +20,10 @@ import { getCurrentRevisionFromHealthPlanPackage } from '../../../gqlHelpers' import { SharedRateCertDisplay } from '../../../common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType' import { DataDetailMissingField } from '../../DataDetail/DataDetailMissingField' import { DataDetailContactField } from '../../DataDetail/DataDetailContactField/DataDetailContactField' -import { v4 as uuidv4 } from 'uuid' -import { useLDClient } from 'launchdarkly-react-client-sdk' -import { featureFlags } from '../../../common-code/featureFlags' import { DocumentDateLookupTableType } from '../../../documentHelpers/makeDocumentDateLookupTable' import useDeepCompareEffect from 'use-deep-compare-effect' import { InlineDocumentWarning } from '../../DocumentWarning' +import { SectionCard } from '../../SectionCard' // Used for refreshed packages names keyed by their package id // package name includes (Draft) for draft packages. type PackageNameType = string @@ -71,22 +69,12 @@ export const RateDetailsSummarySection = ({ statePrograms, onDocumentError, }: RateDetailsSummarySectionProps): React.ReactElement => { - // feature flags state management - const ldClient = useLDClient() - const supportingDocsByRate = ldClient?.variation( - featureFlags.SUPPORTING_DOCS_BY_RATE.flag, - featureFlags.SUPPORTING_DOCS_BY_RATE.defaultValue - ) - const [packageNamesLookup, setPackageNamesLookup] = React.useState(null) const isSubmitted = submission.status === 'SUBMITTED' const isEditing = !isSubmitted && editNavigateTo !== undefined const isPreviousSubmission = usePreviousSubmission() - const submissionLevelRateSupportingDocuments = submission.documents.filter( - (doc) => doc.documentCategories.includes('RATES_RELATED') - ) const { getKey, getBulkDlURL } = useS3() const [zippedFilesURL, setZippedFilesURL] = useState< @@ -208,16 +196,7 @@ export const RateDetailsSummarySection = ({ async function fetchZipUrl() { const keysFromDocs = submission.rateInfos .flatMap((rateInfo) => - supportingDocsByRate - ? rateInfo.rateDocuments.concat( - rateInfo.supportingDocuments - ) - : rateInfo.rateDocuments - ) - .concat( - supportingDocsByRate - ? [] - : submissionLevelRateSupportingDocuments + rateInfo.rateDocuments.concat(rateInfo.supportingDocuments) ) .map((doc) => { const key = getKey(doc.s3URL) @@ -251,15 +230,13 @@ export const RateDetailsSummarySection = ({ getKey, getBulkDlURL, submission, - submissionLevelRateSupportingDocuments, submissionName, - supportingDocsByRate, isSubmitted, isPreviousSubmission, ]) return ( -
    + 0 ? ( submission.rateInfos.map((rateInfo) => { return ( - // When we complete rates refactor we can remove workaround for the react key - +

    'LOADING...' )} - {supportingDocsByRate && !loading ? ( + {!loading ? ( 'LOADING...' )} - + ) }) ) : ( )} - { - // START - This whole block gets deleted when we remove the feature flag. - !supportingDocsByRate && ( - - ) - // This whole block gets deleted when we remove the feature flag - END - } -

    + ) } diff --git a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection.tsx index 610ba3c037..e70e919681 100644 --- a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection.tsx @@ -27,6 +27,7 @@ import { NavLink } from 'react-router-dom' import { packageName } from '../../../common-code/healthPlanFormDataType' import { UploadedDocumentsTableProps } from '../UploadedDocumentsTable/UploadedDocumentsTable' import { useAuth } from '../../../contexts/AuthContext' +import { SectionCard } from '../../SectionCard' // This rate summary pages assumes we are using contract and rates API. // Eventually RateDetailsSummarySection should share code with this code @@ -183,7 +184,7 @@ export const SingleRateSummarySection = ({ return ( -
    @@ -295,8 +296,8 @@ export const SingleRateSummarySection = ({ /> -
    -
    + + {renderDownloadButton(zippedFilesURL)} @@ -313,7 +314,7 @@ export const SingleRateSummarySection = ({ documentDateLookupTable={documentDateLookupTable} caption="Rate supporting documents" /> -
    +
    ) } diff --git a/services/app-web/src/components/SubmissionSummarySection/SubmissionSummarySection.module.scss b/services/app-web/src/components/SubmissionSummarySection/SubmissionSummarySection.module.scss index a27a79953d..1729cd9cf8 100644 --- a/services/app-web/src/components/SubmissionSummarySection/SubmissionSummarySection.module.scss +++ b/services/app-web/src/components/SubmissionSummarySection/SubmissionSummarySection.module.scss @@ -5,12 +5,7 @@ } .summarySection { - margin: units(2) auto; - background: $cms-color-white; - padding: units(2) units(4); - border: 1px solid $theme-color-base-lighter; line-height: units(3); - @include u-radius('md'); h2 { margin: 0; @@ -48,6 +43,12 @@ table:last-of-type { margin-bottom: units(2); } + + // with nested sections, collapse bottom margin/padding for last in list + // rely on margin from the parent .summarySection. Relevant for multi-rate experience + section:last-of-type { + margin-bottom: 0 !important; + } } .contactInfo p { @@ -67,12 +68,8 @@ .rateName { display: block; - margin-block-start: 1.33em; - margin-block-end: 1.33em; - margin-inline-start: 0; - margin-inline-end: 0; font-weight: bold; - font-size: 16px; + margin: 0 } .certifyingActuaryDetail { diff --git a/services/app-web/src/components/SubmissionSummarySection/SubmissionTypeSummarySection/SubmissionTypeSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/SubmissionTypeSummarySection/SubmissionTypeSummarySection.tsx index b2c1db68e5..470ace8298 100644 --- a/services/app-web/src/components/SubmissionSummarySection/SubmissionTypeSummarySection/SubmissionTypeSummarySection.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/SubmissionTypeSummarySection/SubmissionTypeSummarySection.tsx @@ -13,6 +13,7 @@ import { Program } from '../../../gen/gqlClient' import { usePreviousSubmission } from '../../../hooks/usePreviousSubmission' import { booleanAsYesNoUserValue } from '../../../components/Form/FieldYesNo/FieldYesNo' import styles from '../SubmissionSummarySection.module.scss' +import { SectionCard } from '../../SectionCard' export type SubmissionTypeSummarySectionProps = { submission: HealthPlanFormDataType @@ -40,7 +41,10 @@ export const SubmissionTypeSummarySection = ({ const isSubmitted = submission.status === 'SUBMITTED' return ( -
    + -
    + ) } diff --git a/services/app-web/src/components/SubmissionSummarySection/SupportingDocumentsSummarySection/SupportingDocumentsSummarySection.test.tsx b/services/app-web/src/components/SubmissionSummarySection/SupportingDocumentsSummarySection/SupportingDocumentsSummarySection.test.tsx deleted file mode 100644 index 7a0825ae26..0000000000 --- a/services/app-web/src/components/SubmissionSummarySection/SupportingDocumentsSummarySection/SupportingDocumentsSummarySection.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { screen, waitFor } from '@testing-library/react' -import { renderWithProviders } from '../../../testHelpers/jestHelpers' -import { SupportingDocumentsSummarySection } from './SupportingDocumentsSummarySection' -import { - fetchCurrentUserMock, - mockContractAndRatesDraft, - mockStateSubmission, -} from '../../../testHelpers/apolloMocks' - -describe('SupportingDocumentsSummarySection', () => { - const draftSubmission = mockContractAndRatesDraft() - const stateSubmission = mockStateSubmission() - - it('can render uncategorized documents in draft submission without errors', async () => { - const testSubmission = { - ...draftSubmission, - documents: [ - { - s3URL: 's3://foo/bar/test-1', - name: 'supporting docs test 1', - sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], - }, - { - s3URL: 's3://foo/bar/test-2', - name: 'supporting docs test 2', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], - }, - { - s3URL: 's3://foo/bar/test-3', - name: 'supporting docs test 3', - sha256: 'fakesha', - documentCategories: [], - }, - ], - } - - renderWithProviders( - , - { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - } - ) - - await waitFor(() => { - expect( - screen.getByRole('heading', { - level: 2, - name: 'Supporting documents', - }) - ).toBeInTheDocument() - expect( - screen.getByRole('link', { name: 'Edit Supporting documents' }) - ).toHaveAttribute('href', '/documents') - expect(screen.queryByText('supporting docs test 2')).toBeNull() - expect( - screen.getByText('supporting docs test 3') - ).toBeInTheDocument() - }) - }) - - it('can render uncategorized documents state submission without errors', async () => { - const testSubmission = { - ...stateSubmission, - documents: [ - { - s3URL: 's3://foo/bar/test-1', - name: 'supporting docs test 1', - sha256: 'fakesha', - documentCategories: [], - }, - { - s3URL: 's3://foo/bar/test-2', - name: 'supporting docs test 2', - sha256: 'fakesha', - documentCategories: [], - }, - ], - } - - renderWithProviders( - - ) - - await waitFor(() => { - expect( - screen.getByRole('heading', { - level: 2, - name: 'Supporting documents', - }) - ).toBeInTheDocument() - - expect(screen.queryByText('Edit')).not.toBeInTheDocument() - expect( - screen.getByText('supporting docs test 1') - ).toBeInTheDocument() - expect( - screen.getByText('supporting docs test 2') - ).toBeInTheDocument() - }) - }) -}) diff --git a/services/app-web/src/components/SubmissionSummarySection/SupportingDocumentsSummarySection/SupportingDocumentsSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/SupportingDocumentsSummarySection/SupportingDocumentsSummarySection.tsx deleted file mode 100644 index e1d7b6ce14..0000000000 --- a/services/app-web/src/components/SubmissionSummarySection/SupportingDocumentsSummarySection/SupportingDocumentsSummarySection.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { useEffect, useState } from 'react' -import styles from '../SubmissionSummarySection.module.scss' -import { SectionHeader } from '../../SectionHeader' -import { DownloadButton } from '../../DownloadButton' -import { Link } from '@trussworks/react-uswds' -import { useS3 } from '../../../contexts/S3Context' -import { - HealthPlanFormDataType, - SubmissionDocument, -} from '../../../common-code/healthPlanFormDataType' -import { recordJSException } from '../../../otelHelpers' -import useDeepCompareEffect from 'use-deep-compare-effect' -import { InlineDocumentWarning } from '../../DocumentWarning' - -type DocumentWithLink = { url: string | null } & SubmissionDocument - -export type SupportingDocumentsSummarySectionProps = { - submission: HealthPlanFormDataType - editNavigateTo?: string - submissionName?: string - onDocumentError?: (error: true) => void -} -const getUncategorizedDocuments = ( - documents: SubmissionDocument[] -): SubmissionDocument[] => - documents.filter( - (doc) => !doc.documentCategories || doc.documentCategories.length === 0 - ) - -function renderDownloadButton(zippedFilesURL: string | undefined | Error) { - if (zippedFilesURL instanceof Error) { - return ( - - ) - } - return ( - - ) -} - -// This component is only used for supporting docs that are not categorized (not expected behavior but still possible) -// since supporting documents are now displayed in the rate and contract sections -export const SupportingDocumentsSummarySection = ({ - submission, - editNavigateTo, - submissionName, - onDocumentError, -}: SupportingDocumentsSummarySectionProps): React.ReactElement | null => { - const { getURL, getKey, getBulkDlURL } = useS3() - const [refreshedDocs, setRefreshedDocs] = useState([]) - const [zippedFilesURL, setZippedFilesURL] = useState< - string | undefined | Error - >(undefined) - const isSubmitted = submission.status === 'SUBMITTED' - useEffect(() => { - const refreshDocuments = async () => { - const uncategorizedDocuments = getUncategorizedDocuments( - submission.documents - ) - - const newDocuments = await Promise.all( - uncategorizedDocuments.map(async (doc) => { - const key = getKey(doc.s3URL) - if (!key) - return { - ...doc, - url: null, - } - - const documentLink = await getURL(key, 'HEALTH_PLAN_DOCS') - return { - ...doc, - url: documentLink, - } - }) - ).catch((err) => { - console.info(err) - return [] - }) - - setRefreshedDocs(newDocuments) - } - - void refreshDocuments() - }, [submission, getKey, getURL]) - - useDeepCompareEffect(() => { - // skip getting urls of this if this is a previous submission, draft or no uncategorized supporting documents - if (!isSubmitted || refreshedDocs.length === 0) return - - // get all the keys for the documents we want to zip - const uncategorizedDocuments = getUncategorizedDocuments( - submission.documents - ) - - async function fetchZipUrl() { - const keysFromDocs = uncategorizedDocuments - .map((doc) => { - const key = getKey(doc.s3URL) - if (!key) return '' - return key - }) - .filter((key) => key !== '') - - // call the lambda to zip the files and get the url - const zippedURL = await getBulkDlURL( - keysFromDocs, - submissionName + '-supporting-documents.zip', - 'HEALTH_PLAN_DOCS' - ) - - if (zippedURL instanceof Error) { - const msg = `ERROR: getBulkDlURL failed to generate contract document URL. ID: ${submission.id} Message: ${zippedURL}` - console.info(msg) - - if (onDocumentError) { - onDocumentError(true) - } - - recordJSException(msg) - } - - setZippedFilesURL(zippedURL) - } - - void fetchZipUrl() - }, [getKey, getBulkDlURL, submission, submissionName, isSubmitted]) - - const documentsSummary = `${refreshedDocs.length} ${ - refreshedDocs.length === 1 ? 'file' : 'files' - }` - // when there are no uncategorized supporting documents, remove this section entirely - if (refreshedDocs.length === 0) return null - - return ( -
    - - {isSubmitted && renderDownloadButton(zippedFilesURL)} - - {documentsSummary} -
      - {refreshedDocs.map((doc) => ( -
    • - {doc.url ? ( - - {doc.name} - - ) : ( - {doc.name} - )} -
    • - ))} -
    -
    - ) -} diff --git a/services/app-web/src/components/SubmissionSummarySection/SupportingDocumentsSummarySection/index.ts b/services/app-web/src/components/SubmissionSummarySection/SupportingDocumentsSummarySection/index.ts deleted file mode 100644 index f890441125..0000000000 --- a/services/app-web/src/components/SubmissionSummarySection/SupportingDocumentsSummarySection/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SupportingDocumentsSummarySection } from './SupportingDocumentsSummarySection' diff --git a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.test.tsx b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.test.tsx index 7fba92995e..f359b9f147 100644 --- a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.test.tsx +++ b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.test.tsx @@ -18,7 +18,6 @@ describe('UploadedDocumentsTable', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, ] renderWithProviders( @@ -58,16 +57,11 @@ describe('UploadedDocumentsTable', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://foo/bar/test-3', name: 'supporting docs test 3', sha256: 'fakesha', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ] @@ -105,22 +99,16 @@ describe('UploadedDocumentsTable', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', sha256: 'fakesha1', - documentCategories: ['RATES_RELATED' as const], }, { s3URL: 's3://foo/bar/test-3', name: 'supporting docs test 3', sha256: 'fakesha2', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ] const dateLookupTable: DocumentDateLookupTableType = { @@ -168,22 +156,16 @@ describe('UploadedDocumentsTable', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', sha256: 'fakesha1', - documentCategories: ['RATES_RELATED' as const], }, { s3URL: 's3://foo/bar/test-3', name: 'supporting docs test 3', sha256: 'fakesha2', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ] const dateLookupTable: DocumentDateLookupTableType = { @@ -231,22 +213,16 @@ describe('UploadedDocumentsTable', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', sha256: 'fakesha1', - documentCategories: ['RATES_RELATED' as const], }, { s3URL: 's3://foo/bar/test-3', name: 'supporting docs test 3', sha256: 'fakesha2', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ] const dateLookupTable: DocumentDateLookupTableType = { @@ -294,22 +270,16 @@ describe('UploadedDocumentsTable', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', sha256: 'fakesha1', - documentCategories: ['RATES_RELATED' as const], }, { s3URL: 's3://foo/bar/test-3', name: 'supporting docs test 3', sha256: 'fakesha2', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ] const dateLookupTable: DocumentDateLookupTableType = { @@ -351,13 +321,11 @@ describe('UploadedDocumentsTable', () => { { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', - documentCategories: ['CONTRACT_RELATED' as const], sha256: 'fakeSha1', }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', - documentCategories: ['RATES_RELATED' as const], sha256: 'fakeSha2', }, ] @@ -395,22 +363,16 @@ describe('UploadedDocumentsTable', () => { s3URL: 's3://foo/bar/test-1', name: 'supporting docs test 1', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED' as const], }, { s3URL: 's3://foo/bar/test-2', name: 'supporting docs test 2', sha256: 'fakesha1', - documentCategories: ['RATES_RELATED' as const], }, { s3URL: 's3://foo/bar/test-3', name: 'supporting docs test 3', sha256: 'fakesha2', - documentCategories: [ - 'CONTRACT_RELATED' as const, - 'RATES_RELATED' as const, - ], }, ] const dateLookupTable = { diff --git a/services/app-web/src/components/SubmissionSummarySection/index.ts b/services/app-web/src/components/SubmissionSummarySection/index.ts index fdf21965c4..4b8d740013 100644 --- a/services/app-web/src/components/SubmissionSummarySection/index.ts +++ b/services/app-web/src/components/SubmissionSummarySection/index.ts @@ -5,5 +5,4 @@ export { ContactsSummarySection, getActuaryFirm, } from './ContactsSummarySection' -export { SupportingDocumentsSummarySection } from './SupportingDocumentsSummarySection' export { UploadedDocumentsTable } from './UploadedDocumentsTable' diff --git a/services/app-web/src/components/index.ts b/services/app-web/src/components/index.ts index 668b1f3b1a..0c9954b6a4 100644 --- a/services/app-web/src/components/index.ts +++ b/services/app-web/src/components/index.ts @@ -40,7 +40,6 @@ export { ContractDetailsSummarySection, RateDetailsSummarySection, ContactsSummarySection, - SupportingDocumentsSummarySection, UploadedDocumentsTable, } from './SubmissionSummarySection' @@ -85,3 +84,4 @@ export { ActionButton } from './ActionButton' export { Breadcrumbs } from './Breadcrumbs' export { InlineDocumentWarning } from './DocumentWarning' +export { SectionCard } from './SectionCard' diff --git a/services/app-web/src/constants/statutoryRegulatoryAttestation.ts b/services/app-web/src/constants/statutoryRegulatoryAttestation.ts index 024f32e685..39531ca43b 100644 --- a/services/app-web/src/constants/statutoryRegulatoryAttestation.ts +++ b/services/app-web/src/constants/statutoryRegulatoryAttestation.ts @@ -7,7 +7,7 @@ const StatutoryRegulatoryAttestationQuestion = 'Do you attest that this contract complies with all applicable statutory and regulatory requirements including those contained in Title 42 Part 438 and Part 457 of the Code of Federal Regulations (CFR)?' const StatutoryRegulatoryAttestationDescription = - 'Please provide a brief description of the contract’s non-compliance (with regulatory citations) and expected timeframe for remediation' + 'Provide a brief description of any contractual or operational non-compliance, including regulatory citations and expected timeframe for remediation' export { StatutoryRegulatoryAttestation, diff --git a/services/app-web/src/contexts/AuthContext.tsx b/services/app-web/src/contexts/AuthContext.tsx index da79ddebc8..921959a51f 100644 --- a/services/app-web/src/contexts/AuthContext.tsx +++ b/services/app-web/src/contexts/AuthContext.tsx @@ -14,6 +14,7 @@ import { handleApolloError } from '../gqlHelpers/apolloErrors' type LogoutFn = () => Promise export type LoginStatusType = 'LOADING' | 'LOGGED_OUT' | 'LOGGED_IN' +export const MODAL_COUNTDOWN_DURATION = 2 * 60 // session expiration modal counts down for 120 seconds (2 minutes) type AuthContextType = { /* See docs/AuthContext.md for an explanation of some of these variables */ @@ -73,15 +74,10 @@ function AuthProvider({ const sessionExpirationTime = useRef( dayjs(Date.now()).add(minutesUntilExpiration, 'minute') ) - const countdownDurationSeconds: number = - ldClient?.variation( - featureFlags.MODAL_COUNTDOWN_DURATION.flag, - featureFlags.MODAL_COUNTDOWN_DURATION.defaultValue - ) * 60 const [logoutCountdownDuration, setLogoutCountdownDuration] = - useState(countdownDurationSeconds) - const modalCountdownTimers = useRef([]) - const sessionExpirationTimers = useRef([]) + useState(MODAL_COUNTDOWN_DURATION) + const modalCountdownTimers = useRef([]) + const sessionExpirationTimers = useRef([]) const { loading, data, error, refetch } = useFetchCurrentUserQuery({ notifyOnNetworkStatusChange: true, }) @@ -104,7 +100,7 @@ function AuthProvider({ modalCountdownTimers.current = [] } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sessionIsExpiring, countdownDurationSeconds]) // full dep array causes a loop, because we're resetting the dep in the useEffect + }, [sessionIsExpiring]) // full dep array causes a loop, because we're resetting the dep in the useEffect const isAuthenticated = loggedInUser !== undefined @@ -132,8 +128,8 @@ function AuthProvider({ const computedLoginStatus: LoginStatusType = loading ? 'LOADING' : loggedInUser !== undefined - ? 'LOGGED_IN' - : 'LOGGED_OUT' + ? 'LOGGED_IN' + : 'LOGGED_OUT' if (loginStatus !== computedLoginStatus) { setLoginStatus(computedLoginStatus) @@ -198,7 +194,7 @@ function AuthProvider({ if (sessionExpirationTime.current) { insideCountdownDurationPeriod = dayjs(Date.now()).isAfter( dayjs(sessionExpirationTime.current).subtract( - countdownDurationSeconds, + MODAL_COUNTDOWN_DURATION, 'second' ) ) diff --git a/services/app-web/src/documentHelpers/makeDocumentDateLookupTable.test.ts b/services/app-web/src/documentHelpers/makeDocumentDateLookupTable.test.ts index 1e0b8db184..7378a96975 100644 --- a/services/app-web/src/documentHelpers/makeDocumentDateLookupTable.test.ts +++ b/services/app-web/src/documentHelpers/makeDocumentDateLookupTable.test.ts @@ -58,7 +58,6 @@ describe('makeDocumentDateTable', () => { s3URL: 's3://bucketname/testDateDoc/testDateDoc.pdf', name: 'Test Date Doc', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], }, ], contractDocuments: [ @@ -66,7 +65,6 @@ describe('makeDocumentDateTable', () => { s3URL: 's3://bucketname/key/replaced-contract.pdf', name: 'replaced contract', sha256: 'fakesha1', - documentCategories: ['CONTRACT'], }, ], rateInfos: [ @@ -98,7 +96,6 @@ describe('makeDocumentDateTable', () => { s3URL: 's3://bucketname/key/original-contract.pdf', name: 'original contract', sha256: 'fakesha2', - documentCategories: ['CONTRACT'], }, ], }, diff --git a/services/app-web/src/formHelpers/formatters.ts b/services/app-web/src/formHelpers/formatters.ts index 87d53b582b..a3a42e4395 100644 --- a/services/app-web/src/formHelpers/formatters.ts +++ b/services/app-web/src/formHelpers/formatters.ts @@ -1,7 +1,6 @@ import { dayjs } from '../../../app-web/src/common-code/dateHelpers' import { SubmissionDocument, - DocumentCategoryType, ActuaryContact, } from '../common-code/healthPlanFormDataType' import { FileItemT } from '../components' @@ -72,8 +71,7 @@ const formatFormDateForDomain = (attribute: string): Date | undefined => { } const formatDocumentsForDomain = ( - fileItems: FileItemT[], - documentCategory?: DocumentCategoryType[] // if present will override any existing categories on the file items + fileItems: FileItemT[] ): SubmissionDocument[] => { return fileItems.reduce((cleanedFileItems, fileItem) => { if (fileItem.status === 'UPLOAD_ERROR') { @@ -101,8 +99,6 @@ const formatDocumentsForDomain = ( name: fileItem.name, s3URL: fileItem.s3URL, sha256: fileItem.sha256, - documentCategories: - documentCategory || fileItem.documentCategories, }) } return cleanedFileItems @@ -131,7 +127,6 @@ const formatDocumentsForForm = ({ s3URL: undefined, sha256: doc.sha256, status: 'UPLOAD_ERROR', - documentCategories: doc.documentCategories, } } return { @@ -141,7 +136,6 @@ const formatDocumentsForForm = ({ s3URL: doc.s3URL, sha256: doc.sha256, status: 'UPLOAD_COMPLETE', - documentCategories: doc.documentCategories, } }) || [] ) diff --git a/services/app-web/src/gqlHelpers/mutationWrappersForUserFriendlyErrors.ts b/services/app-web/src/gqlHelpers/mutationWrappersForUserFriendlyErrors.ts index aae2163742..d4cd4c605c 100644 --- a/services/app-web/src/gqlHelpers/mutationWrappersForUserFriendlyErrors.ts +++ b/services/app-web/src/gqlHelpers/mutationWrappersForUserFriendlyErrors.ts @@ -152,6 +152,11 @@ export const submitMutationWrapper = async ( } } +/** + * Manually updating the cache for Q&A mutations because the Q&A page is in a layout route that is not unmounted during the Q&A + * workflow. So, when calling Q&A mutations the Q&A page will not refetch the data. The alternative would be to use + * cache.evict() to force a refetch, but would then cause the loading UI to show. + **/ export const createQuestionWrapper = async ( createQuestion: CreateQuestionMutationFn, input: CreateQuestionInput @@ -246,7 +251,8 @@ export const createResponseWrapper = async ( variables: { input }, update(cache, { data }) { if (data) { - const newResponse = data.createQuestionResponse.response + const newResponse = + data.createQuestionResponse.question.responses[0] const result = cache.readQuery( { diff --git a/services/app-web/src/pages/App/AppRoutes.tsx b/services/app-web/src/pages/App/AppRoutes.tsx index 390ca722c9..2b0658aa7f 100644 --- a/services/app-web/src/pages/App/AppRoutes.tsx +++ b/services/app-web/src/pages/App/AppRoutes.tsx @@ -153,12 +153,6 @@ const CMSUserRoutes = ({ showQuestionResponse: boolean stageName?: string }): React.ReactElement => { - const ldClient = useLDClient() - const showRateReviews = ldClient?.variation( - featureFlags.RATE_REVIEWS_DASHBOARD.flag, - featureFlags.RATE_REVIEWS_DASHBOARD.defaultValue - ) - return ( @@ -179,12 +173,10 @@ const CMSUserRoutes = ({ path={`submissions`} element={} /> - {showRateReviews && ( - } - /> - )} + } + /> }> @@ -303,7 +295,7 @@ export const AppRoutes = ({ void extendSession() } updateSessionExpirationState(false) - // Every thirty seconds, check if the current time is within `countdownDuration` of the session expiration time + // Every thirty seconds, check if the current time is within `countdownDurationSeconds` of the session expiration time checkIfSessionsIsAboutToExpire() } diff --git a/services/app-web/src/pages/CMSDashboard/CMSDashboard.test.tsx b/services/app-web/src/pages/CMSDashboard/CMSDashboard.test.tsx index 5c904e5799..d0e8b5cd96 100644 --- a/services/app-web/src/pages/CMSDashboard/CMSDashboard.test.tsx +++ b/services/app-web/src/pages/CMSDashboard/CMSDashboard.test.tsx @@ -8,15 +8,8 @@ import { mockUnlockedHealthPlanPackage, mockValidCMSUser, } from '../../testHelpers/apolloMocks' -import { - ldUseClientSpy, - renderWithProviders, -} from '../../testHelpers/jestHelpers' +import { renderWithProviders } from '../../testHelpers/jestHelpers' import { CMSDashboard, RateReviewsDashboard, SubmissionsDashboard } from './' -import { - FeatureFlagLDConstant, - FlagValue, -} from '../../common-code/featureFlags' import { Navigate, Route, Routes } from 'react-router-dom' import { RoutesRecord } from '../../constants' @@ -42,7 +35,6 @@ describe('CMSDashboard', () => { jest.clearAllMocks() }) it('rate reviews feature flag - should show rate review tab when expected', () => { - ldUseClientSpy({ 'rate-reviews-dashboard': true }) const screen = renderWithProviders(, { apolloProvider: { mocks: [ @@ -65,257 +57,198 @@ describe('CMSDashboard', () => { ).toBeInTheDocument() }) - // delete this test when flag is removed - it('rate reviews feature flag - should hide rate review tab when expected', () => { - ldUseClientSpy({ 'rate-reviews-dashboard': false }) - const screen = renderWithProviders(, { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - indexHealthPlanPackagesMockSuccess([]), - ], - }, - routerProvider: { route: '/dashboard/rate-reviews' }, + describe(`Tests submissions tab`, () => { + it('should display cms dashboard page', async () => { + const screen = renderWithProviders(, { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + indexHealthPlanPackagesMockSuccess([]), + ], + }, + routerProvider: { route: '/dashboard/submissions' }, + }) + expect(screen.findByTestId('cms-dashboard-page')).not.toBeNull() }) - expect(screen.queryByTestId('tabs')).toBeNull() - expect( - screen.queryByRole('heading', { name: 'Rate reviews' }) - ).toBeNull() - }) - - const flagValueTestParameters: { - flagName: FeatureFlagLDConstant - flagValue: FlagValue - testName: string - }[] = [ - { - flagName: 'rate-reviews-dashboard', - flagValue: false, - testName: 'submissions tab - Rate reviews feature flag off', - }, - { - flagName: 'rate-reviews-dashboard', - flagValue: true, - testName: 'submissions tab - Rate reviews feature flag on', - }, - ] - describe.each(flagValueTestParameters)( - `Tests $testName`, - ({ flagName, flagValue }) => { - ldUseClientSpy({ [flagName]: flagValue }) - - it('should display cms dashboard page', async () => { - const screen = renderWithProviders( - , - { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - indexHealthPlanPackagesMockSuccess([]), - ], - }, - routerProvider: { route: '/dashboard/submissions' }, - } - ) - expect(screen.findByTestId('cms-dashboard-page')).not.toBeNull() + it('displays submissions table excluding any in progress drafts', async () => { + const draft = mockDraftHealthPlanPackage() + const submitted = mockSubmittedHealthPlanPackage() + const unlocked = mockUnlockedHealthPlanPackage() + draft.id = 'test-abc-draft' + submitted.id = 'test-abc-submitted' + unlocked.id = 'test-abc-unlocked' + + const submissions = [draft, submitted, unlocked] + + renderWithProviders(, { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + indexHealthPlanPackagesMockSuccess(submissions), + ], + }, + routerProvider: { route: '/dashboard/submissions' }, }) - it('displays submissions table excluding any in progress drafts', async () => { - const draft = mockDraftHealthPlanPackage() - const submitted = mockSubmittedHealthPlanPackage() - const unlocked = mockUnlockedHealthPlanPackage() - draft.id = 'test-abc-draft' - submitted.id = 'test-abc-submitted' - unlocked.id = 'test-abc-unlocked' - - const submissions = [draft, submitted, unlocked] - - renderWithProviders(, { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - indexHealthPlanPackagesMockSuccess(submissions), - ], - }, - routerProvider: { route: '/dashboard/submissions' }, - }) + await screen.findByRole('heading', { name: 'Submissions' }) + const rows = await screen.findAllByRole('row') + rows.shift() // remove the column header row - await screen.findByRole('heading', { name: 'Submissions' }) - const rows = await screen.findAllByRole('row') - rows.shift() // remove the column header row + // confirm initial draft packages don't display to CMS user + expect(rows).toHaveLength(2) - // confirm initial draft packages don't display to CMS user - expect(rows).toHaveLength(2) - - rows.forEach((row) => { - const submissionLink = within(row).queryByRole('link') - expect(submissionLink).not.toHaveAttribute( - 'href', - `/submissions/${draft.id}` - ) - }) + rows.forEach((row) => { + const submissionLink = within(row).queryByRole('link') + expect(submissionLink).not.toHaveAttribute( + 'href', + `/submissions/${draft.id}` + ) }) + }) - it('displays submission type as expected for current revision that is submitted/resubmitted', async () => { - const submitted = mockSubmittedHealthPlanPackage() - submitted.id = '123-4' - const submissions = [submitted] - renderWithProviders(, { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - indexHealthPlanPackagesMockSuccess(submissions), - ], - }, - routerProvider: { route: '/dashboard/submissions' }, - }) - await screen.findByRole('heading', { name: 'Submissions' }) - const row = await screen.findByTestId(`row-${submitted.id}`) - const submissionType = - within(row).getByTestId('submission-type') - expect(submissionType).toHaveTextContent('Contract action only') + it('displays submission type as expected for current revision that is submitted/resubmitted', async () => { + const submitted = mockSubmittedHealthPlanPackage() + submitted.id = '123-4' + const submissions = [submitted] + renderWithProviders(, { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + indexHealthPlanPackagesMockSuccess(submissions), + ], + }, + routerProvider: { route: '/dashboard/submissions' }, }) + await screen.findByRole('heading', { name: 'Submissions' }) + const row = await screen.findByTestId(`row-${submitted.id}`) + const submissionType = within(row).getByTestId('submission-type') + expect(submissionType).toHaveTextContent('Contract action only') + }) - it('displays each health plan package status tag as expected for current revision that is submitted/resubmitted', async () => { - const unlocked = mockUnlockedHealthPlanPackage() - const submitted = mockSubmittedHealthPlanPackage() - submitted.id = 'test-abc-submitted' - unlocked.id = 'test-abc-unlocked' - - const submissions = [unlocked, submitted] - renderWithProviders(, { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - indexHealthPlanPackagesMockSuccess(submissions), - ], - }, - routerProvider: { route: '/dashboard/submissions' }, - }) - await screen.findByRole('heading', { name: 'Submissions' }) - const unlockedRow = await screen.findByTestId( - `row-${unlocked.id}` - ) - const tag1 = - within(unlockedRow).getByTestId('submission-status') - expect(tag1).toHaveTextContent('Unlocked') - - const submittedRow = await screen.findByTestId( - `row-${submitted.id}` - ) - const tag2 = - within(submittedRow).getByTestId('submission-status') - expect(tag2).toHaveTextContent('Submitted') + it('displays each health plan package status tag as expected for current revision that is submitted/resubmitted', async () => { + const unlocked = mockUnlockedHealthPlanPackage() + const submitted = mockSubmittedHealthPlanPackage() + submitted.id = 'test-abc-submitted' + unlocked.id = 'test-abc-unlocked' + + const submissions = [unlocked, submitted] + renderWithProviders(, { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + indexHealthPlanPackagesMockSuccess(submissions), + ], + }, + routerProvider: { route: '/dashboard/submissions' }, }) + await screen.findByRole('heading', { name: 'Submissions' }) + const unlockedRow = await screen.findByTestId(`row-${unlocked.id}`) + const tag1 = within(unlockedRow).getByTestId('submission-status') + expect(tag1).toHaveTextContent('Unlocked') + + const submittedRow = await screen.findByTestId( + `row-${submitted.id}` + ) + const tag2 = within(submittedRow).getByTestId('submission-status') + expect(tag2).toHaveTextContent('Submitted') + }) - it('displays name, type, programs and last update based on previously submitted revision for UNLOCKED package', async () => { - const mockMN = mockMNState() // this is the state used in apolloMocks - - // Set new data on the unlocked form. This would be a state users update and the CMS user should not see this data. - const unlocked = mockUnlockedHealthPlanPackage( - { - submissionType: 'CONTRACT_ONLY', - updatedAt: new Date('2022-01-15'), - programIDs: [mockMN.programs[2].id], - }, - { updatedAt: new Date('2100-01-22') } - ) - unlocked.id = 'test-state-edit-in-progress-unlocked' - - const submissions = [unlocked] - renderWithProviders(, { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - indexHealthPlanPackagesMockSuccess(submissions), - ], - }, - routerProvider: { route: '/dashboard/submissions' }, - }) - await screen.findByRole('heading', { name: 'Submissions' }) - const unlockedRow = await screen.findByTestId( - `row-${unlocked.id}` - ) - - // Confirm UNLOCKED status - const tag1 = - within(unlockedRow).getByTestId('submission-status') - expect(tag1).toHaveTextContent('Unlocked') - - // Confirm we are using previous submitted revision type - const submissionType = - within(unlockedRow).getByTestId('submission-type') - expect(submissionType).toHaveTextContent( - 'Contract action and rate certification' - ) - - const submissionPrograms = - within(unlockedRow).getAllByTestId('program-tag') - // Confirm we are using previous submitted revision programs - expect(submissionPrograms).toHaveLength(3) - const submissionNameLink = - within(unlockedRow).getByTestId('submission-id') - expect(submissionNameLink).toHaveTextContent('MSC+-PMAP-SNBC') - - // Confirm we are using updated at from the previous submitted revision unlock info - const lastUpdated = within(unlockedRow).getByTestId( - 'submission-last-updated' - ) - expect(lastUpdated).toHaveTextContent('01/22/2100') + it('displays name, type, programs and last update based on previously submitted revision for UNLOCKED package', async () => { + const mockMN = mockMNState() // this is the state used in apolloMocks + + // Set new data on the unlocked form. This would be a state users update and the CMS user should not see this data. + const unlocked = mockUnlockedHealthPlanPackage( + { + submissionType: 'CONTRACT_ONLY', + updatedAt: new Date('2022-01-15'), + programIDs: [mockMN.programs[2].id], + }, + { updatedAt: new Date('2100-01-22') } + ) + unlocked.id = 'test-state-edit-in-progress-unlocked' + + const submissions = [unlocked] + renderWithProviders(, { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + indexHealthPlanPackagesMockSuccess(submissions), + ], + }, + routerProvider: { route: '/dashboard/submissions' }, }) + await screen.findByRole('heading', { name: 'Submissions' }) + const unlockedRow = await screen.findByTestId(`row-${unlocked.id}`) + + // Confirm UNLOCKED status + const tag1 = within(unlockedRow).getByTestId('submission-status') + expect(tag1).toHaveTextContent('Unlocked') + + // Confirm we are using previous submitted revision type + const submissionType = + within(unlockedRow).getByTestId('submission-type') + expect(submissionType).toHaveTextContent( + 'Contract action and rate certification' + ) + + const submissionPrograms = + within(unlockedRow).getAllByTestId('program-tag') + // Confirm we are using previous submitted revision programs + expect(submissionPrograms).toHaveLength(3) + const submissionNameLink = + within(unlockedRow).getByTestId('submission-id') + expect(submissionNameLink).toHaveTextContent('MSC+-PMAP-SNBC') + + // Confirm we are using updated at from the previous submitted revision unlock info + const lastUpdated = within(unlockedRow).getByTestId( + 'submission-last-updated' + ) + expect(lastUpdated).toHaveTextContent('01/22/2100') + }) - it('should display filters on cms dashboard', async () => { - const unlocked = mockUnlockedHealthPlanPackage() - const submitted = mockSubmittedHealthPlanPackage() - submitted.id = 'test-abc-submitted' - unlocked.id = 'test-abc-unlocked' - const screen = renderWithProviders( - , - { - apolloProvider: { - mocks: [ - fetchCurrentUserMock({ - statusCode: 200, - user: mockValidCMSUser(), - }), - indexHealthPlanPackagesMockSuccess([ - submitted, - unlocked, - ]), - ], - }, - routerProvider: { route: '/dashboard/submissions' }, - } - ) + it('should display filters on cms dashboard', async () => { + const unlocked = mockUnlockedHealthPlanPackage() + const submitted = mockSubmittedHealthPlanPackage() + submitted.id = 'test-abc-submitted' + unlocked.id = 'test-abc-unlocked' + const screen = renderWithProviders(, { + apolloProvider: { + mocks: [ + fetchCurrentUserMock({ + statusCode: 200, + user: mockValidCMSUser(), + }), + indexHealthPlanPackagesMockSuccess([ + submitted, + unlocked, + ]), + ], + }, + routerProvider: { route: '/dashboard/submissions' }, + }) - await waitFor(() => { - expect( - screen.queryByTestId('cms-dashboard-page') - ).toBeInTheDocument() - expect( - screen.queryByTestId('accordion') - ).toBeInTheDocument() - }) + await waitFor(() => { + expect( + screen.queryByTestId('cms-dashboard-page') + ).toBeInTheDocument() + expect(screen.queryByTestId('accordion')).toBeInTheDocument() }) - } - ) + }) + }) }) diff --git a/services/app-web/src/pages/CMSDashboard/CMSDashboard.tsx b/services/app-web/src/pages/CMSDashboard/CMSDashboard.tsx index 6a4c89e302..44e2541116 100644 --- a/services/app-web/src/pages/CMSDashboard/CMSDashboard.tsx +++ b/services/app-web/src/pages/CMSDashboard/CMSDashboard.tsx @@ -4,19 +4,12 @@ import React from 'react' import styles from '../StateDashboard/StateDashboard.module.scss' import { Tabs, TabPanel } from '../../components' -import { useLDClient } from 'launchdarkly-react-client-sdk' import { Outlet, useLocation } from 'react-router-dom' import { RoutesRecord } from '../../constants' -import { featureFlags } from '../../common-code/featureFlags' const CMSDashboard = (): React.ReactElement => { const { pathname } = useLocation() const loadOnRateReviews = pathname === RoutesRecord.DASHBOARD_RATES - const ldClient = useLDClient() - const showRateReviews = ldClient?.variation( - featureFlags.RATE_REVIEWS_DASHBOARD.flag, - featureFlags.RATE_REVIEWS_DASHBOARD.defaultValue - ) const TAB_NAMES = { RATES: 'Rate reviews', SUBMISSIONS: 'Submissions', @@ -26,40 +19,32 @@ const CMSDashboard = (): React.ReactElement => {
    -

    - Submissions - {showRateReviews && and rate reviews} -

    +

    Submissions and rate reviews

    - - {showRateReviews ? ( - + - - - + + - - - - - ) : ( - - )} + + + +
    diff --git a/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsDashboard.test.tsx b/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsDashboard.test.tsx index 9a8e5bc784..24b7e0fcfd 100644 --- a/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsDashboard.test.tsx +++ b/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsDashboard.test.tsx @@ -1,4 +1,4 @@ -import { ldUseClientSpy, renderWithProviders } from '../../../testHelpers' +import { renderWithProviders } from '../../../testHelpers' import { RateReviewsDashboard } from './RateReviewsDashboard' import { fetchCurrentUserMock, @@ -10,10 +10,6 @@ import { screen, waitFor } from '@testing-library/react' describe('RateReviewsDashboard', () => { it('renders dashboard with rates correctly', async () => { - ldUseClientSpy({ - 'rate-reviews-dashboard': true, - 'rate-filters': true, - }) renderWithProviders(, { apolloProvider: { mocks: [ diff --git a/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsDashboard.tsx b/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsDashboard.tsx index 6f66c1a5e7..a4d378133b 100644 --- a/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsDashboard.tsx +++ b/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsDashboard.tsx @@ -7,18 +7,11 @@ import { recordJSException } from '../../../otelHelpers/tracingHelper' import { Loading } from '../../../components' import { RateInDashboardType, RateReviewsTable } from './RateReviewsTable' -import { useLDClient } from 'launchdarkly-react-client-sdk' -import { featureFlags } from '../../../common-code/featureFlags' import { ErrorFailedRequestPage } from '../../Errors/ErrorFailedRequestPage' import { RateTypeRecord } from '../../../constants/healthPlanPackages' const RateReviewsDashboard = (): React.ReactElement => { const { loggedInUser } = useAuth() - const ldClient = useLDClient() - const showFilters = ldClient?.variation( - featureFlags.RATE_FILTERS.flag, - featureFlags.RATE_FILTERS.defaultValue - ) const { data, loading, error } = useIndexRatesQuery({ fetchPolicy: 'network-only', @@ -112,10 +105,7 @@ const RateReviewsDashboard = (): React.ReactElement => { } else { return (
    - +
    ) } diff --git a/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsTable.test.tsx b/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsTable.test.tsx index 2c5f8ad61a..a6250c261c 100644 --- a/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsTable.test.tsx +++ b/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsTable.test.tsx @@ -61,7 +61,6 @@ describe('RateReviewsTable', () => { renderWithProviders( , { @@ -132,7 +131,6 @@ describe('RateReviewsTable', () => { renderWithProviders( , { @@ -195,7 +193,6 @@ describe('RateReviewsTable', () => { renderWithProviders( , { diff --git a/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsTable.tsx b/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsTable.tsx index b5c3044d06..eea49b15e6 100644 --- a/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsTable.tsx +++ b/services/app-web/src/pages/CMSDashboard/RateReviewsDashboard/RateReviewsTable.tsx @@ -67,7 +67,6 @@ export type RateInDashboardType = { export type RateTableProps = { tableData: RateInDashboardType[] - showFilters?: boolean caption?: string } @@ -191,7 +190,6 @@ type TableVariantConfig = { export const RateReviewsTable = ({ caption, tableData, - showFilters = false, }: RateTableProps): React.ReactElement => { const lastClickedElement = useRef(null) const filterDateRangeRef = useRef(null) @@ -378,12 +376,9 @@ export const RateReviewsTable = ({ filterLength )} applied` - const submissionCount = !showFilters - ? `${tableData.length} ${pluralize('rate', tableData.length)}` - : `Displaying ${filteredRows.length} of ${tableData.length} ${pluralize( - 'rate review', - tableData.length - )}` + const submissionCount = `Displaying ${filteredRows.length} of ${ + tableData.length + } ${pluralize('rate review', tableData.length)}` const updateFilters = ( column: Column, @@ -440,21 +435,11 @@ export const RateReviewsTable = ({ setTableCaption( {caption ?? tableConfig.tableName} - {showFilters && ( - {`, ${filtersApplied}`} - )} + {`, ${filtersApplied}`} {`, ${submissionCount}.`} ) - }, [ - filtersApplied, - submissionCount, - caption, - showFilters, - tableConfig.tableName, - ]) + }, [filtersApplied, submissionCount, caption, tableConfig.tableName]) useLayoutEffect(() => { // Do not set default column state again @@ -474,97 +459,93 @@ export const RateReviewsTable = ({ <> {tableData.length ? ( <> - {showFilters && ( - - - - updateFilters( - stateColumn, - selectedOptions, - 'state' - ) - } - /> - + + + updateFilters( + stateColumn, + selectedOptions, + 'state' + ) + } + /> + + updateFilters( + rateTypeColumn, + selectedOptions, 'rateType' - )} - name="rateType" - label="Rate Type" - filterOptions={rateTypeOptions} - onChange={(selectedOptions) => - updateFilters( - rateTypeColumn, - selectedOptions, - 'rateType' - ) - } - /> - - - updateRatingPeriodFilter( - [date, undefined], - rateDateStartColumn, - 'ratingPeriodStartFrom' - ), - }} - endDateHint="mm/dd/yyyy" - endDateLabel="To" - endDatePickerProps={{ - id: 'ratingPeriodStartTo', - name: 'ratingPeriodStartTo', - defaultValue: getDateRangeFilterFromUrl( - defaultColumnFilters, - 'rateDateStart' - )[1], - onChange: (date) => - updateRatingPeriodFilter( - [undefined, date], - rateDateStartColumn, - 'ratingPeriodStartTo' - ), - }} + ) + } /> - - )} + + + updateRatingPeriodFilter( + [date, undefined], + rateDateStartColumn, + 'ratingPeriodStartFrom' + ), + }} + endDateHint="mm/dd/yyyy" + endDateLabel="To" + endDatePickerProps={{ + id: 'ratingPeriodStartTo', + name: 'ratingPeriodStartTo', + defaultValue: getDateRangeFilterFromUrl( + defaultColumnFilters, + 'rateDateStart' + )[1], + onChange: (date) => + updateRatingPeriodFilter( + [undefined, date], + rateDateStartColumn, + 'ratingPeriodStartTo' + ), + }} + /> +
    - {showFilters && ( -
    - {filtersApplied} -
    - )} +
    + {filtersApplied} +
    {submissionCount}
    diff --git a/services/app-web/src/pages/QuestionResponse/QuestionResponse.test.tsx b/services/app-web/src/pages/QuestionResponse/QuestionResponse.test.tsx index 1ae7e99579..aa3ebb5de6 100644 --- a/services/app-web/src/pages/QuestionResponse/QuestionResponse.test.tsx +++ b/services/app-web/src/pages/QuestionResponse/QuestionResponse.test.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { screen, waitFor, within } from '@testing-library/react' import { Route, Routes } from 'react-router-dom' import { SubmissionSideNav } from '../SubmissionSideNav' @@ -302,7 +301,7 @@ describe('QuestionResponse', () => { ).toBeInTheDocument() }) - const qaSections = await screen.queryByTestId(/.*-qa-section/) + const qaSections = screen.queryByTestId(/.*-qa-section/) //Expect there to be no QA sections expect(qaSections).toBeNull() diff --git a/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx b/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx index d6e3303dbf..f419296ee8 100644 --- a/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx +++ b/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.tsx @@ -143,7 +143,6 @@ export const UploadQuestions = () => { id="questions-upload" name="questions-upload" label="Upload questions" - renderMode="list" aria-required error={showFileUploadError ? fileUploadError : ''} hint={ diff --git a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.test.tsx b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.test.tsx index f1e57278ef..de8aab8017 100644 --- a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.test.tsx +++ b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.test.tsx @@ -67,7 +67,7 @@ describe('UploadResponse', () => { level: 2, }) // Expect text to display correct division from url parameters. - await screen.queryByText(`Questions from ${division.toUpperCase()}`) + screen.queryByText(`Questions from ${division.toUpperCase()}`) // Expect file upload input on page expect(await screen.findByTestId('file-input')).toBeInTheDocument() expect(screen.getByLabelText('Upload response')).toBeInTheDocument() @@ -322,7 +322,7 @@ describe('UploadResponse', () => { await screen.findByTestId('error-alert') expect( - await screen.getByText("We're having trouble loading this page.") + screen.getByText("We're having trouble loading this page.") ).toBeDefined() }) }) diff --git a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx index df8d0735ff..36419551c2 100644 --- a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx +++ b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.tsx @@ -155,7 +155,6 @@ export const UploadResponse = () => { id="response-upload" name="response-upload" label="Upload response" - renderMode="list" aria-required error={showFileUploadError ? fileUploadError : ''} hint={ diff --git a/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.test.tsx b/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.test.tsx index 208aa2e6c5..34d8bec582 100644 --- a/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.test.tsx +++ b/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.test.tsx @@ -63,7 +63,7 @@ describe('EmailSettings', () => { // Count the table rows const tableRows = await within(table).findAllByRole('row') - expect(tableRows).toHaveLength(5) + expect(tableRows).toHaveLength(6) // Check the table headers expect( diff --git a/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.tsx b/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.tsx index 15bd148f7e..10b5743864 100644 --- a/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.tsx +++ b/services/app-web/src/pages/Settings/EmailSettingsTables/EmailSettingsTables.tsx @@ -64,8 +64,16 @@ const EmailsGeneralTable = ({ config }: { config: EmailConfiguration }) => { None - {formatEmails(config?.dmcpEmails)} - DMCP division emails + {formatEmails(config?.dmcpReviewEmails)} + DMCP division emails used for reviews + + All submissions; excluding CHIP programs and PR + state + + + + {formatEmails(config?.dmcpSubmissionEmails)} + DMCP division emails used for submissions All submissions; excluding CHIP programs and PR state diff --git a/services/app-web/src/pages/Settings/Settings.test.tsx b/services/app-web/src/pages/Settings/Settings.test.tsx index 9564b77a5a..b666de47f0 100644 --- a/services/app-web/src/pages/Settings/Settings.test.tsx +++ b/services/app-web/src/pages/Settings/Settings.test.tsx @@ -50,16 +50,15 @@ describe('Settings', () => { expect(tableAutomated).toBeInTheDocument() const tableRows = await within(tableAutomated).findAllByRole('row') - expect(tableRows).toHaveLength(5) + expect(tableRows).toHaveLength(6) // Check analysts table const tableAnalysts = await screen.findByRole('table', { name: 'Analyst emails', }) expect(tableAnalysts).toBeInTheDocument() - const tableRowsAnalysts = await within(tableAnalysts).findAllByRole( - 'row' - ) + const tableRowsAnalysts = + await within(tableAnalysts).findAllByRole('row') expect(tableRowsAnalysts).toHaveLength(2) // Check support table diff --git a/services/app-web/src/pages/StateSubmission/Contacts/Contacts.tsx b/services/app-web/src/pages/StateSubmission/Contacts/Contacts.tsx index da7125d903..5a4f9e93b2 100644 --- a/services/app-web/src/pages/StateSubmission/Contacts/Contacts.tsx +++ b/services/app-web/src/pages/StateSubmission/Contacts/Contacts.tsx @@ -36,6 +36,7 @@ import { PageActions } from '../PageActions' import type { HealthPlanFormPageProps } from '../StateSubmissionForm' import { ActuaryContactFields } from './ActuaryContactFields' import { RoutesRecord } from '../../../constants' +import { SectionCard } from '../../../components' export interface ContactsFormValues { stateContacts: StateContact[] @@ -289,295 +290,311 @@ export const Contacts = ({ aria-describedby="form-guidance" onSubmit={handleSubmit} > -
    -

    State contacts

    -

    - Enter contact information for the state - personnel you'd like to receive all CMS - communication about this submission. -

    - State contacts - - {shouldValidate && ( - - )} - - - {({ remove, push }: FieldArrayRenderProps) => ( -
    - {values.stateContacts.length > 0 && - values.stateContacts.map( - (_stateContact, index) => ( -
    -
    +
    +

    State contacts

    +

    + Enter contact information for the state + personnel you'd like to receive all CMS + communication about this submission. +

    + + State contacts + + + {shouldValidate && ( + + )} + + + {({ + remove, + push, + }: FieldArrayRenderProps) => ( +
    + {values.stateContacts.length > 0 && + values.stateContacts.map( + (_stateContact, index) => ( +
    - - Required - - - - - - - - {index > 0 && ( - - )} -
    -
    - ) - )} - - -
    - )} -
    -
    - - {includeActuaryContacts && ( - <> -
    -

    Additional Actuary Contacts

    - -

    - Provide contact information for any - additional actuaries who worked directly - on this submission. -

    - - Actuary contacts - - - - {({ - remove, - push, - }: FieldArrayRenderProps) => ( -
    - {values.addtlActuaryContacts - .length > 0 && - values.addtlActuaryContacts.map( - ( - _actuaryContact, - index - ) => ( -
    - + - -
    - ) - )} - - + )} +
    +
    + ) + )} + + + + )} + + + + + {includeActuaryContacts && ( + <> + +
    +

    Additional Actuary Contacts

    + +

    + Provide contact information for any + additional actuaries who worked + directly on this submission. +

    + + Actuary contacts + + + + {({ + remove, + push, + }: FieldArrayRenderProps) => ( +
    - Add actuary contact - -
    - )} -
    -
    - -
    -

    Actuaries' communication preference

    - - - Actuarial communication preference - - -
    0 && + values.addtlActuaryContacts.map( + ( + _actuaryContact, + index + ) => ( +
    + + +
    + ) + )} + + + + )} + +
    + + + +
    +

    + Actuaries' communication preference +

    + + + Actuarial communication preference + + - - Required - - {showFieldErrors(`True`) && ( - + Required + + {showFieldErrors(`True`) && ( + + )} + - )} - - -
    -
    -
    + + + + +
    )} diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx index e8e5ac9008..c12422ab70 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx @@ -838,7 +838,6 @@ describe('ContractDetails', () => { name: 'aasdf3423af', sha256: 'fakesha', s3URL: 's3://bucketname/key/fileName', - documentCategories: ['CONTRACT' as const], }, ], } @@ -1014,13 +1013,11 @@ describe('ContractDetails', () => { { name: 'testFile.doc', s3URL: expect.any(String), - documentCategories: ['CONTRACT'], sha256: 'da7d22ce886b5ab262cd7ab28901212a027630a5edf8e88c8488087b03ffd833', // pragma: allowlist secret }, { name: 'testFile.pdf', s3URL: expect.any(String), - documentCategories: ['CONTRACT'], sha256: '6d50607f29187d5b185ffd9d46bc5ef75ce7abb53318690c73e55b6623e25ad5', // pragma: allowlist secret }, ], diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx index 262dcf2606..43cbd8a587 100644 --- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx +++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.tsx @@ -156,10 +156,10 @@ export const ContractDetails = ({ showFileUploadError && hasLoadingFiles ? 'You must wait for all documents to finish uploading before continuing' : showFileUploadError && fileItems.length === 0 - ? ' You must upload at least one document' - : showFileUploadError && !hasValidFiles - ? ' You must remove all documents with error messages before continuing' - : undefined + ? ' You must upload at least one document' + : showFileUploadError && !hasValidFiles + ? ' You must remove all documents with error messages before continuing' + : undefined const documentsErrorKey = fileItems.length === 0 ? 'documents' : '#file-items-list' @@ -189,7 +189,6 @@ export const ContractDetails = ({ s3URL: undefined, status: 'UPLOAD_ERROR', sha256: doc.sha256, - documentCategories: doc.documentCategories, } } return { @@ -199,7 +198,6 @@ export const ContractDetails = ({ s3URL: doc.s3URL, status: 'UPLOAD_COMPLETE', sha256: doc.sha256, - documentCategories: doc.documentCategories, } }) @@ -395,7 +393,6 @@ export const ContractDetails = ({ name: fileItem.name, s3URL: fileItem.s3URL, sha256: fileItem.sha256, - documentCategories: ['CONTRACT'], }) } return formDataDocuments @@ -558,7 +555,6 @@ export const ContractDetails = ({ id="documents" name="documents" label="Upload contract" - renderMode="list" aria-required error={documentsErrorMessage} hint={ diff --git a/services/app-web/src/pages/StateSubmission/Documents/Documents.test.tsx b/services/app-web/src/pages/StateSubmission/Documents/Documents.test.tsx index 52c9b24ca6..ef3de758c8 100644 --- a/services/app-web/src/pages/StateSubmission/Documents/Documents.test.tsx +++ b/services/app-web/src/pages/StateSubmission/Documents/Documents.test.tsx @@ -9,7 +9,6 @@ import { TEST_VIDEO_FILE, TEST_PNG_FILE, dragAndDrop, - ldUseClientSpy, } from '../../../testHelpers/jestHelpers' import { fetchCurrentUserMock, @@ -43,9 +42,7 @@ describe('Documents', () => { screen.getByRole('button', { name: 'Continue' }) ).not.toHaveAttribute('aria-disabled') }) - expect( - screen.getByText('You have not uploaded any files') - ).toBeInTheDocument() + expect(screen.getByText('0 files added')).toBeInTheDocument() }) it('accepts a new document', async () => { @@ -236,11 +233,15 @@ describe('Documents', () => { await userEvent.upload(input, [TEST_XLS_FILE]) + const fileList = screen.getAllByRole('list')[0] + await waitFor(() => { expect( screen.queryAllByText('Duplicate file, please remove') ).toHaveLength(0) - expect(screen.queryAllByRole('row')).toHaveLength(2) + expect(within(fileList).getAllByRole('listitem')).toHaveLength( + 1 + ) }) // note: userEvent.upload does not re-trigger input event when selected files are the same as before, this is why we upload nothing in between await userEvent.upload(input, []) @@ -250,7 +251,9 @@ describe('Documents', () => { expect( screen.queryAllByText('Duplicate file, please remove') ).toHaveLength(1) - expect(screen.queryAllByRole('row')).toHaveLength(3) + expect(within(fileList).getAllByRole('listitem')).toHaveLength( + 2 + ) }) await userEvent.upload(input, []) @@ -260,7 +263,9 @@ describe('Documents', () => { expect( screen.queryAllByText('Duplicate file, please remove') ).toHaveLength(2) - expect(screen.queryAllByRole('row')).toHaveLength(4) + expect(within(fileList).getAllByRole('listitem')).toHaveLength( + 3 + ) }) }) @@ -306,69 +311,6 @@ describe('Documents', () => { screen.queryByText('Duplicate file, please remove') ).toBeNull() }) - - it('not shown in file items list for document categories on initial load in table view, only shown after validation', async () => { - const mockUpdateDraftFn = jest.fn() - renderWithProviders( - , - { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - } - ) - - const input = screen.getByLabelText( - 'Upload contract-supporting documents' - ) - await userEvent.upload(input, [TEST_PDF_FILE]) - await userEvent.upload(input, [TEST_DOC_FILE]) - - await waitFor(() => { - expect( - screen.queryAllByText( - 'Must select at least one category checkbox' - ) - ).toHaveLength(0) - }) - - // check a category for the second row - const rows = screen.getAllByRole('row') - expect(rows).toHaveLength(3) - await userEvent.click( - within(rows[1]).getByRole('checkbox', { - name: 'contract-supporting', - }) - ) - - await waitFor(() => { - expect( - screen.queryAllByText( - 'Must select at least one category checkbox' - ) - ).toHaveLength(0) - }) - - // click continue and enter validation state - await userEvent.click( - screen.getByRole('button', { name: 'Continue' }) - ) - - await waitFor(() => { - expect( - screen.queryAllByText( - 'Must select at least one category checkbox' - ) - ).toHaveLength(1) - }) - }) }) describe('error summary at top of page', () => { @@ -400,9 +342,6 @@ describe('Documents', () => { await waitFor(() => { // error summary messages don't appear on load - expect( - screen.queryAllByText('You must select a document category') - ).toHaveLength(0) expect( screen.queryAllByText('You must remove duplicate files') ).toHaveLength(0) @@ -414,9 +353,6 @@ describe('Documents', () => { ) await waitFor(() => { - expect( - screen.queryAllByText('You must select a document category') - ).toHaveLength(2) expect( screen.queryAllByText('You must remove duplicate files') ).toHaveLength(1) @@ -639,14 +575,14 @@ describe('Documents', () => { // upload one file dragAndDrop(targetEl, [TEST_PDF_FILE]) - const imageElFile1 = screen.getByTestId('file-input-loading-image') + const imageElFile1 = screen.getByTestId('file-input-preview-image') expect(imageElFile1).toHaveClass('is-loading') // upload second file dragAndDrop(targetEl, [TEST_DOC_FILE]) const imageElFile2 = screen.getAllByTestId( - 'file-input-loading-image' + 'file-input-preview-image' )[1] expect(imageElFile2).toHaveClass('is-loading') @@ -942,338 +878,156 @@ describe('Documents', () => { }) }) - describe('Document categories checkbox', () => { - it('present on contract and rates submission', async () => { - const mockUpdateDraftFn = jest.fn() - renderWithProviders( - , + it('checkboxes not present on contract and rates submission', async () => { + const mockDraftSubmission = { + ...mockDraft(), + submissionType: 'CONTRACT_AND_RATES' as const, + documents: [ { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - } - ) - await waitFor(() => { - expect( - screen.getAllByText('Contract-supporting').length - ).toBeGreaterThanOrEqual(1) - expect( - screen.getAllByText('Rate-supporting').length - ).toBeGreaterThanOrEqual(1) - }) - }) - - it('present on contract and rates submission in categories error state', async () => { - const mockUpdateDraftFn = jest.fn() - renderWithProviders( - , - { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - } - ) - - const input = screen.getByLabelText( - 'Upload contract-supporting documents' - ) - - await userEvent.upload(input, [TEST_DOC_FILE]) - - // no errors before validation but checkboxes present - await waitFor(() => { - expect( - screen.queryAllByText('You must select a document category') - ).toHaveLength(0) - - expect( - screen.queryAllByText('Contract-supporting') - ).toHaveLength(1) - expect(screen.queryAllByText('Rate-supporting')).toHaveLength(1) - }) - - await userEvent.click( - screen.getByRole('button', { name: 'Continue' }) - ) + s3URL: 's3://bucketname/key/supporting-documents', + name: 'supporting documents', + sha256: 'fakesha', + }, + ], + } + const mockUpdateDraftFn = jest.fn() + renderWithProviders( + , + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } + ) - // errors after validation and checkboxes still present - await waitFor(() => { - expect( - screen.queryAllByText('You must select a document category') - ).toHaveLength(1) - expect( - screen.queryAllByText('Contract-supporting') - ).toHaveLength(1) - expect(screen.queryAllByText('Rate-supporting')).toHaveLength(1) - }) + await waitFor(() => { + expect(screen.getByText('supporting documents')).toBeInTheDocument() }) - it('not present on contract and rates submission in duplicate name error rows', async () => { - const mockUpdateDraftFn = jest.fn() - renderWithProviders( - , - { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - } - ) - - const input = screen.getByLabelText( - 'Upload contract-supporting documents' - ) - - await userEvent.upload(input, [TEST_DOC_FILE]) - await userEvent.upload(input, [TEST_PDF_FILE]) - await userEvent.upload(input, [TEST_DOC_FILE]) - - const rows = screen.getAllByRole('row') - await waitFor(() => expect(rows).toHaveLength(4)) - - // check a category for the second row - await userEvent.click( - within(rows[2]).getByRole('checkbox', { - name: 'contract-supporting', - }) - ) - - // confirm checkboxes are present or hidden when expected - const missingDocumentCategoriesRow = rows[1] - const validAndHasCategoriesRow = rows[2] - const duplicateNameRow = rows[3] - - expect( - within(missingDocumentCategoriesRow).getAllByRole('checkbox') - ).toHaveLength(2) - - expect( - within(validAndHasCategoriesRow).getAllByRole('checkbox') - ).toHaveLength(2) - expect(within(duplicateNameRow).queryByRole('checkbox')).toBeNull() - - // click continue and enter validation state - await userEvent.click( - screen.getByRole('button', { name: 'Continue' }) - ) - await waitFor(() => { - expect( - screen.queryAllByText('You must select a document category') - ).toHaveLength(1) - }) - - // checkboxes presence is unchanged - expect( - within(missingDocumentCategoriesRow).getAllByRole('checkbox') - ).toHaveLength(2) + expect(screen.queryByText('Contract-supporting')).toBeNull() + expect(screen.queryByText('Rate-supporting')).toBeNull() - expect( - within(validAndHasCategoriesRow).getAllByRole('checkbox') - ).toHaveLength(2) - expect(within(duplicateNameRow).queryByRole('checkbox')).toBeNull() - }) + jest.clearAllMocks() }) - describe('SUPPORTING_DOCS_BY_RATE feature flag on', () => { - it('checkboxes not present on contract and rates submission when SUPPORTING_DOCS_BY_RATE is on', async () => { - ldUseClientSpy({ 'supporting-docs-by-rate': true }) - const mockDraftSubmission = { - ...mockDraft(), - submissionType: 'CONTRACT_AND_RATES' as const, - documents: [ - { - s3URL: 's3://bucketname/key/supporting-documents', - name: 'supporting documents', - sha256: 'fakesha', - documentCategories: ['RATES_RELATED' as const], - }, - ], - } - const mockUpdateDraftFn = jest.fn() - renderWithProviders( - , - { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - } - ) - - await waitFor(() => { - expect( - screen.getByText('supporting documents') - ).toBeInTheDocument() - }) + it('documents are always categorized as CONTRACT_RELATED', async () => { + const mockUpdateDraftFn = jest.fn() - expect(screen.queryByText('Contract-supporting')).toBeNull() - expect(screen.queryByText('Rate-supporting')).toBeNull() + renderWithProviders( + , + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } + ) - jest.clearAllMocks() + const continueButton = screen.getByRole('button', { + name: 'Continue', }) + const input = screen.getByLabelText( + 'Upload contract-supporting documents' + ) - it('documents are always categorized as CONTRACT_RELATED when SUPPORTING_DOCS_BY_RATE is on', async () => { - ldUseClientSpy({ 'supporting-docs-by-rate': true }) - const mockUpdateDraftFn = jest.fn() + expect(input).toBeInTheDocument() - renderWithProviders( - , - { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - } - ) + await userEvent.upload(input, [TEST_DOC_FILE]) - const continueButton = screen.getByRole('button', { - name: 'Continue', - }) - const input = screen.getByLabelText( - 'Upload contract-supporting documents' - ) + await waitFor(() => { + expect(screen.getByText(TEST_DOC_FILE.name)).toBeInTheDocument() + expect(continueButton).toBeInTheDocument() + continueButton.click() + expect(mockUpdateDraftFn).toHaveBeenCalled() + }) - expect(input).toBeInTheDocument() + const updatedDraft = mockUpdateDraftFn.mock.calls[0][0] + + expect(updatedDraft.documents).toHaveLength(1) + expect(updatedDraft.documents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'testFile.doc', + s3URL: expect.anything(), + sha256: expect.anything(), + }), + ]) + ) - await userEvent.upload(input, [TEST_DOC_FILE]) + jest.clearAllMocks() + }) - await waitFor(() => { - expect(screen.getByText(TEST_DOC_FILE.name)).toBeInTheDocument() - expect(continueButton).toBeInTheDocument() - continueButton.click() - expect(mockUpdateDraftFn).toHaveBeenCalled() - }) + it('existing documents categories are not overwritten', async () => { + const mockUpdateDraftFn = jest.fn() - const updatedDraft = mockUpdateDraftFn.mock.calls[0][0] - - expect(updatedDraft.documents).toHaveLength(1) - expect(updatedDraft.documents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: 'testFile.doc', - s3URL: expect.anything(), - sha256: expect.anything(), - documentCategories: ['CONTRACT_RELATED'], - }), - ]) - ) + renderWithProviders( + , + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } + ) - jest.clearAllMocks() + const continueButton = screen.getByRole('button', { + name: 'Continue', }) + const input = screen.getByLabelText( + 'Upload contract-supporting documents' + ) + expect(input).toBeInTheDocument() + await userEvent.upload(input, [TEST_DOC_FILE]) - it('existing documents categories are not overwritten when SUPPORTING_DOCS_BY_RATE is on', async () => { - ldUseClientSpy({ 'supporting-docs-by-rate': true }) - const mockUpdateDraftFn = jest.fn() - - renderWithProviders( - , - { - apolloProvider: { - mocks: [fetchCurrentUserMock({ statusCode: 200 })], - }, - } - ) - - const continueButton = screen.getByRole('button', { - name: 'Continue', - }) - const input = screen.getByLabelText( - 'Upload contract-supporting documents' - ) - expect(input).toBeInTheDocument() - await userEvent.upload(input, [TEST_DOC_FILE]) - - await waitFor(() => { - expect( - screen.getByText('supporting documents') - ).toBeInTheDocument() - expect(screen.getByText(TEST_DOC_FILE.name)).toBeInTheDocument() - expect(continueButton).toBeInTheDocument() - continueButton.click() - expect(mockUpdateDraftFn).toHaveBeenCalled() - }) + await waitFor(() => { + expect(screen.getByText('supporting documents')).toBeInTheDocument() + expect(screen.getByText(TEST_DOC_FILE.name)).toBeInTheDocument() + expect(continueButton).toBeInTheDocument() + continueButton.click() + expect(mockUpdateDraftFn).toHaveBeenCalled() + }) - const updatedDraft = mockUpdateDraftFn.mock.calls[0][0] - - expect(updatedDraft.documents).toHaveLength(2) - expect(updatedDraft.documents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: 'supporting documents', - s3URL: expect.anything(), - sha256: expect.anything(), - documentCategories: ['RATES_RELATED'], - }), - expect.objectContaining({ - name: 'testFile.doc', - s3URL: expect.anything(), - sha256: expect.anything(), - documentCategories: ['CONTRACT_RELATED'], - }), - ]) - ) + const updatedDraft = mockUpdateDraftFn.mock.calls[0][0] + + expect(updatedDraft.documents).toHaveLength(2) + expect(updatedDraft.documents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'supporting documents', + s3URL: expect.anything(), + sha256: expect.anything(), + }), + expect.objectContaining({ + name: 'testFile.doc', + s3URL: expect.anything(), + sha256: expect.anything(), + }), + ]) + ) - jest.clearAllMocks() - }) + jest.clearAllMocks() }) }) diff --git a/services/app-web/src/pages/StateSubmission/Documents/Documents.tsx b/services/app-web/src/pages/StateSubmission/Documents/Documents.tsx index 5aa24e542b..8d6da05a55 100644 --- a/services/app-web/src/pages/StateSubmission/Documents/Documents.tsx +++ b/services/app-web/src/pages/StateSubmission/Documents/Documents.tsx @@ -17,8 +17,6 @@ import { PageActions } from '../PageActions' import classNames from 'classnames' import { ErrorSummary } from '../../../components/Form' import type { HealthPlanFormPageProps } from '../StateSubmissionForm' -import { useLDClient } from 'launchdarkly-react-client-sdk' -import { featureFlags } from '../../../common-code/featureFlags' import { RoutesRecord } from '../../../constants' export const Documents = ({ @@ -27,14 +25,7 @@ export const Documents = ({ updateDraft, }: HealthPlanFormPageProps): React.ReactElement => { const [shouldValidate, setShouldValidate] = useState(false) - const isContractOnly = draftSubmission.submissionType === 'CONTRACT_ONLY' const navigate = useNavigate() - const ldClient = useLDClient() - - const supportingDocsByRate = ldClient?.variation( - featureFlags.SUPPORTING_DOCS_BY_RATE.flag, - featureFlags.SUPPORTING_DOCS_BY_RATE.defaultValue - ) // Documents state management const { deleteFile, uploadFile, scanFile, getKey, getS3URL } = useS3() @@ -44,16 +35,6 @@ export const Documents = ({ ) const [isSubmitting, setIsSubmitting] = useState(false) // mock same behavior as formik isSubmitting - const hasMissingCategories = - /* fileItems must have some document category. a contract-only submission - must have "CONTRACT_RELATED" as the document category. */ - fileItems.length > 0 && - (fileItems.some((docs) => docs.documentCategories.length === 0) || - (isContractOnly && - fileItems.some( - (docs) => - !docs.documentCategories.includes('CONTRACT_RELATED') - ))) const hasLoadingFiles = fileItems.some((item) => item.status === 'PENDING') || fileItems.some((item) => item.status === 'SCANNING') @@ -71,8 +52,6 @@ export const Documents = ({ } else if (item.status === 'UPLOAD_ERROR') { errorsObject[key] = 'You must remove or retry files that failed to upload' - } else if (item.documentCategories.length === 0) { - errorsObject[key] = 'You must select a document category' } }) return errorsObject @@ -82,10 +61,9 @@ export const Documents = ({ const errorSummary = showFileUploadError && hasLoadingFiles ? 'You must wait for all documents to finish uploading before continuing' - : (showFileUploadError && !hasValidFiles) || - (shouldValidate && hasMissingCategories) - ? 'You must remove all documents with error messages before continuing' - : undefined + : showFileUploadError && !hasValidFiles + ? 'You must remove all documents with error messages before continuing' + : undefined // Error summary state management const errorSummaryHeadingRef = React.useRef(null) @@ -113,7 +91,6 @@ export const Documents = ({ s3URL: undefined, sha256: doc.sha256, status: 'UPLOAD_ERROR', - documentCategories: doc.documentCategories, } } return { @@ -123,7 +100,6 @@ export const Documents = ({ s3URL: doc.s3URL, sha256: doc.sha256, status: 'UPLOAD_COMPLETE', - documentCategories: doc.documentCategories, } }) @@ -138,15 +114,6 @@ export const Documents = ({ }: { fileItems: FileItemT[] }) => { - // When supportingDocsByRate flag is on, all documents on the supporting documents page are CONTRACT_RELATED. - // If the files documentCategories contains a category we skip as to not overwrite existing documents. - if (supportingDocsByRate) { - fileItems = fileItems.map((file) => - file.documentCategories.length - ? file - : { ...file, documentCategories: ['CONTRACT_RELATED'] } - ) - } setFileItems(fileItems) } @@ -210,7 +177,7 @@ export const Documents = ({ // Currently documents validation happens (outside of the yup schema, which only handles the formik form data) // if there are any errors present in the documents list and we are in a validation state (relevant for Save as Draft) force user to clear validations to continue if (shouldValidateDocuments) { - if (!hasValidFiles || hasMissingCategories) { + if (!hasValidFiles) { setShouldValidate(true) setFocusErrorSummaryHeading(true) return @@ -244,8 +211,6 @@ export const Documents = ({ name: fileItem.name, s3URL: fileItem.s3URL, sha256: fileItem.sha256, - documentCategories: - fileItem.documentCategories || [], }) } return formDataDocuments @@ -305,7 +270,6 @@ export const Documents = ({ id="documents" name="documents" label="Upload contract-supporting documents" - renderMode={supportingDocsByRate ? 'list' : 'table'} hint={ <> { describe('generic page behavior', () => { @@ -10,7 +11,12 @@ describe('PageActions', () => { + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(3) @@ -35,7 +41,12 @@ describe('PageActions', () => { continueOnClick={jest.fn()} saveAsDraftOnClick={jest.fn()} backOnClick={backAction} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click(screen.getByRole('button', { name: 'Back' })) @@ -48,7 +59,12 @@ describe('PageActions', () => { continueOnClick={jest.fn()} saveAsDraftOnClick={saveAction} backOnClick={jest.fn()} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( @@ -63,7 +79,12 @@ describe('PageActions', () => { continueOnClick={continueAction} saveAsDraftOnClick={jest.fn()} backOnClick={jest.fn()} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( @@ -79,7 +100,12 @@ describe('PageActions', () => { saveAsDraftOnClick={jest.fn()} backOnClick={jest.fn()} disableContinue - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( @@ -97,7 +123,12 @@ describe('PageActions', () => { continueOnClick={jest.fn()} saveAsDraftOnClick={jest.fn()} backOnClick={jest.fn()} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(2) @@ -119,7 +150,12 @@ describe('PageActions', () => { continueOnClick={jest.fn()} saveAsDraftOnClick={jest.fn()} backOnClick={jest.fn()} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(3) @@ -145,7 +181,12 @@ describe('PageActions', () => { continueOnClick={jest.fn()} saveAsDraftOnClick={jest.fn()} backOnClick={backAction} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( @@ -162,7 +203,12 @@ describe('PageActions', () => { continueOnClick={continueAction} saveAsDraftOnClick={jest.fn()} backOnClick={jest.fn()} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( @@ -180,7 +226,12 @@ describe('PageActions', () => { saveAsDraftOnClick={jest.fn()} backOnClick={jest.fn()} disableContinue - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( @@ -196,7 +247,12 @@ describe('PageActions', () => { continueOnClick={jest.fn()} saveAsDraftOnClick={jest.fn()} backOnClick={jest.fn()} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(3) @@ -222,7 +278,12 @@ describe('PageActions', () => { continueOnClick={continueAction} saveAsDraftOnClick={jest.fn()} backOnClick={jest.fn()} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( @@ -238,7 +299,12 @@ describe('PageActions', () => { continueOnClick={jest.fn()} saveAsDraftOnClick={saveAsDraftOnClick} backOnClick={jest.fn()} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( @@ -254,7 +320,12 @@ describe('PageActions', () => { continueOnClick={jest.fn()} saveAsDraftOnClick={jest.fn()} backOnClick={backOnClick} - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( @@ -276,7 +347,12 @@ describe('PageActions', () => { saveAsDraftOnClick={jest.fn()} backOnClick={jest.fn()} actionInProgress - /> + />, + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + } ) await userEvent.click( diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx index 2d1e9ca4fe..240132f246 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.test.tsx @@ -28,7 +28,6 @@ import { TEST_PNG_FILE, dragAndDrop, updateDateRange, - ldUseClientSpy, } from '../../../testHelpers' import { RateDetails } from './RateDetails' import { @@ -92,8 +91,6 @@ describe('RateDetails', () => { }) it('displays correct form guidance', async () => { - ldUseClientSpy({ 'supporting-docs-by-rate': true }) - renderWithProviders( { screen.queryByText(/All fields are required/) ).not.toBeInTheDocument() const requiredLabels = await screen.findAllByText('Required') - expect(requiredLabels).toHaveLength(6) + expect(requiredLabels).toHaveLength(7) const optionalLabels = screen.queryAllByText('Optional') expect(optionalLabels).toHaveLength(1) }) @@ -151,10 +148,12 @@ describe('RateDetails', () => { name: 'Certification of rate ranges of capitation rates per rate cell', }) ).not.toBeChecked() - expect(screen.getByTestId('file-input')).toBeInTheDocument() + expect(screen.getAllByTestId('file-input')).toHaveLength(2) + expect(screen.getAllByTestId('file-input')[0]).toBeInTheDocument() + expect(screen.getAllByTestId('file-input')[1]).toBeInTheDocument() expect( within( - screen.getByTestId('file-input-preview-list') + screen.getAllByTestId('file-input-preview-list')[0] ).queryAllByRole('listitem') ).toHaveLength(0) @@ -252,7 +251,6 @@ describe('RateDetails', () => { }) it('progressively disclose new rate form fields as expected', async () => { - ldUseClientSpy({ 'packages-with-shared-rates': true }) renderWithProviders( { ) await waitFor(() => { - expect(screen.getByTestId('file-input')).toBeInTheDocument() - expect(screen.getByTestId('file-input')).toHaveClass( - 'usa-file-input' - ) + const textInputs = screen.getAllByTestId('file-input') + expect(textInputs).toHaveLength(2) + expect(textInputs[0]).toBeInTheDocument() + expect(textInputs[0]).toHaveClass('usa-file-input') expect( screen.getByRole('button', { name: 'Continue' }) ).not.toHaveAttribute('aria-disabled') expect( within( - screen.getByTestId('file-input-preview-list') + screen.getAllByTestId('file-input-preview-list')[0] ).queryAllByRole('listitem') ).toHaveLength(0) }) @@ -521,8 +519,6 @@ describe('RateDetails', () => { }) it('accepts multiple pdf, word, excel documents for supporting documents', async () => { - ldUseClientSpy({ 'supporting-docs-by-rate': true }) - renderWithProviders( { await clickAddNewRate(screen) await waitFor(() => { - const rateInfoContainers = screen.getAllByRole('group', { - name: /certification/, - }) + const rateInfoContainers = screen.getAllByTestId( + 'rate-certification-form' + ) expect(rateInfoContainers).toHaveLength(2) }) - const rateInfo2 = screen.getAllByRole('group', { - name: /certification/, - })[1] + const rateInfo2 = screen.getAllByTestId( + 'rate-certification-form' + )[1] const continueButton = screen.getByRole('button', { name: 'Continue', @@ -733,7 +729,6 @@ describe('RateDetails', () => { describe('handles rates across submissions', () => { it('correctly checks shared rate certification radios and selects shared packages', async () => { - ldUseClientSpy({ 'packages-with-shared-rates': true }) //Spy on useStatePrograms hook to get up-to-date state programs jest.spyOn(useStatePrograms, 'useStatePrograms').mockReturnValue( mockMNState().programs @@ -965,7 +960,6 @@ describe('RateDetails', () => { }, 10000) it('cannot continue when shared rate radio is unchecked', async () => { - ldUseClientSpy({ 'packages-with-shared-rates': true }) //Spy on useStatePrograms hook to get up-to-date state programs jest.spyOn(useStatePrograms, 'useStatePrograms').mockReturnValue( mockMNState().programs @@ -1095,7 +1089,6 @@ describe('RateDetails', () => { }) it('cannot continue when shared rate radio is checked and no package is selected', async () => { - ldUseClientSpy({ 'packages-with-shared-rates': true }) //Spy on useStatePrograms hook to get up-to-date state programs jest.spyOn(useStatePrograms, 'useStatePrograms').mockReturnValue( mockMNState().programs @@ -1279,7 +1272,7 @@ describe('RateDetails', () => { const input = screen.getByLabelText( 'Upload one rate certification document' ) - const targetEl = screen.getByTestId('file-input-droptarget') + const targetEl = screen.getAllByTestId('file-input-droptarget')[0] await userEvent.upload(input, [TEST_DOC_FILE]) dragAndDrop(targetEl, [TEST_PNG_FILE]) @@ -1327,19 +1320,16 @@ describe('RateDetails', () => { { s3URL: 's3://bucketname/one-one/one-one.png', name: 'one one', - documentCategories: ['CONTRACT_RELATED'], sha256: 'fakeSha1', }, { s3URL: 's3://bucketname/one-two/one-two.png', name: 'one two', - documentCategories: ['CONTRACT_RELATED'], sha256: 'fakeSha2', }, { s3URL: 's3://bucketname/one-three/one-three.png', name: 'one three', - documentCategories: ['CONTRACT_RELATED'], sha256: 'fakeSha3', }, ] @@ -1445,7 +1435,7 @@ describe('RateDetails', () => { name: 'Continue', }) - const targetEl = screen.getByTestId('file-input-droptarget') + const targetEl = screen.getAllByTestId('file-input-droptarget')[0] dragAndDrop(targetEl, [TEST_PNG_FILE]) expect( @@ -1465,8 +1455,6 @@ describe('RateDetails', () => { }) // eslint-disable-next-line jest/no-disabled-tests it('disabled with alert when trying to continue while a file is still uploading', async () => { - ldUseClientSpy({ 'supporting-docs-by-rate': true }) - renderWithProviders( { const input = screen.getByLabelText( 'Upload one rate certification document' ) - const targetEl = screen.getByTestId('file-input-droptarget') + const targetEl = screen.getAllByTestId('file-input-droptarget')[0] await userEvent.upload(input, [TEST_DOC_FILE]) dragAndDrop(targetEl, [TEST_PNG_FILE]) @@ -1612,7 +1600,6 @@ describe('RateDetails', () => { { name: 'aasdf3423af', s3URL: 's3://bucketname/key/fileName', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -1647,7 +1634,6 @@ describe('RateDetails', () => { }) it('when duplicate files present, triggers error alert on click', async () => { - ldUseClientSpy({ 'supporting-docs-by-rate': true }) const mockUpdateDraftFn = jest.fn() renderWithProviders( { const input = screen.getByLabelText( 'Upload one rate certification document' ) - const targetEl = screen.getByTestId('file-input-droptarget') + const targetEl = screen.getAllByTestId('file-input-droptarget')[0] await userEvent.upload(input, [TEST_DOC_FILE]) dragAndDrop(targetEl, [TEST_PNG_FILE]) @@ -1775,7 +1761,6 @@ describe('RateDetails', () => { }) it('when duplicate files present, does not trigger duplicate documents alert on click and silently updates rate and supporting documents lists without duplicates', async () => { - ldUseClientSpy({ 'supporting-docs-by-rate': true }) const mockUpdateDraftFn = jest.fn() renderWithProviders( { { name: 'testFile.doc', s3URL: expect.any(String), - documentCategories: ['RATES'], sha256: 'da7d22ce886b5ab262cd7ab28901212a027630a5edf8e88c8488087b03ffd833', // pragma: allowlist secret }, ]) @@ -1843,13 +1827,11 @@ describe('RateDetails', () => { { name: 'testFile.xls', s3URL: expect.any(String), - documentCategories: ['RATES_RELATED'], sha256: '76dbe3fd2b5c00001d424347bd28047b3bb2196561fc703c04fe254c10964c80', // pragma: allowlist secret }, { name: 'testFile.doc', s3URL: expect.any(String), - documentCategories: ['RATES_RELATED'], sha256: 'da7d22ce886b5ab262cd7ab28901212a027630a5edf8e88c8488087b03ffd833', // pragma: allowlist secret }, ]) @@ -1860,7 +1842,6 @@ describe('RateDetails', () => { // Helper functions const fillOutIndexRate = async (screen: Screen, index: number) => { - ldUseClientSpy({ 'packages-with-shared-rates': true }) const targetRateCert = rateCertifications(screen)[index] expect(targetRateCert).toBeDefined() const withinTargetRateCert = within(targetRateCert) diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx index 1a30906fb8..41a177595f 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx @@ -15,8 +15,6 @@ import { PageActions } from '../PageActions' import type { HealthPlanFormPageProps } from '../StateSubmissionForm' import { useFocus } from '../../../hooks' -import { featureFlags } from '../../../common-code/featureFlags' -import { useLDClient } from 'launchdarkly-react-client-sdk' import { formatActuaryContactsForForm, formatDocumentsForDomain, @@ -31,6 +29,7 @@ import { useS3 } from '../../../contexts/S3Context' import { S3ClientT } from '../../../s3' import { isLoadingOrHasFileErrors } from '../../../components/FileUpload' import { RoutesRecord } from '../../../constants' +import { SectionCard } from '../../../components/SectionCard' // This function is used to get initial form values as well return empty form values when we add a new rate or delete a rate // We need to include the getKey function in params because there are no guarantees currently file is in s3 even if when we load data from API @@ -78,9 +77,9 @@ const generateRateCertFormValues = (params?: { rateInfo?.packagesWithSharedRateCerts === undefined ? undefined : (rateInfo?.packagesWithSharedRateCerts && - rateInfo?.packagesWithSharedRateCerts.length) >= 1 - ? 'YES' - : 'NO', + rateInfo?.packagesWithSharedRateCerts.length) >= 1 + ? 'YES' + : 'NO', } } @@ -106,17 +105,6 @@ export const RateDetails = ({ const navigate = useNavigate() const { getKey } = useS3() - // feature flags state management - const ldClient = useLDClient() - const showPackagesWithSharedRatesDropdown: boolean = ldClient?.variation( - featureFlags.PACKAGES_WITH_SHARED_RATES.flag, - featureFlags.PACKAGES_WITH_SHARED_RATES.defaultValue - ) - const supportingDocsByRate = ldClient?.variation( - featureFlags.SUPPORTING_DOCS_BY_RATE.flag, - featureFlags.SUPPORTING_DOCS_BY_RATE.defaultValue - ) - // form validation state management const [focusErrorSummaryHeading, setFocusErrorSummaryHeading] = React.useState(false) @@ -137,10 +125,7 @@ export const RateDetails = ({ const newRateNameRef = React.useRef(null) const [newRateButtonRef, setNewRateButtonFocus] = useFocus() // This ref.current is always the same element - const rateDetailsFormSchema = RateDetailsFormSchema({ - 'packages-with-shared-rates': showPackagesWithSharedRatesDropdown, - 'supporting-docs-by-rate': supportingDocsByRate, - }) + const rateDetailsFormSchema = RateDetailsFormSchema() React.useEffect(() => { if (focusNewRate) { @@ -188,13 +173,9 @@ export const RateDetails = ({ id: rateInfo.id, rateType: rateInfo.rateType, rateCapitationType: rateInfo.rateCapitationType, - rateDocuments: formatDocumentsForDomain( - rateInfo.rateDocuments, - ['RATES'] - ), + rateDocuments: formatDocumentsForDomain(rateInfo.rateDocuments), supportingDocuments: formatDocumentsForDomain( - rateInfo.supportingDocuments, - ['RATES_RELATED'] + rateInfo.supportingDocuments ), rateDateStart: formatFormDateForDomain(rateInfo.rateDateStart), rateDateEnd: formatFormDateForDomain(rateInfo.rateDateEnd), @@ -320,7 +301,7 @@ export const RateDetails = ({ handleSubmit(e) }} > -
    +
    Rate Details {shouldValidate && ( @@ -365,19 +346,26 @@ export const RateDetails = ({ /> ) )} - + +

    + Additional rate + certification +

    + +
    )} diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts index b4ded13f7d..3ec15222c7 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts @@ -9,28 +9,20 @@ import { Yup.addMethod(Yup.date, 'validateDateFormat', validateDateFormat) -const SingleRateCertSchema = (activeFeatureFlags: FeatureFlagSettings) => +const SingleRateCertSchema = (_activeFeatureFlags: FeatureFlagSettings) => Yup.object().shape({ rateDocuments: validateFileItemsListSingleUpload({ required: true }), - supportingDocuments: activeFeatureFlags['supporting-docs-by-rate'] - ? validateFileItemsList({ required: false }) - : Yup.mixed(), - hasSharedRateCert: activeFeatureFlags['packages-with-shared-rates'] - ? Yup.string().defined('You must select yes or no') - : Yup.string(), - packagesWithSharedRateCerts: activeFeatureFlags[ - 'packages-with-shared-rates' - ] - ? Yup.array() - .when('hasSharedRateCert', { - is: 'YES', - then: Yup.array().min( - 1, - 'You must select at least one submission' - ), - }) - .required() - : Yup.array(), + supportingDocuments: validateFileItemsList({ required: false }), + hasSharedRateCert: Yup.string().defined('You must select yes or no'), + packagesWithSharedRateCerts: Yup.array() + .when('hasSharedRateCert', { + is: 'YES', + then: Yup.array().min( + 1, + 'You must select at least one submission' + ), + }) + .required(), rateProgramIDs: Yup.array().min(1, 'You must select a program'), rateType: Yup.string().defined( 'You must choose a rate certification type' diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx index 0e1e4f53a8..5ac5d903fc 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx @@ -20,6 +20,7 @@ import { FileUpload, PoliteErrorMessage, ProgramSelect, + SectionCard, } from '../../../../components' import styles from '../../StateSubmissionForm.module.scss' @@ -37,8 +38,6 @@ import { } from '../../../../common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType' import { ActuaryContactFields } from '../../Contacts' import { PackagesWithSharedRates } from '../PackagesWithSharedRates/PackagesWithSharedRates' -import { featureFlags } from '../../../../common-code/featureFlags' -import { useLDClient } from 'launchdarkly-react-client-sdk' const isRateTypeEmpty = (values: RateCertFormType): boolean => values.rateType === undefined @@ -119,17 +118,6 @@ export const SingleRateCert = ({ previousDocuments, index = 0, }: SingleRateCertProps): React.ReactElement => { - // feature flags - const ldClient = useLDClient() - const showPackagesWithSharedRatesDropdown: boolean = ldClient?.variation( - featureFlags.PACKAGES_WITH_SHARED_RATES.flag, - featureFlags.PACKAGES_WITH_SHARED_RATES.defaultValue - ) - const supportingDocsByRate = ldClient?.variation( - featureFlags.SUPPORTING_DOCS_BY_RATE.flag, - featureFlags.SUPPORTING_DOCS_BY_RATE.defaultValue - ) - // page level setup const { handleDeleteFile, handleUploadFile, handleScanFile } = useS3() const key = rateInfo.key @@ -146,74 +134,70 @@ export const SingleRateCert = ({ } return ( -
    +

    + {displayAsStandaloneRate ? `Rate certification` - : `Rate certification ${rateCertNumber}` - } - > - - - - Document definitions and requirements - - - {`Upload only one rate certification document. ${ - supportingDocsByRate - ? 'Additional rates can be added later.' - : 'Additional rates and supporting documents can be added later.' - }`} - + : `Rate certification ${rateCertNumber}`} +

    +
    + + + + Document definitions and requirements + + + {`Upload only one rate certification document. Additional rates can be added later.`} + - - This input only accepts one file in PDF, DOC, or - DOCX format. + + This input only accepts one file in PDF, + DOC, or DOCX format. + - - } - accept={ACCEPTED_RATE_CERTIFICATION_FILE_TYPES} - initialItems={rateInfo.rateDocuments} - uploadFile={(file) => - handleUploadFile(file, 'HEALTH_PLAN_DOCS') - } - scanFile={(key) => handleScanFile(key, 'HEALTH_PLAN_DOCS')} - deleteFile={(key) => - handleDeleteFile( - key, - 'HEALTH_PLAN_DOCS', - previousDocuments - ) - } - innerInputRef={multiRatesConfig?.reassignNewRateRef} - onFileItemsUpdate={({ fileItems }) => - setFieldValue( - `${fieldNamePrefix}.rateDocuments`, - fileItems - ) - } - /> - + } + accept={ACCEPTED_RATE_CERTIFICATION_FILE_TYPES} + initialItems={rateInfo.rateDocuments} + uploadFile={(file) => + handleUploadFile(file, 'HEALTH_PLAN_DOCS') + } + scanFile={(key) => + handleScanFile(key, 'HEALTH_PLAN_DOCS') + } + deleteFile={(key) => + handleDeleteFile( + key, + 'HEALTH_PLAN_DOCS', + previousDocuments + ) + } + innerInputRef={multiRatesConfig?.reassignNewRateRef} + onFileItemsUpdate={({ fileItems }) => + setFieldValue( + `${fieldNamePrefix}.rateDocuments`, + fileItems + ) + } + /> + - {supportingDocsByRate && ( @@ -221,7 +205,6 @@ export const SingleRateCert = ({ id={`${fieldNamePrefix}.supportingDocuments`} name={`${fieldNamePrefix}.supportingDocuments`} label="Upload supporting documents" - renderMode="list" aria-required={false} error={showFieldErrors('supportingDocuments')} hint={ @@ -238,9 +221,7 @@ export const SingleRateCert = ({ {`Upload any supporting documents for Rate certification ${rateCertNumber}`} - {supportingDocsByRate - ? 'Additional rates can be added later.' - : 'Additional rates and supporting documents can be added later.'} + Additional rates can be added later. @@ -272,9 +253,7 @@ export const SingleRateCert = ({ } /> - )} - {showPackagesWithSharedRatesDropdown && ( - )} - - - Required - - {showFieldErrors('rateProgramIDs')} - - - - - -
    + + Required - {showFieldErrors('rateType')} + {showFieldErrors('rateProgramIDs')} + + - +
    - Rate certification type definitions - - - -
    - + + Required + + + {showFieldErrors('rateType')} + - -
    -

    - Does the actuary certify capitation rates - specific to each rate cell or a rate range? -

    - - Required - -

    - See 42 CFR §§ 438.4(b) and 438.4(c) -

    - - } - role="radiogroup" - aria-required - > - - {showFieldErrors('rateCapitationType')} - - - -
    -
    + + Rate certification type definitions + + + +
    +
    - {!isRateTypeEmpty(rateInfo) && ( - <> - +
    +

    + Does the actuary certify capitation rates + specific to each rate cell or a rate range? +

    + + Required + +

    + See 42 CFR §§ 438.4(b) and 438.4(c) +

    + + } + role="radiogroup" + aria-required > -
    + {showFieldErrors('rateCapitationType')} + + + +
    + + + {!isRateTypeEmpty(rateInfo) && ( + <> + - - Required - - +
    + + Required + + - - setFieldValue( - `${fieldNamePrefix}.rateDateStart`, - formatUserInputDate(val) - ), - }} - endDateHint="mm/dd/yyyy" - endDateLabel="End date" - endDatePickerProps={{ - disabled: false, - id: `${fieldNamePrefix}.rateDateEnd`, - name: `${fieldNamePrefix}.rateDateEnd`, - 'aria-required': true, - defaultValue: rateInfo.rateDateEnd, - onChange: (val) => - setFieldValue( - `${fieldNamePrefix}.rateDateEnd`, - formatUserInputDate(val) - ), - }} - /> -
    -
    + + setFieldValue( + `${fieldNamePrefix}.rateDateStart`, + formatUserInputDate(val) + ), + }} + endDateHint="mm/dd/yyyy" + endDateLabel="End date" + endDatePickerProps={{ + disabled: false, + id: `${fieldNamePrefix}.rateDateEnd`, + name: `${fieldNamePrefix}.rateDateEnd`, + 'aria-required': true, + defaultValue: rateInfo.rateDateEnd, + onChange: (val) => + setFieldValue( + `${fieldNamePrefix}.rateDateEnd`, + formatUserInputDate(val) + ), + }} + /> +
    +
    - {isRateTypeAmendment(rateInfo) && ( - <> - -
    + - - Required - - + + Required + + - - setFieldValue( - `${fieldNamePrefix}.effectiveDateStart`, - formatUserInputDate(val) - ), - }} - endDateHint="mm/dd/yyyy" - endDateLabel="End date" - endDatePickerProps={{ - disabled: false, - id: `${fieldNamePrefix}.effectiveDateEnd`, - name: `${fieldNamePrefix}.effectiveDateEnd`, - 'aria-required': true, - defaultValue: - rateInfo.effectiveDateEnd, - onChange: (val) => - setFieldValue( - `${fieldNamePrefix}.effectiveDateEnd`, - formatUserInputDate(val) - ), - }} - /> -
    -
    - - )} - - - - Required - -
    + setFieldValue( + `${fieldNamePrefix}.effectiveDateStart`, + formatUserInputDate(val) + ), + }} + endDateHint="mm/dd/yyyy" + endDateLabel="End date" + endDatePickerProps={{ + disabled: false, + id: `${fieldNamePrefix}.effectiveDateEnd`, + name: `${fieldNamePrefix}.effectiveDateEnd`, + 'aria-required': true, + defaultValue: + rateInfo.effectiveDateEnd, + onChange: (val) => + setFieldValue( + `${fieldNamePrefix}.effectiveDateEnd`, + formatUserInputDate(val) + ), + }} + /> +
    + + + )} + - mm/dd/yyyy - - - {showFieldErrors('rateDateCertified')} - + + + Required + +
    + mm/dd/yyyy +
    + + {showFieldErrors('rateDateCertified')} + - - setFieldValue( - `${fieldNamePrefix}.rateDateCertified`, - formatUserInputDate(val) - ) - } - /> -
    - - )} + + setFieldValue( + `${fieldNamePrefix}.rateDateCertified`, + formatUserInputDate(val) + ) + } + /> + + + )} - - - - {index >= 1 && multiRatesConfig && ( - - )} -
    + + + + {index >= 1 && multiRatesConfig && ( + + )} +
    + ) } diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ReviewSubmit.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ReviewSubmit.tsx index 615708ac98..b428fbb194 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/ReviewSubmit.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/ReviewSubmit.tsx @@ -10,7 +10,6 @@ import { ContractDetailsSummarySection, RateDetailsSummarySection, SubmissionTypeSummarySection, - SupportingDocumentsSummarySection, } from '../../../components/SubmissionSummarySection' import { PageActionsContainer } from '../PageActions' import styles from './ReviewSubmit.module.scss' @@ -73,11 +72,6 @@ export const ReviewSubmit = ({ editNavigateTo="../contacts" /> - - [class^='usa-fieldset'] { - padding: units(4); - margin-bottom: units(2); - margin-top: units(2); - background: $cms-color-white; - border: 1px solid $theme-color-base-lighter; - @include u-radius('md'); + // for supporting documents + >& .tableContainer { + &[class^='usa-form'] { + max-width: 100%; + width: 75rem; + } } - - h2 { - margin-top: 0; + // the first fieldset of the form sets up form container + // in cases where form has multiple sub sections using SectionCard - use .withSections class + > [class^='usa-fieldset']:not([class~='with-sections']) { + @include sectionCard } > div[class^='usa-form-group']:not(:first-of-type) { @@ -106,8 +98,12 @@ } } -.rateCertContainer + .rateCertContainer { - margin-top: units(6); +.addRateBtn { + margin-top: 0 !important; // overriding USWDS form btn styles for this case +} + +.rateName { + margin-bottom: 0; } .stateContacts, diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx index 81ff7981da..8bee00d283 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx +++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx @@ -303,7 +303,6 @@ describe('StateSubmissionForm', () => { name: 'somedoc.pdf', s3URL: 's3://bucketName/key/somedoc.pdf', sha256: 'fakesha', - documentCategories: ['CONTRACT_RELATED'], }, ] const mockSubmission = mockDraftHealthPlanPackage({ diff --git a/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx b/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx index 0d996f5156..737ec34552 100644 --- a/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx +++ b/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx @@ -8,7 +8,6 @@ import { ContractDetailsSummarySection, RateDetailsSummarySection, SubmissionTypeSummarySection, - SupportingDocumentsSummarySection, } from '../../components/SubmissionSummarySection' import { usePage } from '../../contexts/PageContext' import { GenericErrorPage } from '../Errors/GenericErrorPage' @@ -139,8 +138,6 @@ export const SubmissionRevisionSummary = (): React.ReactElement => { )} - - ) diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx index 52e4eb3e5a..ff487433bc 100644 --- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx +++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx @@ -575,7 +575,7 @@ describe('SubmissionSummary', () => { ) await waitFor(() => { const rows = screen.getAllByRole('row') - expect(rows).toHaveLength(8) + expect(rows).toHaveLength(10) expect( within(rows[0]).getByText('Date added') ).toBeInTheDocument() diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx index 2164285714..f5fdec6a28 100644 --- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx +++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx @@ -13,7 +13,6 @@ import { ContractDetailsSummarySection, RateDetailsSummarySection, SubmissionTypeSummarySection, - SupportingDocumentsSummarySection, } from '../../components/SubmissionSummarySection' import { SubmissionUnlockedBanner, @@ -225,11 +224,6 @@ export const SubmissionSummary = (): React.ReactElement => { - - { // if the session is expiring, close this modal so the countdown modal can appear diff --git a/services/app-web/src/pages/Wrapper/AuthenticatedRouteWrapper.tsx b/services/app-web/src/pages/Wrapper/AuthenticatedRouteWrapper.tsx index de8415751d..9a1e991145 100644 --- a/services/app-web/src/pages/Wrapper/AuthenticatedRouteWrapper.tsx +++ b/services/app-web/src/pages/Wrapper/AuthenticatedRouteWrapper.tsx @@ -2,13 +2,11 @@ import React, { useState } from 'react' import { Modal } from '../../components/Modal/Modal' import { ModalRef } from '@trussworks/react-uswds' import { createRef, useCallback, useEffect } from 'react' -import { useAuth } from '../../contexts/AuthContext' +import { MODAL_COUNTDOWN_DURATION, useAuth } from '../../contexts/AuthContext' import { AuthModeType } from '../../common-code/config' import { extendSession } from '../Auth/cognitoAuth' -import { featureFlags } from '../../common-code/featureFlags/flags' import styles from '../StateSubmission/ReviewSubmit/ReviewSubmit.module.scss' import { dayjs } from '../../common-code/dateHelpers/dayjs' -import { useLDClient } from 'launchdarkly-react-client-sdk' import { recordJSException } from '../../otelHelpers' import { ErrorAlertSignIn } from '../../components' @@ -35,12 +33,6 @@ export const AuthenticatedRouteWrapper = ({ announcementTimes.push(i) } const modalRef = createRef() - const ldClient = useLDClient() - const countdownDuration: number = - ldClient?.variation( - featureFlags.MODAL_COUNTDOWN_DURATION.flag, - featureFlags.MODAL_COUNTDOWN_DURATION.defaultValue - ) * 60 const logoutSession = useCallback( (forcedSessionSignout: boolean) => { @@ -58,7 +50,7 @@ export const AuthenticatedRouteWrapper = ({ const resetSessionTimeout = () => { updateSessionExpirationState(false) updateSessionExpirationTime() - setLogoutCountdownDuration(countdownDuration) + setLogoutCountdownDuration(MODAL_COUNTDOWN_DURATION) if (authMode !== 'LOCAL') { void extendSession() } diff --git a/services/app-web/src/styles/custom.scss b/services/app-web/src/styles/custom.scss index c87ec897f0..0715a1f6a3 100644 --- a/services/app-web/src/styles/custom.scss +++ b/services/app-web/src/styles/custom.scss @@ -10,6 +10,14 @@ $width-max-contained: 50rem; max-width: $width-max-contained; } +@mixin sectionCard { + padding: units(4); + margin-bottom: units(2); + margin-top: units(2); + background: $cms-color-white; + border: 1px solid $theme-color-base-lighter; + @include u-radius('md'); +} // accessibility .srOnly { position: absolute; diff --git a/services/app-web/src/testHelpers/apolloMocks/emailGQLMock.ts b/services/app-web/src/testHelpers/apolloMocks/emailGQLMock.ts index 7fc953b438..d8b0690140 100644 --- a/services/app-web/src/testHelpers/apolloMocks/emailGQLMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/emailGQLMock.ts @@ -22,7 +22,10 @@ export const fetchEmailSettings = cmsRateHelpEmailAddress: 'rates@example.com', helpDeskEmail: 'helpdesk@example.com', oactEmails: ['testRate@example.com'], - dmcpEmails: ['testPolicy@example.com'], + dmcpReviewEmails: ['testPolicy@example.com'], + dmcpSubmissionEmails: [ + 'testPolicySubmission@example.com', + ], dmcoEmails: ['testDmco@example.com'], }, stateAnalysts: [ diff --git a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts index 32a6be1194..86109054ed 100644 --- a/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/healthPlanFormDataMock.ts @@ -147,7 +147,6 @@ function mockContractAndRatesDraft( s3URL: 's3://bucketname/key/contract', sha256: 'fakesha', name: 'contract', - documentCategories: ['CONTRACT' as const], }, ], contractDateStart: new Date(), @@ -247,7 +246,6 @@ function mockStateSubmission(): LockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/supporting-documents', sha256: 'fakesha', name: 'supporting documents', - documentCategories: ['CONTRACT_RELATED' as const], }, ], contractType: 'BASE', @@ -257,7 +255,6 @@ function mockStateSubmission(): LockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/contract', sha256: 'fakesha', name: 'contract', - documentCategories: ['CONTRACT' as const], }, ], contractDateStart: new Date(), @@ -294,7 +291,6 @@ function mockStateSubmission(): LockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/rate', sha256: 'fakesha', name: 'rate', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [ @@ -302,7 +298,6 @@ function mockStateSubmission(): LockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/supporting-documents', sha256: 'fakesha', name: 'supporting documents', - documentCategories: ['RATES_RELATED' as const], }, ], rateDateStart: new Date(), @@ -355,7 +350,6 @@ function mockStateSubmissionContractAmendment(): LockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/supporting-documents', sha256: 'fakesha', name: 'supporting documents', - documentCategories: ['RATES_RELATED' as const], }, ], contractType: 'AMENDMENT', @@ -365,7 +359,6 @@ function mockStateSubmissionContractAmendment(): LockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/contract', sha256: 'fakesha', name: 'contract', - documentCategories: ['CONTRACT' as const], }, ], contractDateStart: new Date(), @@ -402,7 +395,6 @@ function mockStateSubmissionContractAmendment(): LockedHealthPlanFormDataType { s3URL: 's3://bucketname/key/rate', sha256: 'fakesha', name: 'rate', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], @@ -708,19 +700,16 @@ function mockUnlockedHealthPlanPackageWithDocuments(): HealthPlanPackage { s3URL: 's3://bucketname/one-one/one-one.png', sha256: 'fakesha', name: 'one one', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/one-two/one-two.png', sha256: 'fakesha', name: 'one two', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/one-three/one-three.png', sha256: 'fakesha', name: 'one three', - documentCategories: ['CONTRACT_RELATED'], }, ] const docs2: SubmissionDocument[] = [ @@ -728,19 +717,16 @@ function mockUnlockedHealthPlanPackageWithDocuments(): HealthPlanPackage { s3URL: 's3://bucketname/one-two/one-two.png', sha256: 'fakesha', name: 'one two', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/one-three/one-three.png', sha256: 'fakesha', name: 'one three', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/two-one/two-one.png', sha256: 'fakesha', name: 'two one', - documentCategories: ['CONTRACT_RELATED'], }, ] const docs3: SubmissionDocument[] = [ @@ -748,19 +734,16 @@ function mockUnlockedHealthPlanPackageWithDocuments(): HealthPlanPackage { s3URL: 's3://bucketname/one-two/one-two.png', sha256: 'fakesha', name: 'one two', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/two-one/two-one.png', sha256: 'fakesha', name: 'two one', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/three-one/three-one.png', sha256: 'fakesha', name: 'three one', - documentCategories: ['CONTRACT_RELATED'], }, ] diff --git a/services/app-web/src/testHelpers/apolloMocks/healthPlanPackageGQLMock.ts b/services/app-web/src/testHelpers/apolloMocks/healthPlanPackageGQLMock.ts index d9c474fd8b..82963e1897 100644 --- a/services/app-web/src/testHelpers/apolloMocks/healthPlanPackageGQLMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/healthPlanPackageGQLMock.ts @@ -150,13 +150,11 @@ const mockSubmittedHealthPlanPackageWithRevision = ({ s3URL: 's3://bucketname/1648242632157-Amerigroup Texas, Inc.pdf/Amerigroup Texas, Inc.pdf', sha256: 'fakesha', name: 'Amerigroup Texas, Inc.pdf', - documentCategories: ['CONTRACT'], }, { s3URL: 's3://bucketname/1648490162641-lifeofgalileo.pdf/lifeofgalileo.pdf', sha256: 'fakesha1', name: 'lifeofgalileo.pdf', - documentCategories: ['CONTRACT'], }, ], rateInfos: [ @@ -166,13 +164,11 @@ const mockSubmittedHealthPlanPackageWithRevision = ({ s3URL: 's3://bucketname/1648242665634-Amerigroup Texas, Inc.pdf/Amerigroup Texas, Inc.pdf', sha256: 'fakesha2', name: 'Amerigroup Texas, Inc.pdf', - documentCategories: ['RATES'], }, { s3URL: 's3://bucketname/1648242711421-Amerigroup Texas Inc copy.pdf/Amerigroup Texas Inc copy.pdf', sha256: 'fakesha3', name: 'Amerigroup Texas Inc copy.pdf', - documentCategories: ['RATES'], }, ], supportingDocuments: [ @@ -180,7 +176,6 @@ const mockSubmittedHealthPlanPackageWithRevision = ({ s3URL: 's3://bucketname/1648242873229-covid-ifc-2-flu-rsv-codes 5-5-2021.pdf/covid-ifc-2-flu-rsv-codes 5-5-2021.pdf', sha256: 'fakesha5', name: 'covid-ifc-2-flu-rsv-codes 5-5-2021.pdf', - documentCategories: ['RATES_RELATED'], }, ], actuaryContacts: [], @@ -192,7 +187,6 @@ const mockSubmittedHealthPlanPackageWithRevision = ({ s3URL: 's3://bucketname/1648242711421-529-10-0020-00003_Superior_Health Plan, Inc.pdf/529-10-0020-00003_Superior_Health Plan, Inc.pdf', sha256: 'fakesha3', name: '529-10-0020-00003_Superior_Health Plan, Inc.pdf', - documentCategories: ['CONTRACT_RELATED'], }, ], } @@ -202,19 +196,16 @@ const mockSubmittedHealthPlanPackageWithRevision = ({ s3URL: 's3://bucketname/1648242632157-Amerigroup Texas, Inc.pdf/Amerigroup Texas, Inc.pdf', sha256: 'fakesha', name: 'Amerigroup Texas, Inc.pdf', - documentCategories: ['CONTRACT'], }, { s3URL: 's3://bucketname/1648242665634-Amerigroup Texas, Inc.pdf/Amerigroup Texas, Inc.pdf', sha256: 'fakesha2', name: 'Amerigroup Texas, Inc.pdf', - documentCategories: ['CONTRACT'], }, { s3URL: 's3://bucketname/1648242711421-Amerigroup Texas Inc copy.pdf/Amerigroup Texas Inc copy.pdf', sha256: 'fakesha4', name: 'Amerigroup Texas Inc copy.pdf', - documentCategories: ['CONTRACT'], }, ], rateInfos: [ @@ -224,19 +215,16 @@ const mockSubmittedHealthPlanPackageWithRevision = ({ s3URL: 's3://bucketname/1648242711421-529-10-0020-00003_Superior_Health Plan, Inc.pdf/529-10-0020-00003_Superior_Health Plan, Inc.pdf', sha256: 'fakesha3', name: '529-10-0020-00003_Superior_Health Plan, Inc.pdf', - documentCategories: ['RATES'], }, { s3URL: 's3://bucketname/1648242873229-covid-ifc-2-flu-rsv-codes 5-5-2021.pdf/covid-ifc-2-flu-rsv-codes 5-5-2021.pdf', sha256: 'fakesha5', name: 'covid-ifc-2-flu-rsv-codes 5-5-2021.pdf', - documentCategories: ['RATES'], }, { s3URL: 's3://bucketname/1648242632157-Amerigroup Texas, Inc.pdf/Amerigroup Texas, Inc.pdf', sha256: 'fakesha', name: 'Amerigroup Texas, Inc.pdf', - documentCategories: ['RATES'], }, ], supportingDocuments: [ @@ -244,7 +232,6 @@ const mockSubmittedHealthPlanPackageWithRevision = ({ s3URL: 's3://bucketname/1648242711421-529-10-0020-00003_Superior_Health Plan, Inc.pdf/529-10-0020-00003_Superior_Health Plan, Inc.pdf', sha256: 'fakesha3', name: '529-10-0020-00003_Superior_Health Plan, Inc.pdf', - documentCategories: ['RATES_RELATED'], }, ], actuaryContacts: [], @@ -256,13 +243,11 @@ const mockSubmittedHealthPlanPackageWithRevision = ({ s3URL: 's3://bucketname/1648242665634-Amerigroup Texas, Inc.pdf/Amerigroup Texas, Inc.pdf', sha256: 'fakesha2', name: 'Amerigroup Texas, Inc.pdf', - documentCategories: ['CONTRACT_RELATED'], }, { s3URL: 's3://bucketname/1648242711421-Amerigroup Texas Inc copy.pdf/Amerigroup Texas Inc copy.pdf', sha256: 'fakesha4', name: 'Amerigroup Texas Inc copy.pdf', - documentCategories: ['CONTRACT_RELATED'], }, ], } diff --git a/services/cypress/integration/cmsWorkflow/rateReview.spec.ts b/services/cypress/integration/cmsWorkflow/rateReview.spec.ts index 8866d1ab69..b7d476c7b0 100644 --- a/services/cypress/integration/cmsWorkflow/rateReview.spec.ts +++ b/services/cypress/integration/cmsWorkflow/rateReview.spec.ts @@ -25,11 +25,6 @@ describe('CMS user can view rate reviews', () => { it('and navigate to a specific rate from the rates dashboard', () => { - cy.interceptFeatureFlags({ - 'rates-db-refactor':true, - 'rate-reviews-dashboard': true - }) - cy.apiAssignDivisionToCMSUser(cmsUser(), 'DMCO').then(() => { // Create a new contract and rates submission with two attached rates diff --git a/services/cypress/integration/stateWorkflow/questionResponse/questionResponse.spec.ts b/services/cypress/integration/stateWorkflow/questionResponse/questionResponse.spec.ts index dfedff3bb6..5f8bc3e0b1 100644 --- a/services/cypress/integration/stateWorkflow/questionResponse/questionResponse.spec.ts +++ b/services/cypress/integration/stateWorkflow/questionResponse/questionResponse.spec.ts @@ -8,7 +8,6 @@ describe('Q&A', () => { it('can add questions and responses', () => { cy.interceptFeatureFlags({ 'cms-questions': true, - 'rates-db-refactor': true }) // Assign Division to CMS user zuko diff --git a/services/cypress/package.json b/services/cypress/package.json index c226dc22ed..60b0e08c4b 100644 --- a/services/cypress/package.json +++ b/services/cypress/package.json @@ -41,7 +41,7 @@ "devDependencies": { "husky": "^8.0.1", "lint-staged": "^14.0.1", - "prettier": "^2.4.1" + "prettier": "^3.1.0" }, "dependencies": { "@apollo/client": "^3.4.15", diff --git a/services/cypress/support/index.ts b/services/cypress/support/index.ts index f1391d3cd1..9e64fffd8e 100644 --- a/services/cypress/support/index.ts +++ b/services/cypress/support/index.ts @@ -63,7 +63,6 @@ declare global { fillOutStateContact(): void fillOutAdditionalActuaryContact(): void fillOutSupportingDocuments(): void - waitForDocumentsToLoad( args?: {tableView?: boolean}): void verifyDocumentsHaveNoErrors(): void submitStateSubmissionForm( args?: {success?: boolean, resubmission?: boolean, summary?: string}): void diff --git a/services/cypress/support/launchDarklyCommands.ts b/services/cypress/support/launchDarklyCommands.ts index 1e0c55d156..4c2cce6c57 100644 --- a/services/cypress/support/launchDarklyCommands.ts +++ b/services/cypress/support/launchDarklyCommands.ts @@ -84,9 +84,6 @@ Cypress.Commands.add('stubFeatureFlags', () => { * Useful if you want default feature flags for tests that are different than default values set in common-code featureFlags **/ cy.interceptFeatureFlags({ - 'packages-with-shared-rates': true, - 'rates-db-refactor': true, - 'supporting-docs-by-rate': true, '438-attestation': true }) }) diff --git a/services/cypress/support/stateSubmissionFormCommands.ts b/services/cypress/support/stateSubmissionFormCommands.ts index 9c97b58143..007f2235ee 100644 --- a/services/cypress/support/stateSubmissionFormCommands.ts +++ b/services/cypress/support/stateSubmissionFormCommands.ts @@ -102,7 +102,7 @@ Cypress.Commands.add('fillOutBaseContractDetails', () => { // Contract 438 attestation question cy.findByText('No, the contract does not fully comply with all applicable requirements').click() - cy.findByRole('textbox', {name: 'Please provide a brief description of the contract’s non-compliance (with regulatory citations) and expected timeframe for remediation'}) + cy.findByRole('textbox', {name: 'Provide a brief description of any contractual or operational non-compliance, including regulatory citations and expected timeframe for remediation'}) .type('Non compliance explanation') cy.findByText('Fully executed').click() @@ -182,11 +182,11 @@ Cypress.Commands.add('fillOutAmendmentToBaseContractDetails', () => { // Must be on '/submissions/:id/edit/contract-details' // Contract 438 attestation question cy.findByText('No, the contract does not fully comply with all applicable requirements').click() - cy.findByRole('textbox', {name: 'Please provide a brief description of the contract’s non-compliance (with regulatory citations) and expected timeframe for remediation'}) + cy.findByRole('textbox', {name: 'Provide a brief description of any contractual or operational non-compliance, including regulatory citations and expected timeframe for remediation'}) .type('Non compliance explanation') - + cy.findByText('Unexecuted by some or all parties').click() - + cy.findAllByLabelText('Start date', {timeout: 2000}) .parents() .findByTestId('date-picker-external-input') @@ -462,17 +462,11 @@ Cypress.Commands.add('fillOutSupportingDocuments', () => { }) // for fileupload with the table view and checkboxes- tableView can be assigned to a number that representes how many items in the list should be preses -Cypress.Commands.add('waitForDocumentsToLoad', ({ tableView } = {tableView: false}) => { - if (tableView) { - cy.findAllByTestId('file-input-loading-image', { - timeout: 200_000, - }).should('not.exist') - } else { - // list view is the default behavior - cy.findAllByTestId('file-input-preview-image', { - timeout: 200_000, - }).should('not.have.class', 'is-loading') - } +Cypress.Commands.add('waitForDocumentsToLoad', () => { + // list view is the default behavior + cy.findAllByTestId('file-input-preview-image', { + timeout: 200_000, + }).should('not.have.class', 'is-loading') }) Cypress.Commands.add('verifyDocumentsHaveNoErrors', () => { diff --git a/services/cypress/utils/apollo-test-utils.ts b/services/cypress/utils/apollo-test-utils.ts index f21f8ed01c..0db7246f14 100644 --- a/services/cypress/utils/apollo-test-utils.ts +++ b/services/cypress/utils/apollo-test-utils.ts @@ -79,7 +79,6 @@ const contractOnlyData = (): Partial=> ({ { name: 'Contract Cert.pdf', s3URL: 's3://local-uploads/1684382956834-Contract Cert.pdf/Contract Cert.pdf', - documentCategories: ['CONTRACT'], sha256: 'abc123', }, ], @@ -119,7 +118,6 @@ const contractAndRatesData = (): Partial=> ({ { name: 'Contract Cert.pdf', s3URL: 's3://local-uploads/1684382956834-Contract Cert.pdf/Contract Cert.pdf', - documentCategories: ['CONTRACT'], sha256: 'abc123', }, ], @@ -151,14 +149,12 @@ const contractAndRatesData = (): Partial=> ({ name: 'rate1Document1.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [ { name: 'rate1SupportingDocument1.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['RATES' as const], }], rateProgramIDs: [minnesotaStatePrograms[0].id], actuaryContacts: [ @@ -184,7 +180,6 @@ const contractAndRatesData = (): Partial=> ({ name: 'rate2Document1.pdf', s3URL: 'fakeS3URL', sha256: 'fakesha', - documentCategories: ['RATES' as const], }, ], supportingDocuments: [], diff --git a/services/postgres/README.md b/services/postgres/README.md index 85503940e6..9df566ecac 100644 --- a/services/postgres/README.md +++ b/services/postgres/README.md @@ -70,3 +70,7 @@ Once you have the credentials from Secrets Manager (see above), you can set the A recent copy of our code base will need to be checked out onto the jumpbox to run `prisma` commands. Just `git clone` this repository to the jumpbox and `yarn install` things in order to get the appropriate `prisma` commands. You can now use `npx prisma diff` and the other [prisma tools](https://www.prisma.io/docs/guides/migrate/production-troubleshooting) to fix up the failed migration. + +## Using the ./dev jumpbox command + +The `./dev jumpbox` command allows you to interact with the jumpbox from the CLI. Currently only `./dev jumpbox clone` is implemented. This command will log into the jumpbox, dump the db into a file, and copy that file locally. diff --git a/services/postgres/scripts/authorized_keys b/services/postgres/scripts/authorized_keys index e29b7437a5..839b73be1d 100644 --- a/services/postgres/scripts/authorized_keys +++ b/services/postgres/scripts/authorized_keys @@ -1,4 +1,5 @@ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDaR+UVq61k14jcuSFfoCfTxvB1IyhU3IQFp4OjpiN4fYMBjE9USeNoHon2ux8VTvL0nRc7Zn4g9HemxiDjdawUxh2oJ8GOTsiFTEWic2nf90SnbjBMn1OZELvMiZzoYDjQFvEp+AgETBA5nhrbHyxWQWIBa7A+XqiqnX0lcZ1p+x8sLIl4F0e583lJeuPQPVkpCicf2GDdtG1TnPxltqJgGaeVSONivpxeVofJwG4DCXy1b1xSo1NG0gzy9BWFJwOWKmZAk6nYq+rcxZg+TgU1x5WJ6z8/CS0PMSoTMRRIejm734PSmkGCU+WkR139Dl8o3DvQh/VQD71fxw30aONG98PSBJEUd5IouuiPPNYGP+fuDWgCBkaoA6JKlSVtbneNt1Qkm10FFHqExqzGWaSDeUCh6da3WG1BW4KZcC3MQ8CTEG47LFqUG5TvhklhiAAJH7cGF9W9SU1Beq2A6Wx1R/yGvgH/7U6X0/QfJi1ljY32pPzP2S+gzzOVGJgrMz3qRRgNvcY5k8EMbIuTK2yanFFHuVaWQq/zZW1T376oyHMfWdBB9WAtIKwpCgA5kYUu0XCo3XM0fWibZFIa/cEBNSKH1gEFKCBXolsc2+c4iZtdbG4YCHLgzOOqklERMEeK5dXq9Rz7UjoE91UVIyO2/d+mXmiVDRgtUsiQ34Sxyw== mojo.talantikite@gmail.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCttm1LMpbYCjmDQaKrM2KWF2D/xKMWD2y8azKYiZZj2X6KvVCLFs5uskvcgqXJw4G8ePTWdcO1EqXtHG8yWUMFT6YptalmO5jnz18V9fArt9WttPAzZKB7V/KTs5TxvhQ5h59TCJQGoG0/C+LuDH0ZJWuBv1U/l8yBBmWCJu2b41Kq+Hvclv6eLb+0A14o447paknTOHDheKukx3y44yhEYSoVQcIlHm7vApxGGhhhoiWkrdN0a1U4npM8G1MHdNe0360zSVmmFV6FgxFZPmMOK+xKRHNCgJdd5/8Tua+DDckeYw1c4DYEw/nvvITQs855U35RFOeOLi54gWNtwmhMyJJe8r7+Ls/t/lpOe8o1alE6G+QNb7RV8GJ6kIxyYLiUEExmPzBCur8XsJctG9BWS/yBsmEnasqBaq2HHdQMlbKe/AoZgGPlVWbSFXnHkfVlme0NeZa2ya8Igj9xdoK6cgYM6W/zKlopfQV4fdkpcGq1grVOP4vT/gzLKD8clkU= macrae@KIHW10L6038 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDRrQOylmfnH6ptruU7nUzMvwkV6Q2WCFLJlGxbVTKlFiEepsSb4y/P3ZUL+regv8GZiZkWc+x1hiIGDQxF68nTSHv0G9otHeG44nrrQdeJ1rghy+eptyxIcAenUtP3eRhSR3c2/8IP7XPJutrFbQOADlQqwimKYrk1fdcONWt1TB1uPqk/i9mEdAcFn2VVQnWYbJ/mLwd0EPFqqnPFFnUzayaiYQnEZU9xLSSDYCurPEWuYDipJgyH0xsMloSwpV8U7m/v/4cPHU9E4NpnNY4Ke5DNljeqmZxQWbwZMGTWSYt7um3Zn5entr9iWJf0uu5nC03YPl3mgMxeu5wuui2HKiJBQCL1Yinz7ErRm7vR/Q11F8q3vyxey3tjNaMV7wMibtuQZh2ZdCGMlisoM9G44nS/EwDuuvt6PT87fgr2ur2SucOw52NokC6eO49DqXQXB5AVFnFoUMXTEQxz4Jq4uoEP9fz7cJVHrY5LVmtwy1yxoLujldhlRTg+G6bKOnY8embgFMJ+lBI8/R1N0f2r4EkTp3GwMgsDkZs3H/djaQMEdK7daKGYltwnXPiOR1Q0PNnnGMOctE9hSojiV1FX80aycYFEjmkOZzqUHJHgm6OPzYRC+CB5/OEBT+1MPjPcOXmMGa6JrNyc/LJeA5GOlj9JzRO3fOHnp4DlA4ZHEw== maolin@Maos-MacBook-Pro.local -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDiKdvKxI2HkraeNla7tFTEijQzQzeBy5Ss044+dV9uhPqVNlRQakFpRwReOQRj1drK5Nyk48r6CXMomT0u74MtGqma+17lvFPxXc+6WywwzeXNEWAvmqniB6hsNs+ysyGJuYa3rNuGzUuwxoe1I6ANtI+nIU67J2UHoHp8XATush2w+flVnprUOJBzYiT3xLrPr6NdW+LKUb3+Vyqwx7sc7wZ1Y1eCNRbC/+aVXxt/lQyTxLn5I4beedIUQ6I6jezNdB8yg5GfpNDvwH25d0Z6V1XFkKzCRSNizgfbC4l2lFCZvLc4+3tdbu7pnkW4mSqHlgqnAn+qONsNovzg9Igq7fMfyxU9VZXKXSBMOIem5QKyFQ4mFgrW7RxLt5VX9tjQ7ImsBx77p1qe0CskEX9knZu3bzETFaqAEuVo7/pHu/aMwlsZtuG8cAY+/2AktVc3Twlz01RoGRLULz8YB7oY7uhHKF4U+eLin6dFmrfNHHlsKCR1LUV4BPGYi6V41GM= worku@hiilaptop.lan \ No newline at end of file +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDiKdvKxI2HkraeNla7tFTEijQzQzeBy5Ss044+dV9uhPqVNlRQakFpRwReOQRj1drK5Nyk48r6CXMomT0u74MtGqma+17lvFPxXc+6WywwzeXNEWAvmqniB6hsNs+ysyGJuYa3rNuGzUuwxoe1I6ANtI+nIU67J2UHoHp8XATush2w+flVnprUOJBzYiT3xLrPr6NdW+LKUb3+Vyqwx7sc7wZ1Y1eCNRbC/+aVXxt/lQyTxLn5I4beedIUQ6I6jezNdB8yg5GfpNDvwH25d0Z6V1XFkKzCRSNizgfbC4l2lFCZvLc4+3tdbu7pnkW4mSqHlgqnAn+qONsNovzg9Igq7fMfyxU9VZXKXSBMOIem5QKyFQ4mFgrW7RxLt5VX9tjQ7ImsBx77p1qe0CskEX9knZu3bzETFaqAEuVo7/pHu/aMwlsZtuG8cAY+/2AktVc3Twlz01RoGRLULz8YB7oY7uhHKF4U+eLin6dFmrfNHHlsKCR1LUV4BPGYi6V41GM= worku@hiilaptop.lan +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAk11E+dW51OKueXqW5fQmqAzjeJJVwob8QtiEh5JWAb7RRl7R67pRsoYRGQUp01D6PviEWlGU0gVoBENqhrM18sQCAH9oXoAn1hjwek8tXwh8oUJrGgUJOD/ZaHsRr5oLWUdSB7uYHw17B8VdcuK2EhEQx3dV6uS8ts1Kh+lqb3gdaS5BSQffKszY18TT9Mx9UgP2dwfEqr9cf40K5pm8l3M4G5grJK4taKKG8DopjBjQTMCOK44PBC77BkoEbzJPj+hQ1aZSlPsqcGcGFnzJyhwJkR2WeB5MyIN0eu2y4JKQ4vJWPINR4Jq7CTHtJsZUa39LNl5dJY1MuOFD1v/G4Zj+WsnLMAEpGaSUQ3ZoBC2aya2jfO972e41jNci4NfOtvNiycyJSAg6e6rRu7mfaLag2OUc7ZdAwWcamrYxnWWQjFGaVIzbgn4GHifJ0gtfsKYFHz/8UQctW44G5S3U8Du9UHKA//PaF8MWtLY9fNI6RnF2VlAuT2FiyoqbF5U= meghanmurphy@meghans-mbp.lan diff --git a/services/postgres/serverless.yml b/services/postgres/serverless.yml index 398fd9a042..cecb0c5f1a 100644 --- a/services/postgres/serverless.yml +++ b/services/postgres/serverless.yml @@ -171,6 +171,9 @@ resources: InstanceType: t2.micro ImageId: ami-05bfc1ab11bfbf484 IamInstanceProfile: !Ref PgVMIAMInstanceProfile + Tags: + - Key: mcr-vmuse + Value: jumpbox NetworkInterfaces: - AssociatePublicIpAddress: true DeviceIndex: '0' @@ -181,7 +184,11 @@ resources: UserData: Fn::Base64: !Sub | #!/bin/bash - apt update && apt install unzip postgresql postgresql-contrib -y + # get apt data for postgres-14 + sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + + apt update && apt install unzip postgresql-14 postgresql-contrib -y curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && unzip awscliv2.zip ./aws/install diff --git a/services/ui-auth/package.json b/services/ui-auth/package.json index 02061e864e..9b23dd777c 100644 --- a/services/ui-auth/package.json +++ b/services/ui-auth/package.json @@ -8,7 +8,7 @@ "author": "", "license": "CC0-1.0", "devDependencies": { - "prettier": "^2.2.1", + "prettier": "^3.1.0", "serverless-iam-helper": "CMSgov/serverless-iam-helper", "serverless-stack-termination-protection": "^2.0.2", "serverless-s3-bucket-helper": "CMSgov/serverless-s3-bucket-helper" diff --git a/services/uploads/jest.config.ts b/services/uploads/jest.config.ts index 34ff9b4ff6..816515d886 100644 --- a/services/uploads/jest.config.ts +++ b/services/uploads/jest.config.ts @@ -21,4 +21,7 @@ module.exports = { moduleFileExtensions: ['js', 'json', 'jsx', 'd.ts', 'ts', 'node'], coveragePathIgnorePatterns: [], modulePathIgnorePatterns: ['local_buckets'], + moduleNameMapper: { + '^uuid$': require.resolve('uuid'), + }, } diff --git a/services/uploads/src/lambdas/avScan.ts b/services/uploads/src/lambdas/avScan.ts index 198a44b9da..475bd08744 100644 --- a/services/uploads/src/lambdas/avScan.ts +++ b/services/uploads/src/lambdas/avScan.ts @@ -81,8 +81,7 @@ async function avScan(event: S3Event, _context: Context) { clamAV, s3ObjectKey, s3ObjectBucket, - maxFileSize, - '/tmp/downloads' + maxFileSize ) // Record the duration of the av scan diff --git a/services/uploads/src/lib/avScan.test.ts b/services/uploads/src/lib/avScan.test.ts index 1e48eea9a1..fbe126add3 100644 --- a/services/uploads/src/lib/avScan.test.ts +++ b/services/uploads/src/lib/avScan.test.ts @@ -12,7 +12,6 @@ describe('avScan', () => { it('tags clean for a clean file', async () => { const thisDir = __dirname const tmpDefsDir = await mkdtemp('/tmp/freshclam-') - const tmpScanDir = await mkdtemp('/tmp/clamscan-') const s3Client = NewTestS3UploadsClient() @@ -61,7 +60,6 @@ describe('avScan', () => { goodFileKey, 'test-uploads', MAX_FILE_SIZE, - tmpScanDir ) if (scanResult instanceof Error) { throw scanResult @@ -78,13 +76,11 @@ describe('avScan', () => { expect(virusScanStatus(res2)).toBe('CLEAN') await rm(tmpDefsDir, { force: true, recursive: true }) - await rm(tmpScanDir, { force: true, recursive: true }) }) it('marks infected for an infected file', async () => { const thisDir = __dirname const tmpDefsDir = await mkdtemp('/tmp/freshclam-') - const tmpScanDir = await mkdtemp('/tmp/clamscan-') const s3Client = NewTestS3UploadsClient() @@ -133,7 +129,6 @@ describe('avScan', () => { badFileKey, 'test-uploads', MAX_FILE_SIZE, - tmpScanDir ) if (scanResult instanceof Error) { throw scanResult @@ -150,13 +145,11 @@ describe('avScan', () => { expect(virusScanStatus(res2)).toBe('INFECTED') await rm(tmpDefsDir, { force: true, recursive: true }) - await rm(tmpScanDir, { force: true, recursive: true }) }) it('marks skipped for too big a file (config a smaller max size)', async () => { const thisDir = __dirname const tmpDefsDir = await mkdtemp('/tmp/freshclam-') - const tmpScanDir = await mkdtemp('/tmp/clamscan-') const s3Client = NewTestS3UploadsClient() @@ -205,7 +198,6 @@ describe('avScan', () => { badFileKey, 'test-uploads', 2, - tmpScanDir ) if (scanResult instanceof Error) { throw scanResult @@ -222,13 +214,11 @@ describe('avScan', () => { expect(virusScanStatus(res2)).toBe('SKIPPED') await rm(tmpDefsDir, { force: true, recursive: true }) - await rm(tmpScanDir, { force: true, recursive: true }) }) it('marks error if ClamAV errors', async () => { const thisDir = __dirname const tmpDefsDir = await mkdtemp('/tmp/freshclam-') - const tmpScanDir = await mkdtemp('/tmp/clamscan-') const s3Client = NewTestS3UploadsClient() @@ -281,7 +271,6 @@ describe('avScan', () => { badFileKey, 'test-uploads', MAX_FILE_SIZE, - tmpScanDir ) if (scanResult instanceof Error) { throw scanResult @@ -298,13 +287,11 @@ describe('avScan', () => { expect(virusScanStatus(res2)).toBe('ERROR') await rm(tmpDefsDir, { force: true, recursive: true }) - await rm(tmpScanDir, { force: true, recursive: true }) }) it('returns not found if the key doesnt exist', async () => { const thisDir = __dirname const tmpDefsDir = await mkdtemp('/tmp/freshclam-') - const tmpScanDir = await mkdtemp('/tmp/clamscan-') const s3Client = NewTestS3UploadsClient() @@ -336,7 +323,6 @@ describe('avScan', () => { badFileKey, 'test-uploads', MAX_FILE_SIZE, - tmpScanDir ) if (!(scanResult instanceof Error)) { throw new Error('Didnt error on a nonexistant file') @@ -344,6 +330,5 @@ describe('avScan', () => { expect(scanResult.name).toBe('NotFound') await rm(tmpDefsDir, { force: true, recursive: true }) - await rm(tmpScanDir, { force: true, recursive: true }) }) }) diff --git a/services/uploads/src/lib/avScan.ts b/services/uploads/src/lib/avScan.ts index 9d2ccecb1e..4a3b157a66 100644 --- a/services/uploads/src/lib/avScan.ts +++ b/services/uploads/src/lib/avScan.ts @@ -1,4 +1,5 @@ import { S3UploadsClient } from '../deps/s3' +import { mkdtemp } from 'fs/promises' import { ClamAV } from '../deps/clamAV' import { generateVirusScanTagSet, ScanStatus } from './tags' import { scanFiles } from './scanFiles' @@ -9,7 +10,6 @@ export async function scanFile( key: string, bucket: string, maxFileSize: number, - scanDir: string ): Promise { //You need to verify that you are not getting too large a file //currently lambdas max out at 500MB storage. @@ -24,12 +24,16 @@ export async function scanFile( // tag with skipped. tagResult = 'SKIPPED' } else { + + // make a tmp directory to scan this file in + const tmpScanDir = await mkdtemp('/tmp/clamscan-') + const infectedFiles = await scanFiles( s3Client, clamAV, [key], bucket, - scanDir + tmpScanDir ) if (infectedFiles instanceof Error) { @@ -41,6 +45,7 @@ export async function scanFile( tagResult = 'INFECTED' } } + } const tags = generateVirusScanTagSet(tagResult) diff --git a/yarn.lock b/yarn.lock index 488970c495..ba89bad41d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,9 +31,9 @@ tunnel "^0.0.6" "@adobe/css-tools@^4.3.0": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" - integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== + version "4.3.2" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11" + integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw== "@ampproject/remapping@^2.1.0": version "2.2.0" @@ -79,13 +79,13 @@ tslib "^2.3.0" zen-observable-ts "^1.2.5" -"@apollo/composition@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@apollo/composition/-/composition-2.4.1.tgz#2f080324fccbfd34bb3d83b19975aa695e9272b2" - integrity sha512-3da3gNMYw1OwsPJjH9jGkhDJffK3Wn4pu2m5CeJixpv8g5vBCFxlXOZ1ItsN+oNPkwmUplPUuTAWKAvwGfoYOA== +"@apollo/composition@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@apollo/composition/-/composition-2.6.1.tgz#2230b99536bcae1a61f6dfd9664af036600f6905" + integrity sha512-jj4Y/r15EX5N/e4VtU/ftxNQI8NyYDc2IS3Tq2/pUH57sM0HyCqJzwc0tVsptEAhwgYVzuFgiRXlchMjwer/Zg== dependencies: - "@apollo/federation-internals" "2.4.1" - "@apollo/query-graphs" "2.4.1" + "@apollo/federation-internals" "2.6.1" + "@apollo/query-graphs" "2.6.1" "@apollo/explorer@^3.0.0": version "3.5.0" @@ -98,10 +98,10 @@ whatwg-mimetype "^3.0.0" zen-observable-ts "^1.1.0" -"@apollo/federation-internals@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@apollo/federation-internals/-/federation-internals-2.4.1.tgz#49488b26f30d8b5862de4d3b718f5042a5eb368a" - integrity sha512-C0jI/jApL0DIY2a6ig/RDRDpWinUpc3hSARhyBVNFQX0xtnuW+AjB+lpRZoEoWOZzGjfbaN+zmbUUqkxqjlc9Q== +"@apollo/federation-internals@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@apollo/federation-internals/-/federation-internals-2.6.1.tgz#883d2970f058813136e2867ead5c75737b83a7a4" + integrity sha512-6nsLtspVvJ3+41JDhhJkJIvLKHTZSZempPQjiaoehZJ9SU/8oPZQ9FomF8XGbgEZMKmnc+4/YweonQURusDW5w== dependencies: "@types/uuid" "^9.0.0" chalk "^4.1.0" @@ -109,13 +109,13 @@ uuid "^9.0.0" "@apollo/gateway@^2.2.2": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@apollo/gateway/-/gateway-2.4.1.tgz#5ebf16479706a6f753b1d3becc071129e8e6e2c4" - integrity sha512-rNdaxpStWP+2yG2sQZPXIuHyl9RB3K1AuM4LVH4z207HWdOLYubqmfCJF7aQrhEXuHcOsU675D5BFQ6xcNhv1Q== + version "2.6.1" + resolved "https://registry.yarnpkg.com/@apollo/gateway/-/gateway-2.6.1.tgz#2aa56fc7902950a981e34cffdce11290c680a0af" + integrity sha512-lpTw1u8HAcG3ZIFhL/fYHyd+H4sgbK9K/RPaTpqaYReNCYYj0HRqUl5HrU3mhFbCEbyTyXZzVugIu6aDoTNvaQ== dependencies: - "@apollo/composition" "2.4.1" - "@apollo/federation-internals" "2.4.1" - "@apollo/query-planner" "2.4.1" + "@apollo/composition" "2.6.1" + "@apollo/federation-internals" "2.6.1" + "@apollo/query-planner" "2.6.1" "@apollo/server-gateway-interface" "^1.1.0" "@apollo/usage-reporting-protobuf" "^4.1.0" "@apollo/utils.createhash" "^2.0.0" @@ -169,23 +169,23 @@ "@types/long" "^4.0.0" long "^4.0.0" -"@apollo/query-graphs@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@apollo/query-graphs/-/query-graphs-2.4.1.tgz#baa41a1c0e1ba31f692eb27d3087e299908fb476" - integrity sha512-14dnaZ3DnY/dLOsOI19BQkfkda3cxPmen+t8S/LqAOHMDky/vrgccoDIb3hc7TSlM08EJgRQiVytRjdhQzS3yQ== +"@apollo/query-graphs@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@apollo/query-graphs/-/query-graphs-2.6.1.tgz#235226a8f1f2376c0327098af498785e31d32ffb" + integrity sha512-7D7Rxcmy1/bQ7ZkTFocy+MbG1rhCEyrhCZj8WW5zgwMp0qfr/PCQxB1/fhU4pxdugXWZsgPlNi5o7O8iqeYKxQ== dependencies: - "@apollo/federation-internals" "2.4.1" + "@apollo/federation-internals" "2.6.1" deep-equal "^2.0.5" ts-graphviz "^1.5.4" uuid "^9.0.0" -"@apollo/query-planner@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@apollo/query-planner/-/query-planner-2.4.1.tgz#aff80b92602eb7d9e33d8423d7aa38d4ab81a97e" - integrity sha512-s16fb5/AZMhxdjuNI/Jm1YDRWFZSeSOuNn72YOdnZzkdC1OUNsuc3y8BDn9QY6PK9UK4c6gK4seWOb3HYkxDfQ== +"@apollo/query-planner@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@apollo/query-planner/-/query-planner-2.6.1.tgz#c526734a751432eb83ac3b46883c1b6c4c0813d0" + integrity sha512-BdpqxyRS2ags3suVp9+mmxgK47Vv1SUKfqqCD4O4UO2dIzDadZBM6PzvIan0mGZeVFyZ9dyzM3TZ59Nhik5Rqg== dependencies: - "@apollo/federation-internals" "2.4.1" - "@apollo/query-graphs" "2.4.1" + "@apollo/federation-internals" "2.6.1" + "@apollo/query-graphs" "2.6.1" "@apollo/utils.keyvaluecache" "^2.1.0" chalk "^4.1.0" deep-equal "^2.0.5" @@ -973,6 +973,55 @@ tslib "^2.5.0" uuid "^8.3.2" +"@aws-sdk/client-cloudfront@^3.450.0": + version "3.462.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudfront/-/client-cloudfront-3.462.0.tgz#bc2e0f4f625f3afcb4b1d40290aaf34dfc17f10e" + integrity sha512-MtMyhhyCk2fFTXYm/XOkRguToPDYtALF9B420G7dfVxxcxkTr3EdPn4i7kbetMUy14i7V3AKpXHH6z28joa/4g== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.462.0" + "@aws-sdk/core" "3.451.0" + "@aws-sdk/credential-provider-node" "3.460.0" + "@aws-sdk/middleware-host-header" "3.460.0" + "@aws-sdk/middleware-logger" "3.460.0" + "@aws-sdk/middleware-recursion-detection" "3.460.0" + "@aws-sdk/middleware-signing" "3.461.0" + "@aws-sdk/middleware-user-agent" "3.460.0" + "@aws-sdk/region-config-resolver" "3.451.0" + "@aws-sdk/types" "3.460.0" + "@aws-sdk/util-endpoints" "3.460.0" + "@aws-sdk/util-user-agent-browser" "3.460.0" + "@aws-sdk/util-user-agent-node" "3.460.0" + "@aws-sdk/xml-builder" "3.310.0" + "@smithy/config-resolver" "^2.0.18" + "@smithy/fetch-http-handler" "^2.2.6" + "@smithy/hash-node" "^2.0.15" + "@smithy/invalid-dependency" "^2.0.13" + "@smithy/middleware-content-length" "^2.0.15" + "@smithy/middleware-endpoint" "^2.2.0" + "@smithy/middleware-retry" "^2.0.20" + "@smithy/middleware-serde" "^2.0.13" + "@smithy/middleware-stack" "^2.0.7" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/node-http-handler" "^2.1.9" + "@smithy/protocol-http" "^3.0.9" + "@smithy/smithy-client" "^2.1.15" + "@smithy/types" "^2.5.0" + "@smithy/url-parser" "^2.0.13" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.19" + "@smithy/util-defaults-mode-node" "^2.0.25" + "@smithy/util-endpoints" "^1.0.4" + "@smithy/util-retry" "^2.0.6" + "@smithy/util-stream" "^2.0.20" + "@smithy/util-utf8" "^2.0.2" + "@smithy/util-waiter" "^2.0.13" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + "@aws-sdk/client-cloudwatch-logs@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.6.1.tgz#5e8dba495a2ba9a901b0a1a2d53edef8bd452398" @@ -1011,48 +1060,48 @@ tslib "^2.0.0" "@aws-sdk/client-cognito-identity-provider@^3.202.0", "@aws-sdk/client-cognito-identity-provider@^3.226.0": - version "3.441.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.441.0.tgz#2480745957cf70f60a60f85fe29ed6a981907a17" - integrity sha512-ycRa91qPP4ruRe6MI5Clniiw2J6x/IRMaGnJuoZxIC68mhwBFYb9BYNUpxj1bih9TEW21s3mdX4J/Exp9sQiuw== + version "3.476.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.476.0.tgz#3c2d51dc0dc31089f48f9803934926f2c77b0e95" + integrity sha512-JM3M7i1NFQQYGxYpNTOJ/0jGkVJtwOGo2zQ7XlWX2SWlKXlGMjqiQ2Mv0wfvEWruQNtV1qBUQdQFow0sACFCsg== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/client-sts" "3.441.0" - "@aws-sdk/core" "3.441.0" - "@aws-sdk/credential-provider-node" "3.441.0" - "@aws-sdk/middleware-host-header" "3.433.0" - "@aws-sdk/middleware-logger" "3.433.0" - "@aws-sdk/middleware-recursion-detection" "3.433.0" - "@aws-sdk/middleware-signing" "3.433.0" - "@aws-sdk/middleware-user-agent" "3.438.0" - "@aws-sdk/region-config-resolver" "3.433.0" - "@aws-sdk/types" "3.433.0" - "@aws-sdk/util-endpoints" "3.438.0" - "@aws-sdk/util-user-agent-browser" "3.433.0" - "@aws-sdk/util-user-agent-node" "3.437.0" - "@smithy/config-resolver" "^2.0.16" - "@smithy/fetch-http-handler" "^2.2.4" - "@smithy/hash-node" "^2.0.12" - "@smithy/invalid-dependency" "^2.0.12" - "@smithy/middleware-content-length" "^2.0.14" - "@smithy/middleware-endpoint" "^2.1.3" - "@smithy/middleware-retry" "^2.0.18" - "@smithy/middleware-serde" "^2.0.12" - "@smithy/middleware-stack" "^2.0.6" - "@smithy/node-config-provider" "^2.1.3" - "@smithy/node-http-handler" "^2.1.8" - "@smithy/protocol-http" "^3.0.8" - "@smithy/smithy-client" "^2.1.12" - "@smithy/types" "^2.4.0" - "@smithy/url-parser" "^2.0.12" - "@smithy/util-base64" "^2.0.0" - "@smithy/util-body-length-browser" "^2.0.0" + "@aws-sdk/client-sts" "3.476.0" + "@aws-sdk/core" "3.476.0" + "@aws-sdk/credential-provider-node" "3.476.0" + "@aws-sdk/middleware-host-header" "3.468.0" + "@aws-sdk/middleware-logger" "3.468.0" + "@aws-sdk/middleware-recursion-detection" "3.468.0" + "@aws-sdk/middleware-signing" "3.468.0" + "@aws-sdk/middleware-user-agent" "3.470.0" + "@aws-sdk/region-config-resolver" "3.470.0" + "@aws-sdk/types" "3.468.0" + "@aws-sdk/util-endpoints" "3.470.0" + "@aws-sdk/util-user-agent-browser" "3.468.0" + "@aws-sdk/util-user-agent-node" "3.470.0" + "@smithy/config-resolver" "^2.0.21" + "@smithy/fetch-http-handler" "^2.3.1" + "@smithy/hash-node" "^2.0.17" + "@smithy/invalid-dependency" "^2.0.15" + "@smithy/middleware-content-length" "^2.0.17" + "@smithy/middleware-endpoint" "^2.2.3" + "@smithy/middleware-retry" "^2.0.24" + "@smithy/middleware-serde" "^2.0.15" + "@smithy/middleware-stack" "^2.0.9" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/node-http-handler" "^2.2.1" + "@smithy/protocol-http" "^3.0.11" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + "@smithy/url-parser" "^2.0.15" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.1" "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.16" - "@smithy/util-defaults-mode-node" "^2.0.21" - "@smithy/util-endpoints" "^1.0.2" - "@smithy/util-retry" "^2.0.5" - "@smithy/util-utf8" "^2.0.0" + "@smithy/util-defaults-mode-browser" "^2.0.22" + "@smithy/util-defaults-mode-node" "^2.0.29" + "@smithy/util-endpoints" "^1.0.7" + "@smithy/util-retry" "^2.0.8" + "@smithy/util-utf8" "^2.0.2" tslib "^2.5.0" "@aws-sdk/client-comprehend@3.6.1": @@ -1093,6 +1142,55 @@ tslib "^2.0.0" uuid "^3.0.0" +"@aws-sdk/client-ec2@^3.441.0": + version "3.441.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-ec2/-/client-ec2-3.441.0.tgz#68e840759acb14db94c2ca1e6b40f81b224db112" + integrity sha512-McCx6xHOtBMnGYtpDI1O+MwnipI7Ck705XPPtf30jmhnPJk5oGi9Gnp9wWmOIPfog4R7t7wUwUr49BYCymsigQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.441.0" + "@aws-sdk/core" "3.441.0" + "@aws-sdk/credential-provider-node" "3.441.0" + "@aws-sdk/middleware-host-header" "3.433.0" + "@aws-sdk/middleware-logger" "3.433.0" + "@aws-sdk/middleware-recursion-detection" "3.433.0" + "@aws-sdk/middleware-sdk-ec2" "3.433.0" + "@aws-sdk/middleware-signing" "3.433.0" + "@aws-sdk/middleware-user-agent" "3.438.0" + "@aws-sdk/region-config-resolver" "3.433.0" + "@aws-sdk/types" "3.433.0" + "@aws-sdk/util-endpoints" "3.438.0" + "@aws-sdk/util-user-agent-browser" "3.433.0" + "@aws-sdk/util-user-agent-node" "3.437.0" + "@smithy/config-resolver" "^2.0.16" + "@smithy/fetch-http-handler" "^2.2.4" + "@smithy/hash-node" "^2.0.12" + "@smithy/invalid-dependency" "^2.0.12" + "@smithy/middleware-content-length" "^2.0.14" + "@smithy/middleware-endpoint" "^2.1.3" + "@smithy/middleware-retry" "^2.0.18" + "@smithy/middleware-serde" "^2.0.12" + "@smithy/middleware-stack" "^2.0.6" + "@smithy/node-config-provider" "^2.1.3" + "@smithy/node-http-handler" "^2.1.8" + "@smithy/protocol-http" "^3.0.8" + "@smithy/smithy-client" "^2.1.12" + "@smithy/types" "^2.4.0" + "@smithy/url-parser" "^2.0.12" + "@smithy/util-base64" "^2.0.0" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.16" + "@smithy/util-defaults-mode-node" "^2.0.21" + "@smithy/util-endpoints" "^1.0.2" + "@smithy/util-retry" "^2.0.5" + "@smithy/util-utf8" "^2.0.0" + "@smithy/util-waiter" "^2.0.12" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + uuid "^8.3.2" + "@aws-sdk/client-firehose@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/client-firehose/-/client-firehose-3.6.1.tgz#87a8ef0c18267907b3ce712e6d3de8f36b0a7c7b" @@ -1172,50 +1270,53 @@ tslib "^2.0.0" "@aws-sdk/client-lambda@^3.226.0", "@aws-sdk/client-lambda@^3.241.0": - version "3.382.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-lambda/-/client-lambda-3.382.0.tgz#a3b269808c342e5d55e345e04b71f71b122194ed" - integrity sha512-g/JcxMk0ntJYe8xIpcKNQ12yPbYM2H1bB0xmVs0BsEdAnnKEwmQc1+Sau2gz8BY9vVEyUodDxnDi0hW0qBBKBg== + version "3.454.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-lambda/-/client-lambda-3.454.0.tgz#d19716a95abe0d8dac7801e86a0e032fc85c41b6" + integrity sha512-nYak+ojl0H0AG0WTF2894npak4Uj2slBr09+3lBUz4rwPol93TsUHy8/5GfGLcqPMNnEKOknc4jioJOK7cb2Pw== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/client-sts" "3.382.0" - "@aws-sdk/credential-provider-node" "3.382.0" - "@aws-sdk/middleware-host-header" "3.379.1" - "@aws-sdk/middleware-logger" "3.378.0" - "@aws-sdk/middleware-recursion-detection" "3.378.0" - "@aws-sdk/middleware-signing" "3.379.1" - "@aws-sdk/middleware-user-agent" "3.382.0" - "@aws-sdk/types" "3.378.0" - "@aws-sdk/util-endpoints" "3.382.0" - "@aws-sdk/util-user-agent-browser" "3.378.0" - "@aws-sdk/util-user-agent-node" "3.378.0" - "@smithy/config-resolver" "^2.0.1" - "@smithy/eventstream-serde-browser" "^2.0.1" - "@smithy/eventstream-serde-config-resolver" "^2.0.1" - "@smithy/eventstream-serde-node" "^2.0.1" - "@smithy/fetch-http-handler" "^2.0.1" - "@smithy/hash-node" "^2.0.1" - "@smithy/invalid-dependency" "^2.0.1" - "@smithy/middleware-content-length" "^2.0.1" - "@smithy/middleware-endpoint" "^2.0.1" - "@smithy/middleware-retry" "^2.0.1" - "@smithy/middleware-serde" "^2.0.1" - "@smithy/middleware-stack" "^2.0.0" - "@smithy/node-config-provider" "^2.0.1" - "@smithy/node-http-handler" "^2.0.1" - "@smithy/protocol-http" "^2.0.1" - "@smithy/smithy-client" "^2.0.1" - "@smithy/types" "^2.0.2" - "@smithy/url-parser" "^2.0.1" - "@smithy/util-base64" "^2.0.0" + "@aws-sdk/client-sts" "3.454.0" + "@aws-sdk/core" "3.451.0" + "@aws-sdk/credential-provider-node" "3.451.0" + "@aws-sdk/middleware-host-header" "3.451.0" + "@aws-sdk/middleware-logger" "3.451.0" + "@aws-sdk/middleware-recursion-detection" "3.451.0" + "@aws-sdk/middleware-signing" "3.451.0" + "@aws-sdk/middleware-user-agent" "3.451.0" + "@aws-sdk/region-config-resolver" "3.451.0" + "@aws-sdk/types" "3.451.0" + "@aws-sdk/util-endpoints" "3.451.0" + "@aws-sdk/util-user-agent-browser" "3.451.0" + "@aws-sdk/util-user-agent-node" "3.451.0" + "@smithy/config-resolver" "^2.0.18" + "@smithy/eventstream-serde-browser" "^2.0.13" + "@smithy/eventstream-serde-config-resolver" "^2.0.13" + "@smithy/eventstream-serde-node" "^2.0.13" + "@smithy/fetch-http-handler" "^2.2.6" + "@smithy/hash-node" "^2.0.15" + "@smithy/invalid-dependency" "^2.0.13" + "@smithy/middleware-content-length" "^2.0.15" + "@smithy/middleware-endpoint" "^2.2.0" + "@smithy/middleware-retry" "^2.0.20" + "@smithy/middleware-serde" "^2.0.13" + "@smithy/middleware-stack" "^2.0.7" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/node-http-handler" "^2.1.9" + "@smithy/protocol-http" "^3.0.9" + "@smithy/smithy-client" "^2.1.15" + "@smithy/types" "^2.5.0" + "@smithy/url-parser" "^2.0.13" + "@smithy/util-base64" "^2.0.1" "@smithy/util-body-length-browser" "^2.0.0" - "@smithy/util-body-length-node" "^2.0.0" - "@smithy/util-defaults-mode-browser" "^2.0.1" - "@smithy/util-defaults-mode-node" "^2.0.1" - "@smithy/util-retry" "^2.0.0" - "@smithy/util-stream" "^2.0.1" - "@smithy/util-utf8" "^2.0.0" - "@smithy/util-waiter" "^2.0.1" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.19" + "@smithy/util-defaults-mode-node" "^2.0.25" + "@smithy/util-endpoints" "^1.0.4" + "@smithy/util-retry" "^2.0.6" + "@smithy/util-stream" "^2.0.20" + "@smithy/util-utf8" "^2.0.2" + "@smithy/util-waiter" "^2.0.13" tslib "^2.5.0" "@aws-sdk/client-lex-runtime-service@3.186.3": @@ -1723,48 +1824,96 @@ tslib "^2.5.0" uuid "^8.3.2" -"@aws-sdk/client-ses@^3.226.0": - version "3.427.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-ses/-/client-ses-3.427.0.tgz#e3992274427c8ac65dc0a1b04779b0b79ccb4a48" - integrity sha512-4jZWMiNK3wredVxXGsAt57puuuEYC18AIfaRmm3NRaKdQ3DOOJY6wIB/A5LqfilRPQE2HSNtNk2+dzHKCXa76w== +"@aws-sdk/client-secrets-manager@^3.441.0": + version "3.441.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.441.0.tgz#0824e0453d63515c0309794bd5c6652ed8cbf881" + integrity sha512-XKXO2qGWA9lRz1jykIfl5Lz5g3IRoa+NBnQDXFnTSzw52oOUAoyYOcpN0JL9/5ga1q9UxJtq2Mk3yKlIeRbxHQ== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/client-sts" "3.427.0" - "@aws-sdk/credential-provider-node" "3.427.0" - "@aws-sdk/middleware-host-header" "3.425.0" - "@aws-sdk/middleware-logger" "3.425.0" - "@aws-sdk/middleware-recursion-detection" "3.425.0" - "@aws-sdk/middleware-signing" "3.425.0" - "@aws-sdk/middleware-user-agent" "3.427.0" - "@aws-sdk/region-config-resolver" "3.425.0" - "@aws-sdk/types" "3.425.0" - "@aws-sdk/util-endpoints" "3.427.0" - "@aws-sdk/util-user-agent-browser" "3.425.0" - "@aws-sdk/util-user-agent-node" "3.425.0" - "@smithy/config-resolver" "^2.0.11" - "@smithy/fetch-http-handler" "^2.2.1" - "@smithy/hash-node" "^2.0.10" - "@smithy/invalid-dependency" "^2.0.10" - "@smithy/middleware-content-length" "^2.0.12" - "@smithy/middleware-endpoint" "^2.0.10" - "@smithy/middleware-retry" "^2.0.13" - "@smithy/middleware-serde" "^2.0.10" - "@smithy/middleware-stack" "^2.0.4" - "@smithy/node-config-provider" "^2.0.13" - "@smithy/node-http-handler" "^2.1.6" - "@smithy/protocol-http" "^3.0.6" - "@smithy/smithy-client" "^2.1.9" - "@smithy/types" "^2.3.4" - "@smithy/url-parser" "^2.0.10" + "@aws-sdk/client-sts" "3.441.0" + "@aws-sdk/core" "3.441.0" + "@aws-sdk/credential-provider-node" "3.441.0" + "@aws-sdk/middleware-host-header" "3.433.0" + "@aws-sdk/middleware-logger" "3.433.0" + "@aws-sdk/middleware-recursion-detection" "3.433.0" + "@aws-sdk/middleware-signing" "3.433.0" + "@aws-sdk/middleware-user-agent" "3.438.0" + "@aws-sdk/region-config-resolver" "3.433.0" + "@aws-sdk/types" "3.433.0" + "@aws-sdk/util-endpoints" "3.438.0" + "@aws-sdk/util-user-agent-browser" "3.433.0" + "@aws-sdk/util-user-agent-node" "3.437.0" + "@smithy/config-resolver" "^2.0.16" + "@smithy/fetch-http-handler" "^2.2.4" + "@smithy/hash-node" "^2.0.12" + "@smithy/invalid-dependency" "^2.0.12" + "@smithy/middleware-content-length" "^2.0.14" + "@smithy/middleware-endpoint" "^2.1.3" + "@smithy/middleware-retry" "^2.0.18" + "@smithy/middleware-serde" "^2.0.12" + "@smithy/middleware-stack" "^2.0.6" + "@smithy/node-config-provider" "^2.1.3" + "@smithy/node-http-handler" "^2.1.8" + "@smithy/protocol-http" "^3.0.8" + "@smithy/smithy-client" "^2.1.12" + "@smithy/types" "^2.4.0" + "@smithy/url-parser" "^2.0.12" "@smithy/util-base64" "^2.0.0" "@smithy/util-body-length-browser" "^2.0.0" "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.13" - "@smithy/util-defaults-mode-node" "^2.0.15" - "@smithy/util-retry" "^2.0.3" + "@smithy/util-defaults-mode-browser" "^2.0.16" + "@smithy/util-defaults-mode-node" "^2.0.21" + "@smithy/util-endpoints" "^1.0.2" + "@smithy/util-retry" "^2.0.5" "@smithy/util-utf8" "^2.0.0" - "@smithy/util-waiter" "^2.0.10" + tslib "^2.5.0" + uuid "^8.3.2" + +"@aws-sdk/client-ses@^3.226.0": + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-ses/-/client-ses-3.473.0.tgz#38ef2d74c46b0ba770008a1c234aa43be5934ee0" + integrity sha512-oE3e3PIAlQV+9vhk+ALNPZS84C+xgOtUQ9FHXjmMJBNGvQbJclxRquqjm/7AxADlOrzaX8YmPAUSxyrY3FqSxg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.473.0" + "@aws-sdk/core" "3.468.0" + "@aws-sdk/credential-provider-node" "3.470.0" + "@aws-sdk/middleware-host-header" "3.468.0" + "@aws-sdk/middleware-logger" "3.468.0" + "@aws-sdk/middleware-recursion-detection" "3.468.0" + "@aws-sdk/middleware-signing" "3.468.0" + "@aws-sdk/middleware-user-agent" "3.470.0" + "@aws-sdk/region-config-resolver" "3.470.0" + "@aws-sdk/types" "3.468.0" + "@aws-sdk/util-endpoints" "3.470.0" + "@aws-sdk/util-user-agent-browser" "3.468.0" + "@aws-sdk/util-user-agent-node" "3.470.0" + "@smithy/config-resolver" "^2.0.21" + "@smithy/fetch-http-handler" "^2.3.1" + "@smithy/hash-node" "^2.0.17" + "@smithy/invalid-dependency" "^2.0.15" + "@smithy/middleware-content-length" "^2.0.17" + "@smithy/middleware-endpoint" "^2.2.3" + "@smithy/middleware-retry" "^2.0.24" + "@smithy/middleware-serde" "^2.0.15" + "@smithy/middleware-stack" "^2.0.9" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/node-http-handler" "^2.2.1" + "@smithy/protocol-http" "^3.0.11" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + "@smithy/url-parser" "^2.0.15" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.1" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.22" + "@smithy/util-defaults-mode-node" "^2.0.29" + "@smithy/util-endpoints" "^1.0.7" + "@smithy/util-retry" "^2.0.8" + "@smithy/util-utf8" "^2.0.2" + "@smithy/util-waiter" "^2.0.15" fast-xml-parser "4.2.5" tslib "^2.5.0" @@ -2316,46 +2465,6 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.5.0" -"@aws-sdk/client-sso@3.427.0": - version "3.427.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.427.0.tgz#852f0bb00c7bc5e3d3c8751a6ff4e86a1484726f" - integrity sha512-sFVFEmsQ1rmgYO1SgrOTxE/MTKpeE4hpOkm1WqhLQK7Ij136vXpjCxjH1JYZiHiUzO1wr9t4ex4dlB5J3VS/Xg== - dependencies: - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/middleware-host-header" "3.425.0" - "@aws-sdk/middleware-logger" "3.425.0" - "@aws-sdk/middleware-recursion-detection" "3.425.0" - "@aws-sdk/middleware-user-agent" "3.427.0" - "@aws-sdk/region-config-resolver" "3.425.0" - "@aws-sdk/types" "3.425.0" - "@aws-sdk/util-endpoints" "3.427.0" - "@aws-sdk/util-user-agent-browser" "3.425.0" - "@aws-sdk/util-user-agent-node" "3.425.0" - "@smithy/config-resolver" "^2.0.11" - "@smithy/fetch-http-handler" "^2.2.1" - "@smithy/hash-node" "^2.0.10" - "@smithy/invalid-dependency" "^2.0.10" - "@smithy/middleware-content-length" "^2.0.12" - "@smithy/middleware-endpoint" "^2.0.10" - "@smithy/middleware-retry" "^2.0.13" - "@smithy/middleware-serde" "^2.0.10" - "@smithy/middleware-stack" "^2.0.4" - "@smithy/node-config-provider" "^2.0.13" - "@smithy/node-http-handler" "^2.1.6" - "@smithy/protocol-http" "^3.0.6" - "@smithy/smithy-client" "^2.1.9" - "@smithy/types" "^2.3.4" - "@smithy/url-parser" "^2.0.10" - "@smithy/util-base64" "^2.0.0" - "@smithy/util-body-length-browser" "^2.0.0" - "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.13" - "@smithy/util-defaults-mode-node" "^2.0.15" - "@smithy/util-retry" "^2.0.3" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" - "@aws-sdk/client-sso@3.441.0": version "3.441.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.441.0.tgz#4e35b42bdaf4f10f60d4d1f697f39d67635b467c" @@ -2440,6 +2549,174 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.5.0" +"@aws-sdk/client-sso@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.451.0.tgz#d52b961efa707b6579821942801145a2e1be8121" + integrity sha512-KkYSke3Pdv3MfVH/5fT528+MKjMyPKlcLcd4zQb0x6/7Bl7EHrPh1JZYjzPLHelb+UY5X0qN8+cb8iSu1eiwIQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/core" "3.451.0" + "@aws-sdk/middleware-host-header" "3.451.0" + "@aws-sdk/middleware-logger" "3.451.0" + "@aws-sdk/middleware-recursion-detection" "3.451.0" + "@aws-sdk/middleware-user-agent" "3.451.0" + "@aws-sdk/region-config-resolver" "3.451.0" + "@aws-sdk/types" "3.451.0" + "@aws-sdk/util-endpoints" "3.451.0" + "@aws-sdk/util-user-agent-browser" "3.451.0" + "@aws-sdk/util-user-agent-node" "3.451.0" + "@smithy/config-resolver" "^2.0.18" + "@smithy/fetch-http-handler" "^2.2.6" + "@smithy/hash-node" "^2.0.15" + "@smithy/invalid-dependency" "^2.0.13" + "@smithy/middleware-content-length" "^2.0.15" + "@smithy/middleware-endpoint" "^2.2.0" + "@smithy/middleware-retry" "^2.0.20" + "@smithy/middleware-serde" "^2.0.13" + "@smithy/middleware-stack" "^2.0.7" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/node-http-handler" "^2.1.9" + "@smithy/protocol-http" "^3.0.9" + "@smithy/smithy-client" "^2.1.15" + "@smithy/types" "^2.5.0" + "@smithy/url-parser" "^2.0.13" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.19" + "@smithy/util-defaults-mode-node" "^2.0.25" + "@smithy/util-endpoints" "^1.0.4" + "@smithy/util-retry" "^2.0.6" + "@smithy/util-utf8" "^2.0.2" + tslib "^2.5.0" + +"@aws-sdk/client-sso@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.460.0.tgz#3eeb38eebcecada1153399c598527d1f12c8f0b2" + integrity sha512-p5D9C8LKJs5yoBn5cCs2Wqzrp5YP5BYcP774bhGMFEu/LCIUyWzudwN3+/AObSiq8R8SSvBY2zQD4h+k3NjgTQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/core" "3.451.0" + "@aws-sdk/middleware-host-header" "3.460.0" + "@aws-sdk/middleware-logger" "3.460.0" + "@aws-sdk/middleware-recursion-detection" "3.460.0" + "@aws-sdk/middleware-user-agent" "3.460.0" + "@aws-sdk/region-config-resolver" "3.451.0" + "@aws-sdk/types" "3.460.0" + "@aws-sdk/util-endpoints" "3.460.0" + "@aws-sdk/util-user-agent-browser" "3.460.0" + "@aws-sdk/util-user-agent-node" "3.460.0" + "@smithy/config-resolver" "^2.0.18" + "@smithy/fetch-http-handler" "^2.2.6" + "@smithy/hash-node" "^2.0.15" + "@smithy/invalid-dependency" "^2.0.13" + "@smithy/middleware-content-length" "^2.0.15" + "@smithy/middleware-endpoint" "^2.2.0" + "@smithy/middleware-retry" "^2.0.20" + "@smithy/middleware-serde" "^2.0.13" + "@smithy/middleware-stack" "^2.0.7" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/node-http-handler" "^2.1.9" + "@smithy/protocol-http" "^3.0.9" + "@smithy/smithy-client" "^2.1.15" + "@smithy/types" "^2.5.0" + "@smithy/url-parser" "^2.0.13" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.19" + "@smithy/util-defaults-mode-node" "^2.0.25" + "@smithy/util-endpoints" "^1.0.4" + "@smithy/util-retry" "^2.0.6" + "@smithy/util-utf8" "^2.0.2" + tslib "^2.5.0" + +"@aws-sdk/client-sso@3.470.0": + version "3.470.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.470.0.tgz#2fab6cc63af15a5dccbd985d784e49a3a3c634b4" + integrity sha512-iMXqdXuypE3OK0rggbvSz7vBGlLDG418dNidHhdaeLluMTG/GfHbh1fLOlavhYxRwrsPrtYvFiVkxXFGzXva4w== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/core" "3.468.0" + "@aws-sdk/middleware-host-header" "3.468.0" + "@aws-sdk/middleware-logger" "3.468.0" + "@aws-sdk/middleware-recursion-detection" "3.468.0" + "@aws-sdk/middleware-user-agent" "3.470.0" + "@aws-sdk/region-config-resolver" "3.470.0" + "@aws-sdk/types" "3.468.0" + "@aws-sdk/util-endpoints" "3.470.0" + "@aws-sdk/util-user-agent-browser" "3.468.0" + "@aws-sdk/util-user-agent-node" "3.470.0" + "@smithy/config-resolver" "^2.0.21" + "@smithy/fetch-http-handler" "^2.3.1" + "@smithy/hash-node" "^2.0.17" + "@smithy/invalid-dependency" "^2.0.15" + "@smithy/middleware-content-length" "^2.0.17" + "@smithy/middleware-endpoint" "^2.2.3" + "@smithy/middleware-retry" "^2.0.24" + "@smithy/middleware-serde" "^2.0.15" + "@smithy/middleware-stack" "^2.0.9" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/node-http-handler" "^2.2.1" + "@smithy/protocol-http" "^3.0.11" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + "@smithy/url-parser" "^2.0.15" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.1" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.22" + "@smithy/util-defaults-mode-node" "^2.0.29" + "@smithy/util-endpoints" "^1.0.7" + "@smithy/util-retry" "^2.0.8" + "@smithy/util-utf8" "^2.0.2" + tslib "^2.5.0" + +"@aws-sdk/client-sso@3.476.0": + version "3.476.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.476.0.tgz#48e9e0808438b8cfb26ef170b834dc17a7dda0c2" + integrity sha512-vcGGumQplAtzOhg3MbYmktl69v7BXGtzfpiw4w7i0KjBy/QBy0vt6xQpS7H/24s17/kCw+UXlZR6sFQ/Vj73ag== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/core" "3.476.0" + "@aws-sdk/middleware-host-header" "3.468.0" + "@aws-sdk/middleware-logger" "3.468.0" + "@aws-sdk/middleware-recursion-detection" "3.468.0" + "@aws-sdk/middleware-user-agent" "3.470.0" + "@aws-sdk/region-config-resolver" "3.470.0" + "@aws-sdk/types" "3.468.0" + "@aws-sdk/util-endpoints" "3.470.0" + "@aws-sdk/util-user-agent-browser" "3.468.0" + "@aws-sdk/util-user-agent-node" "3.470.0" + "@smithy/config-resolver" "^2.0.21" + "@smithy/fetch-http-handler" "^2.3.1" + "@smithy/hash-node" "^2.0.17" + "@smithy/invalid-dependency" "^2.0.15" + "@smithy/middleware-content-length" "^2.0.17" + "@smithy/middleware-endpoint" "^2.2.3" + "@smithy/middleware-retry" "^2.0.24" + "@smithy/middleware-serde" "^2.0.15" + "@smithy/middleware-stack" "^2.0.9" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/node-http-handler" "^2.2.1" + "@smithy/protocol-http" "^3.0.11" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + "@smithy/url-parser" "^2.0.15" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.1" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.22" + "@smithy/util-defaults-mode-node" "^2.0.29" + "@smithy/util-endpoints" "^1.0.7" + "@smithy/util-retry" "^2.0.8" + "@smithy/util-utf8" "^2.0.2" + tslib "^2.5.0" + "@aws-sdk/client-sts@3.186.3": version "3.186.3" resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.186.3.tgz#1c12355cb9d3cadc64ab74c91c3d57515680dfbd" @@ -2738,59 +3015,61 @@ fast-xml-parser "4.2.5" tslib "^2.5.0" -"@aws-sdk/client-sts@3.427.0": - version "3.427.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.427.0.tgz#839df8e1aa8795ffbffc7f5d79ccbc6a1220ab33" - integrity sha512-le2wLJKILyWuRfPz2HbyaNtu5kEki+ojUkTqCU6FPDRrqUvEkaaCBH9Awo/2AtrCfRkiobop8RuTTj6cAnpiJg== +"@aws-sdk/client-sts@3.441.0": + version "3.441.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.441.0.tgz#9fcc8ece0274e53fc4234e97d7091f1afe2ade43" + integrity sha512-GL0Cw2v7XL1cn0T+Sk5VHLlgBJoUdMsysXsHa1mFdk0l6XHMAAnwXVXiNnjmoDSPrG0psz7dL2AKzPVRXbIUjA== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/credential-provider-node" "3.427.0" - "@aws-sdk/middleware-host-header" "3.425.0" - "@aws-sdk/middleware-logger" "3.425.0" - "@aws-sdk/middleware-recursion-detection" "3.425.0" - "@aws-sdk/middleware-sdk-sts" "3.425.0" - "@aws-sdk/middleware-signing" "3.425.0" - "@aws-sdk/middleware-user-agent" "3.427.0" - "@aws-sdk/region-config-resolver" "3.425.0" - "@aws-sdk/types" "3.425.0" - "@aws-sdk/util-endpoints" "3.427.0" - "@aws-sdk/util-user-agent-browser" "3.425.0" - "@aws-sdk/util-user-agent-node" "3.425.0" - "@smithy/config-resolver" "^2.0.11" - "@smithy/fetch-http-handler" "^2.2.1" - "@smithy/hash-node" "^2.0.10" - "@smithy/invalid-dependency" "^2.0.10" - "@smithy/middleware-content-length" "^2.0.12" - "@smithy/middleware-endpoint" "^2.0.10" - "@smithy/middleware-retry" "^2.0.13" - "@smithy/middleware-serde" "^2.0.10" - "@smithy/middleware-stack" "^2.0.4" - "@smithy/node-config-provider" "^2.0.13" - "@smithy/node-http-handler" "^2.1.6" - "@smithy/protocol-http" "^3.0.6" - "@smithy/smithy-client" "^2.1.9" - "@smithy/types" "^2.3.4" - "@smithy/url-parser" "^2.0.10" + "@aws-sdk/core" "3.441.0" + "@aws-sdk/credential-provider-node" "3.441.0" + "@aws-sdk/middleware-host-header" "3.433.0" + "@aws-sdk/middleware-logger" "3.433.0" + "@aws-sdk/middleware-recursion-detection" "3.433.0" + "@aws-sdk/middleware-sdk-sts" "3.433.0" + "@aws-sdk/middleware-signing" "3.433.0" + "@aws-sdk/middleware-user-agent" "3.438.0" + "@aws-sdk/region-config-resolver" "3.433.0" + "@aws-sdk/types" "3.433.0" + "@aws-sdk/util-endpoints" "3.438.0" + "@aws-sdk/util-user-agent-browser" "3.433.0" + "@aws-sdk/util-user-agent-node" "3.437.0" + "@smithy/config-resolver" "^2.0.16" + "@smithy/fetch-http-handler" "^2.2.4" + "@smithy/hash-node" "^2.0.12" + "@smithy/invalid-dependency" "^2.0.12" + "@smithy/middleware-content-length" "^2.0.14" + "@smithy/middleware-endpoint" "^2.1.3" + "@smithy/middleware-retry" "^2.0.18" + "@smithy/middleware-serde" "^2.0.12" + "@smithy/middleware-stack" "^2.0.6" + "@smithy/node-config-provider" "^2.1.3" + "@smithy/node-http-handler" "^2.1.8" + "@smithy/protocol-http" "^3.0.8" + "@smithy/smithy-client" "^2.1.12" + "@smithy/types" "^2.4.0" + "@smithy/url-parser" "^2.0.12" "@smithy/util-base64" "^2.0.0" "@smithy/util-body-length-browser" "^2.0.0" "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.13" - "@smithy/util-defaults-mode-node" "^2.0.15" - "@smithy/util-retry" "^2.0.3" + "@smithy/util-defaults-mode-browser" "^2.0.16" + "@smithy/util-defaults-mode-node" "^2.0.21" + "@smithy/util-endpoints" "^1.0.2" + "@smithy/util-retry" "^2.0.5" "@smithy/util-utf8" "^2.0.0" fast-xml-parser "4.2.5" tslib "^2.5.0" -"@aws-sdk/client-sts@3.441.0": - version "3.441.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.441.0.tgz#9fcc8ece0274e53fc4234e97d7091f1afe2ade43" - integrity sha512-GL0Cw2v7XL1cn0T+Sk5VHLlgBJoUdMsysXsHa1mFdk0l6XHMAAnwXVXiNnjmoDSPrG0psz7dL2AKzPVRXbIUjA== +"@aws-sdk/client-sts@3.445.0": + version "3.445.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.445.0.tgz#1286ba3702997ae00cb28eca890116c63a451526" + integrity sha512-ogbdqrS8x9O5BTot826iLnTQ6i4/F5BSi/74gycneCxYmAnYnyUBNOWVnynv6XZiEWyDJQCU2UtMd52aNGW1GA== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/core" "3.441.0" - "@aws-sdk/credential-provider-node" "3.441.0" + "@aws-sdk/core" "3.445.0" + "@aws-sdk/credential-provider-node" "3.445.0" "@aws-sdk/middleware-host-header" "3.433.0" "@aws-sdk/middleware-logger" "3.433.0" "@aws-sdk/middleware-recursion-detection" "3.433.0" @@ -2828,49 +3107,187 @@ fast-xml-parser "4.2.5" tslib "^2.5.0" -"@aws-sdk/client-sts@3.445.0", "@aws-sdk/client-sts@^3.410.0": - version "3.445.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.445.0.tgz#1286ba3702997ae00cb28eca890116c63a451526" - integrity sha512-ogbdqrS8x9O5BTot826iLnTQ6i4/F5BSi/74gycneCxYmAnYnyUBNOWVnynv6XZiEWyDJQCU2UtMd52aNGW1GA== +"@aws-sdk/client-sts@3.454.0": + version "3.454.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.454.0.tgz#6106999e393c264a485fc76add374b375a2da8d5" + integrity sha512-0fDvr8WeB6IYO8BUCzcivWmahgGl/zDbaYfakzGnt4mrl5ztYaXE875WI6b7+oFcKMRvN+KLvwu5TtyFuNY+GQ== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/core" "3.445.0" - "@aws-sdk/credential-provider-node" "3.445.0" - "@aws-sdk/middleware-host-header" "3.433.0" - "@aws-sdk/middleware-logger" "3.433.0" - "@aws-sdk/middleware-recursion-detection" "3.433.0" - "@aws-sdk/middleware-sdk-sts" "3.433.0" - "@aws-sdk/middleware-signing" "3.433.0" - "@aws-sdk/middleware-user-agent" "3.438.0" - "@aws-sdk/region-config-resolver" "3.433.0" - "@aws-sdk/types" "3.433.0" - "@aws-sdk/util-endpoints" "3.438.0" - "@aws-sdk/util-user-agent-browser" "3.433.0" - "@aws-sdk/util-user-agent-node" "3.437.0" - "@smithy/config-resolver" "^2.0.16" - "@smithy/fetch-http-handler" "^2.2.4" - "@smithy/hash-node" "^2.0.12" - "@smithy/invalid-dependency" "^2.0.12" - "@smithy/middleware-content-length" "^2.0.14" - "@smithy/middleware-endpoint" "^2.1.3" - "@smithy/middleware-retry" "^2.0.18" - "@smithy/middleware-serde" "^2.0.12" - "@smithy/middleware-stack" "^2.0.6" - "@smithy/node-config-provider" "^2.1.3" - "@smithy/node-http-handler" "^2.1.8" - "@smithy/protocol-http" "^3.0.8" - "@smithy/smithy-client" "^2.1.12" - "@smithy/types" "^2.4.0" - "@smithy/url-parser" "^2.0.12" - "@smithy/util-base64" "^2.0.0" + "@aws-sdk/core" "3.451.0" + "@aws-sdk/credential-provider-node" "3.451.0" + "@aws-sdk/middleware-host-header" "3.451.0" + "@aws-sdk/middleware-logger" "3.451.0" + "@aws-sdk/middleware-recursion-detection" "3.451.0" + "@aws-sdk/middleware-sdk-sts" "3.451.0" + "@aws-sdk/middleware-signing" "3.451.0" + "@aws-sdk/middleware-user-agent" "3.451.0" + "@aws-sdk/region-config-resolver" "3.451.0" + "@aws-sdk/types" "3.451.0" + "@aws-sdk/util-endpoints" "3.451.0" + "@aws-sdk/util-user-agent-browser" "3.451.0" + "@aws-sdk/util-user-agent-node" "3.451.0" + "@smithy/config-resolver" "^2.0.18" + "@smithy/fetch-http-handler" "^2.2.6" + "@smithy/hash-node" "^2.0.15" + "@smithy/invalid-dependency" "^2.0.13" + "@smithy/middleware-content-length" "^2.0.15" + "@smithy/middleware-endpoint" "^2.2.0" + "@smithy/middleware-retry" "^2.0.20" + "@smithy/middleware-serde" "^2.0.13" + "@smithy/middleware-stack" "^2.0.7" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/node-http-handler" "^2.1.9" + "@smithy/protocol-http" "^3.0.9" + "@smithy/smithy-client" "^2.1.15" + "@smithy/types" "^2.5.0" + "@smithy/url-parser" "^2.0.13" + "@smithy/util-base64" "^2.0.1" "@smithy/util-body-length-browser" "^2.0.0" "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.16" - "@smithy/util-defaults-mode-node" "^2.0.21" - "@smithy/util-endpoints" "^1.0.2" - "@smithy/util-retry" "^2.0.5" - "@smithy/util-utf8" "^2.0.0" + "@smithy/util-defaults-mode-browser" "^2.0.19" + "@smithy/util-defaults-mode-node" "^2.0.25" + "@smithy/util-endpoints" "^1.0.4" + "@smithy/util-retry" "^2.0.6" + "@smithy/util-utf8" "^2.0.2" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + +"@aws-sdk/client-sts@3.462.0": + version "3.462.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.462.0.tgz#7168e8c29e2c3b67aca64841a72acd041c409a65" + integrity sha512-oO6SVGB9kR0dwc4T/M3++TcioBVv26cEpxZGS4BcKMDxSjkCLqJ/jE37aCNNPGTlCAhnuOAwqGjFqYrsehsI1Q== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/core" "3.451.0" + "@aws-sdk/credential-provider-node" "3.460.0" + "@aws-sdk/middleware-host-header" "3.460.0" + "@aws-sdk/middleware-logger" "3.460.0" + "@aws-sdk/middleware-recursion-detection" "3.460.0" + "@aws-sdk/middleware-sdk-sts" "3.461.0" + "@aws-sdk/middleware-signing" "3.461.0" + "@aws-sdk/middleware-user-agent" "3.460.0" + "@aws-sdk/region-config-resolver" "3.451.0" + "@aws-sdk/types" "3.460.0" + "@aws-sdk/util-endpoints" "3.460.0" + "@aws-sdk/util-user-agent-browser" "3.460.0" + "@aws-sdk/util-user-agent-node" "3.460.0" + "@smithy/config-resolver" "^2.0.18" + "@smithy/fetch-http-handler" "^2.2.6" + "@smithy/hash-node" "^2.0.15" + "@smithy/invalid-dependency" "^2.0.13" + "@smithy/middleware-content-length" "^2.0.15" + "@smithy/middleware-endpoint" "^2.2.0" + "@smithy/middleware-retry" "^2.0.20" + "@smithy/middleware-serde" "^2.0.13" + "@smithy/middleware-stack" "^2.0.7" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/node-http-handler" "^2.1.9" + "@smithy/protocol-http" "^3.0.9" + "@smithy/smithy-client" "^2.1.15" + "@smithy/types" "^2.5.0" + "@smithy/url-parser" "^2.0.13" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.19" + "@smithy/util-defaults-mode-node" "^2.0.25" + "@smithy/util-endpoints" "^1.0.4" + "@smithy/util-retry" "^2.0.6" + "@smithy/util-utf8" "^2.0.2" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + +"@aws-sdk/client-sts@3.473.0": + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.473.0.tgz#413aaa82e30b6bd49d783f85af95357a89d2a792" + integrity sha512-ttRZs+sW96cpuoVdys4KZ81yXq4c6xyhGOZIRUpi/YiwB1gnNvCEo5CDFL7PSdW/bjI2ovyUgu8EArq+7KlLwA== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/core" "3.468.0" + "@aws-sdk/credential-provider-node" "3.470.0" + "@aws-sdk/middleware-host-header" "3.468.0" + "@aws-sdk/middleware-logger" "3.468.0" + "@aws-sdk/middleware-recursion-detection" "3.468.0" + "@aws-sdk/middleware-sdk-sts" "3.468.0" + "@aws-sdk/middleware-signing" "3.468.0" + "@aws-sdk/middleware-user-agent" "3.470.0" + "@aws-sdk/region-config-resolver" "3.470.0" + "@aws-sdk/types" "3.468.0" + "@aws-sdk/util-endpoints" "3.470.0" + "@aws-sdk/util-user-agent-browser" "3.468.0" + "@aws-sdk/util-user-agent-node" "3.470.0" + "@smithy/config-resolver" "^2.0.21" + "@smithy/fetch-http-handler" "^2.3.1" + "@smithy/hash-node" "^2.0.17" + "@smithy/invalid-dependency" "^2.0.15" + "@smithy/middleware-content-length" "^2.0.17" + "@smithy/middleware-endpoint" "^2.2.3" + "@smithy/middleware-retry" "^2.0.24" + "@smithy/middleware-serde" "^2.0.15" + "@smithy/middleware-stack" "^2.0.9" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/node-http-handler" "^2.2.1" + "@smithy/protocol-http" "^3.0.11" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + "@smithy/url-parser" "^2.0.15" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.1" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.22" + "@smithy/util-defaults-mode-node" "^2.0.29" + "@smithy/util-endpoints" "^1.0.7" + "@smithy/util-retry" "^2.0.8" + "@smithy/util-utf8" "^2.0.2" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + +"@aws-sdk/client-sts@3.476.0", "@aws-sdk/client-sts@^3.410.0": + version "3.476.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.476.0.tgz#731fe4dc412a8da651689953a91086b160cd0451" + integrity sha512-duMs4tTy3hNuSdV2YFzT6QNlE0PX2RzZqAfO4dTITiEf6QZW/N3UojSZwDRTKZzH+CFKL2gjVhkv7d4ZCy5QvQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/core" "3.476.0" + "@aws-sdk/credential-provider-node" "3.476.0" + "@aws-sdk/middleware-host-header" "3.468.0" + "@aws-sdk/middleware-logger" "3.468.0" + "@aws-sdk/middleware-recursion-detection" "3.468.0" + "@aws-sdk/middleware-user-agent" "3.470.0" + "@aws-sdk/region-config-resolver" "3.470.0" + "@aws-sdk/types" "3.468.0" + "@aws-sdk/util-endpoints" "3.470.0" + "@aws-sdk/util-user-agent-browser" "3.468.0" + "@aws-sdk/util-user-agent-node" "3.470.0" + "@smithy/config-resolver" "^2.0.21" + "@smithy/core" "^1.1.0" + "@smithy/fetch-http-handler" "^2.3.1" + "@smithy/hash-node" "^2.0.17" + "@smithy/invalid-dependency" "^2.0.15" + "@smithy/middleware-content-length" "^2.0.17" + "@smithy/middleware-endpoint" "^2.2.3" + "@smithy/middleware-retry" "^2.0.24" + "@smithy/middleware-serde" "^2.0.15" + "@smithy/middleware-stack" "^2.0.9" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/node-http-handler" "^2.2.1" + "@smithy/protocol-http" "^3.0.11" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + "@smithy/url-parser" "^2.0.15" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.1" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.22" + "@smithy/util-defaults-mode-node" "^2.0.29" + "@smithy/util-endpoints" "^1.0.7" + "@smithy/util-middleware" "^2.0.8" + "@smithy/util-retry" "^2.0.8" + "@smithy/util-utf8" "^2.0.2" fast-xml-parser "4.2.5" tslib "^2.5.0" @@ -3015,6 +3432,34 @@ "@smithy/smithy-client" "^2.1.12" tslib "^2.5.0" +"@aws-sdk/core@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.451.0.tgz#ecd30da40d8e02050a772920485f450ea2a1b804" + integrity sha512-SamWW2zHEf1ZKe3j1w0Piauryl8BQIlej0TBS18A4ACzhjhWXhCs13bO1S88LvPR5mBFXok3XOT6zPOnKDFktw== + dependencies: + "@smithy/smithy-client" "^2.1.15" + tslib "^2.5.0" + +"@aws-sdk/core@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.468.0.tgz#1f356adedd63ef77042a3de10fc4c1fdcce4ad42" + integrity sha512-ezUJR9VvknKoXzNZ4wvzGi1jdkmm+/1dUYQ9Sw4r8bzlJDTsUnWbyvaDlBQh81RuhLtVkaUfTnQKoec0cwlZKQ== + dependencies: + "@smithy/smithy-client" "^2.1.18" + tslib "^2.5.0" + +"@aws-sdk/core@3.476.0": + version "3.476.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.476.0.tgz#1f0eead7ef891391c9b92e5a2065a6b5009bedd4" + integrity sha512-G9CLcxxrSR1FWI1o+Hf/XwIERiQFRhuYxydU7C/QnRP9g5FdE0dxWcIg1U/RJnmkiWTrIG3gRWBXvIw5DCecPw== + dependencies: + "@smithy/core" "^1.1.0" + "@smithy/protocol-http" "^3.0.11" + "@smithy/signature-v4" "^2.0.0" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-env@3.186.0": version "3.186.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.186.0.tgz#55dec9c4c29ebbdff4f3bce72de9e98f7a1f92e1" @@ -3081,16 +3526,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/credential-provider-env@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.425.0.tgz#1f5be812aeed558efaebce641e4c030b86875544" - integrity sha512-J20etnLvMKXRVi5FK4F8yOCNm2RTaQn5psQTGdDEPWJNGxohcSpzzls8U2KcMyUJ+vItlrThr4qwgpHG3i/N0w== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/credential-provider-env@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.433.0.tgz#7cceca1002ba2e79e10a9dfb119442bea7b88e7c" @@ -3101,6 +3536,36 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-env@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.451.0.tgz#7b7429bd2e3fdebf914a88269274190781aeeab2" + integrity sha512-9dAav7DcRgaF7xCJEQR5ER9ErXxnu/tdnVJ+UPmb1NPeIZdESv1A3lxFDEq1Fs8c4/lzAj9BpshGyJVIZwZDKg== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-env@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.460.0.tgz#9649ee6662df2f39027a1497bdb202b50332ef63" + integrity sha512-WWdaRJFuYRc2Ue9NKDy2NIf8pQRNx/QRVmrsk6EkIID8uWlQIOePk3SWTVV0TZIyPrbfSEaSnJRZoShphJ6PAg== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-env@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.468.0.tgz#4196d717d3f5485af863bd1fd84374ea3dcd6210" + integrity sha512-k/1WHd3KZn0EQYjadooj53FC0z24/e4dUZhbSKTULgmxyO62pwh9v3Brvw4WRa/8o2wTffU/jo54tf4vGuP/ZA== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-env@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.6.1.tgz#d8b2dd36836432a9b8ec05a5cf9fe428b04c9964" @@ -3270,22 +3735,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/credential-provider-ini@3.427.0": - version "3.427.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.427.0.tgz#bf52067ed5ef6971c7785d09bdf3c6aa16afc2b1" - integrity sha512-NmH1cO/w98CKMltYec3IrJIIco19wRjATFNiw83c+FGXZ+InJwReqBnruxIOmKTx2KDzd6fwU1HOewS7UjaaaQ== - dependencies: - "@aws-sdk/credential-provider-env" "3.425.0" - "@aws-sdk/credential-provider-process" "3.425.0" - "@aws-sdk/credential-provider-sso" "3.427.0" - "@aws-sdk/credential-provider-web-identity" "3.425.0" - "@aws-sdk/types" "3.425.0" - "@smithy/credential-provider-imds" "^2.0.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/credential-provider-ini@3.441.0": version "3.441.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.441.0.tgz#b7479042eca9d41c713d2664c7d4a4eb169b7b1b" @@ -3318,6 +3767,70 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-ini@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.451.0.tgz#e38315611f70700ad9803316d7030e3472c9789c" + integrity sha512-TySt64Ci5/ZbqFw1F9Z0FIGvYx5JSC9e6gqDnizIYd8eMnn8wFRUscRrD7pIHKfrhvVKN5h0GdYovmMO/FMCBw== + dependencies: + "@aws-sdk/credential-provider-env" "3.451.0" + "@aws-sdk/credential-provider-process" "3.451.0" + "@aws-sdk/credential-provider-sso" "3.451.0" + "@aws-sdk/credential-provider-web-identity" "3.451.0" + "@aws-sdk/types" "3.451.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-ini@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.460.0.tgz#26432ba3cd18084130ea9397a39f1b30cf3893ff" + integrity sha512-1IEUmyaWzt2M3mONO8QyZtPy0f9ccaEjCo48ZQLgptWxUI+Ohga9gPK0mqu1kTJOjv4JJGACYHzLwEnnpltGlA== + dependencies: + "@aws-sdk/credential-provider-env" "3.460.0" + "@aws-sdk/credential-provider-process" "3.460.0" + "@aws-sdk/credential-provider-sso" "3.460.0" + "@aws-sdk/credential-provider-web-identity" "3.460.0" + "@aws-sdk/types" "3.460.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-ini@3.470.0": + version "3.470.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.470.0.tgz#d360d08f893d5d28a3e6a493bbef0989669c2f6a" + integrity sha512-eF22iPO6J2jY+LbuTv5dW0hZBmi6ksRDFFd/zT6TLasrzH2Ex+gAfN3c7rFHF+XAubL0JXFUKFA3UAwoZpO9Zg== + dependencies: + "@aws-sdk/credential-provider-env" "3.468.0" + "@aws-sdk/credential-provider-process" "3.468.0" + "@aws-sdk/credential-provider-sso" "3.470.0" + "@aws-sdk/credential-provider-web-identity" "3.468.0" + "@aws-sdk/types" "3.468.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-ini@3.476.0": + version "3.476.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.476.0.tgz#35c13299a849b0addb9e05157da3f3118f20f386" + integrity sha512-wAeXkCDW0qq/1suVTxGIW2RMQTBKwuhL1dwXt+Fmay0hQe4CKzmlTKFY9bN3CnTuwUCN8ozURimpeFFQ7rmKBw== + dependencies: + "@aws-sdk/credential-provider-env" "3.468.0" + "@aws-sdk/credential-provider-process" "3.468.0" + "@aws-sdk/credential-provider-sso" "3.476.0" + "@aws-sdk/credential-provider-web-identity" "3.468.0" + "@aws-sdk/types" "3.468.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-ini@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.6.1.tgz#0da6d9341e621f8e0815814ed017b88e268fbc3d" @@ -3443,23 +3956,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/credential-provider-node@3.427.0": - version "3.427.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.427.0.tgz#f3bd63bc5ab5b897ce67d5960731f48c89ba7520" - integrity sha512-wYYbQ57nKL8OfgRbl8k6uXcdnYml+p3LSSfDUAuUEp1HKlQ8lOXFJ3BdLr5qrk7LhpyppSRnWBmh2c3kWa7ANQ== - dependencies: - "@aws-sdk/credential-provider-env" "3.425.0" - "@aws-sdk/credential-provider-ini" "3.427.0" - "@aws-sdk/credential-provider-process" "3.425.0" - "@aws-sdk/credential-provider-sso" "3.427.0" - "@aws-sdk/credential-provider-web-identity" "3.425.0" - "@aws-sdk/types" "3.425.0" - "@smithy/credential-provider-imds" "^2.0.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/credential-provider-node@3.441.0": version "3.441.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.441.0.tgz#b286d47c43b48988c7ee4f014dc823afabe5cb16" @@ -3494,6 +3990,74 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-node@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.451.0.tgz#72ccdef2199104379977dc06ea84c8d2a356d545" + integrity sha512-AEwM1WPyxUdKrKyUsKyFqqRFGU70e4qlDyrtBxJnSU9NRLZI8tfEZ67bN7fHSxBUBODgDXpMSlSvJiBLh5/3pw== + dependencies: + "@aws-sdk/credential-provider-env" "3.451.0" + "@aws-sdk/credential-provider-ini" "3.451.0" + "@aws-sdk/credential-provider-process" "3.451.0" + "@aws-sdk/credential-provider-sso" "3.451.0" + "@aws-sdk/credential-provider-web-identity" "3.451.0" + "@aws-sdk/types" "3.451.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-node@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.460.0.tgz#8dff013f8e2a2e2837eaf7400ff42714de7dec4d" + integrity sha512-PbPo92WIgNlF6V4eWKehYGYjTqf0gU9vr09LeQUc3bTm1DJhJw1j+HU/3PfQ8LwTkBQePO7MbJ5A2n6ckMwfMg== + dependencies: + "@aws-sdk/credential-provider-env" "3.460.0" + "@aws-sdk/credential-provider-ini" "3.460.0" + "@aws-sdk/credential-provider-process" "3.460.0" + "@aws-sdk/credential-provider-sso" "3.460.0" + "@aws-sdk/credential-provider-web-identity" "3.460.0" + "@aws-sdk/types" "3.460.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-node@3.470.0": + version "3.470.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.470.0.tgz#9236a27f451fef06e1cb6c744b6b8b3dc3d633a3" + integrity sha512-paySXwzGxBVU+2cVUkRIXafKhYhtO2fJJ3MotR6euvRONK/dta+bhEc5Z4QnTo/gNLoELK/QUC0EGoF+oPfk8g== + dependencies: + "@aws-sdk/credential-provider-env" "3.468.0" + "@aws-sdk/credential-provider-ini" "3.470.0" + "@aws-sdk/credential-provider-process" "3.468.0" + "@aws-sdk/credential-provider-sso" "3.470.0" + "@aws-sdk/credential-provider-web-identity" "3.468.0" + "@aws-sdk/types" "3.468.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-node@3.476.0": + version "3.476.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.476.0.tgz#5c410cb9968b23964dad60b529177e845598fc64" + integrity sha512-BOkFBHYDgH+o6YRkk+QgQz3ro9Ly3RhNGzK5HeH37eyWWWgL1BTgY/cHgX3VNRmuKfIoph3yB2C5+eHKf41XYw== + dependencies: + "@aws-sdk/credential-provider-env" "3.468.0" + "@aws-sdk/credential-provider-ini" "3.476.0" + "@aws-sdk/credential-provider-process" "3.468.0" + "@aws-sdk/credential-provider-sso" "3.476.0" + "@aws-sdk/credential-provider-web-identity" "3.468.0" + "@aws-sdk/types" "3.468.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-node@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.6.1.tgz#0055292a4f0f49d053e8dfcc9174d8d2cf6862bb" @@ -3581,17 +4145,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/credential-provider-process@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.425.0.tgz#d5cd231e1732375fc918912f8083c8c45d9dc2ab" - integrity sha512-YY6tkLdvtb1Fgofp3b1UWO+5vwS14LJ/smGmuGpSba0V7gFJRdcrJ9bcb9vVgAGuMdjzRJ+bUKlLLtqXkaykEw== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/credential-provider-process@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.433.0.tgz#dd51c92480ed620e4c3f989852ee408ab1209d59" @@ -3603,6 +4156,39 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-process@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.451.0.tgz#3dd1d7df235f4eeb99d7e0f16b0e8cd61d555a73" + integrity sha512-HQywSdKeD5PErcLLnZfSyCJO+6T+ZyzF+Lm/QgscSC+CbSUSIPi//s15qhBRVely/3KBV6AywxwNH+5eYgt4lQ== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-process@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.460.0.tgz#3f56d03ed5a0c44d87455465701906bd115ebcd9" + integrity sha512-ng+0FMc4EaxLAwdttCwf2nzNf4AgcqAHZ8pKXUf8qF/KVkoyTt3UZKW7P2FJI01zxwP+V4yAwVt95PBUKGn4YQ== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-process@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.468.0.tgz#770ed72db036c5d011445e5abf4a4bcc4424c486" + integrity sha512-OYSn1A/UsyPJ7Z8Q2cNhTf55O36shPmSsvOfND04nSfu1nPaR+VUvvsP7v+brhGpwC/GAKTIdGAo4blH31BS6A== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-process@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.6.1.tgz#5bf851f3ee232c565b8c82608926df0ad28c1958" @@ -3700,19 +4286,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/credential-provider-sso@3.427.0": - version "3.427.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.427.0.tgz#da54388247c0cf812e024c301a6f188550275850" - integrity sha512-c+tXyS/i49erHs4bAp6vKNYeYlyQ0VNMBgoco0LCn1rL0REtHbfhWMnqDLF6c2n3yIWDOTrQu0D73Idnpy16eA== - dependencies: - "@aws-sdk/client-sso" "3.427.0" - "@aws-sdk/token-providers" "3.427.0" - "@aws-sdk/types" "3.425.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/credential-provider-sso@3.441.0": version "3.441.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.441.0.tgz#ef116fdcc5489088acdfea33036666293d1723cb" @@ -3739,6 +4312,58 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-sso@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.451.0.tgz#f2482985a80f1da78e6b50ffaebbf2297d0f366f" + integrity sha512-Usm/N51+unOt8ID4HnQzxIjUJDrkAQ1vyTOC0gSEEJ7h64NSSPGD5yhN7il5WcErtRd3EEtT1a8/GTC5TdBctg== + dependencies: + "@aws-sdk/client-sso" "3.451.0" + "@aws-sdk/token-providers" "3.451.0" + "@aws-sdk/types" "3.451.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-sso@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.460.0.tgz#e44a768899d3fca30e0eaf2ed0c3c15e2cd2b5ac" + integrity sha512-KnrQieOw17+aHEzE3SwfxjeSQ5ZTe2HeAzxkaZF++GxhNul/PkVnLzjGpIuB9bn71T9a2oNfG3peDUA+m2l2kw== + dependencies: + "@aws-sdk/client-sso" "3.460.0" + "@aws-sdk/token-providers" "3.460.0" + "@aws-sdk/types" "3.460.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-sso@3.470.0": + version "3.470.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.470.0.tgz#12f14557be50a01bc99166610d83ea5be79b154a" + integrity sha512-biGDSh9S9KDR9Tl/8cCPn9g5KPNkXg/CIJIOk3X+6valktbJ2UVYBzi0ZX4vZiudt5ry/Hsu6Pgo+KN1AmBWdg== + dependencies: + "@aws-sdk/client-sso" "3.470.0" + "@aws-sdk/token-providers" "3.470.0" + "@aws-sdk/types" "3.468.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-sso@3.476.0": + version "3.476.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.476.0.tgz#629358a5145e4185cf6cabfb77c400ebf2e009c4" + integrity sha512-jOTaH/T2xm94ebgw2xqPgPzB2OUirgL2YWSE3xCqeFJK0c9J64jz4LORI7/uXVZB4l+20axneUhoyEygQMBxOw== + dependencies: + "@aws-sdk/client-sso" "3.476.0" + "@aws-sdk/token-providers" "3.470.0" + "@aws-sdk/types" "3.468.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-web-identity@3.186.0": version "3.186.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.186.0.tgz#db43f37f7827b553490dd865dbaa9a2c45f95494" @@ -3805,16 +4430,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/credential-provider-web-identity@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.425.0.tgz#c1587cc39be70db2c828aeab7b68a8245bc86f91" - integrity sha512-/0R65TgRzL01JU3SzloivWNwdkbIhr06uY/F5pBHf/DynQqaspKNfdHn6AiozgSVDfwRHFjKBTUy6wvf3QFkuA== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/credential-provider-web-identity@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.433.0.tgz#32403ba9cc47d3c46500f3c8e5e0041d20e4dbe8" @@ -3825,6 +4440,36 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-web-identity@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.451.0.tgz#5dc40768869d5887888c6f178c7831dd2c74cfbe" + integrity sha512-Xtg3Qw65EfDjWNG7o2xD6sEmumPfsy3WDGjk2phEzVg8s7hcZGxf5wYwe6UY7RJvlEKrU0rFA+AMn6Hfj5oOzg== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-web-identity@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.460.0.tgz#480ac1daa62e667672f5ecaa7dbefde808c191a2" + integrity sha512-7OeaZgC3HmJZGE0I0ZiKInUMF2LyA0IZiW85AYFnAZzAIfv1cXk/1UnDAoFIQhOZfnUBXivStagz892s480ryw== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-web-identity@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.468.0.tgz#5befcb593d99a84e16af9e9f285f0d59ed42771f" + integrity sha512-rexymPmXjtkwCPfhnUq3EjO1rSkf39R4Jz9CqiM7OsqK2qlT5Y/V3gnMKn0ZMXsYaQOMfM3cT5xly5R+OKDHlw== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/eventstream-codec@3.186.0": version "3.186.0" resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-codec/-/eventstream-codec-3.186.0.tgz#9da9608866b38179edf72987f2bc3b865d11db13" @@ -4233,13 +4878,13 @@ tslib "^1.8.0" "@aws-sdk/lib-storage@^3.226.0": - version "3.382.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/lib-storage/-/lib-storage-3.382.0.tgz#436978080f9c7bf07e9a45eb727e84f00cb5b0a0" - integrity sha512-KFaMEGBogmAr+FPlz6u8+kCk1Xsp6LuRkb2EABN0wAF98qaV076HgEQac4xW372zjVX1DKCCQnRbRsxx3Ce0/A== + version "3.474.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/lib-storage/-/lib-storage-3.474.0.tgz#63e41097570fc57a4dc3a567866736dcc1661171" + integrity sha512-cTfoBZmzC6OyXFzBOlzo3nFK10oTY/JJiXIzHLHkU5Oy9z4V3CvQlVqkFjbkguG4plYvPTT+2xPeNi45NYagqQ== dependencies: "@smithy/abort-controller" "^2.0.1" - "@smithy/middleware-endpoint" "^2.0.1" - "@smithy/smithy-client" "^2.0.1" + "@smithy/middleware-endpoint" "^2.2.3" + "@smithy/smithy-client" "^2.1.18" buffer "5.6.0" events "3.3.0" stream-browserify "3.0.0" @@ -4530,16 +5175,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/middleware-host-header@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.425.0.tgz#7bca371e1a5611ec20c06bd7017efa1900c367d0" - integrity sha512-E5Gt41LObQ+cr8QnLthwsH3MtVSNXy1AKJMowDr85h0vzqA/FHUkgHyOGntgozzjXT5M0MaSRYxS0xwTR5D4Ew== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/protocol-http" "^3.0.6" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/middleware-host-header@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.433.0.tgz#3b6687ee4021c2b56c96cff61b45a33fb762b1c7" @@ -4550,6 +5185,36 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/middleware-host-header@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.451.0.tgz#016fcd2b0ec58f26ce62c7ff792174bdf580972b" + integrity sha512-j8a5jAfhWmsK99i2k8oR8zzQgXrsJtgrLxc3js6U+525mcZytoiDndkWTmD5fjJ1byU1U2E5TaPq+QJeDip05Q== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-host-header@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.460.0.tgz#ee198c7c03b44338b7f0190201c19e5436cc8ff8" + integrity sha512-qBeDyuJkEuHe87Xk6unvFO9Zg5j6zM8bQOOZITocTLfu9JN0u5V4GQ/yopvpv+nQHmC/MGr0G7p+kIXMrg/Q2A== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-host-header@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.468.0.tgz#6da7b19032e9afccea54fbf8aa10cccd2f817bcf" + integrity sha512-gwQ+/QhX+lhof304r6zbZ/V5l5cjhGRxLL3CjH1uJPMcOAbw9wUlMdl+ibr8UwBZ5elfKFGiB1cdW/0uMchw0w== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/protocol-http" "^3.0.11" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/middleware-host-header@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.6.1.tgz#6e1b4b95c5bfea5a4416fa32f11d8fa2e6edaeff" @@ -4643,15 +5308,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/middleware-logger@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.425.0.tgz#e45f160b84798365e4acf8a283e9664ee9ee131b" - integrity sha512-INE9XWRXx2f4a/r2vOU0tAmgctVp7nEaEasemNtVBYhqbKLZvr9ndLBSgKGgJ8LIcXAoISipaMuFiqIGkFsm7A== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/middleware-logger@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.433.0.tgz#fcd4e31a8f134861cd519477b959c218a3600186" @@ -4661,6 +5317,33 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/middleware-logger@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.451.0.tgz#9ef8ac916199f92ea1bb6c153279727ffa2b0b36" + integrity sha512-0kHrYEyVeB2QBfP6TfbI240aRtatLZtcErJbhpiNUb+CQPgEL3crIjgVE8yYiJumZ7f0jyjo8HLPkwD1/2APaw== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-logger@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.460.0.tgz#3353b146a158a197e2f520dd7f48c75076d06492" + integrity sha512-w2AJ6HOJ+Ggx9+VDKuWBHk5S0ZxYEo2EY2IFh0qtCQ1RDix/ur1QEzOOL5vNjHlZKPv/dseIwhgsTCac8UHXbQ== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-logger@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.468.0.tgz#a1883fb7ad8e156444d30689de4ab897357ef1d8" + integrity sha512-X5XHKV7DHRXI3f29SAhJPe/OxWRFgDWDMMCALfzhmJfCi6Jfh0M14cJKoC+nl+dk9lB+36+jKjhjETZaL2bPlA== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/middleware-logger@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.6.1.tgz#78b3732cf188d5e4df13488db6418f7f98a77d6d" @@ -4735,16 +5418,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/middleware-recursion-detection@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.425.0.tgz#c348ec16ebb7c357bcb403904c24e8da1914961d" - integrity sha512-77gnzJ5b91bgD75L/ugpOyerx6lR3oyS4080X1YI58EzdyBMkDrHM4FbMcY2RynETi3lwXCFzLRyZjWXY1mRlw== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/protocol-http" "^3.0.6" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/middleware-recursion-detection@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.433.0.tgz#5b4b7878ea46c70f507c9ea7c30ad0e5ee4ae6bf" @@ -4755,6 +5428,36 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/middleware-recursion-detection@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.451.0.tgz#333a12d4792788bfcc3cab1028868cf37fb17e76" + integrity sha512-J6jL6gJ7orjHGM70KDRcCP7so/J2SnkN4vZ9YRLTeeZY6zvBuHDjX8GCIgSqPn/nXFXckZO8XSnA7u6+3TAT0w== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-recursion-detection@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.460.0.tgz#4583a78fb15d0b18046a582dd6e0d3f554ad2eb8" + integrity sha512-wmzm1/2NzpcCVCAsGqqiTBK+xNyLmQwTOq63rcW6eeq6gYOO0cyTZROOkVRrrsKWPBigrSFFHvDrEvonOMtKAg== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-recursion-detection@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.468.0.tgz#85b05636a5c2638bf9e15c8b6be17654757e1bf4" + integrity sha512-vch9IQib2Ng9ucSyRW2eKNQXHUPb5jUPCLA5otTW/8nGjcOU37LxQG4WrxO7uaJ9Oe8hjHO+hViE3P0KISUhtA== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/protocol-http" "^3.0.11" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/middleware-retry@3.186.0": version "3.186.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.186.0.tgz#0ff9af58d73855863683991a809b40b93c753ad1" @@ -4818,6 +5521,20 @@ tslib "^1.8.0" uuid "^3.0.0" +"@aws-sdk/middleware-sdk-ec2@3.433.0": + version "3.433.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-ec2/-/middleware-sdk-ec2-3.433.0.tgz#4255ccf5d04a15cd1eaa2625bdb5ba43ce0d2d93" + integrity sha512-R/L0Z9evCaoxmdYKJWFiibi0vcucZjuUruT97X/FRbCoMUcAUlAm+WS4KiIN+jSEzcrFjG3yvcoPZdNbtS0KlQ== + dependencies: + "@aws-sdk/types" "3.433.0" + "@aws-sdk/util-format-url" "3.433.0" + "@smithy/middleware-endpoint" "^2.1.3" + "@smithy/protocol-http" "^3.0.8" + "@smithy/signature-v4" "^2.0.0" + "@smithy/smithy-client" "^2.1.12" + "@smithy/types" "^2.4.0" + tslib "^2.5.0" + "@aws-sdk/middleware-sdk-rds@3.329.0": version "3.329.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-rds/-/middleware-sdk-rds-3.329.0.tgz#b7fc6afce0f9899b296d8a6fb9ce9752db21fa78" @@ -4933,16 +5650,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/middleware-sdk-sts@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.425.0.tgz#a020a04ddb5c6741d43d72afe79c24e6f1bb94b7" - integrity sha512-JFojrg76oKAoBknnr9EL5N2aJ1mRCtBqXoZYST58GSx8uYdFQ89qS65VNQ8JviBXzsrCNAn4vDhZ5Ch5E6TxGQ== - dependencies: - "@aws-sdk/middleware-signing" "3.425.0" - "@aws-sdk/types" "3.425.0" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/middleware-sdk-sts@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.433.0.tgz#9b30f17a922ecc5fd46b93f1edcd20d7146b814f" @@ -4953,6 +5660,36 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/middleware-sdk-sts@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.451.0.tgz#0c70b57523386fe12357b4471cd20b681a27f9aa" + integrity sha512-UJ6UfVUEgp0KIztxpAeelPXI5MLj9wUtUCqYeIMP7C1ZhoEMNm3G39VLkGN43dNhBf1LqjsV9jkKMZbVfYXuwg== + dependencies: + "@aws-sdk/middleware-signing" "3.451.0" + "@aws-sdk/types" "3.451.0" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-sdk-sts@3.461.0": + version "3.461.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.461.0.tgz#746afa5958c22989e4c1a1217fc2a008f7e04bf3" + integrity sha512-sgNxkwKdJ/NZm7SJZBnbYPkbspmzn3lDyRSJH7PTCvyzDBzY2PB6yS/dfnGkitR+PYwromuOYMha37W4su2SOw== + dependencies: + "@aws-sdk/middleware-signing" "3.461.0" + "@aws-sdk/types" "3.460.0" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-sdk-sts@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.468.0.tgz#773ed9f7087b184461c9cda0b442e58cb15c6a5b" + integrity sha512-xRy8NKfHbmafHwdbotdWgHBvRs0YZgk20GrhFJKp43bkqVbJ5bNlh3nQXf1DeFY9fARR84Bfotya4fwCUHWgZg== + dependencies: + "@aws-sdk/middleware-signing" "3.468.0" + "@aws-sdk/types" "3.468.0" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/middleware-serde@3.186.0": version "3.186.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.186.0.tgz#f7944241ad5fb31cb15cd250c9e92147942b9ec6" @@ -5080,19 +5817,6 @@ "@smithy/util-middleware" "^2.0.0" tslib "^2.5.0" -"@aws-sdk/middleware-signing@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.425.0.tgz#fa133b8a76216d0b55558634b09cbe769f16b037" - integrity sha512-ZpOfgJHk7ovQ0sSwg3tU4NxFOnz53lJlkJRf7S+wxQALHM0P2MJ6LYBrZaFMVsKiJxNIdZBXD6jclgHg72ZW6Q== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/protocol-http" "^3.0.6" - "@smithy/signature-v4" "^2.0.0" - "@smithy/types" "^2.3.4" - "@smithy/util-middleware" "^2.0.3" - tslib "^2.5.0" - "@aws-sdk/middleware-signing@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.433.0.tgz#670557ace5b97729dbabb6a991815e44eb0ef03b" @@ -5106,6 +5830,45 @@ "@smithy/util-middleware" "^2.0.5" tslib "^2.5.0" +"@aws-sdk/middleware-signing@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.451.0.tgz#ed7f5665dd048228e00f8e7e5925db32901a7886" + integrity sha512-s5ZlcIoLNg1Huj4Qp06iKniE8nJt/Pj1B/fjhWc6cCPCM7XJYUCejCnRh6C5ZJoBEYodjuwZBejPc1Wh3j+znA== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/signature-v4" "^2.0.0" + "@smithy/types" "^2.5.0" + "@smithy/util-middleware" "^2.0.6" + tslib "^2.5.0" + +"@aws-sdk/middleware-signing@3.461.0": + version "3.461.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.461.0.tgz#e7393f755660eb65a160e64584ad9383724bd2e1" + integrity sha512-aM/7VupHlsgeRG1UZSAQMWJX+2Jam4GG8ZGVAbLfBr9yh9cBwnUUndpUpYI9rU7atA8n+vISr162EbR7WTiFhQ== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/signature-v4" "^2.0.0" + "@smithy/types" "^2.5.0" + "@smithy/util-middleware" "^2.0.6" + tslib "^2.5.0" + +"@aws-sdk/middleware-signing@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.468.0.tgz#d1b5a92c395f55063cfa72ee95e4921b16f4c515" + integrity sha512-s+7fSB1gdnnTj5O0aCCarX3z5Vppop8kazbNSZADdkfHIDWCN80IH4ZNjY3OWqaAz0HmR4LNNrovdR304ojb4Q== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/protocol-http" "^3.0.11" + "@smithy/signature-v4" "^2.0.0" + "@smithy/types" "^2.7.0" + "@smithy/util-middleware" "^2.0.8" + tslib "^2.5.0" + "@aws-sdk/middleware-signing@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.6.1.tgz#e70a2f35d85d70e33c9fddfb54b9520f6382db16" @@ -5248,17 +6011,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/middleware-user-agent@3.427.0": - version "3.427.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.427.0.tgz#a1b7cf9a848dcb4af454922abf5e9714bc4c20aa" - integrity sha512-y9HxYsNvnA3KqDl8w1jHeCwz4P9CuBEtu/G+KYffLeAMBsMZmh4SIkFFCO9wE/dyYg6+yo07rYcnnIfy7WA0bw== - dependencies: - "@aws-sdk/types" "3.425.0" - "@aws-sdk/util-endpoints" "3.427.0" - "@smithy/protocol-http" "^3.0.6" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/middleware-user-agent@3.438.0": version "3.438.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.438.0.tgz#a1165134d5b95e1fbeb841740084b3a43dead18a" @@ -5270,6 +6022,39 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/middleware-user-agent@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.451.0.tgz#33d168e8411be4561eeef69e16c31e41b6f9a0cf" + integrity sha512-8NM/0JiKLNvT9wtAQVl1DFW0cEO7OvZyLSUBLNLTHqyvOZxKaZ8YFk7d8PL6l76LeUKRxq4NMxfZQlUIRe0eSA== + dependencies: + "@aws-sdk/types" "3.451.0" + "@aws-sdk/util-endpoints" "3.451.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-user-agent@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.460.0.tgz#d3f5a420e667b7d9ead4694415748f990f50c7c0" + integrity sha512-0gBSOCr+RtwRUCSRLn9H3RVnj9ercvk/QKTHIr33CgfEdyZtIGpHWUSs6uqiQydPTRzjCm5SfUa6ESGhRVMM6A== + dependencies: + "@aws-sdk/types" "3.460.0" + "@aws-sdk/util-endpoints" "3.460.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-user-agent@3.470.0": + version "3.470.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.470.0.tgz#6cbb09fc8359acdb45c41f6fe5d6612c81f5ad92" + integrity sha512-s0YRGgf4fT5KwwTefpoNUQfB5JghzXyvmPfY1QuFEMeVQNxv0OPuydzo3rY2oXPkZjkulKDtpm5jzIHwut75hA== + dependencies: + "@aws-sdk/types" "3.468.0" + "@aws-sdk/util-endpoints" "3.470.0" + "@smithy/protocol-http" "^3.0.11" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/middleware-user-agent@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.6.1.tgz#6845dfb3bc6187897f348c2c87dec833e6a65c99" @@ -5549,17 +6334,6 @@ "@aws-sdk/types" "3.6.1" tslib "^1.8.0" -"@aws-sdk/region-config-resolver@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.425.0.tgz#b69cc305a4211c9f96f04ac3a10ff9a736ec13cb" - integrity sha512-u7uv/iUOapIJdRgRkO3wnpYsUgV6ponsZJQgVg/8L+n+Vo5PQL5gAcIuAOwcYSKQPFaeK+KbmByI4SyOK203Vw== - dependencies: - "@smithy/node-config-provider" "^2.0.13" - "@smithy/types" "^2.3.4" - "@smithy/util-config-provider" "^2.0.0" - "@smithy/util-middleware" "^2.0.3" - tslib "^2.5.0" - "@aws-sdk/region-config-resolver@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.433.0.tgz#37eb5f40db8af7ba9361aeb28c62b45421e780f0" @@ -5571,6 +6345,28 @@ "@smithy/util-middleware" "^2.0.5" tslib "^2.5.0" +"@aws-sdk/region-config-resolver@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.451.0.tgz#f4de34ebe435832dd6bcdc0a7b9fae14a42fc6de" + integrity sha512-3iMf4OwzrFb4tAAmoROXaiORUk2FvSejnHIw/XHvf/jjR4EqGGF95NZP/n/MeFZMizJWVssrwS412GmoEyoqhg== + dependencies: + "@smithy/node-config-provider" "^2.1.5" + "@smithy/types" "^2.5.0" + "@smithy/util-config-provider" "^2.0.0" + "@smithy/util-middleware" "^2.0.6" + tslib "^2.5.0" + +"@aws-sdk/region-config-resolver@3.470.0": + version "3.470.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.470.0.tgz#74e5c5f7a5633ad8c482503bf940a9330bd1cd09" + integrity sha512-C1o1J06iIw8cyAAOvHqT4Bbqf+PgQ/RDlSyjt2gFfP2OovDpc2o2S90dE8f8iZdSGpg70N5MikT1DBhW9NbhtQ== + dependencies: + "@smithy/node-config-provider" "^2.1.8" + "@smithy/types" "^2.7.0" + "@smithy/util-config-provider" "^2.0.0" + "@smithy/util-middleware" "^2.0.8" + tslib "^2.5.0" + "@aws-sdk/s3-request-presigner@^3.352.0": version "3.354.0" resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.354.0.tgz#4ff1621e1400b4db003d725edbf816da6bcc8d72" @@ -5881,47 +6677,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/token-providers@3.427.0": - version "3.427.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.427.0.tgz#d4b9aacda0a8fdd408bb95bf4b8de919df1227b8" - integrity sha512-4E5E+4p8lJ69PBY400dJXF06LUHYx5lkKzBEsYqWWhoZcoftrvi24ltIhUDoGVLkrLcTHZIWSdFAWSos4hXqeg== - dependencies: - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/middleware-host-header" "3.425.0" - "@aws-sdk/middleware-logger" "3.425.0" - "@aws-sdk/middleware-recursion-detection" "3.425.0" - "@aws-sdk/middleware-user-agent" "3.427.0" - "@aws-sdk/types" "3.425.0" - "@aws-sdk/util-endpoints" "3.427.0" - "@aws-sdk/util-user-agent-browser" "3.425.0" - "@aws-sdk/util-user-agent-node" "3.425.0" - "@smithy/config-resolver" "^2.0.11" - "@smithy/fetch-http-handler" "^2.2.1" - "@smithy/hash-node" "^2.0.10" - "@smithy/invalid-dependency" "^2.0.10" - "@smithy/middleware-content-length" "^2.0.12" - "@smithy/middleware-endpoint" "^2.0.10" - "@smithy/middleware-retry" "^2.0.13" - "@smithy/middleware-serde" "^2.0.10" - "@smithy/middleware-stack" "^2.0.4" - "@smithy/node-config-provider" "^2.0.13" - "@smithy/node-http-handler" "^2.1.6" - "@smithy/property-provider" "^2.0.0" - "@smithy/protocol-http" "^3.0.6" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/smithy-client" "^2.1.9" - "@smithy/types" "^2.3.4" - "@smithy/url-parser" "^2.0.10" - "@smithy/util-base64" "^2.0.0" - "@smithy/util-body-length-browser" "^2.0.0" - "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.13" - "@smithy/util-defaults-mode-node" "^2.0.15" - "@smithy/util-retry" "^2.0.3" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" - "@aws-sdk/token-providers@3.438.0": version "3.438.0" resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.438.0.tgz#e91baa37c9c78cb5b21cae96a12e7e1705c931d3" @@ -5965,6 +6720,135 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.5.0" +"@aws-sdk/token-providers@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.451.0.tgz#fb80e2fa39bb277fb77040a59c88312a115c35bd" + integrity sha512-ij1L5iUbn6CwxVOT1PG4NFjsrsKN9c4N1YEM0lkl6DwmaNOscjLKGSNyj9M118vSWsOs1ZDbTwtj++h0O/BWrQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/middleware-host-header" "3.451.0" + "@aws-sdk/middleware-logger" "3.451.0" + "@aws-sdk/middleware-recursion-detection" "3.451.0" + "@aws-sdk/middleware-user-agent" "3.451.0" + "@aws-sdk/region-config-resolver" "3.451.0" + "@aws-sdk/types" "3.451.0" + "@aws-sdk/util-endpoints" "3.451.0" + "@aws-sdk/util-user-agent-browser" "3.451.0" + "@aws-sdk/util-user-agent-node" "3.451.0" + "@smithy/config-resolver" "^2.0.18" + "@smithy/fetch-http-handler" "^2.2.6" + "@smithy/hash-node" "^2.0.15" + "@smithy/invalid-dependency" "^2.0.13" + "@smithy/middleware-content-length" "^2.0.15" + "@smithy/middleware-endpoint" "^2.2.0" + "@smithy/middleware-retry" "^2.0.20" + "@smithy/middleware-serde" "^2.0.13" + "@smithy/middleware-stack" "^2.0.7" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/node-http-handler" "^2.1.9" + "@smithy/property-provider" "^2.0.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/smithy-client" "^2.1.15" + "@smithy/types" "^2.5.0" + "@smithy/url-parser" "^2.0.13" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.19" + "@smithy/util-defaults-mode-node" "^2.0.25" + "@smithy/util-endpoints" "^1.0.4" + "@smithy/util-retry" "^2.0.6" + "@smithy/util-utf8" "^2.0.2" + tslib "^2.5.0" + +"@aws-sdk/token-providers@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.460.0.tgz#8122fe281fe7d454166893409f280f6b026f47c2" + integrity sha512-EvSIPMI1gXk3gEkdtbZCW+p3Bjmt2gOR1m7ibQD7qLj4l0dKXhp4URgTqB1ExH3S4qUq0M/XSGKbGLZpvunHNg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/middleware-host-header" "3.460.0" + "@aws-sdk/middleware-logger" "3.460.0" + "@aws-sdk/middleware-recursion-detection" "3.460.0" + "@aws-sdk/middleware-user-agent" "3.460.0" + "@aws-sdk/region-config-resolver" "3.451.0" + "@aws-sdk/types" "3.460.0" + "@aws-sdk/util-endpoints" "3.460.0" + "@aws-sdk/util-user-agent-browser" "3.460.0" + "@aws-sdk/util-user-agent-node" "3.460.0" + "@smithy/config-resolver" "^2.0.18" + "@smithy/fetch-http-handler" "^2.2.6" + "@smithy/hash-node" "^2.0.15" + "@smithy/invalid-dependency" "^2.0.13" + "@smithy/middleware-content-length" "^2.0.15" + "@smithy/middleware-endpoint" "^2.2.0" + "@smithy/middleware-retry" "^2.0.20" + "@smithy/middleware-serde" "^2.0.13" + "@smithy/middleware-stack" "^2.0.7" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/node-http-handler" "^2.1.9" + "@smithy/property-provider" "^2.0.0" + "@smithy/protocol-http" "^3.0.9" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/smithy-client" "^2.1.15" + "@smithy/types" "^2.5.0" + "@smithy/url-parser" "^2.0.13" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.19" + "@smithy/util-defaults-mode-node" "^2.0.25" + "@smithy/util-endpoints" "^1.0.4" + "@smithy/util-retry" "^2.0.6" + "@smithy/util-utf8" "^2.0.2" + tslib "^2.5.0" + +"@aws-sdk/token-providers@3.470.0": + version "3.470.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.470.0.tgz#635fa5db3f10919868a9f94be43241fbce206ede" + integrity sha512-rzxnJxEUJiV69Cxsf0AHXTqJqTACITwcSH/PL4lWP4uvtzdrzSi3KA3u2aWHWpOcdE6+JFvdICscsbBSo3/TOg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/middleware-host-header" "3.468.0" + "@aws-sdk/middleware-logger" "3.468.0" + "@aws-sdk/middleware-recursion-detection" "3.468.0" + "@aws-sdk/middleware-user-agent" "3.470.0" + "@aws-sdk/region-config-resolver" "3.470.0" + "@aws-sdk/types" "3.468.0" + "@aws-sdk/util-endpoints" "3.470.0" + "@aws-sdk/util-user-agent-browser" "3.468.0" + "@aws-sdk/util-user-agent-node" "3.470.0" + "@smithy/config-resolver" "^2.0.21" + "@smithy/fetch-http-handler" "^2.3.1" + "@smithy/hash-node" "^2.0.17" + "@smithy/invalid-dependency" "^2.0.15" + "@smithy/middleware-content-length" "^2.0.17" + "@smithy/middleware-endpoint" "^2.2.3" + "@smithy/middleware-retry" "^2.0.24" + "@smithy/middleware-serde" "^2.0.15" + "@smithy/middleware-stack" "^2.0.9" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/node-http-handler" "^2.2.1" + "@smithy/property-provider" "^2.0.0" + "@smithy/protocol-http" "^3.0.11" + "@smithy/shared-ini-file-loader" "^2.0.6" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + "@smithy/url-parser" "^2.0.15" + "@smithy/util-base64" "^2.0.1" + "@smithy/util-body-length-browser" "^2.0.1" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.22" + "@smithy/util-defaults-mode-node" "^2.0.29" + "@smithy/util-endpoints" "^1.0.7" + "@smithy/util-retry" "^2.0.8" + "@smithy/util-utf8" "^2.0.2" + tslib "^2.5.0" + "@aws-sdk/types@3.186.0": version "3.186.0" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.186.0.tgz#f6fb6997b6a364f399288bfd5cd494bc680ac922" @@ -6014,15 +6898,7 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/types@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.425.0.tgz#8d4e94743a69c865a83785a9f3bcfd49945836f7" - integrity sha512-6lqbmorwerN4v+J5dqbHPAsjynI0mkEF+blf+69QTaKKGaxBBVaXgqoqul9RXYcK5MMrrYRbQIMd0zYOoy90kA== - dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@aws-sdk/types@3.433.0", "@aws-sdk/types@^3.1.0", "@aws-sdk/types@^3.222.0": +"@aws-sdk/types@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.433.0.tgz#0f94eae2a4a3525ca872c9ab04e143c01806d755" integrity sha512-0jEE2mSrNDd8VGFjTc1otYrwYPIkzZJEIK90ZxisKvQ/EURGBhNzWn7ejWB9XCMFT6XumYLBR0V9qq5UPisWtA== @@ -6030,6 +6906,30 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/types@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.451.0.tgz#37ab4b25074c6a36152eb36abb7399b3768c2e7b" + integrity sha512-rhK+qeYwCIs+laJfWCcrYEjay2FR/9VABZJ2NRM89jV/fKqGVQR52E5DQqrI+oEIL5JHMhhnr4N4fyECMS35lw== + dependencies: + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/types@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.460.0.tgz#f87602928a57473f724b6efca0158e64f658be71" + integrity sha512-MyZSWS/FV8Bnux5eD9en7KLgVxevlVrGNEP3X2D7fpnUlLhl0a7k8+OpSI2ozEQB8hIU2DLc/XXTKRerHSefxQ== + dependencies: + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/types@3.468.0", "@aws-sdk/types@^3.1.0", "@aws-sdk/types@^3.222.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.468.0.tgz#f97b34fc92a800d1d8b866f47693ae8f3d46517b" + integrity sha512-rx/9uHI4inRbp2tw3Y4Ih4PNZkVj32h7WneSg3MVgVjAoVD5Zti9KhS5hkvsBxfgmQmg0AQbE+b1sy5WGAgntA== + dependencies: + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/types@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.6.1.tgz#00686db69e998b521fcd4a5f81ef0960980f80c4" @@ -6395,15 +7295,6 @@ "@aws-sdk/types" "3.378.0" tslib "^2.5.0" -"@aws-sdk/util-endpoints@3.427.0": - version "3.427.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.427.0.tgz#09f7f36201ba80c1c669a0f4c506fb93de1e66d4" - integrity sha512-rSyiAIFF/EVvity/+LWUqoTMJ0a25RAc9iqx0WZ4tf1UjuEXRRXxZEb+jEZg1bk+pY84gdLdx9z5E+MSJCZxNQ== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/node-config-provider" "^2.0.13" - tslib "^2.5.0" - "@aws-sdk/util-endpoints@3.438.0": version "3.438.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.438.0.tgz#fe79a0ad87fc201c8ecb422f6f040bd300c98df9" @@ -6413,6 +7304,33 @@ "@smithy/util-endpoints" "^1.0.2" tslib "^2.5.0" +"@aws-sdk/util-endpoints@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.451.0.tgz#8719977c3535c6fec719a2854ffe037e02412ddb" + integrity sha512-giqLGBTnRIcKkDqwU7+GQhKbtJ5Ku35cjGQIfMyOga6pwTBUbaK0xW1Sdd8sBQ1GhApscnChzI9o/R9x0368vw== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/util-endpoints" "^1.0.4" + tslib "^2.5.0" + +"@aws-sdk/util-endpoints@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.460.0.tgz#5f47f8716e7e3a008061aaa82d60b23257deaf55" + integrity sha512-myH6kM5WP4IWULHDHMYf2Q+BCYVGlzqJgiBmO10kQEtJSeAGZZ49eoFFYgKW8ZAYB5VnJ+XhXVB1TRA+vR4l5A== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/util-endpoints" "^1.0.4" + tslib "^2.5.0" + +"@aws-sdk/util-endpoints@3.470.0": + version "3.470.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.470.0.tgz#94338991804f24e0225636abd4215b3bb4338c15" + integrity sha512-6N6VvPCmu+89p5Ez/+gLf+X620iQ9JpIs8p8ECZiCodirzFOe8NC1O2S7eov7YiG9IHSuodqn/0qNq+v+oLe0A== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/util-endpoints" "^1.0.7" + tslib "^2.5.0" + "@aws-sdk/util-format-url@3.329.0": version "3.329.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.329.0.tgz#2856379da026adc6355d63bdf6f207a4f8b7c6cb" @@ -6431,6 +7349,16 @@ "@aws-sdk/types" "3.347.0" tslib "^2.5.0" +"@aws-sdk/util-format-url@3.433.0": + version "3.433.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.433.0.tgz#65c11be0e071342ebfeecea04be7bc181ac36699" + integrity sha512-Z6T7I4hELoQ4eeIuKIKx+52B9bc3SCPhjgMcFAFQeesjmHAr0drHyoGNJIat6ckvgI6zzFaeaBZTvWDA2hyDkA== + dependencies: + "@aws-sdk/types" "3.433.0" + "@smithy/querystring-builder" "^2.0.12" + "@smithy/types" "^2.4.0" + tslib "^2.5.0" + "@aws-sdk/util-hex-encoding@3.186.0": version "3.186.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.186.0.tgz#7ed58b923997c6265f4dce60c8704237edb98895" @@ -6656,16 +7584,6 @@ bowser "^2.11.0" tslib "^2.5.0" -"@aws-sdk/util-user-agent-browser@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.425.0.tgz#74d200d461ea2d75a8d4916c230ffe3a20fcb009" - integrity sha512-22Y9iMtjGcFjGILR6/xdp1qRezlHVLyXtnpEsbuPTiernRCPk6zfAnK/ATH77r02MUjU057tdxVkd5umUBTn9Q== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/types" "^2.3.4" - bowser "^2.11.0" - tslib "^2.5.0" - "@aws-sdk/util-user-agent-browser@3.433.0": version "3.433.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.433.0.tgz#b5ed0c0cca0db34a2c1c2ffc1b65e7cdd8dc88ff" @@ -6676,6 +7594,36 @@ bowser "^2.11.0" tslib "^2.5.0" +"@aws-sdk/util-user-agent-browser@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.451.0.tgz#0b511703c3304a5c2fdaa864589246c93ad63dce" + integrity sha512-Ws5mG3J0TQifH7OTcMrCTexo7HeSAc3cBgjfhS/ofzPUzVCtsyg0G7I6T7wl7vJJETix2Kst2cpOsxygPgPD9w== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/types" "^2.5.0" + bowser "^2.11.0" + tslib "^2.5.0" + +"@aws-sdk/util-user-agent-browser@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.460.0.tgz#a4e9fda5d4e2ecafa28d056240e10bddffa1d748" + integrity sha512-FRCzW+TyjKnvxsargPVrjayBfp/rvObYHZyZ2OSqrVw8lkkPCb4e/WZOeIiXZuhdhhoah7wMuo6zGwtFF3bYKg== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/types" "^2.5.0" + bowser "^2.11.0" + tslib "^2.5.0" + +"@aws-sdk/util-user-agent-browser@3.468.0": + version "3.468.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.468.0.tgz#095caecb3fd75104ee38ae81ed78821de0f58e28" + integrity sha512-OJyhWWsDEizR3L+dCgMXSUmaCywkiZ7HSbnQytbeKGwokIhD69HTiJcibF/sgcM5gk4k3Mq3puUhGnEZ46GIig== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/types" "^2.7.0" + bowser "^2.11.0" + tslib "^2.5.0" + "@aws-sdk/util-user-agent-browser@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.6.1.tgz#11b9cc8743392761adb304460f4b54ec8acc2ee6" @@ -6751,16 +7699,6 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/util-user-agent-node@3.425.0": - version "3.425.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.425.0.tgz#847c0d6526a34e174419dcecf0e12cd000158a84" - integrity sha512-SIR4F5uQeeVAi8lv4OgRirtdtNi5zeyogTuQgGi9su8F/WP1N6JqxofcwpUY5f8/oJ2UlXr/tx1f09UHfJJzvA== - dependencies: - "@aws-sdk/types" "3.425.0" - "@smithy/node-config-provider" "^2.0.13" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - "@aws-sdk/util-user-agent-node@3.437.0": version "3.437.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.437.0.tgz#f77729854ddf049ccaba8bae3d8fa279812b4716" @@ -6771,6 +7709,36 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@aws-sdk/util-user-agent-node@3.451.0": + version "3.451.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.451.0.tgz#f2af3f0d3f0389a14a7dbbc835dc94c705c0a39a" + integrity sha512-TBzm6P+ql4mkGFAjPlO1CI+w3yUT+NulaiALjl/jNX/nnUp6HsJsVxJf4nVFQTG5KRV0iqMypcs7I3KIhH+LmA== + dependencies: + "@aws-sdk/types" "3.451.0" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/util-user-agent-node@3.460.0": + version "3.460.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.460.0.tgz#d4adb7b924d89e5d33fc4ae83cfe067b7bb045c4" + integrity sha512-+kSoR9ABGpJ5Xc7v0VwpgTQbgyI4zuezC8K4pmKAGZsSsVWg4yxptoy2bDqoFL7qfRlWviMVTkQRMvR4D44WxA== + dependencies: + "@aws-sdk/types" "3.460.0" + "@smithy/node-config-provider" "^2.1.5" + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@aws-sdk/util-user-agent-node@3.470.0": + version "3.470.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.470.0.tgz#b78605f336859d6c3b5f573cff931ce41f83a27d" + integrity sha512-QxsZ9iVHcBB/XRdYvwfM5AMvNp58HfqkIrH88mY0cmxuvtlIGDfWjczdDrZMJk9y0vIq+cuoCHsGXHu7PyiEAQ== + dependencies: + "@aws-sdk/types" "3.468.0" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@aws-sdk/util-user-agent-node@3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.6.1.tgz#98384095fa67d098ae7dd26f3ccaad028e8aebb6" @@ -8592,115 +9560,115 @@ resolved "https://registry.yarnpkg.com/@enterprise-cmcs/serverless-waf-plugin/-/serverless-waf-plugin-1.3.0.tgz#b2f241f68218b94b62987596888730975c99b019" integrity sha512-Xysp8ejNhkrxCMQ4CximHmphKO1vGt3JGKKzGs0tLX1r4yK8BdobmauX/GmDwAfrzqlHVGDRsdvvY1Grv1W0yw== -"@esbuild/android-arm64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz#bc35990f412a749e948b792825eef7df0ce0e073" - integrity sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw== - -"@esbuild/android-arm@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.2.tgz#edd1c8f23ba353c197f5b0337123c58ff2a56999" - integrity sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q== - -"@esbuild/android-x64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.2.tgz#2dcdd6e6f1f2d82ea1b746abd8da5b284960f35a" - integrity sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w== - -"@esbuild/darwin-arm64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz#55b36bc06d76f5c243987c1f93a11a80d8fc3b26" - integrity sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA== - -"@esbuild/darwin-x64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz#982524af33a6424a3b5cb44bbd52559623ad719c" - integrity sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw== - -"@esbuild/freebsd-arm64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz#8e478a0856645265fe79eac4b31b52193011ee06" - integrity sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ== - -"@esbuild/freebsd-x64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz#01b96604f2540db023c73809bb8ae6cd1692d6f3" - integrity sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw== - -"@esbuild/linux-arm64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz#7e5d2c7864c5c83ec789b59c77cd9c20d2594916" - integrity sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg== - -"@esbuild/linux-arm@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz#c32ae97bc0246664a1cfbdb4a98e7b006d7db8ae" - integrity sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg== - -"@esbuild/linux-ia32@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz#3fc4f0fa026057fe885e4a180b3956e704f1ceaa" - integrity sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ== - -"@esbuild/linux-loong64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz#633bcaea443f3505fb0ed109ab840c99ad3451a4" - integrity sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw== - -"@esbuild/linux-mips64el@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz#e0bff2898c46f52be7d4dbbcca8b887890805823" - integrity sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg== - -"@esbuild/linux-ppc64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz#d75798da391f54a9674f8c143b9a52d1dbfbfdde" - integrity sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw== - -"@esbuild/linux-riscv64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz#012409bd489ed1bb9b775541d4a46c5ded8e6dd8" - integrity sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw== - -"@esbuild/linux-s390x@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz#ece3ed75c5a150de8a5c110f02e97d315761626b" - integrity sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g== - -"@esbuild/linux-x64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz#dea187019741602d57aaf189a80abba261fbd2aa" - integrity sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ== - -"@esbuild/netbsd-x64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz#bbfd7cf9ab236a23ee3a41b26f0628c57623d92a" - integrity sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ== - -"@esbuild/openbsd-x64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz#fa5c4c6ee52a360618f00053652e2902e1d7b4a7" - integrity sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw== - -"@esbuild/sunos-x64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz#52a2ac8ac6284c02d25df22bb4cfde26fbddd68d" - integrity sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw== - -"@esbuild/win32-arm64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz#719ed5870855de8537aef8149694a97d03486804" - integrity sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg== - -"@esbuild/win32-ia32@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz#24832223880b0f581962c8660f8fb8797a1e046a" - integrity sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA== - -"@esbuild/win32-x64@0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz#1205014625790c7ff0e471644a878a65d1e34ab0" - integrity sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw== +"@esbuild/android-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz#276c5f99604054d3dbb733577e09adae944baa90" + integrity sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ== + +"@esbuild/android-arm@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.5.tgz#4a3cbf14758166abaae8ba9c01a80e68342a4eec" + integrity sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA== + +"@esbuild/android-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.5.tgz#21a3d11cd4613d2d3c5ccb9e746c254eb9265b0a" + integrity sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA== + +"@esbuild/darwin-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz#714cb839f467d6a67b151ee8255886498e2b9bf6" + integrity sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw== + +"@esbuild/darwin-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz#2c553e97a6d2b4ae76a884e35e6cbab85a990bbf" + integrity sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA== + +"@esbuild/freebsd-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz#d554f556718adb31917a0da24277bf84b6ee87f3" + integrity sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ== + +"@esbuild/freebsd-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz#288f7358a3bb15d99e73c65c9adaa3dabb497432" + integrity sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ== + +"@esbuild/linux-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz#95933ae86325c93cb6b5e8333d22120ecfdc901b" + integrity sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA== + +"@esbuild/linux-arm@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz#0acef93aa3e0579e46d33b666627bddb06636664" + integrity sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ== + +"@esbuild/linux-ia32@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz#b6e5c9e80b42131cbd6b1ddaa48c92835f1ed67f" + integrity sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ== + +"@esbuild/linux-loong64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz#e5f0cf95a180158b01ff5f417da796a1c09dfbea" + integrity sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw== + +"@esbuild/linux-mips64el@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz#ae36fb86c7d5f641f3a0c8472e83dcb6ea36a408" + integrity sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg== + +"@esbuild/linux-ppc64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz#7960cb1666f0340ddd9eef7b26dcea3835d472d0" + integrity sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q== + +"@esbuild/linux-riscv64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz#32207df26af60a3a9feea1783fc21b9817bade19" + integrity sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag== + +"@esbuild/linux-s390x@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz#b38d5681db89a3723862dfa792812397b1510a7d" + integrity sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw== + +"@esbuild/linux-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz#46feba2ad041a241379d150f415b472fe3885075" + integrity sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A== + +"@esbuild/netbsd-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz#3b5c1fb068f26bfc681d31f682adf1bea4ef0702" + integrity sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g== + +"@esbuild/openbsd-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz#ca6830316ca68056c5c88a875f103ad3235e00db" + integrity sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA== + +"@esbuild/sunos-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz#9efc4eb9539a7be7d5a05ada52ee43cda0d8e2dd" + integrity sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg== + +"@esbuild/win32-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz#29f8184afa7a02a956ebda4ed638099f4b8ff198" + integrity sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg== + +"@esbuild/win32-ia32@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz#f3de07afb292ecad651ae4bb8727789de2d95b05" + integrity sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw== + +"@esbuild/win32-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz#faad84c41ba12e3a0acb52571df9bff37bee75f6" + integrity sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw== "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" @@ -8756,34 +9724,36 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@gitbeaker/core@^21.7.0": - version "21.7.0" - resolved "https://registry.yarnpkg.com/@gitbeaker/core/-/core-21.7.0.tgz#fcf7a12915d39f416e3f316d0a447a814179b8e5" - integrity sha512-cw72rE7tA27wc6JJe1WqeAj9v/6w0S7XJcEji+bRNjTlUfE1zgfW0Gf1mbGUi7F37SOABGCosQLfg9Qe63aIqA== +"@gitbeaker/core@^35.8.1": + version "35.8.1" + resolved "https://registry.yarnpkg.com/@gitbeaker/core/-/core-35.8.1.tgz#b4ce2d08d344ff50e76c38ff81b800bec6dfe851" + integrity sha512-KBrDykVKSmU9Q9Gly8KeHOgdc0lZSa435srECxuO0FGqqBcUQ82hPqUc13YFkkdOI9T1JRA3qSFajg8ds0mZKA== dependencies: - "@gitbeaker/requester-utils" "^21.7.0" - form-data "^3.0.0" + "@gitbeaker/requester-utils" "^35.8.1" + form-data "^4.0.0" li "^1.3.0" + mime "^3.0.0" + query-string "^7.0.0" xcase "^2.0.1" -"@gitbeaker/node@^21.3.0": - version "21.7.0" - resolved "https://registry.yarnpkg.com/@gitbeaker/node/-/node-21.7.0.tgz#2c19613f44ee497a8808c555abec614ebd2dfcad" - integrity sha512-OdM3VcTKYYqboOsnbiPcO0XimXXpYK4gTjARBZ6BWc+1LQXKmqo+OH6oUbyxOoaFu9hHECafIt3WZU3NM4sZTg== +"@gitbeaker/node@^35.8.1": + version "35.8.1" + resolved "https://registry.yarnpkg.com/@gitbeaker/node/-/node-35.8.1.tgz#d67885c827f2d7405afd7e39538a230721756e5c" + integrity sha512-g6rX853y61qNhzq9cWtxIEoe2KDeFBtXAeWMGWJnc3nz3WRump2pIICvJqw/yobLZqmTNt+ea6w3/n92Mnbn3g== dependencies: - "@gitbeaker/core" "^21.7.0" - "@gitbeaker/requester-utils" "^21.7.0" - form-data "^3.0.0" - got "^11.1.4" + "@gitbeaker/core" "^35.8.1" + "@gitbeaker/requester-utils" "^35.8.1" + delay "^5.0.0" + got "^11.8.3" xcase "^2.0.1" -"@gitbeaker/requester-utils@^21.7.0": - version "21.7.0" - resolved "https://registry.yarnpkg.com/@gitbeaker/requester-utils/-/requester-utils-21.7.0.tgz#e9a9cfaf268d2a99eb7bbdc930943240a5f88878" - integrity sha512-eLTaVXlBnh8Qimj6QuMMA06mu/mLcJm3dy8nqhhn/Vm/D25sPrvpGwmbfFyvzj6QujPqtHvFfsCHtyZddL01qA== +"@gitbeaker/requester-utils@^35.8.1": + version "35.8.1" + resolved "https://registry.yarnpkg.com/@gitbeaker/requester-utils/-/requester-utils-35.8.1.tgz#f345cdd05abd4169cfcd239d202db6283eb17dc8" + integrity sha512-MFzdH+Z6eJaCZA5ruWsyvm6SXRyrQHjYVR6aY8POFraIy7ceIHOprWCs1R+0ydDZ8KtBnd8OTHjlJ0sLtSFJCg== dependencies: - form-data "^3.0.0" - query-string "^6.12.1" + form-data "^4.0.0" + qs "^6.10.1" xcase "^2.0.1" "@graphql-codegen/cli@^2.2.2": @@ -10724,16 +11694,16 @@ integrity sha512-5iV2NKZnzxJwZZ4DM5JVbRG/nkhAbzEskKaLBB82PmYGKzaDHuMHP1lcPoD/rtYMlowZgNA/RQndfKvPBPwmXA== "@octokit/action@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@octokit/action/-/action-5.0.2.tgz#c0bbbc6094f81a013556e8b45e3ab552e575f719" - integrity sha512-MisDAUHxASngd6fvh4Klibxmwfszd8GLYEXfKfaeH2A4j3g5BGwZyy17Y0lawZpOGHOfNu8fqY1pKgzXnqBkOA== + version "5.0.6" + resolved "https://registry.yarnpkg.com/@octokit/action/-/action-5.0.6.tgz#ff8de161ae6da37a5cba75ede11c8904d903d3b4" + integrity sha512-jVZctvHzD9Cp1YZ5izcmcu2L9Y08Pe3ldjDu7vC2hyWQwz0fBhGrfKT5s4eNZ19K92UzO40cFzAFf1U4IOf13A== dependencies: "@octokit/auth-action" "^2.0.0" "@octokit/core" "^4.0.0" "@octokit/plugin-paginate-rest" "^6.0.0" "@octokit/plugin-rest-endpoint-methods" "^7.0.0" "@octokit/types" "^9.0.0" - https-proxy-agent "^5.0.1" + https-proxy-agent "^7.0.0" "@octokit/app@^13.1.5": version "13.1.8" @@ -11092,19 +12062,19 @@ resolved "https://registry.yarnpkg.com/@octokit/webhooks-methods/-/webhooks-methods-3.0.0.tgz#4f4443605233f46abc5f85a857ba105095aa1181" integrity sha512-FAIyAchH9JUKXugKMC17ERAXM/56vVJekwXOON46pmUDYfU7uXB4cFY8yc8nYr5ABqVI7KjRKfFt3mZF7OcyUA== -"@octokit/webhooks-types@6.3.4": - version "6.3.4" - resolved "https://registry.yarnpkg.com/@octokit/webhooks-types/-/webhooks-types-6.3.4.tgz#b553da7479edfb04218160c85f16dbbc68251533" - integrity sha512-9E0HNgHqc5v22+9IzCSEZ9iXnBJ/n+GM9gZye0kp7XmzcOfrnAKZzd4km269n6/vVOkmXwT11DbbQFukWOvbdw== +"@octokit/webhooks-types@6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@octokit/webhooks-types/-/webhooks-types-6.11.0.tgz#1fb903bff3f2883490d6ba88d8cb8f8a55f68176" + integrity sha512-AanzbulOHljrku1NGfafxdpTCfw2ENaWzH01N2vqQM+cUFbk868Cgh0xylz0JIM9BoKbfI++bdD6EYX0Q/UTEw== "@octokit/webhooks@^10.0.0": - version "10.1.3" - resolved "https://registry.yarnpkg.com/@octokit/webhooks/-/webhooks-10.1.3.tgz#415bb4f826167b15da4dede81c14cb7a8978ac9a" - integrity sha512-c5uQW0HJbI5mcQpUFcM7LVs1gbdEiHD6OLXZcwxLJeNUmI8Cy9uzfCib6HguARKgnz3tSavYX/teHq7brm05iQ== + version "10.9.2" + resolved "https://registry.yarnpkg.com/@octokit/webhooks/-/webhooks-10.9.2.tgz#1b1e79a70fa5b22a3149b18432cbf3f39dbcb544" + integrity sha512-hFVF/szz4l/Y/GQdKxNmQjUke0XJXK986p+ucIlubTGVPVtVtup5G1jarQfvCMBs9Fvlf9dvH8K83E4lefmofQ== dependencies: "@octokit/request-error" "^3.0.0" "@octokit/webhooks-methods" "^3.0.0" - "@octokit/webhooks-types" "6.3.4" + "@octokit/webhooks-types" "6.11.0" aggregate-error "^3.1.0" "@opentelemetry/api@^1.0.1", "@opentelemetry/api@^1.3.0": @@ -11975,15 +12945,15 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27" integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA== -"@serverless/dashboard-plugin@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@serverless/dashboard-plugin/-/dashboard-plugin-7.1.0.tgz#575a2f87277c08325f27206fb5dbc78a06fccfb6" - integrity sha512-mAiTU2ERsDHdCrXJa/tihh/r+8ZwSuYYBqln3SkwuBD/49ct9QrK7S00cpiqFoY/geMFlHpOkriGzCPz6UP/rw== +"@serverless/dashboard-plugin@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@serverless/dashboard-plugin/-/dashboard-plugin-7.2.0.tgz#9e903e9099c830b34a5a9356d01e940e63252262" + integrity sha512-Gqzgef+KmrX1OxJW9aubrIN6AvPrtDARMv+NegMNEe+pfkZA/IMuZiSyYUaHgARokdw2/IALOysTLgdFJIrXvA== dependencies: "@aws-sdk/client-cloudformation" "^3.410.0" "@aws-sdk/client-sts" "^3.410.0" "@serverless/event-mocks" "^1.1.1" - "@serverless/platform-client" "^4.4.0" + "@serverless/platform-client" "^4.5.1" "@serverless/utils" "^6.14.0" child-process-ext "^3.0.1" chokidar "^3.5.3" @@ -12012,14 +12982,14 @@ "@types/lodash" "^4.14.123" lodash "^4.17.11" -"@serverless/platform-client@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@serverless/platform-client/-/platform-client-4.4.0.tgz#8a1c76ceface3eef6792a35c3e5b295f68beb967" - integrity sha512-urL7SNefRqC2EOFDcpvm8fyn/06B5yXWneKpyGw7ylGt0Qr9JHZCB9TiUeTkIpPUNz0jTvKUaJ2+M/JNEiaVIA== +"@serverless/platform-client@^4.5.1": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@serverless/platform-client/-/platform-client-4.5.1.tgz#db5915bb53339761e704cc3f7d352c7754a79af2" + integrity sha512-XltmO/029X76zi0LUFmhsnanhE2wnqH1xf+WBt5K8gumQA9LnrfwLgPxj+VA+mm6wQhy+PCp7H5SS0ZPu7F2Cw== dependencies: adm-zip "^0.5.5" archiver "^5.3.0" - axios "^0.21.1" + axios "^1.6.2" fast-glob "^3.2.7" https-proxy-agent "^5.0.0" ignore "^5.1.8" @@ -12154,20 +13124,12 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" -"@smithy/abort-controller@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-2.0.1.tgz#ddb5dd8c37016e8fcf772bd9c80e900860d74ae6" - integrity sha512-0s7XjIbsTwZyUW9OwXQ8J6x1UiA1TNCh60Vaw56nHahL7kUZsLhmTlWiaxfLkFtO2Utkj8YewcpHTYpxaTzO+w== - dependencies: - "@smithy/types" "^2.0.2" - tslib "^2.5.0" - -"@smithy/abort-controller@^2.0.12": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-2.0.12.tgz#62cd47c81fa1d7d6c2d6fde0c2f54ea89892fb6a" - integrity sha512-YIJyefe1mi3GxKdZxEBEuzYOeQ9xpYfqnFmWzojCssRAuR7ycxwpoRQgp965vuW426xUAQhCV5rCaWElQ7XsaA== +"@smithy/abort-controller@^2.0.1", "@smithy/abort-controller@^2.0.15": + version "2.0.15" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-2.0.15.tgz#fcec9193da8b86eef1eedc3e71139a99c061db32" + integrity sha512-JkS36PIS3/UCbq/MaozzV7jECeL+BTt4R75bwY8i+4RASys4xOyUS1HsRyUNSqUXFP4QyCz5aNnh3ltuaxv+pw== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/chunked-blob-reader-native@^1.0.2": @@ -12195,15 +13157,29 @@ "@smithy/util-middleware" "^1.0.1" tslib "^2.5.0" -"@smithy/config-resolver@^2.0.1", "@smithy/config-resolver@^2.0.11", "@smithy/config-resolver@^2.0.16": - version "2.0.16" - resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-2.0.16.tgz#f2abf65a21f56731fdab2d39d2df2dd0e377c9cc" - integrity sha512-1k+FWHQDt2pfpXhJsOmNMmlAZ3NUQ98X5tYsjQhVGq+0X6cOBMhfh6Igd0IX3Ut6lEO6DQAdPMI/blNr3JZfMQ== +"@smithy/config-resolver@^2.0.1", "@smithy/config-resolver@^2.0.16", "@smithy/config-resolver@^2.0.18", "@smithy/config-resolver@^2.0.21": + version "2.0.21" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-2.0.21.tgz#97cb1c71f3c8c453fb01169545f98414b3414d7f" + integrity sha512-rlLIGT+BeqjnA6C2FWumPRJS1UW07iU5ZxDHtFuyam4W65gIaOFMjkB90ofKCIh+0mLVQrQFrl/VLtQT/6FWTA== dependencies: - "@smithy/node-config-provider" "^2.1.3" - "@smithy/types" "^2.4.0" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/types" "^2.7.0" "@smithy/util-config-provider" "^2.0.0" - "@smithy/util-middleware" "^2.0.5" + "@smithy/util-middleware" "^2.0.8" + tslib "^2.5.0" + +"@smithy/core@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-1.2.0.tgz#36286de5460905708221313b8b1faebf185761e6" + integrity sha512-l8R89X7+hlt2FEFg+OrNq29LP3h9DfGPmO6ObwT9IXWHD6V7ycpj5u2rVQyIis26ovrgOYakl6nfgmPMm8m1IQ== + dependencies: + "@smithy/middleware-endpoint" "^2.2.3" + "@smithy/middleware-retry" "^2.0.24" + "@smithy/middleware-serde" "^2.0.15" + "@smithy/protocol-http" "^3.0.11" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + "@smithy/util-middleware" "^2.0.8" tslib "^2.5.0" "@smithy/credential-provider-imds@^1.0.1": @@ -12228,15 +13204,15 @@ "@smithy/url-parser" "^2.0.1" tslib "^2.5.0" -"@smithy/credential-provider-imds@^2.0.18": - version "2.0.18" - resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-2.0.18.tgz#9a5b8be3f268bb4ac7b7ef321f57b0e9a61e2940" - integrity sha512-QnPBi6D2zj6AHJdUTo5zXmk8vwHJ2bNevhcVned1y+TZz/OI5cizz5DsYNkqFUIDn8tBuEyKNgbmKVNhBbuY3g== +"@smithy/credential-provider-imds@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-2.1.4.tgz#126adf69eac333f23f8683edbfabdc2b3b2deb15" + integrity sha512-cwPJN1fa1YOQzhBlTXRavABEYRRchci1X79QRwzaNLySnIMJfztyv1Zkst0iZPLMnpn8+CnHu3wOHS11J5Dr3A== dependencies: - "@smithy/node-config-provider" "^2.1.3" - "@smithy/property-provider" "^2.0.13" - "@smithy/types" "^2.4.0" - "@smithy/url-parser" "^2.0.12" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/property-provider" "^2.0.16" + "@smithy/types" "^2.7.0" + "@smithy/url-parser" "^2.0.15" tslib "^2.5.0" "@smithy/eventstream-codec@^1.0.1": @@ -12269,6 +13245,16 @@ "@smithy/util-hex-encoding" "^2.0.0" tslib "^2.5.0" +"@smithy/eventstream-codec@^2.0.13": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-2.0.13.tgz#10c57a80508125a64759e79b42ff848bee8498dc" + integrity sha512-CExbelIYp+DxAHG8RIs0l9QL7ElqhG4ym9BNoSpkPa4ptBQfzJdep3LbOSVJIE2VUdBAeObdeL6EDB3Jo85n3g== + dependencies: + "@aws-crypto/crc32" "3.0.0" + "@smithy/types" "^2.5.0" + "@smithy/util-hex-encoding" "^2.0.0" + tslib "^2.5.0" + "@smithy/eventstream-serde-browser@^1.0.1": version "1.1.0" resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-1.1.0.tgz#466817f1a7bc83b5bc4c4c9fd454cd698cb0e470" @@ -12278,13 +13264,13 @@ "@smithy/types" "^1.2.0" tslib "^2.5.0" -"@smithy/eventstream-serde-browser@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-2.0.1.tgz#7d19409327b6015b19ac926833185be2d5eee357" - integrity sha512-9E1/6ZGF7nB/Td3G1kcatU7VjjP8eZ/p/Q+0KsZc1AUPyv4lR15pmWnWj3iGBEGYI9qZBJ/7a/wPEPayabmA3Q== +"@smithy/eventstream-serde-browser@^2.0.13": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-2.0.13.tgz#3d3ddb347320b736c001e0a4d7cf37962a6cefc9" + integrity sha512-OJ/2g/VxkzA+mYZxV102oX3CsiE+igTSmqq/ir3oEVG2kSIdRC00ryttj/lmL14W06ExNi0ysmfLxQkL8XrAZQ== dependencies: - "@smithy/eventstream-serde-universal" "^2.0.1" - "@smithy/types" "^2.0.2" + "@smithy/eventstream-serde-universal" "^2.0.13" + "@smithy/types" "^2.5.0" tslib "^2.5.0" "@smithy/eventstream-serde-config-resolver@^1.0.1": @@ -12295,12 +13281,12 @@ "@smithy/types" "^1.2.0" tslib "^2.5.0" -"@smithy/eventstream-serde-config-resolver@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-2.0.1.tgz#fa3562f771a0d3dc4bc83ad7b7f437deda53b3ff" - integrity sha512-J8a+8HH8oDPIgq8Px/nPLfu9vpIjQ7XUPtP3orbs8KUh0GznNthSTy1xZP5RXjRqGQEkxPvsHf1po2+QOsgNFw== +"@smithy/eventstream-serde-config-resolver@^2.0.13": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-2.0.13.tgz#36cb39cb4a54c26d780fc9f39406a040dab75614" + integrity sha512-2BI1CbnYuEvAYoWSeWJtPNygbIKiWeSLxCmDLnyM6wQV32Of7VptiQlaFXPxXp4zqn/rs3ocZ/T29rxE4s4Gsg== dependencies: - "@smithy/types" "^2.0.2" + "@smithy/types" "^2.5.0" tslib "^2.5.0" "@smithy/eventstream-serde-node@^1.0.1": @@ -12312,13 +13298,13 @@ "@smithy/types" "^1.2.0" tslib "^2.5.0" -"@smithy/eventstream-serde-node@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-2.0.1.tgz#d456591097f94e4fd29448fd7b33e2e1f79bfe61" - integrity sha512-wklowUz0zXJuqC7FMpriz66J8OAko3z6INTg+iMJWYB1bWv4pc5V7q36PxlZ0RKRbj0u+EThlozWgzE7Stz2Sw== +"@smithy/eventstream-serde-node@^2.0.13": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-2.0.13.tgz#733f021b16692916f0514fdf2a98dc723cf29a31" + integrity sha512-7NbFwPafb924elFxCBDvm48jy/DeSrpFbFQN0uN2ThuY5HrEeubikS0t7WMva4Z4EnRoivpbuT0scb9vUIJKoA== dependencies: - "@smithy/eventstream-serde-universal" "^2.0.1" - "@smithy/types" "^2.0.2" + "@smithy/eventstream-serde-universal" "^2.0.13" + "@smithy/types" "^2.5.0" tslib "^2.5.0" "@smithy/eventstream-serde-universal@^1.1.0": @@ -12330,13 +13316,13 @@ "@smithy/types" "^1.2.0" tslib "^2.5.0" -"@smithy/eventstream-serde-universal@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-2.0.1.tgz#206e1cd437b0da09a2a45af3ddc3b7e3b9789734" - integrity sha512-WPPylIgVZ6wOYVgpF0Rs1LlocYyj248MRtKEEehnDvC+0tV7wmGt7H/SchCh10W4y4YUxuzPlW+mUvVMGmLSVg== +"@smithy/eventstream-serde-universal@^2.0.13": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-2.0.13.tgz#2d7bba2acc36e6625891b0f8b3d42fe49c04f64e" + integrity sha512-j0yFd5UfftM+ia9dxLRbheJDCkCZBHpcEzCsPO8BxVOTbdcX/auVJCv6ov/yvpCKsf4Hv3mOqi0Is1YogM2g3Q== dependencies: - "@smithy/eventstream-codec" "^2.0.1" - "@smithy/types" "^2.0.2" + "@smithy/eventstream-codec" "^2.0.13" + "@smithy/types" "^2.5.0" tslib "^2.5.0" "@smithy/fetch-http-handler@^1.0.1": @@ -12350,15 +13336,15 @@ "@smithy/util-base64" "^1.0.1" tslib "^2.5.0" -"@smithy/fetch-http-handler@^2.0.1", "@smithy/fetch-http-handler@^2.2.1", "@smithy/fetch-http-handler@^2.2.4": - version "2.2.4" - resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-2.2.4.tgz#405716581a5a336f2c162daf4169bff600fc47ce" - integrity sha512-gIPRFEGi+c6V52eauGKrjDzPWF2Cu7Z1r5F8A3j2wcwz25sPG/t8kjsbEhli/tS/2zJp/ybCZXe4j4ro3yv/HA== +"@smithy/fetch-http-handler@^2.0.1", "@smithy/fetch-http-handler@^2.2.4", "@smithy/fetch-http-handler@^2.2.6", "@smithy/fetch-http-handler@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-2.3.1.tgz#aa055db5bf4d78acec97abe6ef24283fa2c18430" + integrity sha512-6MNk16fqb8EwcYY8O8WxB3ArFkLZ2XppsSNo1h7SQcFdDDwIumiJeO6wRzm7iB68xvsOQzsdQKbdtTieS3hfSQ== dependencies: - "@smithy/protocol-http" "^3.0.8" - "@smithy/querystring-builder" "^2.0.12" - "@smithy/types" "^2.4.0" - "@smithy/util-base64" "^2.0.0" + "@smithy/protocol-http" "^3.0.11" + "@smithy/querystring-builder" "^2.0.15" + "@smithy/types" "^2.7.0" + "@smithy/util-base64" "^2.0.1" tslib "^2.5.0" "@smithy/hash-blob-browser@^1.0.1": @@ -12381,14 +13367,14 @@ "@smithy/util-utf8" "^1.0.1" tslib "^2.5.0" -"@smithy/hash-node@^2.0.1", "@smithy/hash-node@^2.0.10", "@smithy/hash-node@^2.0.12": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-2.0.12.tgz#514586ca3f54840322273029eef66c41d9001e39" - integrity sha512-fDZnTr5j9t5qcbeJ037aMZXxMka13Znqwrgy3PAqYj6Dm3XHXHftTH3q+NWgayUxl1992GFtQt1RuEzRMy3NnQ== +"@smithy/hash-node@^2.0.1", "@smithy/hash-node@^2.0.12", "@smithy/hash-node@^2.0.15", "@smithy/hash-node@^2.0.17": + version "2.0.17" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-2.0.17.tgz#9ce5e3f137143e3658759d31a16e068ef94a14fc" + integrity sha512-Il6WuBcI1nD+e2DM7tTADMf01wEPGK8PAhz4D+YmDUVaoBqlA+CaH2uDJhiySifmuKBZj748IfygXty81znKhw== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" "@smithy/util-buffer-from" "^2.0.0" - "@smithy/util-utf8" "^2.0.0" + "@smithy/util-utf8" "^2.0.2" tslib "^2.5.0" "@smithy/hash-stream-node@^1.0.1": @@ -12408,12 +13394,12 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" -"@smithy/invalid-dependency@^2.0.1", "@smithy/invalid-dependency@^2.0.10", "@smithy/invalid-dependency@^2.0.12": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-2.0.12.tgz#de78a5e9457cc397aad0648e18c0260b522fe604" - integrity sha512-p5Y+iMHV3SoEpy3VSR7mifbreHQwVSvHSAz/m4GdoXfOzKzaYC8hYv10Ks7Deblkf7lhas8U+lAp9ThbBM+ZXA== +"@smithy/invalid-dependency@^2.0.1", "@smithy/invalid-dependency@^2.0.12", "@smithy/invalid-dependency@^2.0.13", "@smithy/invalid-dependency@^2.0.15": + version "2.0.15" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-2.0.15.tgz#7653490047bf0ab6042fb812adfbcce857aa2d06" + integrity sha512-dlEKBFFwVfzA5QroHlBS94NpgYjXhwN/bFfun+7w3rgxNvVy79SK0w05iGc7UAeC5t+D7gBxrzdnD6hreZnDVQ== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/is-array-buffer@^1.0.1": @@ -12455,13 +13441,13 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" -"@smithy/middleware-content-length@^2.0.1", "@smithy/middleware-content-length@^2.0.12", "@smithy/middleware-content-length@^2.0.14": - version "2.0.14" - resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-2.0.14.tgz#ee1aa842490cee90b6ac208fb13a7d56d3ed84f2" - integrity sha512-poUNgKTw9XwPXfX9nEHpVgrMNVpaSMZbshqvPxFVoalF4wp6kRzYKOfdesSVectlQ51VtigoLfbXcdyPwvxgTg== +"@smithy/middleware-content-length@^2.0.1", "@smithy/middleware-content-length@^2.0.14", "@smithy/middleware-content-length@^2.0.15", "@smithy/middleware-content-length@^2.0.17": + version "2.0.17" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-2.0.17.tgz#13479173a15d1cd4224e3e21071a27c66a74b653" + integrity sha512-OyadvMcKC7lFXTNBa8/foEv7jOaqshQZkjWS9coEXPRZnNnihU/Ls+8ZuJwGNCOrN2WxXZFmDWhegbnM4vak8w== dependencies: - "@smithy/protocol-http" "^3.0.8" - "@smithy/types" "^2.4.0" + "@smithy/protocol-http" "^3.0.11" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/middleware-endpoint@^1.0.1", "@smithy/middleware-endpoint@^1.0.2": @@ -12475,17 +13461,17 @@ "@smithy/util-middleware" "^1.0.2" tslib "^2.5.0" -"@smithy/middleware-endpoint@^2.0.1", "@smithy/middleware-endpoint@^2.0.10", "@smithy/middleware-endpoint@^2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-2.1.3.tgz#ab7ebff4ecbc9b02ec70dd57179f47c4f16bf03f" - integrity sha512-ZrQ0/YX6hNVTxqMEHtEaDbDv6pNeEji/a5Vk3HuFC5R3ZY8lfoATyxmOGxBVYnF3NUvZLNC7umEv1WzWGWvCGQ== +"@smithy/middleware-endpoint@^2.0.1", "@smithy/middleware-endpoint@^2.1.3", "@smithy/middleware-endpoint@^2.2.0", "@smithy/middleware-endpoint@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-2.2.3.tgz#4069ab6e8d1b485bc0d2384b30f7b37096111ec2" + integrity sha512-nYfxuq0S/xoAjdLbyn1ixeVB6cyH9wYCMtbbOCpcCRYR5u2mMtqUtVjjPAZ/DIdlK3qe0tpB0Q76szFGNuz+kQ== dependencies: - "@smithy/middleware-serde" "^2.0.12" - "@smithy/node-config-provider" "^2.1.3" - "@smithy/shared-ini-file-loader" "^2.2.2" - "@smithy/types" "^2.4.0" - "@smithy/url-parser" "^2.0.12" - "@smithy/util-middleware" "^2.0.5" + "@smithy/middleware-serde" "^2.0.15" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/shared-ini-file-loader" "^2.2.7" + "@smithy/types" "^2.7.0" + "@smithy/url-parser" "^2.0.15" + "@smithy/util-middleware" "^2.0.8" tslib "^2.5.0" "@smithy/middleware-retry@^1.0.1", "@smithy/middleware-retry@^1.0.2", "@smithy/middleware-retry@^1.0.3": @@ -12501,17 +13487,18 @@ tslib "^2.5.0" uuid "^8.3.2" -"@smithy/middleware-retry@^2.0.1", "@smithy/middleware-retry@^2.0.13", "@smithy/middleware-retry@^2.0.18": - version "2.0.18" - resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-2.0.18.tgz#37982552a1d3815148797831df025e470423fc5e" - integrity sha512-VyrHQRldGSb3v9oFOB5yPxmLT7U2sQic2ytylOnYlnsmVOLlFIaI6sW22c+w2675yq+XZ6HOuzV7x2OBYCWRNA== +"@smithy/middleware-retry@^2.0.1", "@smithy/middleware-retry@^2.0.18", "@smithy/middleware-retry@^2.0.20", "@smithy/middleware-retry@^2.0.24": + version "2.0.24" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-2.0.24.tgz#556a39e7d2be32cc61862e020409d3f93e2c5be1" + integrity sha512-q2SvHTYu96N7lYrn3VSuX3vRpxXHR/Cig6MJpGWxd0BWodUQUWlKvXpWQZA+lTaFJU7tUvpKhRd4p4MU3PbeJg== dependencies: - "@smithy/node-config-provider" "^2.1.3" - "@smithy/protocol-http" "^3.0.8" - "@smithy/service-error-classification" "^2.0.5" - "@smithy/types" "^2.4.0" - "@smithy/util-middleware" "^2.0.5" - "@smithy/util-retry" "^2.0.5" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/protocol-http" "^3.0.11" + "@smithy/service-error-classification" "^2.0.8" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" + "@smithy/util-middleware" "^2.0.8" + "@smithy/util-retry" "^2.0.8" tslib "^2.5.0" uuid "^8.3.2" @@ -12523,12 +13510,12 @@ "@smithy/types" "^1.1.1" tslib "^2.5.0" -"@smithy/middleware-serde@^2.0.1", "@smithy/middleware-serde@^2.0.10", "@smithy/middleware-serde@^2.0.12": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-2.0.12.tgz#edc93c400a5ffec6c068419163f9d880bdff5e5b" - integrity sha512-IBeco157lIScecq2Z+n0gq56i4MTnfKxS7rbfrAORveDJgnbBAaEQgYqMqp/cYqKrpvEXcyTjwKHrBjCCIZh2A== +"@smithy/middleware-serde@^2.0.1", "@smithy/middleware-serde@^2.0.12", "@smithy/middleware-serde@^2.0.13", "@smithy/middleware-serde@^2.0.15": + version "2.0.15" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-2.0.15.tgz#9deac4daad1f2a60d5c4e7097658f9ae2eb0a33f" + integrity sha512-FOZRFk/zN4AT4wzGuBY+39XWe+ZnCFd0gZtyw3f9Okn2CJPixl9GyWe98TIaljeZdqWkgrzGyPre20AcW2UMHQ== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/middleware-stack@^1.0.1": @@ -12538,12 +13525,12 @@ dependencies: tslib "^2.5.0" -"@smithy/middleware-stack@^2.0.0", "@smithy/middleware-stack@^2.0.4", "@smithy/middleware-stack@^2.0.6": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-2.0.6.tgz#c58d6e4ffc4498bf47fd27adcddd142395d3ba84" - integrity sha512-YSvNZeOKWLJ0M/ycxwDIe2Ztkp6Qixmcml1ggsSv2fdHKGkBPhGrX5tMzPGMI1yyx55UEYBi2OB4s+RriXX48A== +"@smithy/middleware-stack@^2.0.0", "@smithy/middleware-stack@^2.0.6", "@smithy/middleware-stack@^2.0.7", "@smithy/middleware-stack@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-2.0.9.tgz#60e51697c74258fac087bc739d940f524921a15f" + integrity sha512-bCB5dUtGQ5wh7QNL2ELxmDc6g7ih7jWU3Kx6MYH1h4mZbv9xL3WyhKHojRltThCB1arLPyTUFDi+x6fB/oabtA== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/node-config-provider@^1.0.1": @@ -12556,14 +13543,14 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" -"@smithy/node-config-provider@^2.0.1", "@smithy/node-config-provider@^2.0.13", "@smithy/node-config-provider@^2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-2.1.3.tgz#bf4cee69df08d43618ad4329d234351b14d98ef7" - integrity sha512-J6lXvRHGVnSX3n1PYi+e1L5HN73DkkJpUviV3Ebf+8wSaIjAf+eVNbzyvh/S5EQz7nf4KVfwbD5vdoZMAthAEQ== +"@smithy/node-config-provider@^2.0.1", "@smithy/node-config-provider@^2.1.3", "@smithy/node-config-provider@^2.1.5", "@smithy/node-config-provider@^2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-2.1.8.tgz#8cab8f1172c8cd1146e7997292786909abcae763" + integrity sha512-+w26OKakaBUGp+UG+dxYZtFb5fs3tgHg3/QrRrmUZj+rl3cIuw840vFUXX35cVPTUCQIiTqmz7CpVF7+hdINdQ== dependencies: - "@smithy/property-provider" "^2.0.13" - "@smithy/shared-ini-file-loader" "^2.2.2" - "@smithy/types" "^2.4.0" + "@smithy/property-provider" "^2.0.16" + "@smithy/shared-ini-file-loader" "^2.2.7" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/node-http-handler@^1.0.1", "@smithy/node-http-handler@^1.0.2": @@ -12577,15 +13564,15 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" -"@smithy/node-http-handler@^2.0.1", "@smithy/node-http-handler@^2.1.6", "@smithy/node-http-handler@^2.1.8": - version "2.1.8" - resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-2.1.8.tgz#aad989d5445c43a677e7e6161c6fa4abd0e46023" - integrity sha512-KZylM7Wff/So5SmCiwg2kQNXJ+RXgz34wkxS7WNwIUXuZrZZpY/jKJCK+ZaGyuESDu3TxcaY+zeYGJmnFKbQsA== +"@smithy/node-http-handler@^2.0.1", "@smithy/node-http-handler@^2.1.8", "@smithy/node-http-handler@^2.1.9", "@smithy/node-http-handler@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-2.2.1.tgz#23f6540e565edcae8c558a854fffde3d003451c0" + integrity sha512-8iAKQrC8+VFHPAT8pg4/j6hlsTQh+NKOWlctJBrYtQa4ExcxX7aSg3vdQ2XLoYwJotFUurg/NLqFCmZaPRrogw== dependencies: - "@smithy/abort-controller" "^2.0.12" - "@smithy/protocol-http" "^3.0.8" - "@smithy/querystring-builder" "^2.0.12" - "@smithy/types" "^2.4.0" + "@smithy/abort-controller" "^2.0.15" + "@smithy/protocol-http" "^3.0.11" + "@smithy/querystring-builder" "^2.0.15" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/property-provider@^1.0.1": @@ -12604,12 +13591,12 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@smithy/property-provider@^2.0.13": - version "2.0.13" - resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-2.0.13.tgz#45ee47ad79d638082523f944c49fd2e851312098" - integrity sha512-VJqUf2CbsQX6uUiC5dUPuoEATuFjkbkW3lJHbRnpk9EDC9X+iKqhfTK+WP+lve5EQ9TcCI1Q6R7hrg41FyC54w== +"@smithy/property-provider@^2.0.16": + version "2.0.16" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-2.0.16.tgz#0c15ea8a3e8c8e7012bf5877c79ce754f7d2c06e" + integrity sha512-28Ky0LlOqtEjwg5CdHmwwaDRHcTWfPRzkT6HrhwOSRS2RryAvuDfJrZpM+BMcrdeCyEg1mbcgIMoqTla+rdL8Q== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/protocol-http@^1.0.1", "@smithy/protocol-http@^1.1.0", "@smithy/protocol-http@^1.1.1": @@ -12628,12 +13615,12 @@ "@smithy/types" "^2.0.2" tslib "^2.5.0" -"@smithy/protocol-http@^3.0.6", "@smithy/protocol-http@^3.0.8": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-3.0.8.tgz#0f7c114f6b8e23a57dff7a275d085bac97b9233c" - integrity sha512-SHJvYeWq8q0FK8xHk+xjV9dzDUDjFMT+G1pZbV+XB6OVoac/FSVshlMNPeUJ8AmSkcDKHRu5vASnRqZHgD3qhw== +"@smithy/protocol-http@^3.0.11", "@smithy/protocol-http@^3.0.8", "@smithy/protocol-http@^3.0.9": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-3.0.11.tgz#a9ea712fe7cc3375378ac68d9168a7b6cd0b6f65" + integrity sha512-3ziB8fHuXIRamV/akp/sqiWmNPR6X+9SB8Xxnozzj+Nq7hSpyKdFHd1FLpBkgfGFUTzzcBJQlDZPSyxzmdcx5A== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/querystring-builder@^1.0.1": @@ -12654,6 +13641,15 @@ "@smithy/util-uri-escape" "^2.0.0" tslib "^2.5.0" +"@smithy/querystring-builder@^2.0.15": + version "2.0.15" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-2.0.15.tgz#aa8c889bcaef274b8345be4ddabae3bfedf2cf33" + integrity sha512-e1q85aT6HutvouOdN+dMsN0jcdshp50PSCvxDvo6aIM57LqeXimjfONUEgfqQ4IFpYWAtVixptyIRE5frMp/2A== + dependencies: + "@smithy/types" "^2.7.0" + "@smithy/util-uri-escape" "^2.0.0" + tslib "^2.5.0" + "@smithy/querystring-parser@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-1.0.2.tgz#559d09c46b21e6fbda71e95deda4bcd8a46bdecc" @@ -12662,12 +13658,12 @@ "@smithy/types" "^1.1.1" tslib "^2.5.0" -"@smithy/querystring-parser@^2.0.12": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-2.0.12.tgz#d2c234031e266359716a0c62c8c1208a5bd2557e" - integrity sha512-fytyTcXaMzPBuNtPlhj5v6dbl4bJAnwKZFyyItAGt4Tgm9HFPZNo7a9r1SKPr/qdxUEBzvL9Rh+B9SkTX3kFxg== +"@smithy/querystring-parser@^2.0.15": + version "2.0.15" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-2.0.15.tgz#46c8806a145f46636e4aee2a5d79e7ba68161a4c" + integrity sha512-jbBvoK3cc81Cj1c1TH1qMYxNQKHrYQ2DoTntN9FBbtUWcGhc+T4FP6kCKYwRLXyU4AajwGIZstvNAmIEgUUNTQ== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/service-error-classification@^1.0.3": @@ -12675,12 +13671,12 @@ resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-1.0.3.tgz#c620c1562610d3351985eb6dd04262ca2657ae67" integrity sha512-2eglIYqrtcUnuI71yweu7rSfCgt6kVvRVf0C72VUqrd0LrV1M0BM0eYN+nitp2CHPSdmMI96pi+dU9U/UqAMSA== -"@smithy/service-error-classification@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-2.0.5.tgz#22c84fad456730adfa31cae91d47acd31304c346" - integrity sha512-M0SeJnEgD2ywJyV99Fb1yKFzmxDe9JfpJiYTVSRMyRLc467BPU0qsuuDPzMCdB1mU8M8u1rVOdkqdoyFN8UFTw== +"@smithy/service-error-classification@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-2.0.8.tgz#c9e421312a2def84da025c5efe6de06679c5be95" + integrity sha512-jCw9+005im8tsfYvwwSc4TTvd29kXRFkH9peQBg5R/4DD03ieGm6v6Hpv9nIAh98GwgYg1KrztcINC1s4o7/hg== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" "@smithy/shared-ini-file-loader@^1.0.1": version "1.0.1" @@ -12706,12 +13702,12 @@ "@smithy/types" "^2.3.2" tslib "^2.5.0" -"@smithy/shared-ini-file-loader@^2.2.2": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.2.2.tgz#b52064c5254a01f5c98a821207448de439938667" - integrity sha512-noyQUPn7b1M8uB0GEXc/Zyxq+5K2b7aaqWnLp+hgJ7+xu/FCvtyWy5eWLDjQEsHnAet2IZhS5QF8872OR69uNg== +"@smithy/shared-ini-file-loader@^2.2.7": + version "2.2.7" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.2.7.tgz#4a3bd469703d02c3cc8e36dcba2238c06efa12cb" + integrity sha512-0Qt5CuiogIuvQIfK+be7oVHcPsayLgfLJGkPlbgdbl0lD28nUKu4p11L+UG3SAEsqc9UsazO+nErPXw7+IgDpQ== dependencies: - "@smithy/types" "^2.4.0" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/signature-v4@^1.0.1": @@ -12752,14 +13748,14 @@ "@smithy/util-stream" "^1.0.1" tslib "^2.5.0" -"@smithy/smithy-client@^2.0.1", "@smithy/smithy-client@^2.1.12", "@smithy/smithy-client@^2.1.9": - version "2.1.12" - resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-2.1.12.tgz#a7f10ab846d41ce1042eb81f087c4c9eb438b481" - integrity sha512-XXqhridfkKnpj+lt8vM6HRlZbqUAqBjVC74JIi13F/AYQd/zTj9SOyGfxnbp4mjY9q28LityxIuV8CTinr9r5w== +"@smithy/smithy-client@^2.0.1", "@smithy/smithy-client@^2.1.12", "@smithy/smithy-client@^2.1.15", "@smithy/smithy-client@^2.1.18": + version "2.1.18" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-2.1.18.tgz#f8ce2c0e9614f207256ddcd992403aff40750546" + integrity sha512-7FqdbaJiVaHJDD9IfDhmzhSDbpjyx+ZsfdYuOpDJF09rl8qlIAIlZNoSaflKrQ3cEXZN2YxGPaNWGhbYimyIRQ== dependencies: - "@smithy/middleware-stack" "^2.0.6" - "@smithy/types" "^2.4.0" - "@smithy/util-stream" "^2.0.17" + "@smithy/middleware-stack" "^2.0.9" + "@smithy/types" "^2.7.0" + "@smithy/util-stream" "^2.0.23" tslib "^2.5.0" "@smithy/types@^1.0.0", "@smithy/types@^1.1.0", "@smithy/types@^1.1.1", "@smithy/types@^1.2.0": @@ -12769,10 +13765,10 @@ dependencies: tslib "^2.5.0" -"@smithy/types@^2.0.2", "@smithy/types@^2.3.2", "@smithy/types@^2.3.4", "@smithy/types@^2.3.5", "@smithy/types@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-2.4.0.tgz#ed35e429e3ea3d089c68ed1bf951d0ccbdf2692e" - integrity sha512-iH1Xz68FWlmBJ9vvYeHifVMWJf82ONx+OybPW8ZGf5wnEv2S0UXcU4zwlwJkRXuLKpcSLHrraHbn2ucdVXLb4g== +"@smithy/types@^2.0.2", "@smithy/types@^2.3.2", "@smithy/types@^2.4.0", "@smithy/types@^2.5.0", "@smithy/types@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-2.7.0.tgz#6ed9ba5bff7c4d28c980cff967e6d8456840a4f3" + integrity sha512-1OIFyhK+vOkMbu4aN2HZz/MomREkrAC/HqY5mlJMUJfGrPRwijJDTeiN8Rnj9zUaB8ogXAfIOtZrrgqZ4w7Wnw== dependencies: tslib "^2.5.0" @@ -12785,13 +13781,13 @@ "@smithy/types" "^1.1.1" tslib "^2.5.0" -"@smithy/url-parser@^2.0.1", "@smithy/url-parser@^2.0.10", "@smithy/url-parser@^2.0.12": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-2.0.12.tgz#a4cdd1b66176e48f10d119298f8f90b06b7e8a01" - integrity sha512-qgkW2mZqRvlNUcBkxYB/gYacRaAdck77Dk3/g2iw0S9F0EYthIS3loGfly8AwoWpIvHKhkTsCXXQfzksgZ4zIA== +"@smithy/url-parser@^2.0.1", "@smithy/url-parser@^2.0.12", "@smithy/url-parser@^2.0.13", "@smithy/url-parser@^2.0.15": + version "2.0.15" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-2.0.15.tgz#878d9b61f9eac8834cb611cf1a8a0e5d9a48038c" + integrity sha512-sADUncUj9rNbOTrdDGm4EXlUs0eQ9dyEo+V74PJoULY4jSQxS+9gwEgsPYyiu8PUOv16JC/MpHonOgqP/IEDZA== dependencies: - "@smithy/querystring-parser" "^2.0.12" - "@smithy/types" "^2.4.0" + "@smithy/querystring-parser" "^2.0.15" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/util-base64@^1.0.1", "@smithy/util-base64@^1.0.2": @@ -12802,10 +13798,10 @@ "@smithy/util-buffer-from" "^1.0.2" tslib "^2.5.0" -"@smithy/util-base64@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-2.0.0.tgz#1beeabfb155471d1d41c8d0603be1351f883c444" - integrity sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA== +"@smithy/util-base64@^2.0.0", "@smithy/util-base64@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-2.0.1.tgz#57f782dafc187eddea7c8a1ff2a7c188ed1a02c4" + integrity sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ== dependencies: "@smithy/util-buffer-from" "^2.0.0" tslib "^2.5.0" @@ -12817,10 +13813,10 @@ dependencies: tslib "^2.5.0" -"@smithy/util-body-length-browser@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.0.tgz#5447853003b4c73da3bc5f3c5e82c21d592d1650" - integrity sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg== +"@smithy/util-body-length-browser@^2.0.0", "@smithy/util-body-length-browser@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.1.tgz#424485cc81c640d18c17c683e0e6edb57e8e2ab9" + integrity sha512-NXYp3ttgUlwkaug4bjBzJ5+yIbUbUx8VsSLuHZROQpoik+gRkIBeEG9MPVYfvPNpuXb/puqodeeUXcKFe7BLOQ== dependencies: tslib "^2.5.0" @@ -12886,14 +13882,14 @@ bowser "^2.11.0" tslib "^2.5.0" -"@smithy/util-defaults-mode-browser@^2.0.1", "@smithy/util-defaults-mode-browser@^2.0.13", "@smithy/util-defaults-mode-browser@^2.0.16": - version "2.0.16" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.16.tgz#7d60c4e1d00ed569f47fd6343b822c4ff3c2c9f8" - integrity sha512-Uv5Cu8nVkuvLn0puX+R9zWbSNpLIR3AxUlPoLJ7hC5lvir8B2WVqVEkJLwtixKAncVLasnTVjPDCidtAUTGEQw== +"@smithy/util-defaults-mode-browser@^2.0.1", "@smithy/util-defaults-mode-browser@^2.0.16", "@smithy/util-defaults-mode-browser@^2.0.19", "@smithy/util-defaults-mode-browser@^2.0.22": + version "2.0.22" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.22.tgz#8ef8c36b8c3c2f98f7a62278c3c684d659134269" + integrity sha512-qcF20IHHH96FlktvBRICDXDhLPtpVmtksHmqNGtotb9B0DYWXsC6jWXrkhrrwF7tH26nj+npVTqh9isiFV1gdA== dependencies: - "@smithy/property-provider" "^2.0.13" - "@smithy/smithy-client" "^2.1.12" - "@smithy/types" "^2.4.0" + "@smithy/property-provider" "^2.0.16" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" bowser "^2.11.0" tslib "^2.5.0" @@ -12909,26 +13905,26 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" -"@smithy/util-defaults-mode-node@^2.0.1", "@smithy/util-defaults-mode-node@^2.0.15", "@smithy/util-defaults-mode-node@^2.0.21": - version "2.0.21" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.21.tgz#d10c887b3e641c63e235ce95ba32137fd0bd1838" - integrity sha512-cUEsttVZ79B7Al2rWK2FW03HBpD9LyuqFtm+1qFty5u9sHSdesr215gS2Ln53fTopNiPgeXpdoM3IgjvIO0rJw== +"@smithy/util-defaults-mode-node@^2.0.1", "@smithy/util-defaults-mode-node@^2.0.21", "@smithy/util-defaults-mode-node@^2.0.25", "@smithy/util-defaults-mode-node@^2.0.29": + version "2.0.29" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.29.tgz#6b210aede145a6bf4bd83d9f465948fb300ca577" + integrity sha512-+uG/15VoUh6JV2fdY9CM++vnSuMQ1VKZ6BdnkUM7R++C/vLjnlg+ToiSR1FqKZbMmKBXmsr8c/TsDWMAYvxbxQ== dependencies: - "@smithy/config-resolver" "^2.0.16" - "@smithy/credential-provider-imds" "^2.0.18" - "@smithy/node-config-provider" "^2.1.3" - "@smithy/property-provider" "^2.0.13" - "@smithy/smithy-client" "^2.1.12" - "@smithy/types" "^2.4.0" + "@smithy/config-resolver" "^2.0.21" + "@smithy/credential-provider-imds" "^2.1.4" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/property-provider" "^2.0.16" + "@smithy/smithy-client" "^2.1.18" + "@smithy/types" "^2.7.0" tslib "^2.5.0" -"@smithy/util-endpoints@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-1.0.2.tgz#8be5b840c19661e3830ca10973f775b331bd94cd" - integrity sha512-QEdq+sP68IJHAMVB2ugKVVZEWeKQtZLuf+akHzc8eTVElsZ2ZdVLWC6Cp+uKjJ/t4yOj1qu6ZzyxJQEQ8jdEjg== +"@smithy/util-endpoints@^1.0.2", "@smithy/util-endpoints@^1.0.4", "@smithy/util-endpoints@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-1.0.7.tgz#5a258ac7838dea085660060b515cd2d19f19a4bc" + integrity sha512-Q2gEind3jxoLk6hdKWyESMU7LnXz8aamVwM+VeVjOYzYT1PalGlY/ETa48hv2YpV4+YV604y93YngyzzzQ4IIA== dependencies: - "@smithy/node-config-provider" "^2.1.3" - "@smithy/types" "^2.4.0" + "@smithy/node-config-provider" "^2.1.8" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/util-hex-encoding@^1.0.1": @@ -12973,14 +13969,6 @@ dependencies: tslib "^2.5.0" -"@smithy/util-middleware@^2.0.3": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-2.0.4.tgz#2c406efac04e341c3df6435d71fd9c73e03feb46" - integrity sha512-Pbu6P4MBwRcjrLgdTR1O4Y3c0sTZn2JdOiJNcgL7EcIStcQodj+6ZTXtbyU/WTEU3MV2NMA10LxFc3AWHZ3+4A== - dependencies: - "@smithy/types" "^2.3.5" - tslib "^2.5.0" - "@smithy/util-middleware@^2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-2.0.5.tgz#c63dc491de81641c99ade9309f30c54ad0e28fbd" @@ -12989,6 +13977,22 @@ "@smithy/types" "^2.4.0" tslib "^2.5.0" +"@smithy/util-middleware@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-2.0.6.tgz#fbc23119436baaa1494c11803abaabef8cb3e2c4" + integrity sha512-7W4uuwBvSLgKoLC1x4LfeArCVcbuHdtVaC4g30kKsD1erfICyQ45+tFhhs/dZNeQg+w392fhunCm/+oCcb6BSA== + dependencies: + "@smithy/types" "^2.5.0" + tslib "^2.5.0" + +"@smithy/util-middleware@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-2.0.8.tgz#2ec1da1190d09b69512ce0248ebd5e819e3c8a92" + integrity sha512-qkvqQjM8fRGGA8P2ydWylMhenCDP8VlkPn8kiNuFEaFz9xnUKC2irfqsBSJrfrOB9Qt6pQsI58r3zvvumhFMkw== + dependencies: + "@smithy/types" "^2.7.0" + tslib "^2.5.0" + "@smithy/util-retry@^1.0.1", "@smithy/util-retry@^1.0.2", "@smithy/util-retry@^1.0.3", "@smithy/util-retry@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-1.0.4.tgz#9d95df3884981414163d5f780d38e3529384d9ad" @@ -12997,13 +14001,13 @@ "@smithy/service-error-classification" "^1.0.3" tslib "^2.5.0" -"@smithy/util-retry@^2.0.0", "@smithy/util-retry@^2.0.3", "@smithy/util-retry@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-2.0.5.tgz#1a93721da082301aca61d8b42380369761a7e80d" - integrity sha512-x3t1+MQAJ6QONk3GTbJNcugCFDVJ+Bkro5YqQQK1EyVesajNDqxFtCx9WdOFNGm/Cbm7tUdwVEmfKQOJoU2Vtw== +"@smithy/util-retry@^2.0.0", "@smithy/util-retry@^2.0.5", "@smithy/util-retry@^2.0.6", "@smithy/util-retry@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-2.0.8.tgz#61f8db11e4fe60975cb9fb2eada173f5024a06f3" + integrity sha512-cQTPnVaVFMjjS6cb44WV2yXtHVyXDC5icKyIbejMarJEApYeJWpBU3LINTxHqp/tyLI+MZOUdosr2mZ3sdziNg== dependencies: - "@smithy/service-error-classification" "^2.0.5" - "@smithy/types" "^2.4.0" + "@smithy/service-error-classification" "^2.0.8" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@smithy/util-stream@^1.0.1": @@ -13020,32 +14024,32 @@ "@smithy/util-utf8" "^1.0.1" tslib "^2.5.0" -"@smithy/util-stream@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-2.0.1.tgz#cbe2af5704a6050b9075835a8e7251185901864b" - integrity sha512-2a0IOtwIKC46EEo7E7cxDN8u2jwOiYYJqcFKA6rd5rdXqKakHT2Gc+AqHWngr0IEHUfW92zX12wRQKwyoqZf2Q== +"@smithy/util-stream@^2.0.20": + version "2.0.20" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-2.0.20.tgz#0dbff46b07856b608512688437e685c638d75431" + integrity sha512-tT8VASuD8jJu0yjHEMTCPt1o5E3FVzgdsxK6FQLAjXKqVv5V8InCnc0EOsYrijgspbfDqdAJg7r0o2sySfcHVg== dependencies: - "@smithy/fetch-http-handler" "^2.0.1" - "@smithy/node-http-handler" "^2.0.1" - "@smithy/types" "^2.0.2" - "@smithy/util-base64" "^2.0.0" + "@smithy/fetch-http-handler" "^2.2.6" + "@smithy/node-http-handler" "^2.1.9" + "@smithy/types" "^2.5.0" + "@smithy/util-base64" "^2.0.1" "@smithy/util-buffer-from" "^2.0.0" "@smithy/util-hex-encoding" "^2.0.0" - "@smithy/util-utf8" "^2.0.0" + "@smithy/util-utf8" "^2.0.2" tslib "^2.5.0" -"@smithy/util-stream@^2.0.17": - version "2.0.17" - resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-2.0.17.tgz#4c980891b0943e9e64949d7afcf1ec4a7b510ea8" - integrity sha512-fP/ZQ27rRvHsqItds8yB7jerwMpZFTL3QqbQbidUiG0+mttMoKdP0ZqnvM8UK5q0/dfc3/pN7g4XKPXOU7oRWw== +"@smithy/util-stream@^2.0.23": + version "2.0.23" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-2.0.23.tgz#468ad29913d091092317cfea2d8ac5b866326a07" + integrity sha512-OJMWq99LAZJUzUwTk+00plyxX3ESktBaGPhqNIEVab+53gLULiWN9B/8bRABLg0K6R6Xg4t80uRdhk3B/LZqMQ== dependencies: - "@smithy/fetch-http-handler" "^2.2.4" - "@smithy/node-http-handler" "^2.1.8" - "@smithy/types" "^2.4.0" - "@smithy/util-base64" "^2.0.0" + "@smithy/fetch-http-handler" "^2.3.1" + "@smithy/node-http-handler" "^2.2.1" + "@smithy/types" "^2.7.0" + "@smithy/util-base64" "^2.0.1" "@smithy/util-buffer-from" "^2.0.0" "@smithy/util-hex-encoding" "^2.0.0" - "@smithy/util-utf8" "^2.0.0" + "@smithy/util-utf8" "^2.0.2" tslib "^2.5.0" "@smithy/util-uri-escape@^1.0.1": @@ -13070,10 +14074,10 @@ "@smithy/util-buffer-from" "^1.0.2" tslib "^2.5.0" -"@smithy/util-utf8@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-2.0.0.tgz#b4da87566ea7757435e153799df9da717262ad42" - integrity sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ== +"@smithy/util-utf8@^2.0.0", "@smithy/util-utf8@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-2.0.2.tgz#626b3e173ad137208e27ed329d6bea70f4a1a7f7" + integrity sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA== dependencies: "@smithy/util-buffer-from" "^2.0.0" tslib "^2.5.0" @@ -13087,13 +14091,13 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" -"@smithy/util-waiter@^2.0.1", "@smithy/util-waiter@^2.0.10", "@smithy/util-waiter@^2.0.12": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-2.0.12.tgz#a7348f9fd2bade5f2f3ee7ecf7c43ab86ed244ee" - integrity sha512-3sENmyVa1NnOPoiT2NCApPmu7ukP7S/v7kL9IxNmnygkDldn7/yK0TP42oPJLwB2k3mospNsSePIlqdXEUyPHA== +"@smithy/util-waiter@^2.0.12", "@smithy/util-waiter@^2.0.13", "@smithy/util-waiter@^2.0.15": + version "2.0.15" + resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-2.0.15.tgz#b02a42bf1b82f07973d1756a0ee10fafa1fbf58e" + integrity sha512-9Y+btzzB7MhLADW7xgD6SjvmoYaRkrb/9SCbNGmNdfO47v38rxb90IGXyDtAK0Shl9bMthTmLgjlfYc+vtz2Qw== dependencies: - "@smithy/abort-controller" "^2.0.12" - "@smithy/types" "^2.4.0" + "@smithy/abort-controller" "^2.0.15" + "@smithy/types" "^2.7.0" tslib "^2.5.0" "@storybook/addon-a11y@^6.5.15": @@ -13433,28 +14437,28 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/builder-webpack4@6.5.15": - version "6.5.15" - resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.5.15.tgz#8050b2eec84e055eee9b181e067d9a8aa76e252a" - integrity sha512-1ZkMECUUdiYplhlgyUF5fqW3XU7eWNDJbuPUguyDOeidgJ111WZzBcLuKj+SNrzdNNgXwROCWAFybiNnX33YHQ== +"@storybook/builder-webpack4@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.5.16.tgz#ac468d244835a7f3bd01936398fee47244da35c1" + integrity sha512-YqDIrVNsUo8r9xc6AxsYDLxVYtMgl5Bxk+8/h1adsOko+jAFhdg6hOcAVxEmoSI0TMASOOVMFlT2hr23ppN2rQ== dependencies: "@babel/core" "^7.12.10" - "@storybook/addons" "6.5.15" - "@storybook/api" "6.5.15" - "@storybook/channel-postmessage" "6.5.15" - "@storybook/channels" "6.5.15" - "@storybook/client-api" "6.5.15" - "@storybook/client-logger" "6.5.15" - "@storybook/components" "6.5.15" - "@storybook/core-common" "6.5.15" - "@storybook/core-events" "6.5.15" - "@storybook/node-logger" "6.5.15" - "@storybook/preview-web" "6.5.15" - "@storybook/router" "6.5.15" + "@storybook/addons" "6.5.16" + "@storybook/api" "6.5.16" + "@storybook/channel-postmessage" "6.5.16" + "@storybook/channels" "6.5.16" + "@storybook/client-api" "6.5.16" + "@storybook/client-logger" "6.5.16" + "@storybook/components" "6.5.16" + "@storybook/core-common" "6.5.16" + "@storybook/core-events" "6.5.16" + "@storybook/node-logger" "6.5.16" + "@storybook/preview-web" "6.5.16" + "@storybook/router" "6.5.16" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.5.15" - "@storybook/theming" "6.5.15" - "@storybook/ui" "6.5.15" + "@storybook/store" "6.5.16" + "@storybook/theming" "6.5.16" + "@storybook/ui" "6.5.16" "@types/node" "^14.0.10 || ^16.0.0" "@types/webpack" "^4.41.26" autoprefixer "^9.8.6" @@ -13580,6 +14584,17 @@ global "^4.4.0" telejson "^6.0.8" +"@storybook/channel-websocket@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.5.16.tgz#41f69ca9444a4dfbf72580b4696900c5b1d2b817" + integrity sha512-wJg2lpBjmRC2GJFzmhB9kxlh109VE58r/0WhFtLbwKvPqsvGf82xkBEl6BtBCvIQ4stzYnj/XijjA8qSi2zpOg== + dependencies: + "@storybook/channels" "6.5.16" + "@storybook/client-logger" "6.5.16" + core-js "^3.8.2" + global "^4.4.0" + telejson "^6.0.8" + "@storybook/channels@6.5.10": version "6.5.10" resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.5.10.tgz#fca5b0d1ea8d30b022e805301ed436407c867ac4" @@ -13671,6 +14686,32 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/client-api@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.5.16.tgz#13e5a7c3d1f0f951ec4ef51cfcf2c5aafb560e12" + integrity sha512-i3UwkzzUFw8I+E6fOcgB5sc4oU2fhvaKnqC1mpd9IYGJ9JN9MnGIaVl3Ko28DtFItu/QabC9JsLIJVripFLktQ== + dependencies: + "@storybook/addons" "6.5.16" + "@storybook/channel-postmessage" "6.5.16" + "@storybook/channels" "6.5.16" + "@storybook/client-logger" "6.5.16" + "@storybook/core-events" "6.5.16" + "@storybook/csf" "0.0.2--canary.4566f4d.1" + "@storybook/store" "6.5.16" + "@types/qs" "^6.9.5" + "@types/webpack-env" "^1.16.0" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.21" + memoizerific "^1.11.3" + qs "^6.10.0" + regenerator-runtime "^0.13.7" + store2 "^2.12.0" + synchronous-promise "^2.0.15" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/client-logger@6.5.10": version "6.5.10" resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.5.10.tgz#cfea823a5b8444409daa74f854c5d05367986b34" @@ -13770,6 +14811,32 @@ unfetch "^4.2.0" util-deprecate "^1.0.2" +"@storybook/core-client@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.5.16.tgz#ed2328fd38c6111fe887f6a91b28d9dc2b17092a" + integrity sha512-14IRaDrVtKrQ+gNWC0wPwkCNfkZOKghYV/swCUnQX3rP99defsZK8Hc7xHIYoAiOP5+sc3sweRAxgmFiJeQ1Ig== + dependencies: + "@storybook/addons" "6.5.16" + "@storybook/channel-postmessage" "6.5.16" + "@storybook/channel-websocket" "6.5.16" + "@storybook/client-api" "6.5.16" + "@storybook/client-logger" "6.5.16" + "@storybook/core-events" "6.5.16" + "@storybook/csf" "0.0.2--canary.4566f4d.1" + "@storybook/preview-web" "6.5.16" + "@storybook/store" "6.5.16" + "@storybook/ui" "6.5.16" + airbnb-js-shims "^2.2.1" + ansi-to-html "^0.6.11" + core-js "^3.8.2" + global "^4.4.0" + lodash "^4.17.21" + qs "^6.10.0" + regenerator-runtime "^0.13.7" + ts-dedent "^2.0.0" + unfetch "^4.2.0" + util-deprecate "^1.0.2" + "@storybook/core-common@6.5.10": version "6.5.10" resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.5.10.tgz#6b93449548b0890f5c68d89f0ca78e092026182c" @@ -13964,23 +15031,23 @@ resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-7.3.2.tgz#9d18df7ceb6901225218e538a3d1d720a2f49b46" integrity sha512-DCrM3s+sxLKS8vl0zB+1tZEtcl5XQTOGl46XgRRV/SIBabFbsC0l5pQPswWkTUsIqdREtiT0YUHcXB1+YDyFvA== -"@storybook/core-server@6.5.15": - version "6.5.15" - resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.5.15.tgz#217c40d7a33708b9ac69f73dd51ed9d2f031d19d" - integrity sha512-m+pZwHhCjwryeqTptyyKHSbIjnnPGKoRSnkqLTOpKQf8llZMnNQWUFrn4fx6UDKzxFQ2st2+laV8O2QbMs8qwQ== +"@storybook/core-server@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.5.16.tgz#f40de3413de49388129d29c74e5e48321af03f12" + integrity sha512-/3NPfmNyply395Dm0zaVZ8P9aruwO+tPx4D6/jpw8aqrRSwvAMndPMpoMCm0NXcpSm5rdX+Je4S3JW6JcggFkA== dependencies: "@discoveryjs/json-ext" "^0.5.3" - "@storybook/builder-webpack4" "6.5.15" - "@storybook/core-client" "6.5.15" - "@storybook/core-common" "6.5.15" - "@storybook/core-events" "6.5.15" + "@storybook/builder-webpack4" "6.5.16" + "@storybook/core-client" "6.5.16" + "@storybook/core-common" "6.5.16" + "@storybook/core-events" "6.5.16" "@storybook/csf" "0.0.2--canary.4566f4d.1" - "@storybook/csf-tools" "6.5.15" - "@storybook/manager-webpack4" "6.5.15" - "@storybook/node-logger" "6.5.15" + "@storybook/csf-tools" "6.5.16" + "@storybook/manager-webpack4" "6.5.16" + "@storybook/node-logger" "6.5.16" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.5.15" - "@storybook/telemetry" "6.5.15" + "@storybook/store" "6.5.16" + "@storybook/telemetry" "6.5.16" "@types/node" "^14.0.10 || ^16.0.0" "@types/node-fetch" "^2.5.7" "@types/pretty-hrtime" "^1.0.0" @@ -14015,18 +15082,18 @@ ws "^8.2.3" x-default-browser "^0.4.0" -"@storybook/core@6.5.15": - version "6.5.15" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.5.15.tgz#82e0998d908fc9e66a659e1217072c425d63f9b6" - integrity sha512-T9TjLxbb5P/XvLEoj0dnbtexJa0V3pqCifRlIUNkTJO0nU3PdGLMcKMSyIYWjkthAJ9oBrajnodV0UveM/epTg== +"@storybook/core@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.5.16.tgz#ae994f01327fe81b6e652963c35bac7a74f0da06" + integrity sha512-CEF3QFTsm/VMnMKtRNr4rRdLeIkIG0g1t26WcmxTdSThNPBd8CsWzQJ7Jqu7CKiut+MU4A1LMOwbwCE5F2gmyA== dependencies: - "@storybook/core-client" "6.5.15" - "@storybook/core-server" "6.5.15" + "@storybook/core-client" "6.5.16" + "@storybook/core-server" "6.5.16" -"@storybook/csf-tools@6.5.15": - version "6.5.15" - resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.5.15.tgz#dc5d0fe946c25d60bf201e5180c4fc81b24f763b" - integrity sha512-2LwSD7yE/ccXBc58K4vdKw/oJJg6IpC4WD51rBt2mAl5JUCkxhOq7wG/Z8Wy1lZw2LVuKNTmjVou5blGRI/bTg== +"@storybook/csf-tools@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.5.16.tgz#367889a3ddb33c93261129104ec2958215ec5459" + integrity sha512-+WD4sH/OwAfXZX3IN6/LOZ9D9iGEFcN+Vvgv9wOsLRgsAZ10DG/NK6c1unXKDM/ogJtJYccNI8Hd+qNE/GFV6A== dependencies: "@babel/core" "^7.12.10" "@babel/generator" "^7.12.11" @@ -14050,19 +15117,6 @@ dependencies: lodash "^4.17.15" -"@storybook/docs-tools@6.5.15": - version "6.5.15" - resolved "https://registry.yarnpkg.com/@storybook/docs-tools/-/docs-tools-6.5.15.tgz#c9a3954719c45c3748abd6aaa735e33f5c961912" - integrity sha512-8w78NFOtlJGuIa9vPPsr87J9iQUGmLFh7CrMS2+t9LxW+0oH5MZ8QqPQUHNuTuKsYN3r+QAmmi2pj0auZmCoKA== - dependencies: - "@babel/core" "^7.12.10" - "@storybook/csf" "0.0.2--canary.4566f4d.1" - "@storybook/store" "6.5.15" - core-js "^3.8.2" - doctrine "^3.0.0" - lodash "^4.17.21" - regenerator-runtime "^0.13.7" - "@storybook/docs-tools@6.5.16": version "6.5.16" resolved "https://registry.yarnpkg.com/@storybook/docs-tools/-/docs-tools-6.5.16.tgz#1ec5433eeab63a214d37ffc4660cdaec9704ac39" @@ -14081,20 +15135,20 @@ resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed" integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ== -"@storybook/manager-webpack4@6.5.15": - version "6.5.15" - resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.5.15.tgz#09808b87b510591390765af708ab511ff63a1e5c" - integrity sha512-zRvBTMoaFO6MvHDsDLnt3tsFENhpY3k+e/UIPdqbIDMbUPGGQzxJucAM9aS/kbVSO5IVs8IflVxbeeB/uTIIfA== +"@storybook/manager-webpack4@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.5.16.tgz#7033228d38f048ceff3d403ba918d7f206b926a5" + integrity sha512-5VJZwmQU6AgdsBPsYdu886UKBHQ9SJEnFMaeUxKEclXk+iRsmbzlL4GHKyVd6oGX/ZaecZtcHPR6xrzmA4Ziew== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.5.15" - "@storybook/core-client" "6.5.15" - "@storybook/core-common" "6.5.15" - "@storybook/node-logger" "6.5.15" - "@storybook/theming" "6.5.15" - "@storybook/ui" "6.5.15" + "@storybook/addons" "6.5.16" + "@storybook/core-client" "6.5.16" + "@storybook/core-common" "6.5.16" + "@storybook/node-logger" "6.5.16" + "@storybook/theming" "6.5.16" + "@storybook/ui" "6.5.16" "@types/node" "^14.0.10 || ^16.0.0" "@types/webpack" "^4.41.26" babel-loader "^8.0.0" @@ -14323,23 +15377,23 @@ tslib "^2.0.0" "@storybook/react@^6.5.15": - version "6.5.15" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.5.15.tgz#83e645b16a4d241ec84a8d0015b1a7a2d55c5091" - integrity sha512-iQta2xOs/oK0sw/zpn3g/huvOmvggzi8z2/WholmUmmRiSQRo9lOhRXH0u13T4ja4fEa+u7m58G83xOG6i73Kw== + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.5.16.tgz#f7b82ba87f5bb73b4e4e83cce298a98710a88398" + integrity sha512-cBtNlOzf/MySpNLBK22lJ8wFU22HnfTB2xJyBk7W7Zi71Lm7Uxkhv1Pz8HdiQndJ0SlsAAQOWjQYsSZsGkZIaA== dependencies: "@babel/preset-flow" "^7.12.1" "@babel/preset-react" "^7.12.10" "@pmmmwh/react-refresh-webpack-plugin" "^0.5.3" - "@storybook/addons" "6.5.15" - "@storybook/client-logger" "6.5.15" - "@storybook/core" "6.5.15" - "@storybook/core-common" "6.5.15" + "@storybook/addons" "6.5.16" + "@storybook/client-logger" "6.5.16" + "@storybook/core" "6.5.16" + "@storybook/core-common" "6.5.16" "@storybook/csf" "0.0.2--canary.4566f4d.1" - "@storybook/docs-tools" "6.5.15" - "@storybook/node-logger" "6.5.15" + "@storybook/docs-tools" "6.5.16" + "@storybook/node-logger" "6.5.16" "@storybook/react-docgen-typescript-plugin" "1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.5.15" + "@storybook/store" "6.5.16" "@types/estree" "^0.0.51" "@types/node" "^14.14.20 || ^16.0.0" "@types/webpack-env" "^1.16.0" @@ -14483,13 +15537,13 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/telemetry@6.5.15": - version "6.5.15" - resolved "https://registry.yarnpkg.com/@storybook/telemetry/-/telemetry-6.5.15.tgz#852050c1e54bf704a104e47e4e498d999096e0e7" - integrity sha512-WHMRG6xMkEGscn1q4SotwzV8hxM1g3zHyXPN77iosY5zpOIn/qAzvkmW28V1DPH9jPWMZMizBgG1TIQvUpduXg== +"@storybook/telemetry@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/telemetry/-/telemetry-6.5.16.tgz#b13c8133e02c28e37b7716c987e7414b1ddc5363" + integrity sha512-CWr5Uko1l9jJW88yTXsZTj/3GTabPvw0o7pDPOXPp8JRZiJTxv1JFaFCafhK9UzYbgcRuGfCC8kEWPZims7iKA== dependencies: - "@storybook/client-logger" "6.5.15" - "@storybook/core-common" "6.5.15" + "@storybook/client-logger" "6.5.16" + "@storybook/core-common" "6.5.16" chalk "^4.1.0" core-js "^3.8.2" detect-package-manager "^2.0.1" @@ -14561,6 +15615,26 @@ regenerator-runtime "^0.13.7" resolve-from "^5.0.0" +"@storybook/ui@6.5.16": + version "6.5.16" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.5.16.tgz#c73bf456e672ecf2370b4365070088487fc0ce57" + integrity sha512-rHn/n12WM8BaXtZ3IApNZCiS+C4Oc5+Lkl4MoctX8V7QSml0SxZBB5hsJ/AiWkgbRxjQpa/L/Nt7/Qw0FjTH/A== + dependencies: + "@storybook/addons" "6.5.16" + "@storybook/api" "6.5.16" + "@storybook/channels" "6.5.16" + "@storybook/client-logger" "6.5.16" + "@storybook/components" "6.5.16" + "@storybook/core-events" "6.5.16" + "@storybook/router" "6.5.16" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.5.16" + core-js "^3.8.2" + memoizerific "^1.11.3" + qs "^6.10.0" + regenerator-runtime "^0.13.7" + resolve-from "^5.0.0" + "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" @@ -14698,13 +15772,13 @@ resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.4.tgz#86e04e677cd6c05fa230dd15ac223fa72d1d7090" integrity sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g== -"@testing-library/cypress@^9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-9.0.0.tgz#3facad49c4654a99bbd138f83f33b415d2d6f097" - integrity sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA== +"@testing-library/cypress@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-10.0.1.tgz#15abae0edb83237316ec6d07e152b71a50b38387" + integrity sha512-e8uswjTZIBhaIXjzEcrQQ8nHRWHgZH7XBxKuIWxZ/T7FxfWhCR48nFhUX5nfPizjVOKSThEfOSv67jquc1ASkw== dependencies: "@babel/runtime" "^7.14.6" - "@testing-library/dom" "^8.1.0" + "@testing-library/dom" "^9.0.0" "@testing-library/dom@>=7", "@testing-library/dom@^9.0.0": version "9.0.1" @@ -14720,20 +15794,6 @@ lz-string "^1.5.0" pretty-format "^27.0.2" -"@testing-library/dom@^8.1.0": - version "8.17.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.17.1.tgz#2d7af4ff6dad8d837630fecd08835aee08320ad7" - integrity sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^4.2.0" - aria-query "^5.0.0" - chalk "^4.1.0" - dom-accessibility-api "^0.5.9" - lz-string "^1.4.4" - pretty-format "^27.0.2" - "@testing-library/jest-dom@^6.1.3": version "6.1.3" resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.1.3.tgz#443118c9e4043f96396f120de2c7122504a079c5" @@ -14749,9 +15809,9 @@ redent "^3.0.0" "@testing-library/react@^14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.0.0.tgz#59030392a6792450b9ab8e67aea5f3cc18d6347c" - integrity sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg== + version "14.1.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.1.2.tgz#a2b9e9ee87721ec9ed2d7cfc51cc04e474537c32" + integrity sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg== dependencies: "@babel/runtime" "^7.12.5" "@testing-library/dom" "^9.0.0" @@ -14847,18 +15907,13 @@ dependencies: "@types/node" "*" -"@types/archiver@^5.1.1": - version "5.3.2" - resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.2.tgz#a9f0bcb0f0b991400e7766d35f6e19d163bdadcc" - integrity sha512-IctHreBuWE5dvBDz/0WeKtyVKVRs4h75IblxOACL92wU66v+HGAfEYAOyXkOFphvRJMhuXdI9huDXpX0FC6lCw== +"@types/archiver@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-6.0.2.tgz#0daf8c83359cbde69de1e4b33dcade6a48a929e2" + integrity sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw== dependencies: "@types/readdir-glob" "*" -"@types/aria-query@^4.2.0": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" - integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== - "@types/aria-query@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" @@ -14870,9 +15925,9 @@ integrity sha512-C1rFKGVZ8KwqhwBOYlpoybTSRtxu2433ea6JaO3amc6ubEe08yQoFsPa9aU9YqvX7ppeZ25CnCtC4AH9mhtxsQ== "@types/aws-lambda@^8.10.76", "@types/aws-lambda@^8.10.83": - version "8.10.124" - resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.124.tgz#ea9d0aa36adbbae7a6c37edb072a1d64b49f0d4d" - integrity sha512-PHqK0SuAkFS3tZjceqRXecxxrWIN3VqTicuialtK2wZmvBy7H9WGc3u3+wOgaZB7N8SpSXDpWk9qa7eorpTStg== + version "8.10.130" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.130.tgz#d4a44201f0e47c8320a5868d845ad654f3b4adc2" + integrity sha512-HxTfLeGvD1wTJqIGwcBCpNmHKenja+We1e0cuzeIDFfbEj3ixnlTInyPR/81zAe0Ss/Ip12rFK6XNeMLVucOSg== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.7": version "7.1.19" @@ -15162,6 +16217,14 @@ dependencies: "@types/unist" "*" +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^5.0.0": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57" @@ -15387,7 +16450,7 @@ dependencies: "@types/node" "*" -"@types/node-fetch@2.6.4": +"@types/node-fetch@2.6.4", "@types/node-fetch@^2.5.7", "@types/node-fetch@^2.6.2": version "2.6.4" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg== @@ -15395,18 +16458,12 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node-fetch@^2.5.7", "@types/node-fetch@^2.6.2": - version "2.6.2" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" - integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== +"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^18.11.18": + version "18.18.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.8.tgz#2b285361f2357c8c8578ec86b5d097c7f464cfd6" + integrity sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ== dependencies: - "@types/node" "*" - form-data "^3.0.0" - -"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": - version "18.14.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.2.tgz#c076ed1d7b6095078ad3cf21dfeea951842778b1" - integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA== + undici-types "~5.26.4" "@types/node@^10.1.0": version "10.17.60" @@ -15419,9 +16476,9 @@ integrity sha512-Abq9fBviLV93OiXMu+f6r0elxCzRwc0RC5f99cU892uBITL44pTvgvEqlRlPRi8EGcO1z7Cp8A4d0s/p3J/+Nw== "@types/node@^16.18.39": - version "16.18.61" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.61.tgz#5ea47e3018348bf3bbbe646b396ba5e720310be1" - integrity sha512-k0N7BqGhJoJzdh6MuQg1V1ragJiXTh8VUBAZTWjJ9cUq23SG0F0xavOwZbhiP4J3y20xd6jxKx+xNUhkMAi76Q== + version "16.18.68" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.68.tgz#3155f64a961b3d8d10246c80657f9a7292e3421a" + integrity sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -15444,9 +16501,9 @@ integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== "@types/path-browserify@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/path-browserify/-/path-browserify-1.0.0.tgz#294ec6e88b6b0d340a3897b7120e5b393f16690e" - integrity sha512-XMCcyhSvxcch8b7rZAtFAaierBYdeHXVvg2iYnxOV0MCQHmPuRRmGZPFDRzPayxcGiiSL1Te9UIO+f3cuj0tfw== + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/path-browserify/-/path-browserify-1.0.2.tgz#14b62d4371316016f254b9cca5c10e60ef33dcc0" + integrity sha512-ZkC5IUqqIFPXx3ASTTybTzmQdwHwe2C0u3eL75ldQ6T9E9IWFJodn6hIfbZGab73DfyiHN4Xw15gNxUq2FbvBA== "@types/pg-pool@2.0.3": version "2.0.3" @@ -15511,9 +16568,9 @@ "@types/react" "*" "@types/react-test-renderer@^18.0.0": - version "18.0.3" - resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.3.tgz#67922bf5e5f0096581b1efd67dcdeabdd400cfea" - integrity sha512-4wcNLnY6nIT+L6g94CpzL4CXX2P18JvKPU9CDlaHr3DnbP3GiaQLhDotJqjWlVqOcE4UhLRjp0MtxqwuNKONnA== + version "18.0.7" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.7.tgz#2cfe657adb3688cdf543995eceb2e062b5a68728" + integrity sha512-1+ANPOWc6rB3IkSnElhjv6VLlKg2dSv/OWClUyZimbLsQyBn8Js9Vtdsi3UICJ2rIQ3k2la06dkB+C92QfhKmg== dependencies: "@types/react" "*" @@ -15548,9 +16605,9 @@ "@types/node" "*" "@types/request@^2.48.7": - version "2.48.8" - resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c" - integrity sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ== + version "2.48.12" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30" + integrity sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw== dependencies: "@types/caseless" "*" "@types/node" "*" @@ -15620,9 +16677,9 @@ integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== "@types/sizzle@^2.3.2": - version "2.3.6" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.6.tgz#e39b7123dac4631001939bd4c2a26d46010f2275" - integrity sha512-m04Om5Gz6kbjUwAQ7XJJQ30OdEFsSmAVsvn4NYwcTRyMVpKKa1aPuESw1n2CxS5fYkOQv3nHgDKeNa8e76fUkw== + version "2.3.8" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627" + integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== "@types/sockjs@^0.3.33": version "0.3.33" @@ -15636,6 +16693,13 @@ resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== +"@types/ssh2@^1.11.15", "@types/ssh2@^1.11.9": + version "1.11.18" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.11.18.tgz#0766a52a91f85c39768ccdc8dce1498dcbb02d32" + integrity sha512-7eH4ppQMFlzvn//zhwD54MWaITR1aSc1oFBye9vb76GZ2Y9PSFYdwVIwOlxRXWs5+1hifntXyt+8a6SUbOD7Hg== + dependencies: + "@types/node" "^18.11.18" + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" @@ -15790,14 +16854,14 @@ debug "^4.3.4" "@typescript-eslint/parser@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.5.0.tgz#3d6ed231c5e307c5f5f4a0d86893ec01e92b8c77" - integrity sha512-LMAVtR5GN8nY0G0BadkG0XIe4AcNMeyEy3DyhKGAh9k4pLSMBO7rF29JvDBpZGCmp5Pgz5RLHP6eCpSYZJQDuQ== + version "6.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.15.0.tgz#1af69741cfa314a13c1434d0bdd5a0c3096699d7" + integrity sha512-MkgKNnsjC6QwcMdlNAel24jjkEO/0hQaMDLqP4S9zq5HBAUJNQB6y+3DwLjX7b3l2b37eNAxMPLwb3/kh8VKdA== dependencies: - "@typescript-eslint/scope-manager" "6.5.0" - "@typescript-eslint/types" "6.5.0" - "@typescript-eslint/typescript-estree" "6.5.0" - "@typescript-eslint/visitor-keys" "6.5.0" + "@typescript-eslint/scope-manager" "6.15.0" + "@typescript-eslint/types" "6.15.0" + "@typescript-eslint/typescript-estree" "6.15.0" + "@typescript-eslint/visitor-keys" "6.15.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.33.1": @@ -15824,13 +16888,13 @@ "@typescript-eslint/types" "5.58.0" "@typescript-eslint/visitor-keys" "5.58.0" -"@typescript-eslint/scope-manager@6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz#f2cb20895aaad41b3ad27cc3a338ce8598f261c5" - integrity sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw== +"@typescript-eslint/scope-manager@6.15.0": + version "6.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.15.0.tgz#40e5214a3e9e048aca55ce33381bc61b6b51c32a" + integrity sha512-+BdvxYBltqrmgCNu4Li+fGDIkW9n//NrruzG9X1vBzaNK+ExVXPoGB71kneaVw/Jp+4rH/vaMAGC6JfMbHstVg== dependencies: - "@typescript-eslint/types" "6.5.0" - "@typescript-eslint/visitor-keys" "6.5.0" + "@typescript-eslint/types" "6.15.0" + "@typescript-eslint/visitor-keys" "6.15.0" "@typescript-eslint/type-utils@5.57.0": version "5.57.0" @@ -15857,10 +16921,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.58.0.tgz#54c490b8522c18986004df7674c644ffe2ed77d8" integrity sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g== -"@typescript-eslint/types@6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.5.0.tgz#f4e55cfd99ac5346ea772770bf212a3e689a8f04" - integrity sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w== +"@typescript-eslint/types@6.15.0": + version "6.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.15.0.tgz#a9f7b006aee52b0948be6e03f521814bf435ddd5" + integrity sha512-yXjbt//E4T/ee8Ia1b5mGlbNj9fB9lJP4jqLbZualwpP2BCQ5is6BcWwxpIsY4XKAhmdv3hrW92GdtJbatC6dQ== "@typescript-eslint/typescript-estree@5.33.1": version "5.33.1" @@ -15901,13 +16965,13 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz#1cef6bc822585e9ef89d88834bc902d911d747ed" - integrity sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ== +"@typescript-eslint/typescript-estree@6.15.0": + version "6.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.15.0.tgz#2f8a513df1ce5e6e1ba8e5c6aa52f392ae023fc5" + integrity sha512-7mVZJN7Hd15OmGuWrp2T9UvqR2Ecg+1j/Bp1jXUEY2GZKV6FXlOIoqVDmLpBiEiq3katvj/2n2mR0SDwtloCew== dependencies: - "@typescript-eslint/types" "6.5.0" - "@typescript-eslint/visitor-keys" "6.5.0" + "@typescript-eslint/types" "6.15.0" + "@typescript-eslint/visitor-keys" "6.15.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -15964,12 +17028,12 @@ "@typescript-eslint/types" "5.58.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz#1a6f474a0170a447b76f0699ce6700110fd11436" - integrity sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA== +"@typescript-eslint/visitor-keys@6.15.0": + version "6.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.15.0.tgz#5baf97a7bfeec6f4894d400437055155a46b2330" + integrity sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w== dependencies: - "@typescript-eslint/types" "6.5.0" + "@typescript-eslint/types" "6.15.0" eslint-visitor-keys "^3.4.1" "@vendia/serverless-express@^4.3.9": @@ -16450,6 +17514,13 @@ agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2: dependencies: debug "4" +agent-base@^7.0.2: + version "7.1.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + agentkeepalive@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" @@ -16669,9 +17740,9 @@ apollo-reporting-protobuf@^3.4.0: "@apollo/protobufjs" "1.2.6" apollo-server-core@^3.11.1, apollo-server-core@^3.12.0: - version "3.12.1" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-3.12.1.tgz#ba255c37345db29c48a2e0c064c519a8d62eb5af" - integrity sha512-9SF5WAkkV0FZQ2HVUWI9Jada1U0jg7e8NCN9EklbtvaCeUlOLyXyM+KCWuZ7+dqHxjshbtcwylPHutt3uzoNkw== + version "3.13.0" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-3.13.0.tgz#ad6601fbb34cc97eedca27a9fb0b5738d11cd27d" + integrity sha512-v/g6DR6KuHn9DYSdtQijz8dLOkP78I5JSVJzPkARhDbhpH74QNwrQ2PP2URAPPEDJ2EeZNQDX8PvbYkAKqg+kg== dependencies: "@apollo/utils.keyvaluecache" "^1.0.1" "@apollo/utils.logger" "^1.0.0" @@ -16804,6 +17875,18 @@ archiver-utils@^2.1.0: normalize-path "^3.0.0" readable-stream "^2.0.0" +archiver-utils@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-4.0.1.tgz#66ad15256e69589a77f706c90c6dbcc1b2775d2a" + integrity sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg== + dependencies: + glob "^8.0.0" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash "^4.17.15" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + archiver@^5.3.0, archiver@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.1.tgz#21e92811d6f09ecfce649fbefefe8c79e57cbbb6" @@ -16817,6 +17900,19 @@ archiver@^5.3.0, archiver@^5.3.1: tar-stream "^2.2.0" zip-stream "^4.1.0" +archiver@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-6.0.1.tgz#d56968d4c09df309435adb5a1bbfc370dae48133" + integrity sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ== + dependencies: + archiver-utils "^4.0.1" + async "^3.2.4" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.1.2" + tar-stream "^3.0.0" + zip-stream "^5.0.1" + archy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" @@ -17071,7 +18167,7 @@ asn1.js@^5.2.0: minimalistic-assert "^1.0.0" safer-buffer "^2.1.0" -asn1@~0.2.3: +asn1@^0.2.6, asn1@~0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== @@ -17148,10 +18244,10 @@ async-retry@^1.2.1, async-retry@^1.3.3: dependencies: retry "0.13.1" -async@^3.2.0, async@^3.2.3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== +async@^3.2.0, async@^3.2.3, async@^3.2.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynciterator.prototype@^1.0.0: version "1.0.0" @@ -17272,13 +18368,6 @@ axios@0.26.0: dependencies: follow-redirects "^1.14.8" -axios@^0.21.1: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== - dependencies: - follow-redirects "^1.14.0" - axios@^1.0.0, axios@^1.1.3, axios@^1.6.0: version "1.6.1" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.1.tgz#76550d644bf0a2d469a01f9244db6753208397d7" @@ -17288,6 +18377,15 @@ axios@^1.0.0, axios@^1.1.3, axios@^1.6.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" + integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -17295,6 +18393,11 @@ axobject-query@^3.1.1: dependencies: dequal "^2.0.3" +b4a@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9" + integrity sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw== + babel-jest@^27.4.2, babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -17626,7 +18729,7 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== -bcrypt-pbkdf@^1.0.0: +bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== @@ -18034,6 +19137,11 @@ bufrw@^1.3.0: hexer "^1.5.0" xtend "^4.0.0" +buildcheck@~0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238" + integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== + builtin-modules@^3.1.0, builtin-modules@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -19021,6 +20129,16 @@ compress-commons@^4.1.0: normalize-path "^3.0.0" readable-stream "^3.6.0" +compress-commons@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-5.0.1.tgz#e46723ebbab41b50309b27a0e0f6f3baed2d6590" + integrity sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag== + dependencies: + crc-32 "^1.2.0" + crc32-stream "^5.0.0" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -19344,6 +20462,14 @@ cp-file@^7.0.0: nested-error-stacks "^2.0.0" p-event "^4.1.0" +cpu-features@~0.0.8: + version "0.0.9" + resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.9.tgz#5226b92f0f1c63122b0a3eb84cb8335a4de499fc" + integrity sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ== + dependencies: + buildcheck "~0.0.6" + nan "^2.17.0" + cpy@^8.1.2: version "8.1.2" resolved "https://registry.yarnpkg.com/cpy/-/cpy-8.1.2.tgz#e339ea54797ad23f8e3919a5cffd37bfc3f25935" @@ -19372,6 +20498,14 @@ crc32-stream@^4.0.2: crc-32 "^1.2.0" readable-stream "^3.4.0" +crc32-stream@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-5.0.0.tgz#a97d3a802c8687f101c27cc17ca5253327354720" + integrity sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw== + dependencies: + crc-32 "^1.2.0" + readable-stream "^3.4.0" + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -19805,11 +20939,12 @@ damerau-levenshtein@^1.0.8: integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== danger@^11.2.6: - version "11.2.6" - resolved "https://registry.yarnpkg.com/danger/-/danger-11.2.6.tgz#0c7bffb9aa2daf3eb841ee2d6fba37bd4866d23a" - integrity sha512-EEeuDmUcxPGJ166q7Zzz1WEiV+e0qbPopaX4sXxds8U5doGMdw/8oOUOVye7JiHIBuss3KvQWt4YHZeD3jSCfw== + version "11.3.1" + resolved "https://registry.yarnpkg.com/danger/-/danger-11.3.1.tgz#9df659fb58c15a82d9880231ba3f676c934e565d" + integrity sha512-+slkGnbf0czY7g4LSuYpYkKJgFrb9YIXFJvV5JAuLLF39CXLlUw0iebgeL3ASK1t6RDb8xe+Rk2F5ilh2Hdv2w== dependencies: - "@gitbeaker/node" "^21.3.0" + "@gitbeaker/core" "^35.8.1" + "@gitbeaker/node" "^35.8.1" "@octokit/rest" "^18.12.0" async-retry "1.2.3" chalk "^2.3.0" @@ -19893,16 +21028,11 @@ dayjs@1.10.7: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== -dayjs@^1.10.4: +dayjs@^1.10.4, dayjs@^1.10.5, dayjs@^1.11.8: version "1.11.10" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== -dayjs@^1.10.5, dayjs@^1.11.8: - version "1.11.9" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" - integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== - debounce@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" @@ -19961,7 +21091,7 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.0.tgz#97a7448873b01e92e5ff9117d89a7bca8e63e0fe" integrity sha512-Nv6ENEzyPQ6AItkGwLE2PGKinZZ9g59vSh2BeH6NqPu0OTKZ5ruJsVqh/orbAnqXc9pBbgXAIrc2EyaCj8NpGg== -decode-uri-component@^0.2.0: +decode-uri-component@^0.2.0, decode-uri-component@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== @@ -20190,6 +21320,11 @@ degenerator@^3.0.2: esprima "^4.0.0" vm2 "^3.9.8" +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -21052,32 +22187,32 @@ es6-weak-map@^2.0.3: es6-symbol "^3.1.1" esbuild@^0.19.2: - version "0.19.2" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.2.tgz#b1541828a89dfb6f840d38538767c6130dca2aac" - integrity sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg== + version "0.19.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.5.tgz#53a0e19dfbf61ba6c827d51a80813cf071239a8c" + integrity sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ== optionalDependencies: - "@esbuild/android-arm" "0.19.2" - "@esbuild/android-arm64" "0.19.2" - "@esbuild/android-x64" "0.19.2" - "@esbuild/darwin-arm64" "0.19.2" - "@esbuild/darwin-x64" "0.19.2" - "@esbuild/freebsd-arm64" "0.19.2" - "@esbuild/freebsd-x64" "0.19.2" - "@esbuild/linux-arm" "0.19.2" - "@esbuild/linux-arm64" "0.19.2" - "@esbuild/linux-ia32" "0.19.2" - "@esbuild/linux-loong64" "0.19.2" - "@esbuild/linux-mips64el" "0.19.2" - "@esbuild/linux-ppc64" "0.19.2" - "@esbuild/linux-riscv64" "0.19.2" - "@esbuild/linux-s390x" "0.19.2" - "@esbuild/linux-x64" "0.19.2" - "@esbuild/netbsd-x64" "0.19.2" - "@esbuild/openbsd-x64" "0.19.2" - "@esbuild/sunos-x64" "0.19.2" - "@esbuild/win32-arm64" "0.19.2" - "@esbuild/win32-ia32" "0.19.2" - "@esbuild/win32-x64" "0.19.2" + "@esbuild/android-arm" "0.19.5" + "@esbuild/android-arm64" "0.19.5" + "@esbuild/android-x64" "0.19.5" + "@esbuild/darwin-arm64" "0.19.5" + "@esbuild/darwin-x64" "0.19.5" + "@esbuild/freebsd-arm64" "0.19.5" + "@esbuild/freebsd-x64" "0.19.5" + "@esbuild/linux-arm" "0.19.5" + "@esbuild/linux-arm64" "0.19.5" + "@esbuild/linux-ia32" "0.19.5" + "@esbuild/linux-loong64" "0.19.5" + "@esbuild/linux-mips64el" "0.19.5" + "@esbuild/linux-ppc64" "0.19.5" + "@esbuild/linux-riscv64" "0.19.5" + "@esbuild/linux-s390x" "0.19.5" + "@esbuild/linux-x64" "0.19.5" + "@esbuild/netbsd-x64" "0.19.5" + "@esbuild/openbsd-x64" "0.19.5" + "@esbuild/sunos-x64" "0.19.5" + "@esbuild/win32-arm64" "0.19.5" + "@esbuild/win32-ia32" "0.19.5" + "@esbuild/win32-x64" "0.19.5" escalade@^3.1.1: version "3.1.1" @@ -21128,10 +22263,10 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^8.3.0, eslint-config-prettier@^8.7.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" - integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== eslint-config-react-app@^7.0.1: version "7.0.1" @@ -21804,6 +22939,11 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== +fast-fifo@^1.1.0, fast-fifo@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" @@ -22228,7 +23368,7 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== -follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -22365,10 +23505,11 @@ formidable@^2.0.1: qs "6.9.3" formik@^2.2.9: - version "2.4.2" - resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.2.tgz#a1115457cfb012a5c782cea3ad4b40b2fe36fa18" - integrity sha512-C6nx0hifW2uENP3M6HpPmnAE6HFWCcd8/sqBZEOHZY6lpHJ5qehsfAy43ktpFLEmkBmhiZDei726utcUB9leqg== + version "2.4.5" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.5.tgz#f899b5b7a6f103a8fabb679823e8fafc7e0ee1b4" + integrity sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ== dependencies: + "@types/hoist-non-react-statics" "^3.3.1" deepmerge "^2.1.1" hoist-non-react-statics "^3.3.0" lodash "^4.17.21" @@ -22863,7 +24004,7 @@ glob@^10.2.2: minipass "^5.0.0" path-scurry "^1.7.0" -glob@^7.0.0, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: +glob@^7.0.0, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -22875,10 +24016,10 @@ glob@^7.0.0, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, gl once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.1: - version "8.0.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" - integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== +glob@^8.0.0, glob@^8.0.1, glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -23002,7 +24143,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -got@^11.1.4, got@^11.8.6: +got@^11.8.3, got@^11.8.6: version "11.8.6" resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== @@ -23671,6 +24812,14 @@ https-proxy-agent@5, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" +https-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz#e2645b846b90e96c6e6f347fb5b2e41f1590b09b" + integrity sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -25792,9 +26941,9 @@ jotai-location@^0.5.1: integrity sha512-4S9hDh1wYp4SG4Laq3+Xd0GdW7jwnWlbEuL8U22kv3wUZW892n6gXlNhwU0LaoXnWoGG0gtjSLBuFm5iodFKLg== jotai@^2.2.1: - version "2.4.3" - resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.4.3.tgz#a8eff8ca6de968d6a04616329dd1335ce52e70f3" - integrity sha512-CSAHX9LqWG5WCrU8OgBoZbBJ+Bo9rQU0mPusEF4e0CZ/SNFgurG26vb3UpgvCSJZgYVcUQNiUBM5q86PA8rstQ== + version "2.6.0" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.6.0.tgz#68b5d634f78a9ea55adfb8d92206ef59304b5dd5" + integrity sha512-Vt6hsc04Km4j03l+Ax+Sc+FVft5cRJhqgxt6GTz6GM2eM3DyX3CdBdzcG0z2FrlZToL1/0OAkqDghIyARWnSuQ== js-cookie@^2.2.1: version "2.2.1" @@ -26881,11 +28030,6 @@ luxon@^3.2.0: resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f" integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg== -lz-string@^1.4.4: - version "1.4.4" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" - integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== - lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -26954,29 +28098,7 @@ make-fetch-happen@^10.0.3: socks-proxy-agent "^7.0.0" ssri "^9.0.0" -make-fetch-happen@^11.0.0: - version "11.0.2" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.0.2.tgz#a880370fb2452d528a5ca40b2d6308999773ab17" - integrity sha512-5n/Pq41w/uZghpdlXAY5kIM85RgJThtTH/NYBRAZ9VUOBWV90USaQjwGrw76fZP3Lj5hl/VZjpVvOaRBMoL/2w== - dependencies: - agentkeepalive "^4.2.1" - cacache "^17.0.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^4.0.0" - minipass-collect "^1.0.2" - minipass-fetch "^3.0.0" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.3" - promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" - ssri "^10.0.0" - -make-fetch-happen@^11.0.1, make-fetch-happen@^11.1.0: +make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1, make-fetch-happen@^11.1.0: version "11.1.1" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz#85ceb98079584a9523d4bf71d32996e7e208549f" integrity sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w== @@ -27294,6 +28416,11 @@ mime@2.6.0, mime@^2.3.1, mime@^2.4.4: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -27633,6 +28760,11 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== +nan@^2.17.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" + integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== + nanoclone@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" @@ -27710,9 +28842,9 @@ netmask@^2.0.2: integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== neverthrow@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/neverthrow/-/neverthrow-6.0.0.tgz#bacd7661cade296ccc5c35760bb3b679214155b6" - integrity sha512-kPZKRs4VkdloCGQXPoP84q4sT/1Z+lYM61AXyV8wWa2hnuo5KpPBF2S3crSFnMrOgUISmEBP8Vo/ngGZX60NhA== + version "6.1.0" + resolved "https://registry.yarnpkg.com/neverthrow/-/neverthrow-6.1.0.tgz#51a6e9ce2e06600045b3c1b37aecc536d267bf95" + integrity sha512-xNbNjp/6M5vUV+mststgneJN9eJeJCDSYSBTaf3vxgvcKooP+8L0ATFpM8DGfmH7UWKJeoa24Qi33tBP9Ya3zA== next-tick@1, next-tick@^1.0.0, next-tick@^1.1.0: version "1.1.0" @@ -27868,6 +29000,19 @@ node-schedule@^2.1.0: long-timeout "0.1.1" sorted-array-functions "^1.3.0" +node-ssh@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/node-ssh/-/node-ssh-13.1.0.tgz#fca947200d61db9a5f9915e27c252e133b79e6bf" + integrity sha512-GLcw49yFd9+rUpP+FgX6wrF/N90cmuDl2n0i8d3L828b6riRjkb9w3SS+XvviRWbrAhLxuMKywFqxvQDZQ1bug== + dependencies: + "@types/ssh2" "^1.11.9" + is-stream "^2.0.0" + make-dir "^3.1.0" + sb-promise-queue "^2.1.0" + sb-scandir "^3.1.0" + shell-escape "^0.2.0" + ssh2 "^1.11.0" + node.extend@~2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/node.extend/-/node.extend-2.0.2.tgz#b4404525494acc99740f3703c496b7d5182cc6cc" @@ -29832,10 +30977,10 @@ prettier-linter-helpers@^1.0.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18" integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w== -prettier@^2.2.1, prettier@^2.3.2, prettier@^2.4.1: - version "2.8.7" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450" - integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw== +prettier@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e" + integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1, pretty-bytes@^5.6.0: version "5.6.0" @@ -30225,7 +31370,7 @@ qs@6.9.3: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== -qs@^6.10.0, qs@^6.10.3, qs@^6.11.0: +qs@^6.10.0, qs@^6.10.1, qs@^6.10.3, qs@^6.11.0: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== @@ -30244,12 +31389,12 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== -query-string@^6.12.1: - version "6.14.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" - integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== +query-string@^7.0.0: + version "7.1.3" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" + integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== dependencies: - decode-uri-component "^0.2.0" + decode-uri-component "^0.2.2" filter-obj "^1.1.0" split-on-first "^1.0.0" strict-uri-encode "^2.0.0" @@ -30279,6 +31424,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -30575,9 +31725,9 @@ react-select-event@^5.5.0: "@testing-library/dom" ">=7" react-select@^5.7.7: - version "5.7.7" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.7.tgz#dbade9dbf711ef2a181970c10f8ab319ac37fbd0" - integrity sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw== + version "5.8.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.8.0.tgz#bd5c467a4df223f079dd720be9498076a3f085b5" + integrity sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA== dependencies: "@babel/runtime" "^7.12.0" "@emotion/cache" "^11.4.0" @@ -30746,16 +31896,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^3.6.2: +readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -30771,10 +31912,10 @@ readable-web-to-node-stream@^3.0.0: dependencies: readable-stream "^3.6.0" -readdir-glob@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.2.tgz#b185789b8e6a43491635b6953295c5c5e3fd224c" - integrity sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA== +readdir-glob@^1.0.0, readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== dependencies: minimatch "^5.1.0" @@ -31513,6 +32654,18 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" +sb-promise-queue@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sb-promise-queue/-/sb-promise-queue-2.1.0.tgz#7e44bebef643f75d809a3db7f605b815d877a04d" + integrity sha512-zwq4YuP1FQFkGx2Q7GIkZYZ6PqWpV+bg0nIO1sJhWOyGyhqbj0MsTvK6lCFo5TQwX5pZr6SCQ75e8PCDCuNvkg== + +sb-scandir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/sb-scandir/-/sb-scandir-3.1.0.tgz#31c346abb5184b73c5a25b286858f4299aa8756c" + integrity sha512-70BVm2xz9jn94zSQdpvYrEG101/UV9TVGcfWr9T5iob3QhCK4lYXeculfBqPGFv3XTeKgx4dpWyYIDeZUqo4kg== + dependencies: + sb-promise-queue "^2.1.0" + scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" @@ -31807,9 +32960,9 @@ serverless-s3-local@^0.7.1: shelljs "^0.8.5" serverless-s3-sync@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/serverless-s3-sync/-/serverless-s3-sync-3.1.0.tgz#58a669d928303a2e69ec121a951ad3a8f075b754" - integrity sha512-qgbPQkq8i70tBB6lUHUpdvitGJAHjq+m5xIg8mrjiCv412+5LEXnDi5P5AegJ54CEGWmmZaxSA9lvYPrAnfOiA== + version "3.2.0" + resolved "https://registry.yarnpkg.com/serverless-s3-sync/-/serverless-s3-sync-3.2.0.tgz#3005566ad85a4c825fb0ef882e7cb0b0d3443ba6" + integrity sha512-V7rUA/5oU1fxvaX7zM+ZeNEyZ/aGWRtuUpmGX4HVgbQgcfyrAvf0/MZ68qu9KqUdbqSy7//sMa63KQjSo7XXiA== dependencies: "@auth0/s3" "^1.0.0" bluebird "^3.5.4" @@ -31829,15 +32982,15 @@ serverless-stack-termination-protection@^2.0.2: integrity sha512-pPUVJmH5if8A12KVSTMqNjo7u8rfW0/vJO+UFp19lvkY0t8jfoO3R3DySwTDVcB+jzaCrU/NyEHPtbiDLVjSSw== serverless-webpack@^5.12.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/serverless-webpack/-/serverless-webpack-5.12.0.tgz#61dafbcf63884a64d991de0b38f6bd71bc5ae266" - integrity sha512-krXmz20rT4X9WkiT6nnH/o+gOVof+SFXKTsCTOY2eDF+9PS4UKtGlzZkjNyDV62lCQgD7nmXPbdY2hYoGmyeYg== + version "5.13.0" + resolved "https://registry.yarnpkg.com/serverless-webpack/-/serverless-webpack-5.13.0.tgz#0159dfeae10da502748db0bc761dfa2548cde22e" + integrity sha512-isMEbXbAK1F8YZJfeKgYA5uNuXPFzdHwZyRA9SuMGXVY2L8t1JIzPvRDLZiT4F3uQm16woyal+uaoDyxQo13vg== dependencies: archiver "^5.3.1" bluebird "^3.7.2" find-yarn-workspace-root "^2.0.0" - fs-extra "^9.1.0" - glob "^7.2.3" + fs-extra "^11.1.1" + glob "^8.1.0" is-builtin-module "^3.2.1" lodash "^4.17.21" semver "^7.3.8" @@ -31845,12 +32998,12 @@ serverless-webpack@^5.12.0: ts-node ">= 8.3.0" serverless@^3.26.0, serverless@^3.27.0: - version "3.36.0" - resolved "https://registry.yarnpkg.com/serverless/-/serverless-3.36.0.tgz#bd7b030299b6ce7f75cb405793c6e2f1c1354a83" - integrity sha512-VY7UzP4u1/yuTNpF2Wssrru16qhhReLCjgL2oeHCvhujxPyTFv9TQGSlLhaT0ZUCXhRBphwVwITTRopo6NSUgA== + version "3.38.0" + resolved "https://registry.yarnpkg.com/serverless/-/serverless-3.38.0.tgz#9275763cab3ec1cd29635520cf24b9b5e7202583" + integrity sha512-NJE1vOn8XmQEqfU9UxmVhkUFaCRmx6FhYw/jITN863WlOt4Y3PQbj3hwQyIb5QS1ZrXFq5ojklwewUXH7xGpdA== dependencies: - "@serverless/dashboard-plugin" "^7.1.0" - "@serverless/platform-client" "^4.4.0" + "@serverless/dashboard-plugin" "^7.2.0" + "@serverless/platform-client" "^4.5.1" "@serverless/utils" "^6.13.1" abort-controller "^3.0.0" ajv "^8.12.0" @@ -31989,6 +33142,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shell-escape@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133" + integrity sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw== + shell-quote@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" @@ -32416,6 +33574,17 @@ sqlstring@2.3.1: resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" integrity sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ== +ssh2@^1.11.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.14.0.tgz#8f68440e1b768b66942c9e4e4620b2725b3555bb" + integrity sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA== + dependencies: + asn1 "^0.2.6" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.8" + nan "^2.17.0" + sshpk@^1.14.1: version "1.18.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" @@ -32593,6 +33762,14 @@ streamsink@~1.2.0: resolved "https://registry.yarnpkg.com/streamsink/-/streamsink-1.2.0.tgz#efafee9f1e22d3591ed7de3dcaa95c3f5e79f73c" integrity sha512-MJ440L2+j2vmc1v8Z/BkMx3X+HsJ++V7mgDROboQKxqCLZdNbu+AeSwQbayXw3LPHVAMxw+h7ZJUnyFYl/zp2g== +streamx@^2.15.0: + version "2.15.6" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.6.tgz#28bf36997ebc7bf6c08f9eba958735231b833887" + integrity sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw== + dependencies: + fast-fifo "^1.1.0" + queue-tick "^1.0.1" + strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" @@ -33161,6 +34338,15 @@ tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" +tar-stream@^3.0.0: + version "3.1.6" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.6.tgz#6520607b55a06f4a2e2e04db360ba7d338cc5bab" + integrity sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + tar@6.1.11: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" @@ -33361,9 +34547,9 @@ throttle-debounce@^3.0.1: integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg== throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g== + version "1.0.1" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.1.tgz#304ec51631c3b770c65c6c6f76938b384000f4d5" + integrity sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ== through2@^2.0.0: version "2.0.5" @@ -33668,13 +34854,13 @@ ts-pnp@^1.1.6: integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== tsconfig-paths-webpack-plugin@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.0.tgz#84008fc3e3e0658fdb0262758b07b4da6265ff1a" - integrity sha512-fw/7265mIWukrSHd0i+wSwx64kYUSAKPfxRDksjKIYTxSAp9W9/xcZVBF4Kl0eqQd5eBpAQ/oQrc5RyM/0c1GQ== + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz#3c6892c5e7319c146eee1e7302ed9e6f2be4f763" + integrity sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA== dependencies: chalk "^4.1.0" enhanced-resolve "^5.7.0" - tsconfig-paths "^4.0.0" + tsconfig-paths "^4.1.2" tsconfig-paths@^3.14.1: version "3.14.1" @@ -33686,15 +34872,6 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tsconfig-paths@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.0.tgz#f8ef7d467f08ae3a695335bf1ece088c5538d2c1" - integrity sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow== - dependencies: - json5 "^2.2.1" - minimist "^1.2.6" - strip-bom "^3.0.0" - tsconfig-paths@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz#4819f861eef82e6da52fb4af1e8c930a39ed979a" @@ -33947,6 +35124,11 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + undici@^5.12.0, undici@^5.8.0: version "5.26.3" resolved "https://registry.yarnpkg.com/undici/-/undici-5.26.3.tgz#ab3527b3d5bb25b12f898dfd22165d472dd71b79" @@ -34385,9 +35567,9 @@ uuid@^8.0.0, uuid@^8.3.2: integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== uuid@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== v8-compile-cache-lib@^3.0.1: version "3.0.1" @@ -34789,9 +35971,9 @@ webpack@4: webpack-sources "^1.4.1" "webpack@>=4.43.0 <6.0.0", webpack@^5.64.4, webpack@^5.72.0, webpack@^5.75.0, webpack@^5.9.0: - version "5.88.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" - integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== + version "5.89.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" + integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.0" @@ -35568,6 +36750,15 @@ zip-stream@^4.1.0: compress-commons "^4.1.0" readable-stream "^3.6.0" +zip-stream@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-5.0.1.tgz#cf3293bba121cad98be2ec7f05991d81d9f18134" + integrity sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA== + dependencies: + archiver-utils "^4.0.1" + compress-commons "^5.0.1" + readable-stream "^3.6.0" + zod@^3.10.1, zod@^3.11.6: version "3.22.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"