Skip to content

Feat/purchase email #733

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 49 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
9541113
work
Apr 11, 2025
0178c16
work
Apr 12, 2025
3bce02b
work
Apr 14, 2025
960f720
emails
Apr 18, 2025
2c52a55
improvements
Apr 18, 2025
a9ff918
more work
Apr 23, 2025
1fe4514
work
Apr 24, 2025
ffae0ac
work
May 12, 2025
0bd6d26
potentially fin other than tests, typing and testing
May 12, 2025
4761aba
test fixes
May 12, 2025
51f1dd0
test fixes
May 13, 2025
7f1da53
node tests work
May 13, 2025
10fe4f1
tests
May 13, 2025
4cd7bd5
merge dev
May 13, 2025
1aae745
merge dev fixes
May 13, 2025
a9ac4c1
merge fix
May 13, 2025
58e8f71
some tests fixed
May 14, 2025
e116d34
some fixes
May 14, 2025
62967ad
fixes
May 14, 2025
aa4fb82
some fixes
May 14, 2025
9c6de0c
more fixes
May 14, 2025
5447744
more fixes
May 14, 2025
1a63f44
tweak branches
May 14, 2025
e488513
fixes
May 15, 2025
f57c724
work
May 16, 2025
0a9414a
almost done
May 19, 2025
c4874b2
reduce latency
May 19, 2025
d6423ca
fixes
May 20, 2025
e39bfd2
merge dev
May 20, 2025
4781af8
some tests
May 20, 2025
620ef3d
toggle notification stack based on domain existing
May 20, 2025
e7ee95c
lint fix
May 20, 2025
619310e
coderabbit fix
May 20, 2025
fa77e90
code rabbit fixes
May 20, 2025
45ba745
code rabbit fixes
May 20, 2025
9e9fb09
rem comma
May 20, 2025
b6c893b
fixes
May 20, 2025
5778903
wip
May 27, 2025
60d51c6
wip
May 28, 2025
c9c35c5
wip
May 28, 2025
052bedf
fix parameters for event bus test
landonshumway-ia May 28, 2025
20a6247
wip
May 28, 2025
fb954dc
WIP
May 28, 2025
675a89f
maybe done
May 28, 2025
e28846e
fix casing
May 28, 2025
428e9c0
number fix
May 28, 2025
1860c60
pr fixes
May 30, 2025
9749595
linter fixes
May 30, 2025
5e89ea9
final fix
May 30, 2025
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 @@ -28,6 +28,7 @@ def __init__(
batch_size: int,
encryption_key: IKey,
alarm_topic: ITopic,
dlq_count_alarm_threshold: int = 10,
):
super().__init__(scope, construct_id)

Expand Down Expand Up @@ -92,7 +93,11 @@ def __init__(
)

self._add_queue_alarms(
retention_period=retention_period, queue=self.queue, dlq=self.dlq, alarm_topic=alarm_topic
retention_period=retention_period,
queue=self.queue,
dlq=self.dlq,
alarm_topic=alarm_topic,
dlq_count_alarm_threshold=dlq_count_alarm_threshold,
)

QueryDefinition(
Expand All @@ -113,6 +118,7 @@ def _add_queue_alarms(
queue: IQueue,
dlq: IQueue,
alarm_topic: ITopic,
dlq_count_alarm_threshold: int = 10,
):
# Alarm if messages are older than half the queue retention period
message_age_alarm = Alarm(
Expand All @@ -135,7 +141,7 @@ def _add_queue_alarms(
'DLQMessagesAlarm',
metric=dlq.metric_approximate_number_of_messages_visible(),
evaluation_periods=1,
threshold=10,
threshold=dlq_count_alarm_threshold,
actions_enabled=True,
alarm_description=f'{dlq.node.path} high message volume',
comparison_operator=ComparisonOperator.GREATER_THAN_THRESHOLD,
Expand Down
7 changes: 5 additions & 2 deletions backend/compact-connect/common_constructs/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ def license_type_names(self):
@cached_property
def license_type_abbreviations(self):
"""Flattened list of all license type names across all compacts"""
return [typ['abbreviation'] for compact_license_types
in self.node.get_context('license_types').values() for typ in compact_license_types]
return [
typ['abbreviation']
for compact_license_types in self.node.get_context('license_types').values()
for typ in compact_license_types
]

@cached_property
def license_types(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ export class Lambda implements LambdaInterface {
event.templateVariables.endDate
);
break;
case 'privilegePurchaseProviderNotification':
await this.emailService.sendPrivilegePurchaseProviderNotificationEmail(
event.templateVariables.transactionDate,
event.templateVariables.privileges,
event.templateVariables.totalCost,
event.templateVariables.costLineItems,
event.specificEmails
);
break;
default:
logger.info('Unsupported email template provided', { template: event.template });
throw new Error(`Unsupported email template: ${event.template}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,10 @@ export abstract class BaseEmailService {
report['root']['data']['childrenIds'].push(blockHeaderId);
}

protected insertBody(report: TReaderDocument, bodyText: string) {
protected insertBody(
report: TReaderDocument,
bodyText: string,
textAlign: 'center' | 'right' | 'left' | null = null) {
const blockId = `block-${crypto.randomUUID()}`;

report[blockId] = {
Expand All @@ -333,9 +336,208 @@ export abstract class BaseEmailService {
}
};

if (textAlign && report[blockId]['data']['style']) {
report[blockId]['data']['style']['textAlign'] = textAlign;
}

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

protected insertTuple(report: TReaderDocument, keyText: string, valueText: string) {
const containerBlockId = `block-${crypto.randomUUID()}`;
const keyBlockId = `block-${crypto.randomUUID()}`;
const valueBlockId = `block-${crypto.randomUUID()}`;


report[keyBlockId] = {
'type': 'Text',
'data': {
'style': {
'fontWeight': 'bold',
'padding': {
'top': 16,
'bottom': 0,
'right': 12,
'left': 24
}
},
'props': {
'text': keyText
}
}
};

report[valueBlockId] = {
'type': 'Text',
'data': {
'style': {
'color': '#525252',
'fontSize': 14,
'fontWeight': 'normal',
'padding': {
'top': 0,
'bottom': 0,
'right': 24,
'left': 24
}
},
'props': {
'text': valueText
}
}
};

report[containerBlockId] = {
'type': 'Container',
'data': {
'style': {
'padding': {
'top': 0,
'bottom': 0,
'right': 72,
'left': 76
}
},
'props': {
'childrenIds': [
keyBlockId,
valueBlockId
]
}
}
};

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

protected insertTwoColumnTable(report: TReaderDocument, title: string, rows: { left: string, right: string }[]) {
const titleBlockId = `block-${crypto.randomUUID()}`;


report[titleBlockId] = {
'type': 'Text',
'data': {
'style': {
'fontWeight': 'bold',
'padding': {
'top': 24,
'bottom': 16,
'right': 24,
'left': 68
}
},
'props': {
'text': title
}
}
};

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

rows.forEach((row) => {
this.insertTwoColumnRow(report, row.left, row.right, false, 6);
});
}

protected insertTwoColumnRow(
report: TReaderDocument,
leftContent: string,
rightContent: string,
isBold: boolean,
bottomPadding: number
) {
const containerId = `block-${crypto.randomUUID()}`;
const leftCellId = `block-${crypto.randomUUID()}`;
const rightCellId = `block-${crypto.randomUUID()}`;

report[leftCellId] = {
'type': 'Text',
'data': {
'style': {
'fontWeight': 'normal',
'textAlign': 'left',
'padding': {
'top': 0,
'bottom': 0,
'right': 24,
'left': 24
}
},
'props': {
'text': leftContent
}
}
};

report[rightCellId] = {
'type': 'Text',
'data': {
'style': {
'fontWeight': 'normal',
'textAlign': 'right',
'padding': {
'top': 0,
'bottom': 0,
'right': 24,
'left': 24
}
},
'props': {
'text': rightContent
}
}
};

report[containerId] = {
'type': 'ColumnsContainer',
'data': {
'style': {
'padding': {
'top': 0,
'bottom': bottomPadding || 6,
'right': 44,
'left': 44
}
},
'props': {
'fixedWidths': [
null,
null,
null
],
'columnsCount': 2,
'columnsGap': 10,
'columns': [
{
'childrenIds': [
leftCellId
]
},
{
'childrenIds': [
rightCellId
]
},
{
'childrenIds': []
}
]
}
}
};

if (
isBold
&& report[leftCellId]['data']['style']
&& report[rightCellId]['data']['style']
) {
report[leftCellId]['data']['style']['fontWeight'] = 'bold';
report[rightCellId]['data']['style']['fontWeight'] = 'bold';
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,75 @@ export class EmailNotificationService extends BaseEmailService {
]
});
}

/**
* Sends an email notification to a provider when they purchase privilege(s)
* @param specificEmails - The email adresses(s) to send the email to, in this case always the provider's email
* @param transactionDate - The date the transaction occured
* @param privileges - The relevant privilege data necessary to generate teh email
* @param totalCost - The total cost of the transaction
* @param costLineItems - The line items involved in the purchase transaction
*/
public async sendPrivilegePurchaseProviderNotificationEmail(
transactionDate: string,
privileges: {
jurisdiction: string,
licenseTypeAbbrev: string,
privilegeId: string
}[],
totalCost: string,
costLineItems: {
name: string,
quantity: string,
unitPrice: string
}[],
specificEmails: string[] = []
): Promise<void> {
this.logger.info('Sending provider privilege purchase notification email', { providerEmail: specificEmails[0] });

const recipients = specificEmails;

if (recipients.length === 0) {
throw new Error(`No recipients found`);
}

const emailContent = this.getNewEmailTemplate();
const headerText = `Privilege Purchase Confirmation`;
const subject = `Compact Connect Privilege Purchase Confirmation`;
const bodyText = `This email is to confirm you successfully purchased the following privileges on ${transactionDate}`;

this.insertHeader(emailContent, headerText);
this.insertBody(emailContent, bodyText, 'center');

privileges.forEach((privilege) => {
const titleText = `${privilege.licenseTypeAbbrev.toUpperCase()} - ${privilege.jurisdiction.toUpperCase()}`;
const privilegeIdText = `Privilege Id: ${privilege.privilegeId}`;

this.insertTuple(emailContent, titleText, privilegeIdText);
});

const rows = costLineItems.map((lineItem) => {
const quantityNum = parseInt(lineItem.quantity, 10);
const unitPriceNum = Number(lineItem.unitPrice);


const quantityText = quantityNum > 1 ? `x ${quantityNum}` : '';
const left = `${lineItem.name} ${quantityText}`;
const right = `$${(unitPriceNum * quantityNum).toFixed(2)}`;

return { left, right };
});

const totalCostDisplay = `$${Number(totalCost).toFixed(2)}`;

this.insertTwoColumnTable(emailContent, 'Cost breakdown', rows);

this.insertTwoColumnRow(emailContent, 'Total', totalCostDisplay, true, 24);

this.insertFooter(emailContent);

const htmlContent = renderToStaticMarkup(emailContent, { rootBlockId: 'root' });

await this.sendEmail({ htmlContent, subject, recipients, errorMessage: 'Unable to send provider privilege purchase notification email' });
}
}
Loading