-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #88 from koblas/tn_mf
fix: added Tunisa MF number
- Loading branch information
Showing
5 changed files
with
210 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
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 @@ | ||
export * as mf from './mf'; |
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,37 @@ | ||
import { validate, format } from './mf'; | ||
import { InvalidComponent, InvalidFormat } from '../exceptions'; | ||
|
||
describe('tn/mf', () => { | ||
it('format:1496298 T P N 000', () => { | ||
const result = format('1496298 T P N 000'); | ||
|
||
expect(result).toEqual('1496298/T/P/N/000'); | ||
}); | ||
|
||
it('fvalidate:121J', () => { | ||
const result = validate('121J'); | ||
|
||
expect(result.isValid && result.compact).toEqual('0000121J'); | ||
}); | ||
|
||
test.each(['1234567/M/A/E/001', '1282182 W', '121J'])( | ||
'validate:%s', | ||
value => { | ||
const result = validate(value); | ||
|
||
expect(result.isValid).toEqual(true); | ||
}, | ||
); | ||
|
||
it('validate:aaaaaaa/M/A/E/abc', () => { | ||
const result = validate('aaaaaaa/M/A/E/abc'); | ||
|
||
expect(result.error).toBeInstanceOf(InvalidFormat); | ||
}); | ||
|
||
it('validate:1234567/M/A/X/000', () => { | ||
const result = validate('1234567/M/A/X/000'); | ||
|
||
expect(result.error).toBeInstanceOf(InvalidComponent); | ||
}); | ||
}); |
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,169 @@ | ||
/** | ||
* MF (Matricule Fiscal, Tunisia tax number). | ||
* | ||
* The MF consists of 4 parts: the "identifiant fiscal", the "code TVA", the "code | ||
* catégorie" and the "numéro d'etablissement secondaire". | ||
* | ||
* The "identifiant fiscal" consists of 2 parts: the "identifiant unique" and the | ||
* "clef de contrôle". The "identifiant unique" is composed of 7 digits. The "clef | ||
* de contrôle" is a letter, excluding "I", "O" and "U" because of their | ||
* similarity to "1", "0" and "4". | ||
* | ||
* The "code TVA" is a letter that tells which VAT regime is being used. The valid | ||
* values are "A", "P", "B", "D" and "N". | ||
* | ||
* The "code catégorie" is a letter that tells the category the contributor | ||
* belongs to. The valid values are "M", "P", "C", "N" and "E". | ||
* | ||
* The "numéro d'etablissement secondaire" consists of 3 digits. It is usually | ||
* "000", but it can be "001", "002"... depending on the branches. If it is not | ||
* "000" then "code catégorie" must be "E". | ||
* | ||
* Source | ||
* https://futurexpert.tn/2019/10/22/structure-et-utilite-du-matricule-fiscal/ | ||
* https://www.registre-entreprises.tn/ | ||
* | ||
* VAT | ||
*/ | ||
|
||
import * as exceptions from '../exceptions'; | ||
import { strings } from '../util'; | ||
import { Validator, ValidateReturn } from '../types'; | ||
|
||
function clean(input: string): ReturnType<typeof strings.cleanUnicode> { | ||
return strings.cleanUnicode(input, ' /.-'); | ||
} | ||
|
||
const validRe = /^(\d+)(.*)$/; | ||
|
||
function compactImpl(input: string): ReturnType<typeof strings.cleanUnicode> { | ||
const [value, err] = clean(input); | ||
|
||
if (err) { | ||
return ['', err]; | ||
} | ||
|
||
const match = value.match(validRe); | ||
|
||
if (match && match.length === 3) { | ||
return [match[1].padStart(7, '0') + match[2], null]; | ||
} | ||
|
||
return [value, null]; | ||
} | ||
|
||
const VALID_CONTROL_KEYS = [ | ||
'A', | ||
'B', | ||
'C', | ||
'D', | ||
'E', | ||
'F', | ||
'G', | ||
'H', | ||
'J', | ||
'K', | ||
'L', | ||
'M', | ||
'N', | ||
'P', | ||
'Q', | ||
'R', | ||
'S', | ||
'T', | ||
'V', | ||
'W', | ||
'X', | ||
'Y', | ||
'Z', | ||
]; | ||
const VALID_TVA_CODES = ['A', 'P', 'B', 'D', 'N']; | ||
const VALID_CATEGORY_CODES = ['M', 'P', 'C', 'N', 'E']; | ||
|
||
const impl: Validator = { | ||
name: 'Tunisia tax number', | ||
localName: 'Matricule Fiscal', | ||
abbreviation: 'MF', | ||
|
||
compact(input: string): string { | ||
const [value, error] = compactImpl(input); | ||
|
||
if (error) { | ||
throw error; | ||
} | ||
|
||
return value; | ||
}, | ||
|
||
format(input: string): string { | ||
const [value, error] = compactImpl(input); | ||
|
||
if (error) { | ||
return input; | ||
} | ||
|
||
const [front, key, tva, category, rest] = strings.splitAt( | ||
value, | ||
7, | ||
8, | ||
9, | ||
10, | ||
); | ||
|
||
if (value.length === 8) { | ||
return `${front}/${key}`; | ||
} | ||
|
||
return `${front}/${key}/${tva}/${category}/${rest}`; | ||
}, | ||
|
||
validate(input: string): ValidateReturn { | ||
const [value, error] = compactImpl(input); | ||
|
||
if (error) { | ||
return { isValid: false, error }; | ||
} | ||
if (value.length !== 8 && value.length !== 13) { | ||
return { isValid: false, error: new exceptions.InvalidLength() }; | ||
} | ||
|
||
const [front, key, tva, category, rest] = strings.splitAt( | ||
value, | ||
7, | ||
8, | ||
9, | ||
10, | ||
); | ||
|
||
if (!strings.isdigits(front)) { | ||
return { isValid: false, error: new exceptions.InvalidFormat() }; | ||
} | ||
if (!VALID_CONTROL_KEYS.includes(key)) { | ||
return { isValid: false, error: new exceptions.InvalidComponent() }; | ||
} | ||
if (value.length !== 8) { | ||
if (!VALID_TVA_CODES.includes(tva)) { | ||
return { isValid: false, error: new exceptions.InvalidComponent() }; | ||
} | ||
if (!VALID_CATEGORY_CODES.includes(category)) { | ||
return { isValid: false, error: new exceptions.InvalidComponent() }; | ||
} | ||
if (!strings.isdigits(rest)) { | ||
return { isValid: false, error: new exceptions.InvalidComponent() }; | ||
} | ||
if (rest !== '000' && category !== 'E') { | ||
return { isValid: false, error: new exceptions.InvalidComponent() }; | ||
} | ||
} | ||
|
||
return { | ||
isValid: true, | ||
compact: value, | ||
isIndividual: false, | ||
isCompany: false, | ||
}; | ||
}, | ||
}; | ||
|
||
export const { name, localName, abbreviation, validate, format, compact } = | ||
impl; |