Skip to content

CCM-15080: contact detail verification#905

Open
harrim91 wants to merge 18 commits intomainfrom
feature/CCM-15080_contact-detail-verification
Open

CCM-15080: contact detail verification#905
harrim91 wants to merge 18 commits intomainfrom
feature/CCM-15080_contact-detail-verification

Conversation

@harrim91
Copy link
Copy Markdown
Contributor

@harrim91 harrim91 commented Apr 9, 2026

Description

Adds infrastructure and API endpoint for storing contact detail verification requests

  • Email addresses and phone numbers validated and normalised using rules adapted from NHS Notify Core
  • Phone numbers - only UK mobile numbers allowed (including Channel Islands + Isle of Man)
  • Email - same validation rules as core
  • 6 digit OTP generated using crypto.randomInt. Hashed OTP is stored in the database.
  • Verification uses a "latest wins" model. If verification is re-requested for an unverified contact detail, then the old record and OTP is overwritten and will become invalid.
  • If contact detail has already been verified, it does not get overwritten.

Context

Type of changes

  • Refactoring (non-breaking change)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would change existing functionality)
  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I am familiar with the contributing guidelines
  • I have followed the code style of the project
  • I have added tests to cover my changes
  • I have updated the documentation accordingly
  • This PR is a result of pair or mob programming

Sensitive Information Declaration

To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including PII (Personal Identifiable Information) / PID (Personal Identifiable Data) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter.

  • I confirm that neither PII/PID nor sensitive data are included in this PR and the codebase changes.

@harrim91 harrim91 requested review from a team as code owners April 9, 2026 11:39
import { makeVerifiedContactDetail } from 'helpers/factories/contact-details-factory';

const generateEmailAddress = () =>
faker.internet.email({ provider: 'nhs.net' }).toLowerCase();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why faker instead of having consistent test data?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faker was already installed and it was easier than thinking of a new email address for each test case. I didn't want to use the same email address between test cases because of the uniqueness constraint.

},
data: {
type: 'EMAIL',
value: ` ${email.toUpperCase()} `,
Copy link
Copy Markdown
Contributor

@chris-elliott-nhsd chris-elliott-nhsd Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the use of toUpperCase?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to prove the normalisation is working. Input is an uppercase email address padded with whitespace, output is a lowercase email address with no padding.

'',
'email@subdomain.nhs.net',
`l${'o'.repeat(310)}ng@nhs.net`,
'email@notnhsdotnet.com',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to support domains other than nhs.net (sorry! restricting emails to only nhs.net was a requirement until recently. i only learned today that we need to support any domain)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, no problem

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made this use exactly the same rules as Core.

});

test.describe('sms verification', () => {
for (const locale of ['GB', 'GG', 'IM', 'JE'] as Locale[]) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is very interesting. where did this list of supported locales come from?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was mentioned the other week that we only wanted to support UK mobile numbers. I adapted the phone number verification from core, which had tests relating to the channel islands, so I went with accepting those too - they all use the +44 prefix. Happy to be corrected if this is wrong.

ttl: this.getUnverifiedTtl(now),
createdAt: now.toISOString(),
createdBy: `INTERNAL_USER#${user.internalUserId}`,
updatedAt: now.toISOString(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is updatedAt/updatedBy meaningful here? like once it's been created the only change that can be made to it is to change the status to VERIFIED, and it'd be very difficult for that to be done by a different user to the one that created it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure, I think I just saw it on one of the REFCOMs and thought there was no harm in having it.

name, # To support backup and restore which will result in a new name otherwise
]
}
}
Copy link
Copy Markdown
Contributor

@chris-elliott-nhsd chris-elliott-nhsd Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have an index to get by ID? will be needed for later parts of contact details verification, and it's simpler if it's just in there from the start (sorry! this was on a REFCOM that did not make it onto the ticket)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this the primary index on the table because it enforces the uniqueness constraint. Otherwise you need to list all of the contact details for the client and check the uniqueness in memory.
For the other access patterns, I was going to add GSI's - primarily one to look up by ID. But I felt this was the best way to enforce the uniqueness.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I haven't added them yet as they're not needed for this ticket - they will be needed in CCM-15087 or CCM-15088

new PutCommand({
TableName: this.tableName,
Item: {
PK: `CLIENT#${user.clientId}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this schema doesn't match at all what's on the ticket (or what's on the REFCOM that the ticket is based on) - if there are good reasons for doing it like this instead we should have a chat about it because changing it to work like this has huge implications for the rest of the contact details verification work

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed the keys to match whats on the REFCOM, PK/SK was just sort of habitual 😆

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants