-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(scalars): add swedish personal number (#2181)
* feat(scalars): add swedish personal number * Changeset --------- Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
- Loading branch information
Showing
8 changed files
with
269 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'graphql-scalars': minor | ||
--- | ||
|
||
Add new Swedish Personal Number scalar |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { GraphQLScalarType, Kind } from 'graphql'; | ||
import { createGraphQLError } from '../../error.js'; | ||
|
||
// Swedish Personal Number also known as 'personnummer' in swedish: | ||
// https://www.skatteverket.se/privat/folkbokforing/personnummer.4.3810a01c150939e893f18c29.html | ||
// Algorithm: | ||
// https://swedish.identityinfo.net/personalidentitynumber | ||
|
||
const SESSN_PATTERNS = ['YYYYMMDDXXXX', 'YYMMDDXXXX']; | ||
|
||
function _isValidSwedishPersonalNumber(value: string): boolean { | ||
// Remove any non-digit characters | ||
const pno: string = value.replace(/\D/g, ''); | ||
// Check if the cleaned number has the correct length (10 or 12 digits) | ||
if (pno.length !== 10 && pno.length !== 12) { | ||
return false; | ||
} | ||
|
||
// Validate the birthdate | ||
if (!_isValidDate(pno)) { | ||
return false; | ||
} | ||
|
||
// Check the checksum for numbers | ||
if (!_isValidChecksum(pno)) { | ||
return false; | ||
} | ||
|
||
// If all checks pass, the personal number is valid | ||
return true; | ||
} | ||
|
||
function _isValidDate(pno: string): boolean { | ||
let year: number; | ||
let month: number; | ||
let day: number; | ||
|
||
if (pno.length === 10) { | ||
year = Number(pno.substring(0, 2)); | ||
// Adjust the input 'year' to a four-digit year based on the assumption that two-digit years greater than the current year are in the past century (1900s), | ||
// while two-digit years less than or equal to the current year are in the current or upcoming century (2000s). | ||
year = year > Number(String(new Date().getFullYear()).substring(2)) ? 1900 + year : 2000 + year; | ||
month = Number(pno.substring(2, 4)); | ||
day = Number(pno.substring(4, 6)); | ||
} else { | ||
year = Number(pno.substring(0, 4)); | ||
month = Number(pno.substring(4, 6)); | ||
day = Number(pno.substring(6, 8)); | ||
} | ||
|
||
const date = new Date(year, month - 1, day); | ||
|
||
return date.getFullYear() === year && date.getMonth() + 1 === month && date.getDate() === day; | ||
} | ||
|
||
function _isValidChecksum(pno: string): boolean { | ||
const shortPno: string = pno.length === 12 ? pno.substring(2, 12) : pno; | ||
const digits: number[] = shortPno.split('').map(Number); | ||
let sum: number = 0; | ||
|
||
for (let i: number = 0; i < digits.length; i++) { | ||
let digit = digits[i]; | ||
|
||
// Double every second digit from the right | ||
if (i % 2 === digits.length % 2) { | ||
digit *= 2; | ||
if (digit > 9) { | ||
digit -= 9; | ||
} | ||
} | ||
|
||
sum += digit; | ||
} | ||
|
||
// Check if the sum is a multiple of 10 | ||
return sum % 10 === 0; | ||
} | ||
|
||
function _checkString(value: any): void { | ||
if (typeof value !== 'string') { | ||
throw createGraphQLError(`Value is not string: ${value}`); | ||
} | ||
} | ||
|
||
function _checkSSN(value: string): void { | ||
if (!_isValidSwedishPersonalNumber(value)) { | ||
throw createGraphQLError(`Value is not a valid swedish personal number: ${value}`); | ||
} | ||
} | ||
|
||
export const GraphQLSESSN: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType({ | ||
name: 'SESSN', | ||
description: | ||
'A field whose value conforms to the standard personal number (personnummer) formats for Sweden', | ||
|
||
serialize(value) { | ||
_checkString(value); | ||
_checkSSN(value as string); | ||
|
||
return value; | ||
}, | ||
|
||
parseValue(value) { | ||
_checkString(value); | ||
_checkSSN(value as string); | ||
|
||
return value; | ||
}, | ||
|
||
parseLiteral(ast) { | ||
if (ast.kind !== Kind.STRING) { | ||
throw createGraphQLError( | ||
`Can only validate strings as swedish personal number but got a: ${ast.kind}`, | ||
{ nodes: ast }, | ||
); | ||
} | ||
|
||
if (!_isValidSwedishPersonalNumber(ast.value)) { | ||
throw createGraphQLError(`Value is not a valid swedish personal number: ${ast.value}`, { | ||
nodes: ast, | ||
}); | ||
} | ||
|
||
return ast.value; | ||
}, | ||
|
||
extensions: { | ||
codegenScalarType: 'string', | ||
jsonSchema: { | ||
title: 'SESSN', | ||
oneOf: SESSN_PATTERNS.map((pattern: string) => ({ | ||
type: 'string', | ||
length: pattern.length, | ||
pattern, | ||
})), | ||
}, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { Kind } from 'graphql/language'; | ||
import { GraphQLSESSN } from '../../src/scalars/ssn/SE.js'; | ||
|
||
// List was taken from https://www.uc.se/developer/consumer-reports/getting-started/ | ||
// and https://skatteverket.entryscape.net/catalog/9/datasets/147 | ||
const SSNs = [ | ||
'194907011813', | ||
'4907011813', | ||
'194006128989', | ||
'4006128989', | ||
'196512233666', | ||
'193303190718', | ||
'3303190718', | ||
'195207199398', | ||
'5207199398', | ||
]; | ||
|
||
describe(`SSN => SE`, () => { | ||
describe(`valid`, () => { | ||
it(`serialize`, () => { | ||
for (const value of SSNs) { | ||
expect(GraphQLSESSN.serialize(value)).toEqual(value); | ||
} | ||
}); | ||
|
||
it(`parseValue`, () => { | ||
for (const value of SSNs) { | ||
expect(GraphQLSESSN.parseValue(value)).toEqual(value); | ||
} | ||
}); | ||
|
||
it(`parseLiteral`, () => { | ||
for (const value of SSNs) { | ||
expect( | ||
GraphQLSESSN.parseLiteral( | ||
{ | ||
value, | ||
kind: Kind.STRING, | ||
}, | ||
{}, | ||
), | ||
).toEqual(value); | ||
} | ||
}); | ||
}); | ||
|
||
describe(`invalid`, () => { | ||
describe(`not a valid swedish personal number`, () => { | ||
it(`serialize`, () => { | ||
expect(() => GraphQLSESSN.serialize(123456789012)).toThrow(/Value is not string/); | ||
expect(() => GraphQLSESSN.serialize(`this is not a swedish personal number`)).toThrow( | ||
/Value is not a valid swedish personal number: this is not a swedish personal number/, | ||
); | ||
expect(() => GraphQLSESSN.serialize(`123456789012`)).toThrow( | ||
/Value is not a valid swedish personal number: 123456789012/, | ||
); | ||
expect(() => GraphQLSESSN.serialize(`194907011811`)).toThrow( | ||
/Value is not a valid swedish personal number: 194907011811/, | ||
); | ||
expect(() => GraphQLSESSN.serialize(`4907011811`)).toThrow( | ||
/Value is not a valid swedish personal number: 4907011811/, | ||
); | ||
}); | ||
|
||
it(`parseValue`, () => { | ||
expect(() => GraphQLSESSN.serialize(123456789012)).toThrow(/Value is not string/); | ||
expect(() => GraphQLSESSN.parseValue(`this is not a swedish personal number`)).toThrow( | ||
/Value is not a valid/, | ||
); | ||
expect(() => GraphQLSESSN.parseValue(`123456789012`)).toThrow( | ||
/Value is not a valid swedish personal number: 123456789012/, | ||
); | ||
expect(() => GraphQLSESSN.serialize(`194907011811`)).toThrow( | ||
/Value is not a valid swedish personal number: 194907011811/, | ||
); | ||
expect(() => GraphQLSESSN.serialize(`4907011811`)).toThrow( | ||
/Value is not a valid swedish personal number: 4907011811/, | ||
); | ||
}); | ||
|
||
it(`parseLiteral`, () => { | ||
expect(() => | ||
GraphQLSESSN.parseLiteral({ value: 123456789012, kind: Kind.INT } as any, {}), | ||
).toThrow(/Can only validate strings as swedish personal number but got a: IntValue/); | ||
|
||
expect(() => | ||
GraphQLSESSN.parseLiteral({ value: `123456789012`, kind: Kind.INT } as any, {}), | ||
).toThrow(/Can only validate strings as swedish personal number but got a: IntValue/); | ||
|
||
expect(() => | ||
GraphQLSESSN.parseLiteral( | ||
{ value: `this is not a swedish personal number`, kind: Kind.STRING }, | ||
{}, | ||
), | ||
).toThrow( | ||
/Value is not a valid swedish personal number: this is not a swedish personal number/, | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# SSN | ||
|
||
## SE (Swedish Personal Number `personnummer`) | ||
|
||
Accepts the value in the following formats: | ||
|
||
- YYYYMMDDXXXX | ||
- YYMMDDXXXX | ||
|
||
In case of 10 digit format, it adjusts the 'year' to a four-digit year based on the assumption that | ||
two-digit years greater than the current year are in the past century (1900s), while two-digit years | ||
less than or equal to the current year are in the current or upcoming century (2000s). | ||
|
||
Reference: | ||
https://www.skatteverket.se/privat/folkbokforing/personnummer.4.3810a01c150939e893f18c29.html |