Skip to content

Commit b9df5d4

Browse files
authored
Feat/aa notifications (#864)
### Description List - Added events for privilege encumbrance/lift - Updated all encumbrance/lifts to publish consistent events - Added event listeners for license/privilege encumbrance/lift specifically for notifications - Added email templates for provider/state, license/privilege, encumbrance/lift ### Testing List - All the tests - Review email notification content - Code review Closes #675 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced comprehensive email notifications for license and privilege encumbrance and lifting events, notifying both providers and jurisdictions. * Added support for privilege encumbrance event publishing and handling. * Enhanced event-driven notification listeners for various encumbrance scenarios. * **Bug Fixes** * Improved handling of license type lookups and validation with clearer error messages. * **Refactor** * Modularized event listener and notification stack setup for better maintainability and flexibility. * Updated event publishing to use standardized source strings and effective dates. * **Tests** * Added extensive unit and integration test coverage for encumbrance event handling, notifications, and infrastructure components. * **Chores** * Upgraded dependencies (boto3, botocore, cdk-nag, ruff) to latest versions. * Improved documentation formatting and consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent bac214f commit b9df5d4

Some content is hidden

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

45 files changed

+5825
-202
lines changed

backend/compact-connect/stacks/event_listener_stack/queue_event_listener.py renamed to backend/compact-connect/common_constructs/queue_event_listener.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,46 @@
33
from aws_cdk import Duration
44
from aws_cdk.aws_cloudwatch import Alarm, ComparisonOperator, Metric, Stats, TreatMissingData
55
from aws_cdk.aws_cloudwatch_actions import SnsAction
6-
from aws_cdk.aws_events import EventPattern, Rule
6+
from aws_cdk.aws_events import EventBus, EventPattern, Rule
77
from aws_cdk.aws_events_targets import SqsQueue
8+
from aws_cdk.aws_kms import IKey
89
from aws_cdk.aws_lambda import IFunction
9-
from common_constructs.queued_lambda_processor import QueuedLambdaProcessor
10+
from aws_cdk.aws_sns import ITopic
1011
from constructs import Construct
1112

12-
from stacks import persistent_stack as ps
13+
from common_constructs.queued_lambda_processor import QueuedLambdaProcessor
1314

1415

1516
class QueueEventListener(Construct):
1617
"""
1718
This construct defines resources for an event listener that puts events on a queue to be processed by a lambda
1819
function.
20+
21+
This construct creates:
22+
- A QueuedLambdaProcessor for reliable message processing
23+
- An EventBridge rule to route events to the queue
24+
- CloudWatch alarms for monitoring failures
1925
"""
26+
default_visibility_timeout = Duration.minutes(5)
27+
default_retention_period = Duration.hours(12)
28+
default_max_batching_window = Duration.seconds(15)
2029

2130
def __init__(
2231
self,
2332
scope: Construct,
2433
construct_id: str,
2534
*,
26-
data_event_bus: ps.EventBus,
35+
data_event_bus: EventBus,
2736
listener_function: IFunction,
2837
listener_detail_type: str,
29-
persistent_stack: ps.PersistentStack,
38+
encryption_key: IKey,
39+
alarm_topic: ITopic,
40+
visibility_timeout: Duration = default_visibility_timeout,
41+
retention_period: Duration = default_retention_period,
42+
max_batching_window: Duration = default_max_batching_window,
43+
max_receive_count: int = 3,
44+
batch_size: int = 10,
45+
dlq_count_alarm_threshold: int = 1,
3046
**kwargs,
3147
):
3248
super().__init__(scope, construct_id, **kwargs)
@@ -44,22 +60,21 @@ def __init__(
4460
treat_missing_data=TreatMissingData.NOT_BREACHING,
4561
)
4662

47-
self.lambda_failure_alarm.add_alarm_action(SnsAction(persistent_stack.alarm_topic))
63+
self.lambda_failure_alarm.add_alarm_action(SnsAction(alarm_topic))
4864

4965
# Create the QueuedLambdaProcessor
5066
self.queue_processor = QueuedLambdaProcessor(
5167
self,
5268
f'{construct_id}QueueProcessor',
5369
process_function=listener_function,
54-
visibility_timeout=Duration.minutes(5),
55-
retention_period=Duration.hours(12),
56-
max_batching_window=Duration.seconds(15),
57-
max_receive_count=3,
58-
batch_size=10,
59-
encryption_key=persistent_stack.shared_encryption_key,
60-
alarm_topic=persistent_stack.alarm_topic,
61-
# We want to be aware if any communications failed to send, so we'll set this threshold to 1
62-
dlq_count_alarm_threshold=1,
70+
visibility_timeout=visibility_timeout,
71+
retention_period=retention_period,
72+
max_batching_window=max_batching_window,
73+
max_receive_count=max_receive_count,
74+
batch_size=batch_size,
75+
encryption_key=encryption_key,
76+
alarm_topic=alarm_topic,
77+
dlq_count_alarm_threshold=dlq_count_alarm_threshold,
6378
)
6479

6580
# Create rule to route specified detail events to the SQS queue
@@ -91,4 +106,4 @@ def __init__(
91106
treat_missing_data=TreatMissingData.NOT_BREACHING,
92107
)
93108

94-
self.event_bridge_failure_alarm.add_alarm_action(SnsAction(persistent_stack.alarm_topic))
109+
self.event_bridge_failure_alarm.add_alarm_action(SnsAction(alarm_topic))

backend/compact-connect/common_constructs/ssm_parameter_utility.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,8 @@ def load_data_event_bus_from_ssm_parameter(scope: Construct) -> EventBus:
3131
the event bus ARN in SSM Parameter Store rather than using a direct reference,
3232
which helps avoid issues with CloudFormation stack updates.
3333
34-
Args:
35-
scope: The CDK construct scope
36-
37-
Returns:
38-
The EventBus construct
34+
:param scope: The CDK construct scope
35+
:return: The EventBus construct
3936
"""
4037
data_event_bus_arn = StringParameter.from_string_parameter_name(
4138
scope,

backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Context } from 'aws-lambda';
88
import { EnvironmentVariablesService } from '../lib/environment-variables-service';
99
import { CompactConfigurationClient } from '../lib/compact-configuration-client';
1010
import { JurisdictionClient } from '../lib/jurisdiction-client';
11-
import { EmailNotificationService } from '../lib/email';
11+
import { EmailNotificationService, EncumbranceNotificationService } from '../lib/email';
1212
import { EmailNotificationEvent, EmailNotificationResponse } from '../lib/models/email-notification-service-event';
1313

1414
const environmentVariables = new EnvironmentVariablesService();
@@ -22,6 +22,7 @@ interface LambdaProperties {
2222

2323
export class Lambda implements LambdaInterface {
2424
private readonly emailService: EmailNotificationService;
25+
private readonly encumbranceService: EncumbranceNotificationService;
2526

2627
constructor(props: LambdaProperties) {
2728
const compactConfigurationClient = new CompactConfigurationClient({
@@ -41,6 +42,14 @@ export class Lambda implements LambdaInterface {
4142
compactConfigurationClient: compactConfigurationClient,
4243
jurisdictionClient: jurisdictionClient
4344
});
45+
46+
this.encumbranceService = new EncumbranceNotificationService({
47+
logger: logger,
48+
sesClient: props.sesClient,
49+
s3Client: props.s3Client,
50+
compactConfigurationClient: compactConfigurationClient,
51+
jurisdictionClient: jurisdictionClient
52+
});
4453
}
4554

4655
/**
@@ -77,8 +86,8 @@ export class Lambda implements LambdaInterface {
7786
if (!event.jurisdiction) {
7887
throw new Error('Missing required jurisdiction field.');
7988
}
80-
if (!event.templateVariables.privilegeId
81-
|| !event.templateVariables.providerFirstName
89+
if (!event.templateVariables.privilegeId
90+
|| !event.templateVariables.providerFirstName
8291
|| !event.templateVariables.providerLastName) {
8392
throw new Error('Missing required template variables for privilegeDeactivationJurisdictionNotification template.');
8493
}
@@ -135,6 +144,170 @@ export class Lambda implements LambdaInterface {
135144
event.specificEmails
136145
);
137146
break;
147+
case 'licenseEncumbranceProviderNotification':
148+
if (!event.templateVariables.providerFirstName
149+
|| !event.templateVariables.providerLastName
150+
|| !event.templateVariables.encumberedJurisdiction
151+
|| !event.templateVariables.licenseType
152+
|| !event.templateVariables.effectiveStartDate) {
153+
throw new Error('Missing required template variables for licenseEncumbranceProviderNotification template.');
154+
}
155+
await this.encumbranceService.sendLicenseEncumbranceProviderNotificationEmail(
156+
event.compact,
157+
event.specificEmails || [],
158+
event.templateVariables.providerFirstName,
159+
event.templateVariables.providerLastName,
160+
event.templateVariables.encumberedJurisdiction,
161+
event.templateVariables.licenseType,
162+
event.templateVariables.effectiveStartDate
163+
);
164+
break;
165+
case 'licenseEncumbranceStateNotification':
166+
if (!event.jurisdiction) {
167+
throw new Error('Missing required jurisdiction field for licenseEncumbranceStateNotification template.');
168+
}
169+
if (!event.templateVariables.providerFirstName
170+
|| !event.templateVariables.providerLastName
171+
|| !event.templateVariables.providerId
172+
|| !event.templateVariables.encumberedJurisdiction
173+
|| !event.templateVariables.licenseType
174+
|| !event.templateVariables.effectiveStartDate) {
175+
throw new Error('Missing required template variables for licenseEncumbranceStateNotification template.');
176+
}
177+
await this.encumbranceService.sendLicenseEncumbranceStateNotificationEmail(
178+
event.compact,
179+
event.jurisdiction,
180+
event.templateVariables.providerFirstName,
181+
event.templateVariables.providerLastName,
182+
event.templateVariables.providerId,
183+
event.templateVariables.encumberedJurisdiction,
184+
event.templateVariables.licenseType,
185+
event.templateVariables.effectiveStartDate
186+
);
187+
break;
188+
case 'licenseEncumbranceLiftingProviderNotification':
189+
if (!event.templateVariables.providerFirstName
190+
|| !event.templateVariables.providerLastName
191+
|| !event.templateVariables.liftedJurisdiction
192+
|| !event.templateVariables.licenseType
193+
|| !event.templateVariables.effectiveLiftDate) {
194+
throw new Error('Missing required template variables for licenseEncumbranceLiftingProviderNotification template.');
195+
}
196+
await this.encumbranceService.sendLicenseEncumbranceLiftingProviderNotificationEmail(
197+
event.compact,
198+
event.specificEmails || [],
199+
event.templateVariables.providerFirstName,
200+
event.templateVariables.providerLastName,
201+
event.templateVariables.liftedJurisdiction,
202+
event.templateVariables.licenseType,
203+
event.templateVariables.effectiveLiftDate
204+
);
205+
break;
206+
case 'licenseEncumbranceLiftingStateNotification':
207+
if (!event.jurisdiction) {
208+
throw new Error('Missing required jurisdiction field for licenseEncumbranceLiftingStateNotification template.');
209+
}
210+
if (!event.templateVariables.providerFirstName
211+
|| !event.templateVariables.providerLastName
212+
|| !event.templateVariables.providerId
213+
|| !event.templateVariables.liftedJurisdiction
214+
|| !event.templateVariables.licenseType
215+
|| !event.templateVariables.effectiveLiftDate) {
216+
throw new Error('Missing required template variables for licenseEncumbranceLiftingStateNotification template.');
217+
}
218+
await this.encumbranceService.sendLicenseEncumbranceLiftingStateNotificationEmail(
219+
event.compact,
220+
event.jurisdiction,
221+
event.templateVariables.providerFirstName,
222+
event.templateVariables.providerLastName,
223+
event.templateVariables.providerId,
224+
event.templateVariables.liftedJurisdiction,
225+
event.templateVariables.licenseType,
226+
event.templateVariables.effectiveLiftDate
227+
);
228+
break;
229+
case 'privilegeEncumbranceProviderNotification':
230+
if (!event.templateVariables.providerFirstName
231+
|| !event.templateVariables.providerLastName
232+
|| !event.templateVariables.encumberedJurisdiction
233+
|| !event.templateVariables.licenseType
234+
|| !event.templateVariables.effectiveStartDate) {
235+
throw new Error('Missing required template variables for privilegeEncumbranceProviderNotification template.');
236+
}
237+
await this.encumbranceService.sendPrivilegeEncumbranceProviderNotificationEmail(
238+
event.compact,
239+
event.specificEmails || [],
240+
event.templateVariables.providerFirstName,
241+
event.templateVariables.providerLastName,
242+
event.templateVariables.encumberedJurisdiction,
243+
event.templateVariables.licenseType,
244+
event.templateVariables.effectiveStartDate
245+
);
246+
break;
247+
case 'privilegeEncumbranceStateNotification':
248+
if (!event.jurisdiction) {
249+
throw new Error('Missing required jurisdiction field for privilegeEncumbranceStateNotification template.');
250+
}
251+
if (!event.templateVariables.providerFirstName
252+
|| !event.templateVariables.providerLastName
253+
|| !event.templateVariables.providerId
254+
|| !event.templateVariables.encumberedJurisdiction
255+
|| !event.templateVariables.licenseType
256+
|| !event.templateVariables.effectiveStartDate) {
257+
throw new Error('Missing required template variables for privilegeEncumbranceStateNotification template.');
258+
}
259+
await this.encumbranceService.sendPrivilegeEncumbranceStateNotificationEmail(
260+
event.compact,
261+
event.jurisdiction,
262+
event.templateVariables.providerFirstName,
263+
event.templateVariables.providerLastName,
264+
event.templateVariables.providerId,
265+
event.templateVariables.encumberedJurisdiction,
266+
event.templateVariables.licenseType,
267+
event.templateVariables.effectiveStartDate
268+
);
269+
break;
270+
case 'privilegeEncumbranceLiftingProviderNotification':
271+
if (!event.templateVariables.providerFirstName
272+
|| !event.templateVariables.providerLastName
273+
|| !event.templateVariables.liftedJurisdiction
274+
|| !event.templateVariables.licenseType
275+
|| !event.templateVariables.effectiveLiftDate) {
276+
throw new Error('Missing required template variables for privilegeEncumbranceLiftingProviderNotification template.');
277+
}
278+
await this.encumbranceService.sendPrivilegeEncumbranceLiftingProviderNotificationEmail(
279+
event.compact,
280+
event.specificEmails || [],
281+
event.templateVariables.providerFirstName,
282+
event.templateVariables.providerLastName,
283+
event.templateVariables.liftedJurisdiction,
284+
event.templateVariables.licenseType,
285+
event.templateVariables.effectiveLiftDate
286+
);
287+
break;
288+
case 'privilegeEncumbranceLiftingStateNotification':
289+
if (!event.jurisdiction) {
290+
throw new Error('Missing required jurisdiction field for privilegeEncumbranceLiftingStateNotification template.');
291+
}
292+
if (!event.templateVariables.providerFirstName
293+
|| !event.templateVariables.providerLastName
294+
|| !event.templateVariables.providerId
295+
|| !event.templateVariables.liftedJurisdiction
296+
|| !event.templateVariables.licenseType
297+
|| !event.templateVariables.effectiveLiftDate) {
298+
throw new Error('Missing required template variables for privilegeEncumbranceLiftingStateNotification template.');
299+
}
300+
await this.encumbranceService.sendPrivilegeEncumbranceLiftingStateNotificationEmail(
301+
event.compact,
302+
event.jurisdiction,
303+
event.templateVariables.providerFirstName,
304+
event.templateVariables.providerLastName,
305+
event.templateVariables.providerId,
306+
event.templateVariables.liftedJurisdiction,
307+
event.templateVariables.licenseType,
308+
event.templateVariables.effectiveLiftDate
309+
);
310+
break;
138311
case 'multipleRegistrationAttemptNotification':
139312
if (!event.specificEmails?.length) {
140313
throw new Error('No recipients found for multiple registration attempt notification email');

backend/compact-connect/lambdas/nodejs/lib/email/base-email-service.ts

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,9 @@ export abstract class BaseEmailService {
313313
protected insertBody(
314314
report: TReaderDocument,
315315
bodyText: string,
316-
textAlign: 'center' | 'right' | 'left' | null = null) {
316+
textAlign: 'center' | 'right' | 'left' | null = null,
317+
markdown: boolean = false
318+
) {
317319
const blockId = `block-${crypto.randomUUID()}`;
318320

319321
report[blockId] = {
@@ -331,7 +333,8 @@ export abstract class BaseEmailService {
331333
}
332334
},
333335
'props': {
334-
'text': bodyText
336+
'text': bodyText,
337+
'markdown': markdown
335338
}
336339
}
337340
};
@@ -538,34 +541,6 @@ export abstract class BaseEmailService {
538541
report['root']['data']['childrenIds'].push(containerId);
539542
}
540543

541-
protected insertMarkdownBody(report: TReaderDocument, bodyText: string) {
542-
const blockId = `block-${crypto.randomUUID()}`;
543-
544-
report[blockId] = {
545-
'type': 'Text',
546-
'data': {
547-
'style': {
548-
'fontSize': 16,
549-
'fontWeight': 'normal',
550-
'textAlign': 'left',
551-
'color': '#09122B',
552-
'padding': {
553-
'top': 24,
554-
'bottom': 24,
555-
'right': 40,
556-
'left': 40
557-
}
558-
},
559-
'props': {
560-
'markdown': true,
561-
'text': bodyText
562-
}
563-
}
564-
};
565-
566-
report['root']['data']['childrenIds'].push(blockId);
567-
}
568-
569544
protected insertFooter(report: TReaderDocument) {
570545
const blockId = `block-footer`;
571546

0 commit comments

Comments
 (0)