Skip to content

Commit 5d1b07a

Browse files
committed
Merge branch 'development' into feat/aa-notifications
2 parents 66c10a4 + bac214f commit 5d1b07a

File tree

87 files changed

+4509
-250
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+4509
-250
lines changed

backend/bin/pre-commit

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,27 @@ if [ -n "$files" ]; then
9494
ruff check $files || exit "$?"
9595
fi
9696

97+
9798
# Check dependencies for known vulnerabilities
9899
pip-audit || exit "$?"
99100

101+
# Lint all typescript files
102+
103+
files=$(git diff-index \
104+
--cached \
105+
--name-only \
106+
$against \
107+
--relative \
108+
--no-renames \
109+
--diff-filter=dr \
110+
-- '*.ts')
111+
if [ -n "$files" ]; then
112+
(
113+
cd compact-connect/lambdas/nodejs/
114+
yarn lint:ingest || exit "$?"
115+
) || exit "$?"
116+
fi
117+
100118
# Run the back-end tests
101119
bin/run_tests.sh -no || exit "$?"
102120

backend/compact-connect/lambdas/nodejs/ingest-event-reporter/handler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
22
import { SESClient } from '@aws-sdk/client-ses';
3+
import { S3Client } from '@aws-sdk/client-s3';
34
import { Lambda } from './lambda';
45

56

67
const lambda = new Lambda({
78
dynamoDBClient: new DynamoDBClient(),
9+
s3Client: new S3Client(),
810
sesClient: new SESClient(),
911
});
1012

backend/compact-connect/lambdas/nodejs/ingest-event-reporter/lambda.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface LambdaProperties {
2626
*/
2727
export class Lambda implements LambdaInterface {
2828
private readonly jurisdictionClient: JurisdictionClient;
29+
private readonly compactConfigurationClient: CompactConfigurationClient;
2930
private readonly eventClient: EventClient;
3031
private readonly emailService: IngestEventEmailService;
3132

@@ -35,7 +36,7 @@ export class Lambda implements LambdaInterface {
3536
dynamoDBClient: props.dynamoDBClient,
3637
});
3738

38-
const compactConfigurationClient = new CompactConfigurationClient({
39+
this.compactConfigurationClient = new CompactConfigurationClient({
3940
logger: logger,
4041
dynamoDBClient: props.dynamoDBClient,
4142
});
@@ -48,7 +49,7 @@ export class Lambda implements LambdaInterface {
4849
logger: logger,
4950
sesClient: props.sesClient,
5051
s3Client: props.s3Client,
51-
compactConfigurationClient: compactConfigurationClient,
52+
compactConfigurationClient: this.compactConfigurationClient,
5253
jurisdictionClient: this.jurisdictionClient
5354
});
5455
}
@@ -62,6 +63,17 @@ export class Lambda implements LambdaInterface {
6263

6364
// Loop over each compact the system knows about
6465
for (const compact of environmentVariables.getCompacts()) {
66+
let compactConfig;
67+
68+
try {
69+
compactConfig = await this.compactConfigurationClient.getCompactConfiguration(compact);
70+
} catch (error) {
71+
const errorMessage = error instanceof Error ? error.message : String(error);
72+
73+
logger.warn('Compact configuration not found, skipping compact', { compact, error: errorMessage });
74+
continue;
75+
}
76+
6577
const jurisdictionConfigs = await this.jurisdictionClient.getJurisdictionConfigurations(compact);
6678

6779
// Loop over each jurisdiction that we have contacts configured for
@@ -74,7 +86,7 @@ export class Lambda implements LambdaInterface {
7486
if (ingestEvents.ingestFailures.length || ingestEvents.validationErrors.length) {
7587
const messageId = await this.emailService.sendReportEmail(
7688
ingestEvents,
77-
compact,
89+
compactConfig.compactName,
7890
jurisdictionConfig.jurisdictionName,
7991
jurisdictionConfig.jurisdictionOperationsTeamEmails
8092
);
@@ -113,31 +125,34 @@ export class Lambda implements LambdaInterface {
113125
&& weeklyIngestEvents.ingestSuccesses.length
114126
) {
115127
const messageId = await this.emailService.sendAllsWellEmail(
116-
compact,
128+
compactConfig.compactName,
117129
jurisdictionConfig.jurisdictionName,
118130
jurisdictionConfig.jurisdictionOperationsTeamEmails
119131
);
120132

121133
logger.info(
122134
'Sent alls well email',
123135
{
124-
compact: compact,
136+
compact: compactConfig.compactName,
125137
jurisdiction: jurisdictionConfig.postalAbbreviation,
126138
message_id: messageId
127139
}
128140
);
129141
}
130142
else if(!weeklyIngestEvents.ingestSuccesses.length) {
131143
const messageId = await this.emailService.sendNoLicenseUpdatesEmail(
132-
compact,
144+
compactConfig.compactName,
133145
jurisdictionConfig.jurisdictionName,
134-
jurisdictionConfig.jurisdictionOperationsTeamEmails
146+
[
147+
...jurisdictionConfig.jurisdictionOperationsTeamEmails,
148+
...compactConfig.compactOperationsTeamEmails
149+
]
135150
);
136151

137152
logger.warn(
138153
'No licenses uploaded withinin the last week',
139154
{
140-
compact: compact,
155+
compact: compactConfig.compactName,
141156
jurisdiction: jurisdictionConfig.postalAbbreviation,
142157
message_id: messageId
143158
}

backend/compact-connect/lambdas/nodejs/tests/ingest-event-reporter.test.ts

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mockClient } from 'aws-sdk-client-mock';
22
import 'aws-sdk-client-mock-jest';
33
import { Context, EventBridgeEvent } from 'aws-lambda';
4-
import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb';
4+
import { DynamoDBClient, QueryCommand, GetItemCommand } from '@aws-sdk/client-dynamodb';
55
import { SendEmailCommand, SESClient } from '@aws-sdk/client-ses';
66
import { S3Client } from '@aws-sdk/client-s3';
77

@@ -12,7 +12,8 @@ import {
1212
SAMPLE_INGEST_FAILURE_ERROR_RECORD,
1313
SAMPLE_JURISDICTION_CONFIGURATION,
1414
SAMPLE_VALIDATION_ERROR_RECORD,
15-
SAMPLE_INGEST_SUCCESS_RECORD
15+
SAMPLE_INGEST_SUCCESS_RECORD,
16+
SAMPLE_COMPACT_CONFIGURATION
1617
} from './sample-records';
1718

1819

@@ -134,6 +135,11 @@ describe('Nightly runs', () => {
134135
}
135136
});
136137

138+
// Mock GetItemCommand for compact configuration
139+
mockDynamoDBClient.on(GetItemCommand).resolves({
140+
Item: SAMPLE_COMPACT_CONFIGURATION
141+
});
142+
137143
lambda = new Lambda({
138144
dynamoDBClient: asDynamoDBClient(mockDynamoDBClient),
139145
s3Client: asS3Client(mockS3Client),
@@ -162,8 +168,16 @@ describe('Nightly runs', () => {
162168
}
163169
);
164170

165-
// Verify an event report was sent
166-
expect(mockSendReportEmail).toHaveBeenCalled();
171+
// Verify an event report was sent with correct parameters
172+
expect(mockSendReportEmail).toHaveBeenCalledWith(
173+
expect.objectContaining({
174+
ingestFailures: [expect.objectContaining({ eventType: 'license.ingest-failure' })],
175+
validationErrors: [expect.objectContaining({ eventType: 'license.validation-error' })]
176+
}),
177+
'Audiology and Speech Language Pathology', // compactName instead of abbreviation
178+
'Ohio', // jurisdictionName
179+
['[email protected]'] // jurisdiction operations emails
180+
);
167181
expect(mockSendAllsWellEmail).not.toHaveBeenCalled();
168182
});
169183

@@ -188,6 +202,11 @@ describe('Nightly runs', () => {
188202
}
189203
});
190204

205+
// Mock GetItemCommand for compact configuration
206+
mockDynamoDBClient.on(GetItemCommand).resolves({
207+
Item: SAMPLE_COMPACT_CONFIGURATION
208+
});
209+
191210
lambda = new Lambda({
192211
dynamoDBClient: asDynamoDBClient(mockDynamoDBClient),
193212
s3Client: asS3Client(mockS3Client),
@@ -226,6 +245,12 @@ describe('Nightly runs', () => {
226245
const mockDynamoDBClient = mockClient(DynamoDBClient);
227246
const mockS3Client = mockClient(S3Client);
228247

248+
// Mock GetItemCommand to succeed (compact config found)
249+
mockDynamoDBClient.on(GetItemCommand).resolves({
250+
Item: SAMPLE_COMPACT_CONFIGURATION
251+
});
252+
253+
// Mock QueryCommand to fail (jurisdictions query fails)
229254
mockDynamoDBClient.on(QueryCommand).rejects(new Error('DynamoDB error'));
230255

231256
lambda = new Lambda({
@@ -240,6 +265,31 @@ describe('Nightly runs', () => {
240265
SAMPLE_CONTEXT
241266
)).rejects.toThrow('DynamoDB error');
242267
});
268+
269+
it('should skip compact and continue processing when compact configuration is not found', async () => {
270+
const mockDynamoDBClient = mockClient(DynamoDBClient);
271+
const mockS3Client = mockClient(S3Client);
272+
273+
// Mock GetItemCommand to reject with "not found" error for compact configuration
274+
mockDynamoDBClient.on(GetItemCommand).rejects(new Error('No configuration found for compact: aslp'));
275+
276+
lambda = new Lambda({
277+
dynamoDBClient: asDynamoDBClient(mockDynamoDBClient),
278+
s3Client: asS3Client(mockS3Client),
279+
sesClient: asSESClient(mockSESClient)
280+
});
281+
282+
// Should not throw an error, should complete successfully
283+
await expect(lambda.handler(
284+
SAMPLE_NIGHTLY_EVENT,
285+
SAMPLE_CONTEXT
286+
)).resolves.toBeUndefined();
287+
288+
// Verify no emails were sent since we skipped all compacts
289+
expect(mockSendReportEmail).not.toHaveBeenCalled();
290+
expect(mockSendAllsWellEmail).not.toHaveBeenCalled();
291+
expect(mockSendNoLicenseUpdatesEmail).not.toHaveBeenCalled();
292+
});
243293
});
244294

245295

@@ -307,6 +357,11 @@ describe('Weekly runs', () => {
307357
}
308358
});
309359

360+
// Mock GetItemCommand for compact configuration
361+
mockDynamoDBClient.on(GetItemCommand).resolves({
362+
Item: SAMPLE_COMPACT_CONFIGURATION
363+
});
364+
310365
lambda = new Lambda({
311366
dynamoDBClient: asDynamoDBClient(mockDynamoDBClient),
312367
s3Client: asS3Client(mockS3Client),
@@ -326,9 +381,13 @@ describe('Weekly runs', () => {
326381
}
327382
);
328383

329-
// Verify an "All's Well" email was sent
384+
// Verify an "All's Well" email was sent with correct parameters
330385
expect(mockSendReportEmail).not.toHaveBeenCalled();
331-
expect(mockSendAllsWellEmail).toHaveBeenCalled();
386+
expect(mockSendAllsWellEmail).toHaveBeenCalledWith(
387+
'Audiology and Speech Language Pathology', // compactName instead of abbreviation
388+
'Ohio', // jurisdictionName
389+
['[email protected]'] // jurisdiction operations emails
390+
);
332391
expect(mockSendNoLicenseUpdatesEmail).not.toHaveBeenCalled();
333392
});
334393

@@ -354,6 +413,11 @@ describe('Weekly runs', () => {
354413
}
355414
});
356415

416+
// Mock GetItemCommand for compact configuration
417+
mockDynamoDBClient.on(GetItemCommand).resolves({
418+
Item: SAMPLE_COMPACT_CONFIGURATION
419+
});
420+
357421
lambda = new Lambda({
358422
dynamoDBClient: asDynamoDBClient(mockDynamoDBClient),
359423
s3Client: asS3Client(mockS3Client),
@@ -373,10 +437,26 @@ describe('Weekly runs', () => {
373437
}
374438
);
375439

376-
// Verify an "All's Well" email was sent
440+
// Verify the compact configuration was fetched
441+
expect(mockDynamoDBClient).toHaveReceivedCommandWith(
442+
GetItemCommand,
443+
{
444+
TableName: 'compact-table',
445+
Key: {
446+
'pk': { S: 'aslp#CONFIGURATION' },
447+
'sk': { S: 'aslp#CONFIGURATION' }
448+
}
449+
}
450+
);
451+
452+
// Verify no license updates email was sent with correct parameters
377453
expect(mockSendReportEmail).not.toHaveBeenCalled();
378454
expect(mockSendAllsWellEmail).not.toHaveBeenCalled();
379-
expect(mockSendNoLicenseUpdatesEmail).toHaveBeenCalled();
455+
expect(mockSendNoLicenseUpdatesEmail).toHaveBeenCalledWith(
456+
'Audiology and Speech Language Pathology', // compactName instead of abbreviation
457+
'Ohio', // jurisdictionName
458+
['[email protected]', '[email protected]'] // combined jurisdiction and compact operations emails
459+
);
380460
});
381461

382462
it('should send a report email and not an alls well, when there were errors', async () => {
@@ -419,6 +499,11 @@ describe('Weekly runs', () => {
419499
}
420500
});
421501

502+
// Mock GetItemCommand for compact configuration
503+
mockDynamoDBClient.on(GetItemCommand).resolves({
504+
Item: SAMPLE_COMPACT_CONFIGURATION
505+
});
506+
422507
lambda = new Lambda({
423508
dynamoDBClient: asDynamoDBClient(mockDynamoDBClient),
424509
s3Client: asS3Client(mockS3Client),

backend/compact-connect/lambdas/nodejs/tests/sample-records.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,37 @@ export const SAMPLE_UNMARSHALLED_JURISDICTION_CONFIGURATION = {
322322
'postalAbbreviation': 'oh',
323323
'type': 'jurisdiction',
324324
};
325+
326+
export const SAMPLE_COMPACT_CONFIGURATION = {
327+
'pk': { 'S': 'aslp#CONFIGURATION' },
328+
'sk': { 'S': 'aslp#CONFIGURATION' },
329+
'compactAdverseActionsNotificationEmails': { 'L': [{ 'S': '[email protected]' }]},
330+
'compactCommissionFee': {
331+
'M': {
332+
'feeAmount': { 'N': '3.5' },
333+
'feeType': { 'S': 'FLAT_RATE' }
334+
}
335+
},
336+
'compactAbbr': { 'S': 'aslp' },
337+
'compactName': { 'S': 'Audiology and Speech Language Pathology' },
338+
'compactOperationsTeamEmails': { 'L': [{ 'S': '[email protected]' }]},
339+
'compactSummaryReportNotificationEmails': { 'L': [{ 'S': '[email protected]' }]},
340+
'dateOfUpdate': { 'S': '2024-12-10T19:27:28+00:00' },
341+
'type': { 'S': 'compact' }
342+
};
343+
344+
export const SAMPLE_UNMARSHALLED_COMPACT_CONFIGURATION = {
345+
'pk': 'aslp#CONFIGURATION',
346+
'sk': 'aslp#CONFIGURATION',
347+
'compactAdverseActionsNotificationEmails': ['[email protected]'],
348+
'compactCommissionFee': {
349+
'feeAmount': 3.5,
350+
'feeType': 'FLAT_RATE'
351+
},
352+
'compactAbbr': 'aslp',
353+
'compactName': 'Audiology and Speech Language Pathology',
354+
'compactOperationsTeamEmails': ['[email protected]'],
355+
'compactSummaryReportNotificationEmails': ['[email protected]'],
356+
'dateOfUpdate': '2024-12-10T19:27:28+00:00',
357+
'type': 'compact'
358+
};

backend/compact-connect/lambdas/python/common/requirements-dev.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
#
55
# pip-compile --no-emit-index-url compact-connect/lambdas/python/common/requirements-dev.in
66
#
7-
boto3==1.38.38
7+
boto3==1.39.0
88
# via moto
9-
botocore==1.38.38
9+
botocore==1.39.0
1010
# via
1111
# boto3
1212
# moto

backend/compact-connect/lambdas/python/common/requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
#
55
# pip-compile --no-emit-index-url compact-connect/lambdas/python/common/requirements.in
66
#
7-
aws-lambda-powertools==3.14.0
7+
aws-lambda-powertools==3.15.1
88
# via -r compact-connect/lambdas/python/common/requirements.in
9-
boto3==1.38.38
9+
boto3==1.39.0
1010
# via -r compact-connect/lambdas/python/common/requirements.in
11-
botocore==1.38.38
11+
botocore==1.39.0
1212
# via
1313
# boto3
1414
# s3transfer

backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
#
55
# pip-compile --no-emit-index-url compact-connect/lambdas/python/compact-configuration/requirements-dev.in
66
#
7-
boto3==1.38.38
7+
boto3==1.39.0
88
# via moto
9-
botocore==1.38.38
9+
botocore==1.39.0
1010
# via
1111
# boto3
1212
# moto

0 commit comments

Comments
 (0)