Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
env:
fail-fast: true
- name: Code style (phpcs)
run: phpcs -q --report=checkstyle src/ tests/ | cs2pr
run: phpcs -q --report=checkstyle | cs2pr

php-cs-fixer:
name: Code style (php-cs-fixer)
Expand Down
6 changes: 3 additions & 3 deletions .phive/phars.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="php-cs-fixer" version="^3.6.0" installed="3.6.0" location="./tools/php-cs-fixer" copy="false"/>
<phar name="php-cs-fixer" version="^3.8.0" installed="3.8.0" location="./tools/php-cs-fixer" copy="false"/>
<phar name="phpcs" version="^3.6.2" installed="3.6.2" location="./tools/phpcs" copy="false"/>
<phar name="phpcbf" version="^3.6.2" installed="3.6.2" location="./tools/phpcbf" copy="false"/>
<phar name="phpstan" version="^1.4.6" installed="1.4.6" location="./tools/phpstan" copy="false"/>
<phar name="psalm" version="^4.21.0" installed="4.21.0" location="./tools/psalm" copy="false"/>
<phar name="phpstan" version="^1.6.3" installed="1.6.3" location="./tools/phpstan" copy="false"/>
<phar name="psalm" version="^4.22.0" installed="4.22.0" location="./tools/psalm" copy="false"/>
<phar name="infection" version="^0.23.0" installed="0.23.0" location="./tools/infection" copy="false"/>
</phive>
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@ Provee métodos para crear una cadena de caracteres que es una clave RFC:
- `RfcFaker::mexicanRfcMoral()` para persona moral (12 posiciones).
- `RfcFaker::mexicanRfc()` indistintamente una persona moral o física.

## Dígito verificador

Se puede obtener el dígito verificador calculado con el método `Rfc::calculateCheckSum()`,
así como conocer si el dígito verificador coincide con el método `Rfc::doesCheckSumMatch()`.

La además provee la clase `CheckSum` para realizar el cálculo del dígito verificador de un RFC.
Cabe mencionar que, si bien debería ser siempre coincidente, hay algunos casos donde esto
no se respeta (SAT, ¿todo bien?), por ejemplo, el caso de *Restaurantes TOKS* ha tenido
los RFC `RT0840921REA` (dígito `A`) y también `RT0840921RE4`.

Puede ver el procedimiento del dígito verificador en alguno de estos enlaces:

- <https://www.studocu.com/es-mx/document/universidad-del-valle-de-mexico/administracion/algoritmo-para-generar-el-rfc-con-homoclave-para-personas-fisicas-y-morales/12002840>
- <https://solucionfactible.com/sfic/capitulos/timbrado/rfc-digito-verificador.jsp>

## Desarrollo

Para entender esta librería en el ámbito de desarrollo (para extender o modificar), lee los siguientes documentos:
Expand Down
10 changes: 10 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ Utilizamos [Versionado Semántico 2.0.0](SEMVER.md).

Los cambios no liberados no requieren de una nueva versión y son incluidos en la rama principal.

## Versión 1.1.2

Se actualiza la clase `CheckSum` y se mejoran las pruebas unitarias sobre la misma.
Gracias a `@fitorec` por sus sugerencias en el [PR #14](https://github.com/phpcfdi/rfc/pull/14).

Se actualizan las versiones de las herramientas de desarrollo.

Al ejecutar el trabajo de integración continua en el trabajo `phpcs` se usan los directorios según
el archivo de configuración.

## Versión 1.1.1

Se actualiza la expresión regular para lectura de RFC con las recomendaciones de simplificación:
Expand Down
26 changes: 14 additions & 12 deletions src/CheckSum.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,25 @@ final class CheckSum
{
private const DICTIONARY = [0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, 8 => 8, 9 => 9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15, 'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23, '&' => 24, 'O' => 25, 'P' => 26, 'Q' => 27, 'R' => 28, 'S' => 29, 'T' => 30, 'U' => 31, 'V' => 32, 'W' => 33, 'X' => 34, 'Y' => 35, 'Z' => 36, ' ' => 37, '#' => 38];

private const DIGIT_OVERRIDE = [10 => 'A', 11 => '0'];

public function calculate(string $rfc): string
{
// 'Ñ' translated to '#' because 'Ñ' is multibyte 0xC3 0xB1
// 'Ñ' cambia a '#' porque 'Ñ' es multi-byte 0xC3 0xB1
$chars = str_split(str_replace('Ñ', '#', $rfc), 1);
array_pop($chars); // remove predefined checksum
$length = count($chars);
$sum = (11 === $length) ? 481 : 0; // 481 para morales, 0 para físicas
$j = $length + 1;
array_pop($chars); // remover el dígito predefinido

// Valor inicial de la suma: 481 para morales, 0 para físicas
$sum = (12 === $length) ? 481 : 0;
// suma de valores: Σ(Vi * (Pi + 1))
foreach ($chars as $i => $char) {
$sum += self::DICTIONARY[$char] * ($j - $i);
$sum += (self::DICTIONARY[$char] ?? 0) * ($length - $i);
}
$digit = strval(11 - $sum % 11);
if ('11' === $digit) {
$digit = '0';
} elseif ('10' === $digit) {
$digit = 'A';
}
return $digit;

// posibles valores: [1, 2, ..., 10, 11] porque $sum % 11 => int<0, 10>
$digit = 11 - $sum % 11;
// se retorna 10 => 0, 11 => A o el valor obtenido
return self::DIGIT_OVERRIDE[$digit] ?? strval($digit);
}
}
27 changes: 18 additions & 9 deletions tests/Unit/CheckSumTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,29 @@

final class CheckSumTest extends TestCase
{
public function testCheckSum(): void
/** @return array<string, array{string, string}> */
public function providerCheckSum(): array
{
$expected = 'A';
$rfc = 'COSC8001137NA';
return [
'física 0' => ['CAMA911215CJ0', '0'],
'física A' => ['COSC8001137NA', 'A'],
'física [1-9]' => ['SORC591116FJ6', '6'],

$checksum = new CheckSum();
$this->assertSame($expected, $checksum->calculate($rfc));
'moral A' => ['DIM8701081LA', 'A'],
'moral 0' => ['A&A050908GT0', '0'],
'moral [1-9]' => ['SAT970701NN3', '3'],

'multibyte' => ['AÑÑ801231JK0', '0'],

'empty rfc' => ['', '0'],
'invalid rfc' => ['$', '0'],
'invalid chars' => ['AAA010101$$$', '7'], // $ is managed as 0
];
}

public function testCheckSumWithMultiByte(): void
/** @dataProvider providerCheckSum */
public function testCheckSum(string $rfc, string $expected): void
{
$expected = '0';
$rfc = 'AÑÑ801231JK0';

$checksum = new CheckSum();
$this->assertSame($expected, $checksum->calculate($rfc));
}
Expand Down