Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ How you can help! This library currently support about half the countries in the
| Bulgaria | BG | EGN | Person | ЕГН, Единен граждански номер, Bulgarian personal identity codes |
| Bulgaria | BG | PNF | Person | PNF (ЛНЧ, Личен номер на чужденец, Bulgarian number of a foreigner). |
| Bulgaria | BG | VAT | Company | Идентификационен номер по ДДС, Bulgarian VAT number |
| Bolivia | BO | CI | Person | Person Identifier (Cédula de Identidad) |
| Brazil | BR | CPF | Person | Brazilian identity number (Cadastro de Pessoas Físicas) |
| Brazil | BR | CNPJ | Company | Brazilian company number (Cadastro Nacional da Pessoa Jurídica) |
| Belarus | BY | UNP | Person/Company | Учетный номер плательщика, the Belarus VAT number |
Expand Down
183 changes: 183 additions & 0 deletions src/bo/ci.spec.ts
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);
Comment on lines +147 to +150

Choose a reason for hiding this comment

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

medium

This test case expects an InvalidLength error for the input 1234567-1C. However, the validation logic in ci.ts correctly identifies that '1C' is not a valid format for a department code (which must be two letters) and returns an InvalidFormat error. The implementation's behavior seems more accurate here. Please consider updating this test to expect InvalidFormat to align with the validation logic.

});

// 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);
});
});
142 changes: 142 additions & 0 deletions src/bo/ci.ts
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})$/);

Choose a reason for hiding this comment

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

medium

The \w character class in the regex is too permissive as it includes the underscore character (_), which is not a valid character for the extension part according to the validate function. To ensure consistency between format and validate, it's better to use a more specific character set.

Suggested change
const match = value.match(/^(\d{7,8})([A-Z]{2})(\w{0,2})$/);
const match = value.match(/^(\d{7,8})([A-Z]{2})([A-Z0-9]{0,2})$/);


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;
1 change: 1 addition & 0 deletions src/bo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as ci from './ci';
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as AZ from './az';
import * as BA from './ba';
import * as BE from './be';
import * as BG from './bg';
import * as BO from './bo';
import * as BR from './br';
import * as BY from './by';
import * as BZ from './bz';
Expand Down Expand Up @@ -103,6 +104,7 @@ export const stdnum: Record<string, Record<string, Validator>> = {
BA,
BE,
BG,
BO,
BR,
BY,
BZ,
Expand Down