Skip to content

Commit 29243c9

Browse files
authored
Merge pull request #17 from eclipxe13/retenciones-2.0
Retenciones 2.0 (versión 3.2.0)
2 parents f1e2b21 + d705b36 commit 29243c9

15 files changed

+355
-36
lines changed

README.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
[![Coverage Status][badge-coverage]][coverage]
99
[![Total Downloads][badge-downloads]][downloads]
1010

11-
> Genera expresiones de CFDI 4.4, CFDI 3.3, CFDI 3.2 y RET 1.0
11+
> Genera expresiones de CFDI 4.0, CFDI 3.3, CFDI 3.2, RET 1.0 y RET 2.0
1212
1313
:us: The documentation of this project is in spanish as this is the natural language for intended audience.
1414

1515
:mexico: La documentación del proyecto está en español porque ese es el lenguaje principal de los usuarios.
1616

17-
Esta librería contiene objetos de ayuda para crear expresiones de CFDI 3.2, CFDI 3.3, CFDI 3.4 y RET 1.0
17+
Esta librería contiene objetos de ayuda para crear expresiones de CFDI 3.2, CFDI 3.3, CFDI 4.0, RET 1.0 y RET 2.0
1818
de acuerdo a la información técnica del SAT en el Anexo 20.
1919

2020
Estas expresiones se utilizan principalmente para dos motivos:
@@ -42,6 +42,12 @@ Ejemplo de expresión para RET 1.0:
4242
?&re=XAXX010101000&nr=12345678901234567890%tt=1234567890.123456&id=ad662d33-6934-459c-a128-BDf0393f0f44
4343
```
4444

45+
Ejemplo de expresión para RET 2.0:
46+
47+
```text
48+
https://prodretencionverificacion.clouda.sat.gob.mx/?id=AAAAAAAA-BBBB-CCCC-DDDD-000000000000&re=Ñ&A010101AAA&nr=0000000000000000000X&tt=123456.78&fe=qsIe6w==
49+
```
50+
4551
## Instalación
4652

4753
Usa [composer](https://getcomposer.org/)

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "phpcfdi/cfdi-expresiones",
3-
"description": "Genera expresiones de CFDI 4.4, CFDI 3.3, CFDI 3.2 y RET 1.0",
3+
"description": "Genera expresiones de CFDI 4.0, CFDI 3.3, CFDI 3.2, RET 1.0 y RET 2.0",
44
"keywords": ["phpcfdi", "sat", "cfdi"],
55
"homepage": "https://github.com/phpcfdi/cfdi-expresiones",
66
"license": "MIT",

docs/CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ versión, aunque sí su incorporación en la rama principal de trabajo, generalm
1111

1212
## Listado de cambios
1313

14+
### Version 3.2.0 2022-06-27
15+
16+
#### Soporte de RET 2.0
17+
18+
Se agrega el soporte de RET 2.0 con fundamento en el Anexo 20.
19+
20+
#### Documentación
21+
22+
Se corrigen los textos que tenían una versión incorrecta y se refieren a CFDI 4.0.
23+
1424
### Version 3.1.0 2022-06-16
1525

1626
#### Soporte de CFDI 4.0

src/DiscoverExtractor.php

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PhpCfdi\CfdiExpresiones\Extractors\Comprobante33;
1111
use PhpCfdi\CfdiExpresiones\Extractors\Comprobante40;
1212
use PhpCfdi\CfdiExpresiones\Extractors\Retenciones10;
13+
use PhpCfdi\CfdiExpresiones\Extractors\Retenciones20;
1314

1415
class DiscoverExtractor implements ExpressionExtractorInterface
1516
{
@@ -31,6 +32,7 @@ public function defaultExtractors(): array
3132
new Comprobante40(),
3233
new Comprobante33(),
3334
new Comprobante32(),
35+
new Retenciones20(),
3436
new Retenciones10(),
3537
];
3638
}

src/Extractors/Retenciones20.php

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpCfdi\CfdiExpresiones\Extractors;
6+
7+
use DOMDocument;
8+
use PhpCfdi\CfdiExpresiones\Exceptions\AttributeNotFoundException;
9+
use PhpCfdi\CfdiExpresiones\Exceptions\UnmatchedDocumentException;
10+
use PhpCfdi\CfdiExpresiones\ExpressionExtractorInterface;
11+
use PhpCfdi\CfdiExpresiones\Internal\DOMHelper;
12+
use PhpCfdi\CfdiExpresiones\Internal\MatchDetector;
13+
14+
class Retenciones20 implements ExpressionExtractorInterface
15+
{
16+
use Standards\FormatForeignTaxId20;
17+
use Standards\FormatRfcXml;
18+
use Standards\FormatTotal18x6;
19+
use Standards\FormatSelloLast8;
20+
21+
/** @var MatchDetector */
22+
private $matchDetector;
23+
24+
public function __construct()
25+
{
26+
$this->matchDetector = new MatchDetector(
27+
'http://www.sat.gob.mx/esquemas/retencionpago/2',
28+
'retenciones:Retenciones',
29+
'Version',
30+
'2.0'
31+
);
32+
}
33+
34+
public function uniqueName(): string
35+
{
36+
return 'RET20';
37+
}
38+
39+
public function matches(DOMDocument $document): bool
40+
{
41+
return $this->matchDetector->matches($document);
42+
}
43+
44+
public function obtain(DOMDocument $document): array
45+
{
46+
if (! $this->matches($document)) {
47+
throw new UnmatchedDocumentException('The document is not a RET 2.0');
48+
}
49+
50+
$helper = new DOMHelper($document);
51+
52+
$uuid = $helper->getAttribute(
53+
'retenciones:Retenciones',
54+
'retenciones:Complemento',
55+
'tfd:TimbreFiscalDigital',
56+
'UUID'
57+
);
58+
59+
$rfcEmisor = $helper->getAttribute('retenciones:Retenciones', 'retenciones:Emisor', 'RfcE');
60+
[$rfcReceptorKey, $rfcReceptor] = $this->obtainReceptorValues($helper);
61+
$total = $helper->getAttribute('retenciones:Retenciones', 'retenciones:Totales', 'MontoTotOperacion');
62+
$sello = $helper->getAttribute('retenciones:Retenciones', 'Sello');
63+
64+
return [
65+
'id' => $uuid,
66+
're' => $rfcEmisor,
67+
$rfcReceptorKey => $rfcReceptor,
68+
'tt' => $total,
69+
'fe' => $sello,
70+
];
71+
}
72+
73+
public function extract(DOMDocument $document): string
74+
{
75+
return $this->format($this->obtain($document));
76+
}
77+
78+
public function format(array $values): string
79+
{
80+
$receptorKey = 'rr';
81+
if (isset($values['rr'])) {
82+
$values[$receptorKey] = $this->formatRfc($values[$receptorKey]);
83+
}
84+
if (isset($values['nr'])) {
85+
$receptorKey = 'nr';
86+
$values['nr'] = $this->formatForeignTaxId($values['nr']);
87+
}
88+
return 'https://prodretencionverificacion.clouda.sat.gob.mx/?'
89+
. implode('&', [
90+
'id=' . ($values['id'] ?? ''),
91+
're=' . $this->formatRfc($values['re'] ?? ''),
92+
$receptorKey . '=' . ($values[$receptorKey] ?? ''),
93+
'tt=' . $this->formatTotal($values['tt'] ?? ''),
94+
'fe=' . $this->formatSello($values['fe'] ?? ''),
95+
]);
96+
}
97+
98+
/** @return array{string, string} */
99+
private function obtainReceptorValues(DOMHelper $helper): array
100+
{
101+
$rfcReceptorKey = 'rr';
102+
$rfcReceptor = $helper->findAttribute(
103+
'retenciones:Retenciones',
104+
'retenciones:Receptor',
105+
'retenciones:Nacional',
106+
'RfcR'
107+
);
108+
109+
if (null === $rfcReceptor) {
110+
$rfcReceptorKey = 'nr';
111+
$rfcReceptor = $helper->findAttribute(
112+
'retenciones:Retenciones',
113+
'retenciones:Receptor',
114+
'retenciones:Extranjero',
115+
'NumRegIdTribR'
116+
);
117+
}
118+
119+
if (null === $rfcReceptor) {
120+
throw new AttributeNotFoundException('RET 2.0 receiver tax id cannot be found');
121+
}
122+
123+
return [$rfcReceptorKey, $rfcReceptor];
124+
}
125+
}

tests/Unit/DOMDocumentsTestCase.php

+14
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ public function documentCfdi32(): DOMDocument
3030
return $document;
3131
}
3232

33+
public function documentRet20Mexican(): DOMDocument
34+
{
35+
$document = new DOMDocument();
36+
$document->load($this->filePath('ret20-mexican-fake.xml'));
37+
return $document;
38+
}
39+
40+
public function documentRet20Foreign(): DOMDocument
41+
{
42+
$document = new DOMDocument();
43+
$document->load($this->filePath('ret20-foreign-fake.xml'));
44+
return $document;
45+
}
46+
3347
public function documentRet10Mexican(): DOMDocument
3448
{
3549
$document = new DOMDocument();

tests/Unit/DiscoverExtractorTest.php

+27-23
Original file line numberDiff line numberDiff line change
@@ -12,48 +12,50 @@
1212
use PhpCfdi\CfdiExpresiones\Extractors\Comprobante33;
1313
use PhpCfdi\CfdiExpresiones\Extractors\Comprobante40;
1414
use PhpCfdi\CfdiExpresiones\Extractors\Retenciones10;
15+
use PhpCfdi\CfdiExpresiones\Extractors\Retenciones20;
1516

1617
class DiscoverExtractorTest extends DOMDocumentsTestCase
1718
{
1819
public function testUniqueName(): void
1920
{
20-
$extrator = new DiscoverExtractor();
21-
$this->assertSame('discover', $extrator->uniqueName());
21+
$extractor = new DiscoverExtractor();
22+
$this->assertSame('discover', $extractor->uniqueName());
2223
}
2324

24-
public function testGenericExtratorUsesDefaults(): void
25+
public function testGenericExtractorUsesDefaults(): void
2526
{
26-
$extrator = new DiscoverExtractor();
27-
$currentExpressionExtractors = $extrator->currentExpressionExtractors();
27+
$extractor = new DiscoverExtractor();
28+
$currentExpressionExtractors = $extractor->currentExpressionExtractors();
2829
$this->assertGreaterThan(0, count($currentExpressionExtractors));
2930
$this->assertContainsOnlyInstancesOf(ExpressionExtractorInterface::class, $currentExpressionExtractors);
3031
}
3132

3233
public function testDefaultExtractorContainsKnownClasses(): void
3334
{
34-
$extrator = new DiscoverExtractor();
35-
$extractorClasses = array_map('get_class', $extrator->defaultExtractors());
35+
$extractor = new DiscoverExtractor();
36+
$extractorClasses = array_map('get_class', $extractor->defaultExtractors());
3637
$this->assertContains(Retenciones10::class, $extractorClasses);
38+
$this->assertContains(Retenciones20::class, $extractorClasses);
3739
$this->assertContains(Comprobante32::class, $extractorClasses);
3840
$this->assertContains(Comprobante33::class, $extractorClasses);
3941
$this->assertContains(Comprobante40::class, $extractorClasses);
40-
$this->assertCount(4, $extractorClasses);
42+
$this->assertCount(5, $extractorClasses);
4143
}
4244

4345
public function testDontMatchUsingEmptyDocument(): void
4446
{
4547
$document = new DOMDocument();
46-
$extrator = new DiscoverExtractor();
47-
$this->assertFalse($extrator->matches($document));
48+
$extractor = new DiscoverExtractor();
49+
$this->assertFalse($extractor->matches($document));
4850
}
4951

5052
public function testThrowExceptionOnUnmatchedDocument(): void
5153
{
5254
$document = new DOMDocument();
53-
$extrator = new DiscoverExtractor();
55+
$extractor = new DiscoverExtractor();
5456
$this->expectException(UnmatchedDocumentException::class);
5557
$this->expectExceptionMessage('Cannot discover any DiscoverExtractor that matches with document');
56-
$extrator->extract($document);
58+
$extractor->extract($document);
5759
}
5860

5961
/** @return array<string, array{DOMDocument, string}> */
@@ -63,6 +65,8 @@ public function providerExpressionOnValidDocuments(): array
6365
'Cfdi40' => [$this->documentCfdi40(), 'CFDI40'],
6466
'Cfdi33' => [$this->documentCfdi33(), 'CFDI33'],
6567
'Cfdi32' => [$this->documentCfdi32(), 'CFDI32'],
68+
'Ret20Mexican' => [$this->documentRet20Mexican(), 'RET20'],
69+
'Ret20Foreign' => [$this->documentRet20Foreign(), 'RET20'],
6670
'Ret10Mexican' => [$this->documentRet10Mexican(), 'RET10'],
6771
'Ret10Foreign' => [$this->documentRet10Foreign(), 'RET10'],
6872
];
@@ -74,9 +78,9 @@ public function providerExpressionOnValidDocuments(): array
7478
*/
7579
public function testExpressionOnValidDocuments(DOMDocument $document): void
7680
{
77-
$extrator = new DiscoverExtractor();
78-
$this->assertTrue($extrator->matches($document));
79-
$this->assertNotEmpty($extrator->extract($document));
81+
$extractor = new DiscoverExtractor();
82+
$this->assertTrue($extractor->matches($document));
83+
$this->assertNotEmpty($extractor->extract($document));
8084
}
8185

8286
/**
@@ -86,24 +90,24 @@ public function testExpressionOnValidDocuments(DOMDocument $document): void
8690
*/
8791
public function testExtractProducesTheSameResultsAsObtainAndFormat(DOMDocument $document, string $type): void
8892
{
89-
$extrator = new DiscoverExtractor();
90-
$values = $extrator->obtain($document);
91-
$expression = $extrator->format($values, $type);
92-
$expectedExpression = $extrator->extract($document);
93+
$extractor = new DiscoverExtractor();
94+
$values = $extractor->obtain($document);
95+
$expression = $extractor->format($values, $type);
96+
$expectedExpression = $extractor->extract($document);
9397
$this->assertSame($expression, $expectedExpression);
9498
}
9599

96100
public function testFormatUsingNoType(): void
97101
{
98-
$extrator = new DiscoverExtractor();
102+
$extractor = new DiscoverExtractor();
99103
$this->expectException(UnmatchedDocumentException::class);
100104
$this->expectExceptionMessage('DiscoverExtractor requires type key with an extractor identifier');
101-
$extrator->format([]);
105+
$extractor->format([]);
102106
}
103107

104108
public function testFormatUsingCfdi33(): void
105109
{
106-
$extrator = new DiscoverExtractor();
107-
$this->assertNotEmpty($extrator->format([], 'CFDI33'));
110+
$extractor = new DiscoverExtractor();
111+
$this->assertNotEmpty($extractor->format([], 'CFDI33'));
108112
}
109113
}

tests/Unit/Extractors/Comprobante32Test.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ class Comprobante32Test extends DOMDocumentsTestCase
1313
{
1414
public function testUniqueName(): void
1515
{
16-
$extrator = new Comprobante32();
17-
$this->assertSame('CFDI32', $extrator->uniqueName());
16+
$extractor = new Comprobante32();
17+
$this->assertSame('CFDI32', $extractor->uniqueName());
1818
}
1919

2020
public function testMatchesCfdi32(): void

tests/Unit/Extractors/Comprobante33Test.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ class Comprobante33Test extends DOMDocumentsTestCase
1313
{
1414
public function testUniqueName(): void
1515
{
16-
$extrator = new Comprobante33();
17-
$this->assertSame('CFDI33', $extrator->uniqueName());
16+
$extractor = new Comprobante33();
17+
$this->assertSame('CFDI33', $extractor->uniqueName());
1818
}
1919

2020
public function testMatchesCfdi33(): void

tests/Unit/Extractors/Comprobante40Test.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ class Comprobante40Test extends DOMDocumentsTestCase
1313
{
1414
public function testUniqueName(): void
1515
{
16-
$extrator = new Comprobante40();
17-
$this->assertSame('CFDI40', $extrator->uniqueName());
16+
$extractor = new Comprobante40();
17+
$this->assertSame('CFDI40', $extractor->uniqueName());
1818
}
1919

2020
public function testMatchesCfdi40(): void

tests/Unit/Extractors/Retenciones10Test.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ class Retenciones10Test extends DOMDocumentsTestCase
1414
{
1515
public function testUniqueName(): void
1616
{
17-
$extrator = new Retenciones10();
18-
$this->assertSame('RET10', $extrator->uniqueName());
17+
$extractor = new Retenciones10();
18+
$this->assertSame('RET10', $extractor->uniqueName());
1919
}
2020

2121
public function testMatchesRetenciones10(): void
@@ -47,8 +47,8 @@ public function testExtractRetenciones10Mexican(): void
4747
public function providerCfdiDifferentVersions(): array
4848
{
4949
return [
50-
'CFDI 3.3' => [$this->documentCfdi33()],
51-
'CFDI 4.0' => [$this->documentCfdi40()],
50+
'RET 2.0 Mexican' => [$this->documentRet20Mexican()],
51+
'RET 2.0 Foreign' => [$this->documentRet20Foreign()],
5252
];
5353
}
5454

0 commit comments

Comments
 (0)