Skip to content

Feat/aa notifications #864

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,46 @@
from aws_cdk import Duration
from aws_cdk.aws_cloudwatch import Alarm, ComparisonOperator, Metric, Stats, TreatMissingData
from aws_cdk.aws_cloudwatch_actions import SnsAction
from aws_cdk.aws_events import EventPattern, Rule
from aws_cdk.aws_events import EventBus, EventPattern, Rule
from aws_cdk.aws_events_targets import SqsQueue
from aws_cdk.aws_kms import IKey
from aws_cdk.aws_lambda import IFunction
from common_constructs.queued_lambda_processor import QueuedLambdaProcessor
from aws_cdk.aws_sns import ITopic
from constructs import Construct

from stacks import persistent_stack as ps
from common_constructs.queued_lambda_processor import QueuedLambdaProcessor


class QueueEventListener(Construct):
"""
This construct defines resources for an event listener that puts events on a queue to be processed by a lambda
function.

This construct creates:
- A QueuedLambdaProcessor for reliable message processing
- An EventBridge rule to route events to the queue
- CloudWatch alarms for monitoring failures
"""
default_visibility_timeout = Duration.minutes(5)
default_retention_period = Duration.hours(12)
default_max_batching_window = Duration.seconds(15)

def __init__(
self,
scope: Construct,
construct_id: str,
*,
data_event_bus: ps.EventBus,
data_event_bus: EventBus,
listener_function: IFunction,
listener_detail_type: str,
persistent_stack: ps.PersistentStack,
encryption_key: IKey,
alarm_topic: ITopic,
visibility_timeout: Duration = default_visibility_timeout,
retention_period: Duration = default_retention_period,
max_batching_window: Duration = default_max_batching_window,
max_receive_count: int = 3,
batch_size: int = 10,
dlq_count_alarm_threshold: int = 1,
**kwargs,
):
super().__init__(scope, construct_id, **kwargs)
Expand All @@ -44,22 +60,21 @@ def __init__(
treat_missing_data=TreatMissingData.NOT_BREACHING,
)

self.lambda_failure_alarm.add_alarm_action(SnsAction(persistent_stack.alarm_topic))
self.lambda_failure_alarm.add_alarm_action(SnsAction(alarm_topic))

# Create the QueuedLambdaProcessor
self.queue_processor = QueuedLambdaProcessor(
self,
f'{construct_id}QueueProcessor',
process_function=listener_function,
visibility_timeout=Duration.minutes(5),
retention_period=Duration.hours(12),
max_batching_window=Duration.seconds(15),
max_receive_count=3,
batch_size=10,
encryption_key=persistent_stack.shared_encryption_key,
alarm_topic=persistent_stack.alarm_topic,
# We want to be aware if any communications failed to send, so we'll set this threshold to 1
dlq_count_alarm_threshold=1,
visibility_timeout=visibility_timeout,
retention_period=retention_period,
max_batching_window=max_batching_window,
max_receive_count=max_receive_count,
batch_size=batch_size,
encryption_key=encryption_key,
alarm_topic=alarm_topic,
dlq_count_alarm_threshold=dlq_count_alarm_threshold,
)

# Create rule to route specified detail events to the SQS queue
Expand Down Expand Up @@ -91,4 +106,4 @@ def __init__(
treat_missing_data=TreatMissingData.NOT_BREACHING,
)

self.event_bridge_failure_alarm.add_alarm_action(SnsAction(persistent_stack.alarm_topic))
self.event_bridge_failure_alarm.add_alarm_action(SnsAction(alarm_topic))
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,8 @@ def load_data_event_bus_from_ssm_parameter(scope: Construct) -> EventBus:
the event bus ARN in SSM Parameter Store rather than using a direct reference,
which helps avoid issues with CloudFormation stack updates.

Args:
scope: The CDK construct scope

Returns:
The EventBus construct
:param scope: The CDK construct scope
:return: The EventBus construct
"""
data_event_bus_arn = StringParameter.from_string_parameter_name(
scope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Context } from 'aws-lambda';
import { EnvironmentVariablesService } from '../lib/environment-variables-service';
import { CompactConfigurationClient } from '../lib/compact-configuration-client';
import { JurisdictionClient } from '../lib/jurisdiction-client';
import { EmailNotificationService } from '../lib/email';
import { EmailNotificationService, EncumbranceNotificationService } from '../lib/email';
import { EmailNotificationEvent, EmailNotificationResponse } from '../lib/models/email-notification-service-event';

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

export class Lambda implements LambdaInterface {
private readonly emailService: EmailNotificationService;
private readonly encumbranceService: EncumbranceNotificationService;

constructor(props: LambdaProperties) {
const compactConfigurationClient = new CompactConfigurationClient({
Expand All @@ -41,6 +42,14 @@ export class Lambda implements LambdaInterface {
compactConfigurationClient: compactConfigurationClient,
jurisdictionClient: jurisdictionClient
});

this.encumbranceService = new EncumbranceNotificationService({
logger: logger,
sesClient: props.sesClient,
s3Client: props.s3Client,
compactConfigurationClient: compactConfigurationClient,
jurisdictionClient: jurisdictionClient
});
}

/**
Expand Down Expand Up @@ -77,8 +86,8 @@ export class Lambda implements LambdaInterface {
if (!event.jurisdiction) {
throw new Error('Missing required jurisdiction field.');
}
if (!event.templateVariables.privilegeId
|| !event.templateVariables.providerFirstName
if (!event.templateVariables.privilegeId
|| !event.templateVariables.providerFirstName
|| !event.templateVariables.providerLastName) {
throw new Error('Missing required template variables for privilegeDeactivationJurisdictionNotification template.');
}
Expand Down Expand Up @@ -135,6 +144,170 @@ export class Lambda implements LambdaInterface {
event.specificEmails
);
break;
case 'licenseEncumbranceProviderNotification':
if (!event.templateVariables.providerFirstName
|| !event.templateVariables.providerLastName
|| !event.templateVariables.encumberedJurisdiction
|| !event.templateVariables.licenseType
|| !event.templateVariables.effectiveStartDate) {
throw new Error('Missing required template variables for licenseEncumbranceProviderNotification template.');
}
await this.encumbranceService.sendLicenseEncumbranceProviderNotificationEmail(
event.compact,
event.specificEmails || [],
event.templateVariables.providerFirstName,
event.templateVariables.providerLastName,
event.templateVariables.encumberedJurisdiction,
event.templateVariables.licenseType,
event.templateVariables.effectiveStartDate
);
break;
case 'licenseEncumbranceStateNotification':
if (!event.jurisdiction) {
throw new Error('Missing required jurisdiction field for licenseEncumbranceStateNotification template.');
}
if (!event.templateVariables.providerFirstName
|| !event.templateVariables.providerLastName
|| !event.templateVariables.providerId
|| !event.templateVariables.encumberedJurisdiction
|| !event.templateVariables.licenseType
|| !event.templateVariables.effectiveStartDate) {
throw new Error('Missing required template variables for licenseEncumbranceStateNotification template.');
}
await this.encumbranceService.sendLicenseEncumbranceStateNotificationEmail(
event.compact,
event.jurisdiction,
event.templateVariables.providerFirstName,
event.templateVariables.providerLastName,
event.templateVariables.providerId,
event.templateVariables.encumberedJurisdiction,
event.templateVariables.licenseType,
event.templateVariables.effectiveStartDate
);
break;
case 'licenseEncumbranceLiftingProviderNotification':
if (!event.templateVariables.providerFirstName
|| !event.templateVariables.providerLastName
|| !event.templateVariables.liftedJurisdiction
|| !event.templateVariables.licenseType
|| !event.templateVariables.effectiveLiftDate) {
throw new Error('Missing required template variables for licenseEncumbranceLiftingProviderNotification template.');
}
await this.encumbranceService.sendLicenseEncumbranceLiftingProviderNotificationEmail(
event.compact,
event.specificEmails || [],
event.templateVariables.providerFirstName,
event.templateVariables.providerLastName,
event.templateVariables.liftedJurisdiction,
event.templateVariables.licenseType,
event.templateVariables.effectiveLiftDate
);
break;
case 'licenseEncumbranceLiftingStateNotification':
if (!event.jurisdiction) {
throw new Error('Missing required jurisdiction field for licenseEncumbranceLiftingStateNotification template.');
}
if (!event.templateVariables.providerFirstName
|| !event.templateVariables.providerLastName
|| !event.templateVariables.providerId
|| !event.templateVariables.liftedJurisdiction
|| !event.templateVariables.licenseType
|| !event.templateVariables.effectiveLiftDate) {
throw new Error('Missing required template variables for licenseEncumbranceLiftingStateNotification template.');
}
await this.encumbranceService.sendLicenseEncumbranceLiftingStateNotificationEmail(
event.compact,
event.jurisdiction,
event.templateVariables.providerFirstName,
event.templateVariables.providerLastName,
event.templateVariables.providerId,
event.templateVariables.liftedJurisdiction,
event.templateVariables.licenseType,
event.templateVariables.effectiveLiftDate
);
break;
case 'privilegeEncumbranceProviderNotification':
if (!event.templateVariables.providerFirstName
|| !event.templateVariables.providerLastName
|| !event.templateVariables.encumberedJurisdiction
|| !event.templateVariables.licenseType
|| !event.templateVariables.effectiveStartDate) {
throw new Error('Missing required template variables for privilegeEncumbranceProviderNotification template.');
}
await this.encumbranceService.sendPrivilegeEncumbranceProviderNotificationEmail(
event.compact,
event.specificEmails || [],
event.templateVariables.providerFirstName,
event.templateVariables.providerLastName,
event.templateVariables.encumberedJurisdiction,
event.templateVariables.licenseType,
event.templateVariables.effectiveStartDate
);
break;
case 'privilegeEncumbranceStateNotification':
if (!event.jurisdiction) {
throw new Error('Missing required jurisdiction field for privilegeEncumbranceStateNotification template.');
}
if (!event.templateVariables.providerFirstName
|| !event.templateVariables.providerLastName
|| !event.templateVariables.providerId
|| !event.templateVariables.encumberedJurisdiction
|| !event.templateVariables.licenseType
|| !event.templateVariables.effectiveStartDate) {
throw new Error('Missing required template variables for privilegeEncumbranceStateNotification template.');
}
await this.encumbranceService.sendPrivilegeEncumbranceStateNotificationEmail(
event.compact,
event.jurisdiction,
event.templateVariables.providerFirstName,
event.templateVariables.providerLastName,
event.templateVariables.providerId,
event.templateVariables.encumberedJurisdiction,
event.templateVariables.licenseType,
event.templateVariables.effectiveStartDate
);
break;
case 'privilegeEncumbranceLiftingProviderNotification':
if (!event.templateVariables.providerFirstName
|| !event.templateVariables.providerLastName
|| !event.templateVariables.liftedJurisdiction
|| !event.templateVariables.licenseType
|| !event.templateVariables.effectiveLiftDate) {
throw new Error('Missing required template variables for privilegeEncumbranceLiftingProviderNotification template.');
}
await this.encumbranceService.sendPrivilegeEncumbranceLiftingProviderNotificationEmail(
event.compact,
event.specificEmails || [],
event.templateVariables.providerFirstName,
event.templateVariables.providerLastName,
event.templateVariables.liftedJurisdiction,
event.templateVariables.licenseType,
event.templateVariables.effectiveLiftDate
);
break;
case 'privilegeEncumbranceLiftingStateNotification':
if (!event.jurisdiction) {
throw new Error('Missing required jurisdiction field for privilegeEncumbranceLiftingStateNotification template.');
}
if (!event.templateVariables.providerFirstName
|| !event.templateVariables.providerLastName
|| !event.templateVariables.providerId
|| !event.templateVariables.liftedJurisdiction
|| !event.templateVariables.licenseType
|| !event.templateVariables.effectiveLiftDate) {
throw new Error('Missing required template variables for privilegeEncumbranceLiftingStateNotification template.');
}
await this.encumbranceService.sendPrivilegeEncumbranceLiftingStateNotificationEmail(
event.compact,
event.jurisdiction,
event.templateVariables.providerFirstName,
event.templateVariables.providerLastName,
event.templateVariables.providerId,
event.templateVariables.liftedJurisdiction,
event.templateVariables.licenseType,
event.templateVariables.effectiveLiftDate
);
break;
case 'multipleRegistrationAttemptNotification':
if (!event.specificEmails?.length) {
throw new Error('No recipients found for multiple registration attempt notification email');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,9 @@ export abstract class BaseEmailService {
protected insertBody(
report: TReaderDocument,
bodyText: string,
textAlign: 'center' | 'right' | 'left' | null = null) {
textAlign: 'center' | 'right' | 'left' | null = null,
markdown: boolean = false
) {
const blockId = `block-${crypto.randomUUID()}`;

report[blockId] = {
Expand All @@ -331,7 +333,8 @@ export abstract class BaseEmailService {
}
},
'props': {
'text': bodyText
'text': bodyText,
'markdown': markdown
}
}
};
Expand Down Expand Up @@ -538,34 +541,6 @@ export abstract class BaseEmailService {
report['root']['data']['childrenIds'].push(containerId);
}

protected insertMarkdownBody(report: TReaderDocument, bodyText: string) {
const blockId = `block-${crypto.randomUUID()}`;

report[blockId] = {
'type': 'Text',
'data': {
'style': {
'fontSize': 16,
'fontWeight': 'normal',
'textAlign': 'left',
'color': '#09122B',
'padding': {
'top': 24,
'bottom': 24,
'right': 40,
'left': 40
}
},
'props': {
'markdown': true,
'text': bodyText
}
}
};

report['root']['data']['childrenIds'].push(blockId);
}

protected insertFooter(report: TReaderDocument) {
const blockId = `block-footer`;

Expand Down
Loading