-
Notifications
You must be signed in to change notification settings - Fork 32
fix: add Bolivian CI (Cédula de Identidad) validator #144
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| import { validate, format } from './ci'; | ||
| import { InvalidLength, InvalidFormat, InvalidComponent } from '../exceptions'; | ||
|
|
||
| describe('bo/ci', () => { | ||
| // Format tests | ||
| it('format:1234567SC', () => { | ||
| const result = format('1234567SC'); | ||
|
|
||
| expect(result).toEqual('1234567-SC'); | ||
| }); | ||
|
|
||
| it('format:12345678LP', () => { | ||
| const result = format('12345678LP'); | ||
|
|
||
| expect(result).toEqual('12345678-LP'); | ||
| }); | ||
|
|
||
| it('format:1234567SC1', () => { | ||
| const result = format('1234567SC1'); | ||
|
|
||
| expect(result).toEqual('1234567-SC-1'); | ||
| }); | ||
|
|
||
| it('format:1234567-OR-AB', () => { | ||
| const result = format('1234567-OR-AB'); | ||
|
|
||
| expect(result).toEqual('1234567-OR-AB'); | ||
| }); | ||
|
|
||
| it('format: 9876543.PT.2', () => { | ||
| const result = format('9876543.PT.2'); | ||
|
|
||
| expect(result).toEqual('9876543-PT-2'); | ||
| }); | ||
|
|
||
| // Valid cases | ||
| it('validate:1234567-SC', () => { | ||
| const result = validate('1234567-SC'); | ||
|
|
||
| expect(result.isValid && result.compact).toEqual('1234567SC'); | ||
| }); | ||
|
|
||
| it('validate:12345678-LP', () => { | ||
| const result = validate('12345678-LP'); | ||
|
|
||
| expect(result.isValid && result.compact).toEqual('12345678LP'); | ||
| }); | ||
|
|
||
| it('validate:1234567-OR-1', () => { | ||
| const result = validate('1234567-OR-1'); | ||
|
|
||
| expect(result.isValid && result.compact).toEqual('1234567OR1'); | ||
| }); | ||
|
|
||
| it('validate:9876543.CB.AB', () => { | ||
| const result = validate('9876543.CB.AB'); | ||
|
|
||
| expect(result.isValid && result.compact).toEqual('9876543CBAB'); | ||
| }); | ||
|
|
||
| it('validate:5555555-PT', () => { | ||
| const result = validate('5555555-PT'); | ||
|
|
||
| expect(result.isValid && result.compact).toEqual('5555555PT'); | ||
| }); | ||
|
|
||
| it('validate:12345678-CH-A', () => { | ||
| const result = validate('12345678-CH-A'); | ||
|
|
||
| expect(result.isValid && result.compact).toEqual('12345678CHA'); | ||
| }); | ||
|
|
||
| it('validate:7654321-TJ-99', () => { | ||
| const result = validate('7654321-TJ-99'); | ||
|
|
||
| expect(result.isValid && result.compact).toEqual('7654321TJ99'); | ||
| }); | ||
|
|
||
| it('validate:8888888-BE', () => { | ||
| const result = validate('8888888-BE'); | ||
|
|
||
| expect(result.isValid && result.compact).toEqual('8888888BE'); | ||
| }); | ||
|
|
||
| it('validate:11111111-PD-B', () => { | ||
| const result = validate('11111111-PD-B'); | ||
|
|
||
| expect(result.isValid && result.compact).toEqual('11111111PDB'); | ||
| }); | ||
|
|
||
| // Invalid length - too short | ||
| it('validate:123-SC', () => { | ||
| const result = validate('123-SC'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidLength); | ||
| }); | ||
|
|
||
| it('validate:123456-LP', () => { | ||
| const result = validate('123456-LP'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidLength); | ||
| }); | ||
|
|
||
| // Invalid length - too long | ||
| it('validate:123456789-OR', () => { | ||
| const result = validate('123456789-OR'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidLength); | ||
| }); | ||
|
|
||
| // Invalid format - extension too long (3 characters) | ||
| it('validate:1234567-SC-ABC', () => { | ||
| const result = validate('1234567-SC-ABC'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidFormat); | ||
| }); | ||
|
|
||
| // Invalid format - special characters | ||
| it('validate:1234567=SC', () => { | ||
| const result = validate('1234567=SC'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidFormat); | ||
| }); | ||
|
|
||
| // Invalid format - letters instead of numbers | ||
| it('validate:ABCDEFG-SC', () => { | ||
| const result = validate('ABCDEFG-SC'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidFormat); | ||
| }); | ||
|
|
||
| // Invalid format - special characters in extension | ||
| it('validate:1234567-SC-@#', () => { | ||
| const result = validate('1234567-SC-@#'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidFormat); | ||
| }); | ||
|
|
||
| // Invalid length - department code with numbers (total length check) | ||
| it('validate:9999999-12', () => { | ||
| const result = validate('9999999-12'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidLength); | ||
| }); | ||
|
|
||
| // Invalid length - department code mixed (total length check) | ||
| it('validate:1234567-1C', () => { | ||
| const result = validate('1234567-1C'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidLength); | ||
| }); | ||
|
|
||
| // Invalid department code | ||
| it('validate:1234567-XX', () => { | ||
| const result = validate('1234567-XX'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidComponent); | ||
| }); | ||
|
|
||
| it('validate:1234567-BR', () => { | ||
| const result = validate('1234567-BR'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidComponent); | ||
| }); | ||
|
|
||
| it('validate:12345678-ZZ', () => { | ||
| const result = validate('12345678-ZZ'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidComponent); | ||
| }); | ||
|
|
||
| it('validate:1234567-AA-1', () => { | ||
| const result = validate('1234567-AA-1'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidComponent); | ||
| }); | ||
|
|
||
| it('validate:9876543-QQ', () => { | ||
| const result = validate('9876543-QQ'); | ||
|
|
||
| expect(result.error).toBeInstanceOf(InvalidComponent); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,142 @@ | ||||||
| /** | ||||||
| * CI - Cédula de Identidad (Bolivia) | ||||||
| * | ||||||
| * The Bolivian CI is a national identity document. | ||||||
| * It consists of 7-8 digits followed by a department code and an optional extension. | ||||||
| * | ||||||
| * Format: XXXXXXX-DD-E or XXXXXXXX-DD-E | ||||||
| * Where: | ||||||
| * - X = digits (7 or 8) | ||||||
| * - DD = department code (2 letters) | ||||||
| * - E = extension (optional, 1-2 characters) | ||||||
| * | ||||||
| * Source: https://www.segip.gob.bo/ | ||||||
| */ | ||||||
|
|
||||||
| import * as exceptions from '../exceptions'; | ||||||
| import { strings } from '../util'; | ||||||
| import { Validator, ValidateReturn } from '../types'; | ||||||
|
|
||||||
| // Valid department codes for Bolivia | ||||||
| const VALID_DEPARTMENTS = [ | ||||||
| 'LP', // La Paz | ||||||
| 'OR', // Oruro | ||||||
| 'PT', // Potosí | ||||||
| 'CB', // Cochabamba | ||||||
| 'CH', // Chuquisaca | ||||||
| 'TJ', // Tarija | ||||||
| 'SC', // Santa Cruz | ||||||
| 'BE', // Beni | ||||||
| 'PD', // Pando | ||||||
| ]; | ||||||
|
|
||||||
| function clean(input: string): ReturnType<typeof strings.cleanUnicode> { | ||||||
| return strings.cleanUnicode(input, ' -.'); | ||||||
| } | ||||||
|
|
||||||
| const impl: Validator = { | ||||||
| name: 'Bolivian National Identity Card', | ||||||
| localName: 'Cédula de Identidad', | ||||||
| abbreviation: 'CI', | ||||||
|
|
||||||
| compact(input: string): string { | ||||||
| const [value, err] = clean(input); | ||||||
|
|
||||||
| if (err) { | ||||||
| throw err; | ||||||
| } | ||||||
|
|
||||||
| return value; | ||||||
| }, | ||||||
|
|
||||||
| format(input: string): string { | ||||||
| const [value] = clean(input); | ||||||
|
|
||||||
| // Format: XXXXXXX-DD or XXXXXXX-DD-E | ||||||
| const match = value.match(/^(\d{7,8})([A-Z]{2})(\w{0,2})$/); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
|
|
||||||
| if (!match) { | ||||||
| return value; | ||||||
| } | ||||||
|
|
||||||
| const [, number, dept, ext] = match; | ||||||
|
|
||||||
| if (ext) { | ||||||
| return `${number}-${dept}-${ext}`; | ||||||
| } | ||||||
|
|
||||||
| return `${number}-${dept}`; | ||||||
| }, | ||||||
|
|
||||||
| validate(input: string): ValidateReturn { | ||||||
| const [value, error] = clean(input); | ||||||
|
|
||||||
| if (error) { | ||||||
| return { isValid: false, error }; | ||||||
| } | ||||||
|
|
||||||
| // Basic format check: must contain only alphanumeric characters | ||||||
| if (!/^[A-Z0-9]+$/i.test(value)) { | ||||||
| return { isValid: false, error: new exceptions.InvalidFormat() }; | ||||||
| } | ||||||
|
|
||||||
| // More flexible initial match to extract parts | ||||||
| const basicMatch = value.match(/^(\d+)(.*)$/); | ||||||
|
|
||||||
| if (!basicMatch) { | ||||||
| return { isValid: false, error: new exceptions.InvalidFormat() }; | ||||||
| } | ||||||
|
|
||||||
| const [, numberPart, rest] = basicMatch; | ||||||
|
|
||||||
| // Validate number part length (7 or 8 digits) | ||||||
| if (numberPart.length < 7 || numberPart.length > 8) { | ||||||
| return { isValid: false, error: new exceptions.InvalidLength() }; | ||||||
| } | ||||||
|
|
||||||
| // Check if we have at least department code | ||||||
| if (rest.length < 2) { | ||||||
| return { isValid: false, error: new exceptions.InvalidLength() }; | ||||||
| } | ||||||
|
|
||||||
| // Extract department and extension | ||||||
| const department = rest.substring(0, 2).toUpperCase(); | ||||||
| const extension = rest.substring(2); | ||||||
|
|
||||||
| // Department must be letters only | ||||||
| if (!/^[A-Z]{2}$/.test(department)) { | ||||||
| return { isValid: false, error: new exceptions.InvalidFormat() }; | ||||||
| } | ||||||
|
|
||||||
| // Validate department code | ||||||
| if (!VALID_DEPARTMENTS.includes(department)) { | ||||||
| return { isValid: false, error: new exceptions.InvalidComponent() }; | ||||||
| } | ||||||
|
|
||||||
| // Validate extension if present (alphanumeric, max 2 chars) | ||||||
| if (extension.length > 2) { | ||||||
| return { isValid: false, error: new exceptions.InvalidFormat() }; | ||||||
| } | ||||||
|
|
||||||
| if (extension && !/^[A-Z0-9]+$/i.test(extension)) { | ||||||
| return { isValid: false, error: new exceptions.InvalidFormat() }; | ||||||
| } | ||||||
|
|
||||||
| // Final length check | ||||||
| const totalLength = | ||||||
| numberPart.length + department.length + extension.length; | ||||||
| if (totalLength < 9 || totalLength > 12) { | ||||||
| return { isValid: false, error: new exceptions.InvalidLength() }; | ||||||
| } | ||||||
|
|
||||||
| return { | ||||||
| isValid: true, | ||||||
| compact: value.toUpperCase(), | ||||||
| isIndividual: true, | ||||||
| isCompany: false, | ||||||
| }; | ||||||
| }, | ||||||
| }; | ||||||
|
|
||||||
| export const { name, localName, abbreviation, validate, format, compact } = | ||||||
| impl; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * as ci from './ci'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test case expects an
InvalidLengtherror for the input1234567-1C. However, the validation logic inci.tscorrectly identifies that '1C' is not a valid format for a department code (which must be two letters) and returns anInvalidFormaterror. The implementation's behavior seems more accurate here. Please consider updating this test to expectInvalidFormatto align with the validation logic.