// - NPI format validation, client side. // // THIS IS THE CONVENIENCE COPY. The gate is the server (`Practitioner/put.ts`); this exists so a // provider entering their own NPI sees the problem in the field instead of receiving a 400 after pressing // Save. - a client-side check is never the gate, and the // two implementations are deliberately identical so they cannot disagree about what "valid" means. // // FORMAT, NOT IDENTITY. A well-formed NPI is not a verified one - registry lookup against NPPES is its // own card and is unbuilt. We deliberately do NOT call NPPES from the browser // ( ); when real verification lands it goes behind our own // endpoint with a deterministic fallback. // // Format still matters more than it sounds: a typo'd NPI is WORSE for identity matching than an absent // one, because it either fails to link a human who should be linked or links two who should not. // measured three false-positive NPI pairs in the seed data alone. const TEN_DIGITS = /^\d{10}$/; /** * The official NPI check digit: Luhn over the number prefixed with the NPPES issuer id `80840`. * A single mistyped digit fails, which is the whole reason this beats a length check. */ export function isValidNpi(value: string): boolean { const npi = value.trim(); if (!TEN_DIGITS.test(npi)) return false; const digits = ('80840' + npi.slice(0, 9)).split('').map(Number); let sum = 0; // Luhn from the right: double every second digit, subtracting 9 when the result exceeds 9. for (let i = digits.length - 1, position = 0; i >= 0; i--, position++) { let digit = digits[i]; if (position % 2 === 0) { digit *= 2; if (digit > 9) digit -= 9; } sum += digit; } const checkDigit = (10 - (sum % 10)) % 10; return checkDigit === Number(npi[9]); } export const NPI_FORMAT_MESSAGE = 'An NPI is 10 digits and includes a check digit, so a single mistyped digit is rejected.';