Skip to content

Feat/update practitioner email #877

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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3dde8a1
Add optional email fields to provider record
landonshumway-ia Jun 20, 2025
0004751
Add email endpoints with api models
landonshumway-ia Jun 20, 2025
e97a509
Implement logic to verify and change practitioner emails
landonshumway-ia Jun 20, 2025
4852c55
Add email templates to notification service
landonshumway-ia Jun 20, 2025
68fca56
sync API model to latest provider detail api response
landonshumway-ia Jun 20, 2025
5168fcf
PR feedback - use secrets module for code generation
landonshumway-ia Jun 21, 2025
8d34511
Add smoke test for email update flow
landonshumway-ia Jun 23, 2025
e0fc024
Improve formatting of verification code email
landonshumway-ia Jun 23, 2025
4e0a3d9
Improve formatting of email messages for verification
landonshumway-ia Jun 23, 2025
88228a7
linter/format
landonshumway-ia Jun 23, 2025
4fa1a06
Update API spec to latest
landonshumway-ia Jun 23, 2025
42eb7d9
rename smoke test file
landonshumway-ia Jun 23, 2025
59de4a8
update postman collection
landonshumway-ia Jun 23, 2025
4b862b1
Add missing NPDB categories
landonshumway-ia Jun 23, 2025
cc72962
PR feedback
landonshumway-ia Jun 26, 2025
483a015
Update registration to check new live field
landonshumway-ia Jun 26, 2025
c30f1f7
Use new insertBody method to center text
landonshumway-ia Jul 1, 2025
3a1cf9d
rollback cognito if dynamo fails to update user record
landonshumway-ia Jul 1, 2025
cfd47ff
PR feedback - combine node tests into one
landonshumway-ia Jul 7, 2025
be0c68f
PR feedback - add check in smoke test for invalid code
landonshumway-ia Jul 8, 2025
9c4998a
PR feedback - add wrapper for cognito call
landonshumway-ia Jul 8, 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
8,102 changes: 4,310 additions & 3,792 deletions backend/compact-connect/docs/api-specification/latest-oas30.json

Large diffs are not rendered by default.

954 changes: 609 additions & 345 deletions backend/compact-connect/docs/postman/postman-collection.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,32 @@ export class Lambda implements LambdaInterface {
event.specificEmails
);
break;
case 'providerEmailVerificationCode':
if (!event.specificEmails?.length) {
throw new Error('No recipients found for provider email verification code email');
}
if (!event.templateVariables?.verificationCode) {
throw new Error('Missing required template variables for providerEmailVerificationCode template');
}
await this.emailService.sendProviderEmailVerificationCode(
event.compact,
event.specificEmails[0],
event.templateVariables.verificationCode
);
break;
case 'providerEmailChangeNotification':
if (!event.specificEmails?.length) {
throw new Error('No recipients found for provider email change notification email');
}
if (!event.templateVariables?.newEmailAddress) {
throw new Error('Missing required template variables for providerEmailChangeNotification template');
}
await this.emailService.sendProviderEmailChangeNotification(
event.compact,
event.specificEmails[0],
event.templateVariables.newEmailAddress
);
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 @@ -367,4 +367,60 @@ export class EmailNotificationService extends BaseEmailService {

await this.sendEmail({ htmlContent, subject, recipients, errorMessage: 'Unable to send multiple registration attempt notification email' });
}

/**
* Sends a verification code to a provider's new email address during email change process
* @param compact - The compact name
* @param providerEmail - The new email address to send the verification code to
* @param verificationCode - The 4-digit verification code
*/
public async sendProviderEmailVerificationCode(
compact: string,
providerEmail: string,
verificationCode: string
): Promise<void> {
this.logger.info('Sending provider email verification code', { compact: compact, providerEmail: providerEmail });

const recipients = [providerEmail];

const report = this.getNewEmailTemplate();
const subject = `Verify Your New Email Address - Compact Connect`;
const bodyText = `Please use the following verification code to complete your email address change:\n\n## ${verificationCode}\n\nThis code will expire in 15 minutes.\n\nIf you did not request this email change, please contact support immediately.`;

this.insertHeader(report, 'Email Update Verification');
this.insertBody(report, bodyText, 'center', true);
this.insertFooter(report);

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

await this.sendEmail({ htmlContent, subject, recipients, errorMessage: 'Unable to send email verification code' });
}

/**
* Sends a notification to the old email address when a provider's email is successfully changed
* @param compact - The compact name
* @param oldEmailAddress - The previous email address to notify
* @param newEmailAddress - The new email address that was set
*/
public async sendProviderEmailChangeNotification(
compact: string,
oldEmailAddress: string,
newEmailAddress: string
): Promise<void> {
this.logger.info('Sending provider email change notification', { compact: compact, oldEmailAddress: oldEmailAddress });

const recipients = [oldEmailAddress];

const report = this.getNewEmailTemplate();
const subject = `Email Address Changed - Compact Connect`;
const bodyText = `This is to notify you that your Compact Connect account email address has been changed to the following:\n\n${newEmailAddress}\n\nPlease use the new email address to login to your account from now on. If you did not make this change, please contact support immediately.`;

this.insertHeader(report, 'Email Address Changed');
this.insertBody(report, bodyText, 'center');
this.insertFooter(report);

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

await this.sendEmail({ htmlContent, subject, recipients, errorMessage: 'Unable to send email change notification' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1174,4 +1174,147 @@ describe('EmailNotificationServiceLambda', () => {
.toThrow('Missing required jurisdiction field for privilegeEncumbranceLiftingStateNotification template.');
});
});

describe('Provider Email Verification Code', () => {
const SAMPLE_PROVIDER_EMAIL_VERIFICATION_CODE_EVENT: EmailNotificationEvent = {
template: 'providerEmailVerificationCode',
recipientType: 'SPECIFIC',
compact: 'aslp',
specificEmails: ['[email protected]'],
templateVariables: {
verificationCode: '1234'
}
};

it('should successfully send provider email verification code email', async () => {
const response = await lambda.handler(SAMPLE_PROVIDER_EMAIL_VERIFICATION_CODE_EVENT, {} as any);

expect(response).toEqual({
message: 'Email message sent'
});

// Verify email was sent
expect(mockSESClient).toHaveReceivedCommandWith(SendEmailCommand, {
Destination: {
ToAddresses: ['[email protected]']
},
Message: {
Body: {
Html: {
Charset: 'UTF-8',
Data: expect.any(String)
}
},
Subject: {
Charset: 'UTF-8',
Data: 'Verify Your New Email Address - Compact Connect'
}
},
Source: 'Compact Connect <[email protected]>'
});

// Get the actual HTML content for detailed validation
const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0];
const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data;

expect(htmlContent).toBeDefined();
expect(htmlContent).toContain('Please use the following verification code to complete your email address change');
expect(htmlContent).toContain('<h2>1234</h2>');
expect(htmlContent).toContain('This code will expire in 15 minutes');
expect(htmlContent).toContain('If you did not request this email change, please contact support immediately');
expect(htmlContent).toContain('Email Update Verification');
});

it('should throw error when no recipients found', async () => {
const eventWithNoRecipients: EmailNotificationEvent = {
...SAMPLE_PROVIDER_EMAIL_VERIFICATION_CODE_EVENT,
specificEmails: []
};

await expect(lambda.handler(eventWithNoRecipients, {} as any))
.rejects
.toThrow('No recipients found for provider email verification code email');
});

it('should throw error when verification code is missing', async () => {
const eventWithMissingCode: EmailNotificationEvent = {
...SAMPLE_PROVIDER_EMAIL_VERIFICATION_CODE_EVENT,
templateVariables: {}
};

await expect(lambda.handler(eventWithMissingCode, {} as any))
.rejects
.toThrow('Missing required template variables for providerEmailVerificationCode template');
});
});

describe('Provider Email Change Notification', () => {
const SAMPLE_PROVIDER_EMAIL_CHANGE_NOTIFICATION_EVENT: EmailNotificationEvent = {
template: 'providerEmailChangeNotification',
recipientType: 'SPECIFIC',
compact: 'aslp',
specificEmails: ['[email protected]'],
templateVariables: {
newEmailAddress: '[email protected]'
}
};

it('should successfully send provider email change notification email', async () => {
const response = await lambda.handler(SAMPLE_PROVIDER_EMAIL_CHANGE_NOTIFICATION_EVENT, {} as any);

expect(response).toEqual({
message: 'Email message sent'
});

// Verify email was sent with correct parameters
expect(mockSESClient).toHaveReceivedCommandWith(SendEmailCommand, {
Destination: {
ToAddresses: ['[email protected]']
},
Message: {
Body: {
Html: {
Charset: 'UTF-8',
Data: expect.any(String)
}
},
Subject: {
Charset: 'UTF-8',
Data: 'Email Address Changed - Compact Connect'
}
},
Source: 'Compact Connect <[email protected]>'
});

// Get the actual HTML content for detailed validation
const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0];
const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data;

expect(htmlContent).toBeDefined();
expect(htmlContent).toContain('This is to notify you that your Compact Connect account email address has been changed to the following:');
expect(htmlContent).toContain('[email protected]');
});

it('should throw error when no recipients found', async () => {
const eventWithNoRecipients: EmailNotificationEvent = {
...SAMPLE_PROVIDER_EMAIL_CHANGE_NOTIFICATION_EVENT,
specificEmails: []
};

await expect(lambda.handler(eventWithNoRecipients, {} as any))
.rejects
.toThrow('No recipients found for provider email change notification email');
});

it('should throw error when new email address is missing', async () => {
const eventWithMissingEmail: EmailNotificationEvent = {
...SAMPLE_PROVIDER_EMAIL_CHANGE_NOTIFICATION_EVENT,
templateVariables: {}
};

await expect(lambda.handler(eventWithMissingEmail, {} as any))
.rejects
.toThrow('Missing required template variables for providerEmailChangeNotification template');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -698,4 +698,88 @@ describe('EmailNotificationService', () => {
)).rejects.toThrow('No recipients found');
});
});

describe('Provider Email Verification Code', () => {
it('should send email verification code with all expected content', async () => {
const verificationCode = '1234';

await emailService.sendProviderEmailVerificationCode(
'aslp',
'[email protected]',
verificationCode
);

// Check overall email structure and each content piece
// Verify email was sent
expect(mockSESClient).toHaveReceivedCommandWith(SendEmailCommand, {
Destination: {
ToAddresses: ['[email protected]']
},
Message: {
Body: {
Html: {
Charset: 'UTF-8',
Data: expect.any(String)
}
},
Subject: {
Charset: 'UTF-8',
Data: 'Verify Your New Email Address - Compact Connect'
}
},
Source: 'Compact Connect <[email protected]>'
});

// Get the actual HTML content for detailed validation
const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0];
const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data;

expect(htmlContent).toBeDefined();
expect(htmlContent).toContain('Please use the following verification code to complete your email address change');
expect(htmlContent).toContain(`<h2>${verificationCode}</h2>`);
expect(htmlContent).toContain('This code will expire in 15 minutes');
expect(htmlContent).toContain('If you did not request this email change, please contact support immediately');
expect(htmlContent).toContain('Email Update Verification');
});
});

describe('Provider Email Change Notification', () => {
it('should send email change notification with all expected content', async () => {
await emailService.sendProviderEmailChangeNotification(
'aslp',
'[email protected]',
'[email protected]'
);

// Verify email was sent
expect(mockSESClient).toHaveReceivedCommandWith(SendEmailCommand, {
Destination: {
ToAddresses: ['[email protected]']
},
Message: {
Body: {
Html: {
Charset: 'UTF-8',
Data: expect.any(String)
}
},
Subject: {
Charset: 'UTF-8',
Data: 'Email Address Changed - Compact Connect'
}
},
Source: 'Compact Connect <[email protected]>'
});

// Get the actual HTML content for detailed validation
const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0];
const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data;

expect(htmlContent).toBeDefined();
expect(htmlContent).toContain('Please use the new email address to login to your account from now on.');
expect(htmlContent).toContain('If you did not make this change, please contact support immediately.');
expect(htmlContent).toContain('Email Address Changed');
expect(htmlContent).toContain('[email protected]');
});
});
});
Loading