diff --git a/.gitattributes b/.gitattributes
index b7335593..d8a38b83 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,11 +4,11 @@
# Do not put this files on a distribution package (by .gitignore)
/vendor export-ignore
/build export-ignore
-/wiki export-ignore
/composer.lock export-ignore
+/node_modules export-ignore
+/package-lock.json export-ignore
# Do not put this files on a distribution package
-/docs/ export-ignore
/tests/ export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
@@ -18,3 +18,6 @@
/.travis.yml export-ignore
/phpcs.xml.dist export-ignore
/phpunit.xml.dist export-ignore
+/mkdocs.yml export-ignore
+/.markdownlint.json export-ignore
+
diff --git a/.gitignore b/.gitignore
index b7b30019..6ac272bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
# do not include this files on git
/vendor
/build
-/wiki
/composer.lock
+/node_modules
+/package-lock.json
+
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 00000000..fec68810
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,11 @@
+{
+ "line-length": false,
+ "no-inline-html": true,
+ "blank_lines": {
+ "maximum": 2
+ },
+ "no-trailing-punctuation": {
+ "punctuation": ".,;:"
+ }
+}
+
diff --git a/.travis.yml b/.travis.yml
index 8826f117..7f383ca5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,9 @@ php:
- 7.1
- 7.2
+before_install:
+ - nvm install 8
+
# This triggers builds to run on the new TravisCI infrastructure.
# See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/
sudo: false
@@ -20,6 +23,8 @@ env:
before_script:
- travis_retry composer install --no-interaction --prefer-dist
+ - travis_retry npm install
+ - travis_retry pip install --user mkdocs
- phpenv config-rm xdebug.ini
script:
@@ -34,6 +39,8 @@ script:
vendor/bin/phpunit
fi
- vendor/bin/phpstan.phar --no-progress analyse --level max src/ tests/
+ - node node_modules/markdownlint-cli/markdownlint.js *.md docs/
+ - ~/.local/bin/mkdocs build --strict --site-dir build/docs
after_script:
# upload test covegare to scrutinizer
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 2e19ed88..2992f411 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -22,8 +22,8 @@ include:
Examples of unacceptable behavior by participants include:
-* The use of sexualized language or imagery and unwelcome sexual attention or
-advances
+* The use of sexualized language or imagery and unwelcome sexual attention
+ or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
diff --git a/README.md b/README.md
index 0b82ed1f..1a536d9d 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ and is written in **spanish language** since is the language of the intented aud
**Atención: este proyecto se migrará a `phpcfdi/cfdiutils`, aun no hay fecha planeada**
-## Main features:
+## Main features
- Create CFDI version 3.3 based on a friendly extendable non-xml objects (`nodes`)
- Read CFDI version 3.2 and 3.3
@@ -30,18 +30,19 @@ and is written in **spanish language** since is the language of the intented aud
if not then the document was modified after signature.
- Validate the "Complemento de recepción de pagos"
- Helper objects to deal with:
- - `Cadena de origen` generation
- - Extract information from CER files or `Certificado` attribute
- - Calculate `Comprobante` sums based on the list of `Conceptos`
- - Retrieve the CFDI version information
+ - `Cadena de origen` generation
+ - Extract information from CER files or `Certificado` attribute
+ - Calculate `Comprobante` sums based on the list of `Conceptos`
+ - Retrieve the CFDI version information
- Keep a local copy of the tree of XSD and XSLT file dependences from SAT
- Keep a local copy of certificates to avoid download them each time
-- Check the SAT WebService to get the status of a CDI ('Activo', 'Cancelado' & 'No encontrado')
+- Check the SAT WebService to get the status of a CDI ('Activo', 'Cancelado' & 'No encontrado')
## Installation
Use [composer](https://getcomposer.org/), so please run
+
```shell
composer require eclipxe/cfdiutils
```
@@ -49,17 +50,12 @@ composer require eclipxe/cfdiutils
## Major versions
-- Version 1.x **deprecated** was deprecated time ago, that version didn't do much anyway
-- Version 2.x **current** has a lot of features and helper objects
-- Version 3.x **future** will be released with the following backward compatibility breaks:
- - Rename `\CfdiUtils\CadenaOrigen\CadenaOrigenBuilder` to `\CfdiUtils\CadenaOrigen\DOMBuilder`
- - Rename `\CfdiUtils\CadenaOrigen\DefaultLocations` to `\CfdiUtils\CadenaOrigen\CfdiDefaultLocations`
- - Remove `\CfdiUtils\CadenaOrigen\CadenaOrigenLocations`
- - Remove `\CfdiUtils\PemPrivateKey\PemPrivateKey::isOpened` to `\CfdiUtils\PemPrivateKey\PemPrivateKey::isOpen`
- - Remove `static` methods from `\CfdiUtils\CfdiVersion`, create an instance of the class
- - Remove `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, create an instance of the class
-
-It could be possible that version 3 will be migrated to a different project under the group [PhpCfdi]
+- Version 1.x **deprecated** was deprecated time ago, that version didn't do much anyway.
+- Version 2.x **current** has a lot of features and helper objects.
+- Version 3.x **future** will be released with backward compatibility breaks.
+ - See [docs/CHANGELOG.md](docs/CHANGELOG.md) for backward compatibility breaks.
+ - It may change to PHP 7.1
+ - It could be possible to migrate to phpcfdi/cfiutils under [phpCfdi](https://github.com/phpCfdi) organization
## PHP Support
diff --git a/composer.json b/composer.json
index 1ffdfc7f..eaf126de 100644
--- a/composer.json
+++ b/composer.json
@@ -57,7 +57,8 @@
"scripts": {
"build": [
"@fix-style",
- "@qa"
+ "@qa",
+ "@docs"
],
"check-style": [
"vendor/bin/php-cs-fixer fix --dry-run --verbose",
@@ -71,6 +72,10 @@
"vendor/bin/phpstan.phar analyse --no-progress --level max src/ tests/",
"@test"
],
+ "docs": [
+ "node_modules/markdownlint-cli/markdownlint.js *.md docs/",
+ "mkdocs build --strict --site-dir build/docs"
+ ],
"test": [
"vendor/bin/phplint",
"vendor/bin/phpunit"
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 47c63feb..501027ba 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,23 +1,51 @@
-# Backward compatibility breaks (not released yet), plan for version 3.0
+# CfdiUtils Changelog file
+
+## Backward compatibility breaks (not released yet), plan for version 3.0
+
- Remove deprecated classes:
- - `\CfdiUtils\CadenaOrigen\CadenaOrigenBuilder`
- - `\CfdiUtils\CadenaOrigen\DefaultLocations`
- - `\CfdiUtils\CadenaOrigen\CadenaOrigenLocations`
+ - `\CfdiUtils\CadenaOrigen\CadenaOrigenBuilder`
+ - `\CfdiUtils\CadenaOrigen\DefaultLocations`
+ - `\CfdiUtils\CadenaOrigen\CadenaOrigenLocations`
- Remove `\CfdiUtils\PemPrivateKey\PemPrivateKey::isOpened` to `\CfdiUtils\PemPrivateKey\PemPrivateKey::isOpen`
-- Remove `static` methods from `\CfdiUtils\CfdiVersion`, instead create an instance of the class
-- Remove `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, instead create an instance of the class
-
+- Remove `static` methods from `\CfdiUtils\CfdiVersion`, instead create an instance of the class
+- Remove `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, instead create an instance of the class
+- Remove `trigger_error` on `\CfdiUtils\Elements\Cfdi33\Comprobante::getCfdiRelacionados` when called with arguments.
+
+
+## Version 2.6.0 2018-07-06 - bugfixes, quickreader & welcome readthedocs & mkdocs
+
+- Create `QuickReader`, utility for easy navigate and extract information from a CFDI
+- Fix `Rfc` to don't throw an exception if checksum fails, SAT is not following its own standard
+- Add `Rfc::checkSum()` and `Rfc::checkSumMatch()` to know if the Rfc is following the checksum
+- Fix tests that expect Rfc checksum failure
+- Fix tests comments on `testDescuentoNotSetIfAllConceptosDoesNotHaveDescuento`
+- Fix `CfdiUtils\Elements\Cfdi33\Comprobante::getCfdiRelacionados` to don't receive a parameter.
+ - For backwards compatibility when it receive a parameter do the same thing but trigger a E_USER_NOTICE
+ - Create an special test case `ComprobanteGetCfdiRelacionadosTest` that catched the E_USER_NOTICE error
+- Add `CfdiUtils\Elements\Cfdi33\Comprobante::addCfdiRelacionados(array $attributes)`
+- Add `CfdiUtils\Elements\Cfdi33\Comprobante::multiCfdiRelacionado(array $attributes)`
+- Add tests to assert that `Comprobante/Impuestos/(Traslados/Traslado|Retenciones/Retencion)@Impuesto` is rounded
+- Minor fix at docblocks for packed arguments
+- Change all documentation to move from GitHub Wiki to ReadTheDocs
+ - More documentation pages & a lot of fixes
+ - Add `.markdownlint.json` to run with `markdownlint-cli` (`node`), add to travis build process
+ - Add `mkdocs.yml` to run with `mkdocs` (`python`), add to travis build process
+ - Fix markdown files according to markdownlint
+ - Add `composer docs` and append to general `composer build`
+
+
+## Version 2.5.1 2018-06-26
-# Version 2.5.1 2018-06-26
- Fix edge case for validations `SELLO03` and `SELLO04` on `SelloDigitalCertificado`.
In some cases, the authority does not require a certificate from emisor and uses one certificate for its own.
Therefore, the `Rfc` and `Nombre` of the `Emisor` does not match with the certificate. This produces a false error.
To avoid this issue, the validation of `Rfc` and `Nombre` matching with the certificate data must not perform when:
- * The `cfdi:Comprobante@NoCertificado` is the same as the `tfd:TimbreFiscalDigital@NoCertificadoSAT`
- * The "complemento" `registrofiscal:CFDIRegistroFiscal` exists
+ - The `cfdi:Comprobante@NoCertificado` is the same as the `tfd:TimbreFiscalDigital@NoCertificadoSAT`
+ - The "complemento" `registrofiscal:CFDIRegistroFiscal` exists
-# Version 2.5.0 2018-05-24
+## Version 2.5.0 2018-05-24
+
- Add validations for `http://www.sat.gob.mx/Pagos` at namespace `\CfdiUtils\Validate\Cfdi33\RecepcionPagos`
This is a big change that includes more than 50 validators that work in cascade.
It implements almost all of the validations from the SAT "Matriz de errores".
@@ -25,17 +53,18 @@
- Remove non existent validators discovery `Cfdi33/Timbre`
- Move logic of version discovery to a new class, change `CfdiVersion` and `TfdVersion` to implement this logic
- Deprecate `static` methods from `\CfdiUtils\CfdiVersion`, instead create an instance of the class
-- Deprecate `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, instead create an instance of the class
+- Deprecate `static` methods from `\CfdiUtils\TimbreFiscalDigital\TfdVersion`, instead create an instance of the class
- Fix deprecation notices existent docblocks
- Update deprecation notice to README
- Replace TODO with a more explained version
-# Version 2.4.6 2018-05-24
+## Version 2.4.6 2018-05-24
+
- Fix validation of TIPOCOMP06, it was not checking correctly.
- Fix bug in validators that does not respect when the resolver does not have local path:
- - `CfdiUtils\Validate\Cfdi33\Standard\TimbreFiscalDigitalSello`
- - `CfdiUtils\Validate\Cfdi33\Xml\XmlFollowSchema`
+ - `CfdiUtils\Validate\Cfdi33\Standard\TimbreFiscalDigitalSello`
+ - `CfdiUtils\Validate\Cfdi33\Xml\XmlFollowSchema`
- Fix bug when removing a `schemaLocation` attribute in `CfdiUtils\Cleaner\Cleaner`
- Refactor `CfdiUtils\ConsultaCfdiSat\WebService::request` and move the SOAP call
to a protected method, this allow better testing of the class by mocking the call
@@ -49,14 +78,15 @@
- Remove `CfdiUtils\Elements\Pagos10\Pago::multiImpuestos`,
it should never exists and must not have any use case.
- Improve testing on:
- - `CfdiUtils\Elements\Pagos10\Pagos`
- - `CfdiUtils\Validate\Cfdi33\Standard\ConceptoImpuestos`
+ - `CfdiUtils\Elements\Pagos10\Pagos`
+ - `CfdiUtils\Validate\Cfdi33\Standard\ConceptoImpuestos`
- Improve docblocks and fix typos in several files
- Add new parameter to development script `tests/validate.php`:
`--no-cache` that tell resolver to not use local cache.
- Improve travis disabling xdebug always and only use it in phpunit code coverage
-# Version 2.4.5 2018-05-12
+## Version 2.4.5 2018-05-12
+
- Fix: change xml namespace prefix `pagos10` to `pago10`
- Refactor `CfdiUtils\Certificado\SerialNumber::baseConvert`
- Add `CfdiUtils\Certificado\SerialNumber::asDecimal()`
@@ -68,19 +98,21 @@
- Add util `\CfdiUtils\Utils\Rfc`, help to work with strict RFC validations
- Add `\CfdiUtils\Validate\Cfdi33\Standard\ReceptorRfc` to validate the RFC of the CFDI receiver
- Add `\CfdiUtils\Validate\Cfdi33\Standard\EmisorRfc` to validate the RFC of the CFDI emitter
- - Fix `CfdiUtilsTests\CfdiValidator33Test::testValidateWithCorrectData` since used RFC is not valid
- - Fix `CfdiUtilsTests\CreateComprobanteCaseTest::testCreateCfdiUsingComprobanteElement` since used RFC is not valid
+ - Fix `CfdiUtilsTests\CfdiValidator33Test::testValidateWithCorrectData` since used RFC is not valid
+ - Fix `CfdiUtilsTests\CreateComprobanteCaseTest::testCreateCfdiUsingComprobanteElement` since used RFC is not valid
- Add docblocks to `CfdiUtils\Cfdi`
- Building:
- - Add .phplint.yml to export-ignore (standard line)
- - Travis-CI: Declare `FULL_BUILD_PHP_VERSION` for easy understanding
+ - Add .phplint.yml to export-ignore (standard line)
+ - Travis-CI: Declare `FULL_BUILD_PHP_VERSION` for easy understanding
- Add more dependences: `ext-dom`, `ext-xsl`, `ext-simplexml`, `ext-mbstring`
-# Version 2.4.4 2018-05-11
+## Version 2.4.4 2018-05-11
+
- FIX: Unable to load a PEM file using filename on windows (Closes #33)
- Do not use bcmath function to convert from decimal to hexadecimal the serial number of a certificate
-# Version 2.4.3 2018-04-26
+## Version 2.4.3 2018-04-26
+
- FIX: The attribute `cfdi:Comprobante@Descuento` must not be deleted if any attribute
`cfdi:Comprobante/cfdi:Conceptos/cfdi:Concepto@Descuento` exists. (Closes: #50)
- FIX: When validating a CFDI, the validator `CfdiUtils\Validate\Cfdi33\Standard\SelloDigitalCertificado`
@@ -88,7 +120,8 @@
- Add a **development** script `tests/validate.php` to validate existing files.
WARNING: This can change at any time! Do not depend on this file or its results!
-# Version 2.4.2 2018-04-23
+## Version 2.4.2 2018-04-23
+
- Fix `\CfdiUtils\Nodes\XmlNodeExporter::export`, it was not appending root element to xml document.
- Allow `\CfdiUtils\Nodes\XmlNodeUtils::nodeToXmlString` to export including xml header ``.
Default behavior is to not include xml header, it remains unchanged.
@@ -96,16 +129,17 @@
- By default, `\DOMDocument` objects are created with version 1.0 and encoding UTF-8.
- Add tests to validate previous changes.
-# Version 2.4.1 2018-04-11
+## Version 2.4.1 2018-04-11
+
- Fix `\CfdiUtils\Certificado\Certificado` when reading serial number.
- Use `serialNumberHex` if available, if not then use `serialNumber` and convert to hex.
- Move serial number string conversion to class `\CfdiUtils\Certificado\SerialNumber`.
This class is not for public use but for use inside `Certificate`.
-# Version 2.4.0 2018-02-08
+## Version 2.4.0 2018-02-08
+
- Add the feature to order the children nodes for a `CfdiUtils\Nodes\Nodes` object.
- This feature is used in the namespace `CfdiUtils\Elements` to set the correct order of the
- children nodes without worry about the creation order.
+ This feature is used in the namespace `CfdiUtils\Elements` to set the correct order of the children nodes without worry about the creation order.
- Add `CfdiUtils\Elements\Addenda` helper class.
- Add `CfdiUtils\Elements\Pagos10` namespace for "complemento de pagos 1.0".
- Add `CfdiUtils\Cleaner\Cleaner` utility class that allows to remove `cfdi:Addenda`,
@@ -113,41 +147,46 @@
- Build: The project no longer depends on `jakub-onderka/php-parallel-lint`,
now uses `overtrue/phplint` that does the same task but stores a cache.
-# Version 2.3.2 2018-01-29
+## Version 2.3.2 2018-01-29
+
- Fix how total is formatted in the expression of `\CfdiUtils\ConsultaCfdiSat\RequestParameters`
- - Version 3.2 was removing zero trailing decimals instead of using 6 fixed chars
- - Version 3.3 was not using 1 leading zero (for integers) and 1 trailing zero (for decimals)
+ - Version 3.2 was removing zero trailing decimals instead of using 6 fixed chars
+ - Version 3.3 was not using 1 leading zero (for integers) and 1 trailing zero (for decimals)
- On method `\CfdiUtils\Certificado\NodeCertificado::obtain()` change logic
and throw exception if temporary file cannot be created
-# Version 2.3.1 2018-01-25
+## Version 2.3.1 2018-01-25
+
- Add elements helpers `CfdiUtils\Elements\Tfd11\TimbreFiscalDigital` to work with "TimbreFiscalDigital"
-# Version 2.3.0 2018-01-25
+
+## Version 2.3.0 2018-01-25
+
- Add a client `\CfdiUtils\ConsultaCfdiSat\WebService` for the SAT WebService
- https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?singleWsdl
+ `https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?singleWsdl`
- Fix bug, must use `children()` method instead of `children` property.
Did not appears before because the variable using the property was always
a `Node` but other implementation of `NodeInterface` would cause this to break.
- Add a lot of fixes in docblocks to move `@param $var` to `@param type $var`.
- Add extensions requirements to composer.json: libxml, openssl & soap.
- Upgrade `phpstan/phpstan-shim` to version 0.9.1, the not-simple-to-see bug fixed
- in this version was found by `phpstan` - https://github.com/phpstan/phpstan
+ in this version was found by `phpstan` -
-# Version 2.2.0 2018-01-24
+## Version 2.2.0 2018-01-24
+
- Refactor namespace `\CfdiUtils\CadenaOrigen` (backwards compatible):
- - Instead of one only xslt builder now it includes:
- - `DOMBuilder`: Uses the regular PHP based method
- - `GenkgoXslBuilder`: Uses the library genkgo/xsl xslt version 2 library
- - `SaxonbCliBuilder`: Uses the command line saxonb-xslt command
- - Build process implementations must return `XsltBuildException` (before they return `RuntimeException`)
- - All builders must implement `XsltBuilderInterface`
- - Add `XsltBuilderPropertyInterface` and `XsltBuilderPropertyTrait`.
- It does not have `hasXsltBuilderProperty`method.
- - `DefaultLocations` has been deprecated in favor of `CfdiDefaultLocations`
- - `CadenaOrigenBuilder` has been deprecated in favor of `DOMBuilder`
- - `CadenaOrigenLocations` has been deprecated, will not be replaced
+ - Instead of one only xslt builder now it includes:
+ - `DOMBuilder`: Uses the regular PHP based method
+ - `GenkgoXslBuilder`: Uses the library genkgo/xsl xslt version 2 library
+ - `SaxonbCliBuilder`: Uses the command line saxonb-xslt command
+ - Build process implementations must return `XsltBuildException` (before they return `RuntimeException`)
+ - All builders must implement `XsltBuilderInterface`
+ - Add `XsltBuilderPropertyInterface` and `XsltBuilderPropertyTrait`.
+ It does not have `hasXsltBuilderProperty`method.
+ - `DefaultLocations` has been deprecated in favor of `CfdiDefaultLocations`
+ - `CadenaOrigenBuilder` has been deprecated in favor of `DOMBuilder`
+ - `CadenaOrigenLocations` has been deprecated, will not be replaced
- Implement `XsltBuilderPropertyInterface` and `XsltBuilderPropertyTrait` in objects that use
to create `CadenaOrigenBuilder` objects.
- For `CfdiCreator33` and `CfdiValidator33` will create a default DOMBuilder object if none set.
@@ -156,7 +195,8 @@
- Improve the tests.
-# Version 2.1.0 2018-01-17
+## Version 2.1.0 2018-01-17
+
- Fix `SumasConceptos` to work also with "ImpuestosLocales"
- Add elements helpers `CfdiUtils\Elements\ImpLocal10\ImpuestosLocales` to work with "ImpuestosLocales"
- Add `CfdiUtils\Certificado\CerRetriever` that works with `CfdiUtils\XmlResolver\XmlResolver` to download
@@ -167,12 +207,14 @@
- Update test with `cfdi33-valid.xml` to allow fail `TimbreFiscalDigitalSello`
- Travis: Remove xdebug for all but PHP 7.0
-# Version 2.0.1 2018-01-03
+## Version 2.0.1 2018-01-03
+
- Small bugfixes thanks to scrutinizer-ci.com
- Fix some docblocks
- Travis: Build also with PHP 7.2
-# Version 2.0.0 2018-01-01
+## Version 2.0.0 2018-01-01
+
- This library has been changed deeply.
- It can write CFDI version 3.3 using `CfdiUtils\Elements\Cfdi33` and helper class `CfdiUtils\CfdiCreator33`
- It can read CFDI files version 3.2 and 3.3 using `CfdiUtils\Cfdi`
@@ -183,12 +225,14 @@
- Include wiki for documentation
-# Version 1.0.3 2017-10-09
+## Version 1.0.3 2017-10-09
+
- Fix a bug to read the RFC when a certificate does not contain the pattern RFC / CURP but only RFC in the
subject x500UniqueIdentifier field
-# Version 1.0.2 2017-09-28 - Thanks phpstan!
+## Version 1.0.2 2017-09-28 - Thanks phpstan!
+
- After using `phpstan/phpstan` change the execution plan on `CadenaOrigenLocations`.
The function previous function `throwLibXmlErrorOrMessage(string $message)` always
throw an exception but it was not clear in the flow of `build` method.
@@ -198,9 +242,11 @@
- Check with `isset` that `LibXMLError::$message` exists, phpstan was failing for this.
-# Version 1.0.1 2017-09-27
+## Version 1.0.1 2017-09-27
+
- Remove Travis CI PHP nightly builds, it fail with require-dev dependencies.
-# Version 1.0.0 2017-09-27
+## Version 1.0.0 2017-09-27
+
- Initial release
diff --git a/docs/TODO.md b/docs/TODO.md
index 4dfc675c..a9396bcb 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -1,38 +1,49 @@
-# eclipxe/CfdiUtils To Do List
+# Lista de tareas pendientes e ideas
-### Prepare for version 3
+## Documentación del proyecto
-Version 3 will deprecate some classes and methods, it may be good point of start to migrate the project
-to a new namespace `PhpCfdi\CfdiUtils` that is managed by
+Documentar los otros helpers de `Elements`:
+
+- Complemento de comercio exterior
+- Impuestos locales
+- Pagos
+
+Documentar los validadores:
-#### Deprecations:
+- Revisar todos los validadores documentados en CFDI
+- Pagos
-### CfdiVersion & TfdVersion
+## Prepare for version 3
+
+Version 3 will deprecate some classes and methods, it may be good point of start to migrate the project
+to a new namespace `PhpCfdi\CfdiUtils`
+
+
+## CfdiVersion & TfdVersion
The classes `CfdiUtils\CfdiVersion` and `CfdiUtils\TimbreFiscalDigital\CfdiVersion`
share the same logic and methos. They are detected as code smell and it would be better
to have a single class to implement the logic and extend that class to provide configuration.
-### Status of a Cfdi using the SAT webservice
+## Status of a Cfdi using the SAT webservice
This is already implemented in `CfdiUtils\ConsultaCfdiSat\WebService` but there are two
ideas than need a solution:
-* Find a way to not depend on PHP SOAP but in something that can do async
+- Find a way to not depend on PHP SOAP but in something that can do async
request and configure the connection like setting a proxy, maybe depending on guzzle.
+- Create a cache of the WSDL page (?)
-* Create a cache of the WSDL page (?)
-
-### Validation rules for Pagos
+## Validation rules for Pagos
The validation rules for "Complemento de Recepción de pagos" are included since version 2.6 but
they require more cases of use and a better understanding of the rules published by SAT.
-### Validation rules for ComercioExterior
+## Validation rules for ComercioExterior
Create validation rules for "Complemento de Comercio Exterior"
@@ -43,7 +54,7 @@ Create validation rules for "Complemento de Comercio Exterior"
This will be implemented on a different project, for testing proposes there is the file `tests/validate.php`
-
+
### Implement catalogs published by SAT
This will be implemented on a different project.
diff --git a/docs/componentes/cadena-de-origen.md b/docs/componentes/cadena-de-origen.md
new file mode 100644
index 00000000..5352abfb
--- /dev/null
+++ b/docs/componentes/cadena-de-origen.md
@@ -0,0 +1,121 @@
+# Cadena de origen
+
+El SAT utiliza este método de generar cadenas originales para agrupar
+en una forma y orden determinados información que no debería ser alterada.
+
+Una vez que se cuenta con la cadena de origen, se genera una firma con la llave
+privada que puede ser verificada con un certificado (llave pública).
+
+Si algún dato que es un componente para la cadena de origen fue modificado
+entonces producirá un sello diferente o la verificación será negativa.
+
+Esto significa que un CFDI puede ser alterado posteriormente a su elaboración.
+Por ejemplo, se le puede agregar una adenda o poner/quitar formato al XML,
+pues ni el nodo Addenda ni el "XML Whitespace" forman parte de la cadena de origen.
+
+Incluso, es frecuente "reparar" un CFDI que tiene errores como una adenda sin XSD
+o errores sintácticos como poner un número par de rutas en el `xsi:schemaLocation`
+o eliminar espacios de nombres no utilizados.
+
+
+## Método del SAT para generar cadenas de origen
+
+El método que utiliza el SAT es convertir archivos XML (o parte de los archivos)
+a texto simple utilizando la tecnología XSLT.
+
+Nota: Este no es el único método, siguiendo la especificación del Anexo 20
+y todas y cada una de las especificaciones de los complementos se podría
+hacer un generador de cadenas de origen.
+
+Yo ya lo hice antes y es mucho código para fabricar y testear, además de
+que deberá cambiar conforme cambien las especificaciones.
+
+
+## Generar una cadena de origen
+
+Para generar cadenas de origen tenemos diferentes implementaciones de
+la interfaz `\CfdiUtils\CadenaOrigen\XsltBuilderInterface`.
+Contiene un único método `build(string $xmlContent, string $xsltLocation)`.
+
+El `$xmlContent` es el XML que se desea convertir y `$xsltLocation` es la
+ubicación del archivo XSLT (local o remoto).
+
+Las implementaciones son:
+
+- `DOMBuilder`: Genera la transformación usando PHP, aunque no existe
+ soporte nativo para Xslt versión 2, la transformación es compatible
+ y genera el resultado esperado.
+- `GenkgoXslBuilder`: Funciona igual que `DOMBuilder` pero al momento de hacer
+ la transformación utiliza la librería [genkgo/xsl](https://github.com/genkgo/xsl)
+ que es una implementación de Xslt versión 2 en PHP.
+ Para usarla debes hacer algo como `composer require genkgo/xsl`.
+- `SaxonbCliBuilder`: Utiliza la herramienta
+ [Saxon-B XSLT Processor](https://en.wikipedia.org/wiki/Saxon_XSLT) desde la
+ ejecución por línea de comandos. Esta utilería presume la implementación de Xslt versión 2.
+ Para usarla debes hacer algo como `apt-get install libsaxonb-java`.
+
+
+### Generar la cadena de origen de un Comprobante
+
+Se puede seguir esta receta:
+
+```php
+resolveCadenaOrigenLocation('3.3');
+
+// fabricar la cadena de origen
+$builder = new DOMBuilder();
+$cadenaorigen = $builder->build($xmlContent, $location);
+```
+
+Sin embargo, en la práctica es poco probable que desees generar la cadena de origen.
+Básicamente porque si estás creando un CFDI esta será generada automáticamente.
+Si estás leyendo o validando también será generada automáticamente por los validadores.
+
+
+### Generar la cadena de origen de un Timbre Fiscal Digital
+
+A diferencia de la cadena de origen del Comprobante, la cadena de origen del Timbre Fiscal Digital
+sí se necesita al menos para mostrarla en la representación impresa del CFDI.
+
+Para ello puedes utilizar la clase `\CfdiUtils\TimbreFiscalDigital\TfdCadenaDeOrigen`.
+Esta clase solo funciona con TFD versiones 1.0 y 1.1. En caso de otra versión genera una excepción.
+
+Esta clase requiere de un `XmlResolver` y puede establecerse
+en el constructor o en el método `setXmlResolver`.
+
+```php
+';
+
+$builder = new TfdCadenaDeOrigen();
+$builder->setXmlResolver(new XmlResolver());
+
+$tfdCadenaOrigen = $builder->build($tfdXmlString);
+```
+
+## PHP y XLST versión 2
+
+Es importante notar que hasta el momento (enero/2018) no es posible en PHP
+procesar XSLT versión 2.0. Sin embargo el procesador que sí tiene PHP genera
+las cadenas de origen a pesar de la versión.
+Esto no garantiza que si el SAT modifica los archivos XSLT utilizando
+características incompatibles se producirá el resultado correcto.
+
+En la versión 2.2.0 de la librería se ha implementado Saxon-B y Genkgo Xsl
+como alternativas al método de PHP. Las tres entregan el mismo resultado en los test.
+La que se usa de forma predeterminada es la de PHP `DOMBuilder`.
diff --git a/docs/componentes/certificado.md b/docs/componentes/certificado.md
new file mode 100644
index 00000000..3b3e2b23
--- /dev/null
+++ b/docs/componentes/certificado.md
@@ -0,0 +1,67 @@
+# Certificado
+
+La clase `\CfdiUtils\Certificado\Certificado` obtiene la información de un archivo de tipo certificado.
+
+El archivo puede ser un archivo en formato PEM o en formato CER.
+En este último caso es convertido a PEM y luego interpretado.
+
+Una vez cargado el certificado permite obtener los siguientes datos:
+
+- RFC
+- Nombre
+- Número de serie
+- Válido desde y hasta
+- Llave pública
+- Nombre del archivo cargado
+
+Adicionalmente cuenta con los métodos:
+
+- Permite verificar si una llave privada corresponde a este certificado:
+
+ `belongsTo(string $pemKeyFile, string $passPhrase = ''): bool`
+
+- Permite verificar si una firma dada corresponde a los datos, como por ejemplo,
+ si el sello corresponde con la cadena de origen.
+
+ `verify(string $data, string $signature, int $algorithm = OPENSSL_ALGO_SHA256): bool`
+
+
+## Relación con `\CfdiUtils\Certificado\NodeCertificado`
+
+La clase `\CfdiUtils\Certificado\Certificado` funciona con un archivo previamente almacenado.
+Para extraer un certificado de un CFDI se ofrece la clase `\CfdiUtils\Certificado\NodeCertificado`.
+
+
+## Acerca de los formatos de archivo
+
+El certificado (el archivo extensión CER) puede ser leído directamente.
+
+La llave privada (el archivo extensión KEY) debe ser convertido a tipo PEM
+para ser correctamente interpretado por esta clase (en realidad por PHP).
+
+## Comandos útiles de openssl
+
+- Obtener información del certificado:
+
+```shell
+openssl x509 -nameopt utf8,sep_multiline,lname -inform DER -noout -dates -serial \
+ -subject -fingerprint -pubkey -in CSD01_AAA010101AAA.cer
+```
+
+- Convertir la llave privada a un archivo PEM sin contraseña:
+
+```shell
+openssl pkcs8 -inform DER -in CSD01_AAA010101AAA.key -out CSD01_AAA010101AAA.key.pem
+```
+
+- Establecer la contraseña a un archivo PEM:
+
+```shell
+openssl rsa -in CSD01_AAA010101AAA.key.pem -des3 -out CSD01_AAA010101AAA_password.key.pem
+```
+
+- Convertir el certificado a formato PEM:
+
+```shell
+openssl x509 -inform DER -outform PEM -in CSD01_AAA010101AAA.cer -pubkey -out CSD01_AAA010101AAA.cer.pem
+```
diff --git a/docs/componentes/elements.md b/docs/componentes/elements.md
new file mode 100644
index 00000000..e8112e69
--- /dev/null
+++ b/docs/componentes/elements.md
@@ -0,0 +1,71 @@
+# Estructura de datos Elements
+
+El espacio de nombres `CfdiUtils\Elements` es una especialización de [`CfdiUtils\Nodes`](nodes.md).
+
+Se trata solo de una estructura de datos, no caigas en la tentación de insertar lógica más allá de la propia estructura.
+
+Todo *elemento* es un *nodo*, entonces todos los métodos y propiedades de `NodeInterface` están presentes.
+
+Entonces, para escribir atributos se recomienda usar la forma de arreglo, por ejemplo:
+`$comprobante['Descuento'] = '1234.56'` para establecer el atributo "Descuento" a "1234.56".
+
+Cualquier elemento debe cumplir con la interfaz `CfdiUtils\Elements\Common\ElementInterface`
+que es una extensión de `CfdiUtils\Nodes\NodeInterface` y agrega:
+
+- `getElementName(): string`: Devuelve el nombre del elemento, como `cfdi:Complemento`
+- `getFixedAttributes(): array`: Establece la lista de nodos predefinidos al crearse (útil para Complementos y Comprobante)
+
+En última instancia, un *elemento* (`ElementInterface`) es un *nodo* (`NodeInterface`)
+por lo que puedes utilizar a bajo nivel todo el poder de los nodos para trabajar con esta estructura de datos.
+
+
+## Nomenclatura genérica
+
+Los elementos deberían seguir esta nomenclatura genérica para nombrar sus métodos.
+
+
+### Prefijo `get*`
+
+La nomenclatura con el prefijo `get*` se escribe en la forma `ElementoPadre::getElementoHijo(): ElementoHijo`
+y se espera devolver una única instancia de `ElementoHijo`. Si la instancia no existe entonces se crea.
+
+
+### Prefijo `add*`
+
+La nomenclatura con el prefijo `add*` se escribe la forma `ElementoPadre::addElementoHijo($attributes): ElementoHijo`
+y se espera crear una instancia de `ElementoHijo` con los atributos datos, agregarla a los hijos de `ElementoPadre`
+y la instancia de `ElementoHijo` creada.
+
+Cuando se utiliza `add*` hay dos comportamientos esperados:
+
+- Si solo debe haber un hijo de un determinado tipo, entonces no se crea uno nuevo y se ocupa el existente.
+- Si puede haber más de un hijo de un determinado tipo, entonces se crea uno nuevo y se agrega a los hijos.
+
+Por eso, como solo debe haber un nodo emisor dentro de un comprobante,
+entonces `Comprobante::addEmisor(['RegimenFiscal' => '601'])` tiene este comportamiento:
+
+- Se obtiene el elemento `Emisor`, si no existe se crea uno vacío.
+- Se escriben los atributos pasados al elemento obtenido.
+- Se devuelve el elemento.
+
+Por el contrario, como puede haber varios Cfdi Relacionados, entonces
+`CfdiRelacionados::addCfdiRelacionado(['UUID' => $uuid])` tiene este comportamiento:
+
+- Se crea un elemento de tipo `CfdiRelacionado` con los atributos pasados.
+- Se agrega el elemento recién creado a los hijos de `CfdiRelacionados`.
+- Se devuelve el elemento creado.
+
+Existe un caso donde lo que se espera entregar como atributo al pefijo `add*` es en realidad un hijo.
+Esto sucede en `addComplemento` y `addAddenda`.
+
+
+### Prefijo `multi*`
+
+La nomenclatura con el prefijo `multi*` se escribe la forma `ElementoPadre::multiElementoHijo(...$attributes): ElementoPadre`
+y se espera crear múltiples una instancia de `ElementoHijo` con los atributos datos, agregarla a los hijos de `ElementoPadre`
+y la instancia de `ElementoPadre` creada.
+
+Otra forma de decirlo: es como los métodos `add*` pero se le pueden mandar varios arreglos de atributos y se creará un elemento para cada parámetro enviado.
+
+Por lo anterior, `CfdiRelacionados::multiCfdiRelacionado([ ['UUID' => $uuid1], ['UUID' => $uuid2] ])` agregará dos hijos
+y devolverá la misma instancia del objeto llamado.
diff --git a/docs/componentes/estado-sat.md b/docs/componentes/estado-sat.md
new file mode 100644
index 00000000..0bbd620a
--- /dev/null
+++ b/docs/componentes/estado-sat.md
@@ -0,0 +1,130 @@
+# Consulta del estado de un CFDI en el WebService del SAT
+
+El SAT cuenta con un webservice para consultar el estado de un CFDI.
+
+- Servicio:
+- Documentación:
+
+Para poderlo consumir se han implementado varias clases dentro del espacio de nonbres `\CfdiUtils\ConsultaCfdiSat`.
+
+- `WebService`: Objeto que permite consumir el servicio.
+- `Config`: Objeto que permite configurar la consulta.
+- `RequestParameters`: Objeto que contiene los parámetros del CFDI que se va a consultar.
+- `StatusResponse`: Objeto que contiene la respuesta del servicio.
+
+
+## Datos que se requieren para hacer una consulta
+
+Ocurre que el webservice de consulta SAT toma como entrada la información que está dentro del código QR del CFDI.
+Este código tiene diferentes representaciones para diferentes versiones, entre los cambios más significativos están:
+
+- En el CFDI 3.3 se incluye una ruta
+- En el CFDI 3.3 se incluyen los últimos 8 caracteres del sello (de los cuales los últimos 2 siempre serán `==`)
+- En el CFDI 3.3 el total del comprobante se expresa con diferente formato
+- El orden de los componentes cambian de la versión 3.2 a la versión 3.3
+
+Por lo anterior, al construir la cadena de consulta, dependemos de la versión del CFDI.
+
+
+### Acerca del total en la expresión impresa en CFDI 3.3
+
+El SAT extablece en el Anexo 20 que el total se debe expresar como:
+
+- Total del comprobante máximo a 25 posiciones
+- 18 posiciones para los enteros
+- 1 para caracter `"."`
+- 6 para los decimales
+- Se deben omitir los ceros no significativos
+- Precedido por el texto `"&tt="`
+- Todo el conjunto va de 7 a 29 posiciones
+
+Por lo anterior, al excluir los ceros no significativos, no considerar como
+no significativo si el grupo (decimal o entero) no existe, por ejemplo:
+
+Si el total es por 99 centavos entonces la expresión deberá ser `&tt=0.99`
+Si el total es por 1 peso entonces la expresión deberá ser `&tt=1.0`
+Si el total es por cero entonces la expresión deberá ser `&tt=0.0`
+
+
+## Configuración de la consulta
+
+La consulta se puede configurar enviándole un objeto `Config` al consumidor.
+Las opciones disponibles son:
+
+- `timeout`: Define en segundos el tiempo máximo de espera, por omisión es 10.
+- `verifyPeer`: Define si SSL debe verificar el certificado de conexión.
+- `wsUrl`: Define la ubicación del WSDL.
+
+La consulta usa la librería de SOAP de PHP y por el momento no es posible configurarla
+con un contexto o un cliente de conexión, por lo que si estás detrás de un proxy lo mejor
+que puedes hacer es poner en la lista blanca el recurso del SAT o instalar un proxy inverso
+y cambiar la URL.
+
+
+## Datos que entrega la consulta
+
+El servicio entrega dos valores: estado de la consulta y estado del cfdi
+
+El estado de la consulta tiene tres posibles respuestas:
+
+- `S - Comprobante obtenido satisfactoriamente`
+- `N - 601: La expresión impresa proporcionada no es válida`
+- `N - 602: Comprobante no encontrado` de este no he podido encontrar un input que me lo devuelva
+
+El estado del cfdi tiene tres posibles respuestas:
+
+- `Vigente`
+- `No Encontrado`
+- `Cancelado`
+
+Dado lo anterior, los estados normales que podría entregar el servicio son:
+
+Consulta | CFDI | Explicación
+-------- | ------------- | ---------------------------------------------------------------
+S | Vigente | La consulta fue hecha y al momento el CFDI estaba vigente
+S | Cancelado | La consulta fue hecha y al momento el CFDI estaba cancelado
+N - 601 | No Encontrado | La consulta fue hecha pero la información impresa es incorrecta
+N - 602 | No Encontrado | La consulta fue hecha pero el CFDI no existe
+
+El problema que encontré es que alterando solo 1 dato (el total) esperaba encontrar un estado de `N - 602`
+pero el estado devuelto fue `N - 601`.
+
+
+## Ejemplo de uso
+
+```php
+request($request);
+
+// suponiendo que la consulta fue hecha y el resultado es que el CFDI está cancelado
+$response->responseWasOk(); // true
+$response->isVigente(); // false
+$response->isCancelled(); // true
+$response->isNotFound(); // false
+$response->getCode(); // S - ...
+$response->getCfdi(); // Cancelado
+```
+
+## Posibles futuros cambios
+
+Usar alguna librería como o
+en lugar de la extensión SOAP de PHP.
+
+Esto podría llevar a mejores opciones de configuración como establecer un proxy o generar consultas asíncronas.
+
+Crear un objeto que permita, a partir de un contenido XML, generar objeto `RequestParameters` apropiado.
diff --git a/docs/componentes/nodes.md b/docs/componentes/nodes.md
new file mode 100644
index 00000000..73f0d873
--- /dev/null
+++ b/docs/componentes/nodes.md
@@ -0,0 +1,142 @@
+# Estructura de datos Node
+
+Esta estructura de datos permite administrar en memoria una colección de nodos con hijos de tipo nodo
+donde cada uno tiene una colección de atributos. Los nodos no tienen referencia de padre.
+
+
+## Objeto `CfdiUtils\Nodes\Node`
+
+Esta es la estructura básica. Un nodo debe tener un nombre y esta propiedad no se puede cambiar.
+Su contructor admite tres parámetros:
+
+- `string $name`: Nombre del nodo, se eliminan espacios en blanco al inicio y al final, no permite vacíos.
+- `string[] $attributes`: arreglo de elementos clave/valor que serán importados como atributos
+- `string[] $nodes`: arreglo de elementos `Node` que serán importados como nodos hijo
+
+
+### Atributos de nodos `attributes(): CfdiUtils\Nodes\Attributes`
+
+Se accesa a sus atributos utilizando la forma de arreglos de php siguiendo estas reglas básicas:
+
+- La lectura de un nodo siempre devuelve una cadena de caracteres aunque el atributo no exista.
+- La escritura de un nodo es siempre con una cadena de caracteres, también puede ser un objeto
+ que implemente el método `__toString()`
+
+Los atributos se manejan con una colección de tipo `Attributes` y se pueden obtener usando el método
+`attributes()`.
+
+```php
+ '1'
+]);
+
+echo $node['id']; // '1'
+echo $node['no-existe']; // cadena de caracteres vacía
+echo isset($node['no-existe']) ? 'sí' : 'no'; // no
+$node['atributo'] = 'valor'; // establece el valor
+unset($node['foo']); // elimina el atributo 'foo'
+
+// recorrer la colección de atributos
+foreach ($node->attributes() as $attributeName => $attributeValue) {
+ echo $attributeName, ': ', $attributeValue;
+}
+```
+
+
+### Nodos (`children(): CfdiUtils\Nodes\Nodes`)
+
+Los nodos hijos se manejan a través de una colección de nodos `Nodes`.
+Se puede acceder al objeto `Nodes` usando el método `children()`.
+
+Cuanto se itera el objeto en realidad se está iterando sobre la colección de nodos.
+
+La clase `Node` tiene estos métodos de ayuda que sirven para trabajar directamente sobre la colección Nodes:
+
+- iterador: el `foreach` se realiza sobre la colección de nodos.
+- `addChild(Node $node)`: agrega un nodo en la colección de nodos.
+
+
+### Métodos de búsqueda
+
+Un objeto de tipo `Node` tiene los siguientes métodos para poder interactuar con sus hijos:
+
+- `searchAttribute(string ...$searchPath): string`: Devuelve el valor de un atributo según una búsqueda.
+- `searchNode(string ...$searchPath): Node|NULL`: Devuelve un objeto de tipo `Node` o `NULL` según una búsqueda.
+- `searchNodes(string ...$searchPath): Nodes`: Devuelve un objeto de tipo `Nodes` según una búsqueda.
+
+La búsqueda se refiere a los nombres de los hijos, por ejemplo:
+`$node->searchNode('orden', 'articulos', 'articulo')`
+busca dentro de los hijos de `$node` el **primer** nodo llamado `orden`,
+si existe busca el primer nodo dentro de `orden` que se llame `articulos`,
+si existe busca el primer nodo dentro de `articulos` que se llame `articulo`,
+si existe devuelve dicho elemento.
+Si alguno no existiera entonces devuelve un valor nulo.
+
+De la misma forma, `$node->searchNodes('orden', 'articulos', 'articulo')` devuelve una colección
+con los elementos llamados `articulo` que están dentro de `orden/articulos`.
+
+Nota: Si se agrega o elimina un elemento a colección devuelta por `searchNodes`, dicho nodo no se agregará o modificará en el padre.
+Esto es porque la colección devuelta por `searchNodes` es simplemente una agrupación adicional a la estructura principal
+de nodos. Sin embargo, los hijos de esta colección sí hacen referencia a los nodos de la estructura principal,
+por lo que cualquier cambio a los hijos sí será reflejado.
+
+Cuando se require obtener solamente un valor de atributo de un nodo se puede utilizar `searchAtribute`,
+la búsqueda se comporta igual que `searchNode`. Si el nodo buscado no existe devolverá una cadena vacía.
+Por ejemplo: `$node->searchNodes('orden', 'articulos', 'nota')` devolverá el atributo `nota` de `orden/articulos`.
+
+
+## Clase CfdiUtils\Nodes\Nodes
+
+Esta clase representa una colección de `Node`. Al iterar en el objeto se recorrerá cada uno de los nodos.
+
+Se pueden hacer las operaciones básicas como:
+`add(Node $node)`,
+`indexOf(Node $node)`,
+`remove(Node $node)`,
+`removeAll()`,
+`exists(Node $node)`,
+`get(int $index)`.
+
+Adicionalmente se pueden usar los métodos:
+`firstNodeWithName(string name): Node|null`,
+`getNodesByName(string $nodeName): Nodes` y
+`importFromArray(Nodes[] $nodes)`
+
+
+## Clase CfdiUtils\Nodes\Attributes
+
+Esta clase representa una colección de atributos identificados por nombre.
+Al iterar en el objeto se devolverá cada uno de los attributos en forma de clave/valor.
+
+Adicionalmente esta clase permite el uso de acceso como arreglo, por lo que permite:
+
+- `$attributes[$name]` como equivalente de `$attributes->get($name)`
+- `$attributes[$name] = $value` como equivalente de `$attributes->set($name, $value)`
+- `isset($attributes[$name])` como equivalente de `$attributes->exists($name)`
+- `unset($attributes[$name])` como equivalente de `$attributes->remove($name)`
+
+Se pueden hacer las operaciones básicas como:
+
+- `get(string $name): string`
+- `set(string $name, string $value)`
+- `remove(string $name)`
+- `removeAll()`
+- `exists(string $name)`
+
+
+## XmlNodeUtils
+
+Esta es una clase de utilerías que contiene métodos estáticos que permiten crear estructuras de nodos desde XML
+y generar XML a partir de los nodos. Recuerde que los nodos solo pueden almacenar atributos y nodos hijos.
+
+Actualmente permite exportar e importar a/desde: `DOMDocument`, `DOMElement`, `SimpleXmlElement` y `string` (con contenido válido).
+
+**Advertencias:**
+
+- Los nodos no tienen campo de contenido y no son una reescritura fiel de DOM.
+- Los nodos solo contienen atributos e hijos.
+- Importar XML que no siga la estructura de atributos/hijos exclusivamente puede resultar en pérdida de datos.
diff --git a/docs/componentes/xmlresolver.md b/docs/componentes/xmlresolver.md
new file mode 100644
index 00000000..6e2dd63d
--- /dev/null
+++ b/docs/componentes/xmlresolver.md
@@ -0,0 +1,116 @@
+# Almacenamiento local de recursos del SAT
+
+El SAT publica diferentes recursos para diferentes tareas,
+los recursos más usuales son:
+
+- Archivos XSD: Son archivos de esquemas XML y sirven para comprobar que
+ un archivo es correcto con respecto a ciertas reglas.
+- Archivos XSLT: Son archivos de transformaciones XML y sirven para transformar
+ el contenido de un archivo XML en otro contenido.
+ El SAT los utiliza para generar cadenas de origen.
+- Archivos CER: Son archivos de certificado comúnmente utilizados para verificar
+ que una firma es válida con respecto a un emisor.
+ La firma es lo que el sat llama sello y el emisor se distingue por un certificado.
+
+Estos recursos están disponibles en internet, pero son grandes y tienen cambios esporádicos. Por ejemplo, el archivo de catálogos del SAT mide 6.3 MB.
+Por ello es conveniente tener una copia local de los recursos.
+
+El problema viene cuando esos recursos no se pueden simplemente descargar y almacenar.
+Muchos recursos dependen de otros y sus rutas de dependencia no son relativas,
+por esto es necesario descargar y manipular los recursos para cambiar las dependencias.
+
+Por suerte esta librería viene con una utilería para mantener copias locales de los recursos según nos convenga.
+
+Internamente, cuando se solicita un recurso, la librería busca la mejor opción según esté configurada:
+
+- Si no se ha configurado un repositorio local entonces devuelve la ruta del recurso remoto.
+- Si se ha configurado un repositorio local entonces busca si existe.
+ - Si existe devuelve la ruta del recurso local.
+ - Si no existe lo descarga y devuelve la ruta del recurso local.
+
+
+## Repositorio local por defecto
+
+Por defecto, la librería utilizará como repositorio local el lugar donde esté instalada y le agregará:
+`/build/resources/`. Por lo que, generalmente, si estás usando `composer` entonces el lugar donde están
+los recursos es: `/vendor/eclipxe/cfdiutils/build/resources/`.
+
+
+## Cómo manipular el lugar por defecto del repositorio
+
+La forma de modificarlo es creando una instancia del objeto `CfdiUtils\XmlResolver\XmlResolver`
+y especificando la dirección del repositorio en `localPath`, esto se puede lograr con el constructor del objeto
+o bien con el método `setLocalPath(string $localPath = null)`.
+En ambos casos (constructor o método) se aplican las siguientes reglas:
+
+- Si se pasa una cadena de caracteres vacía se desconfigura el repositorio local,
+ por lo que no se almacenarán recursos localmente.
+- Si se pasa `null` el valor se establece a `defaultLocalPath()`,
+ es decir `/build/resources/`.
+- Si se pasa otro valor entonces será usado, por ejemplo `/tmp/sat/`.
+
+Después, es necesario que ese objeto se utilice en los otros objetos que estamos usando, por ejemplo:
+
+```php
+setXmlResolver($myResolver);
+
+// ponerlo utilizando el constructor
+$cfdiValidator = new \CfdiUtils\CfdiValidator33($myResolver);
+```
+
+Mi recomendación es que, si vas a modificar o desactivar el repositorio local, tengas
+una fábrica de objetos (factory pattern) o bien una función que siempre te devuelva
+el objeto configurado tal y como lo necesitas.
+
+
+## Cómo invalidar el caché del almacenamiento local
+
+**En corto: Simplemente bórralos.**
+
+No se trata de un caché, porque no hay fechas de caducidad de los recursos.
+
+Cuando descargas algún recurso este podría descargar hijos y a su vez estos podrían descargar nuevos hijos.
+De igual forma, no solo se descargan recursos del SAT, también podrían descargarse recursos de terceros.
+Por eso te recomiendo que, si hubo algún cambio en los archivos XSD del SAT elimines entonces cualquier archivo
+de tipo `*.xsd` dentro de la carpeta `/www/www.sat.gob.mx`.
+
+
+## Configurando el objeto que se encarga de la descarga de archivos
+
+Imagina ahora que tu proyecto corre en un servidor dentro de una red corporativa que tiene
+salida a internet usando un proxy con usuario y contraseña.
+La librería por defecto no puede obtener los recursos que necesita.
+Sin embargo, para ello existe la interface `\XmlResourceRetriever\Downloader\DownloaderInterface`
+(esta interface no pertenece a este proyecto, pertenece a `XmlResourceRetriever`).
+
+Tu puedes implementar el `DownloaderInterface` en una clase que utilice `curl` o `guzzle`
+o ejecute un comando en la shell como `wget` y luego crear tu objeto `XmlResolver` con este descargador.
+
+```php
+setDownloader($myDownloader);
+
+// establecer el descargador a un descargador simple (ver PhpDownloader)
+$myResolver->setDownloader(null);
+```
diff --git a/docs/contribuir/guia-desarrollador.md b/docs/contribuir/guia-desarrollador.md
new file mode 100644
index 00000000..56c5bc05
--- /dev/null
+++ b/docs/contribuir/guia-desarrollador.md
@@ -0,0 +1,82 @@
+# Guía para desarrollar CfdiUtils
+
+Esta es una guía rápida que pretende guiarte para que puedas desarrollar la librería.
+
+## Código de conducta
+
+Revisa nuesto [COC][] y nuestra página de [CONTRIBUTING][].
+
+En resumen:
+
+* No toleraremos la discriminación y el maltrato.
+* Cuando reportes un problema procura documentar lo más posible tu caso.
+* Cuando desees contribuir al código, realiza pruebas.
+* Apégate al estándar de codificación.
+* Usa las herramientas básicas, antes de enviar tu PR ejecuta: `composer build`.
+
+## Dependencias de desarrollo
+
+Requieres tener instalado y disponible `git` `composer` y `php`.
+
+Opcionalmente podrías tener instalado `saxonb-xslt`.
+
+El proyecto es compatible con PHP 7.0.
+Respeta esta compatilibilidad, no agregues características de versiones superiores.
+
+## Primeros pasos
+
+Descargar el proyecto
+
+```shell
+git clone https://github.com/eclipxe13/cfdiutils
+mkdir -p cfdiutils/build/
+```
+
+Instalar las dependencias, opcionalmente puedes poner `--prefer-dist` para instalar
+los paquetes con
+
+```shell
+composer install
+```
+
+## Pruebas
+
+Para probar que todos los archivos no contienen errores de sintaxis
+
+```shell
+vendor/bin/phplint
+```
+
+Para probar que no se están violando las reglas de estilo
+
+```shell
+vendor/bin/phpcs -sp --colors src/ tests/
+vendor/bin/php-cs-fixer fix --using-cache=no --dry-run --verbose
+```
+
+
+El proyecto viene acompañado de archivos de pruebas de PHPUnit
+
+```shell
+vendor/bin/phpunit
+```
+
+También ejecutamos PHPStan sobre archivos de orígenes y pruebas
+
+```shell
+vendor/bin/phpstan.phar analyse --level max src/ tests/
+```
+
+
+## Comandos de ayuda
+
+Para corregir todos los problemas de estilo que encuentre
+
+```shell
+vendor/bin/php-cs-fixer fix --verbose
+vendor/bin/phpcbf --colors -sp src/ tests/
+```
+
+
+[coc]: https://github.com/eclipxe13/CfdiUtils/blob/master/CODE_OF_CONDUCT.md
+[contributing]: https://github.com/eclipxe13/CfdiUtils/blob/master/CONTRIBUTING.md
\ No newline at end of file
diff --git a/docs/contribuir/guia-documentador.md b/docs/contribuir/guia-documentador.md
new file mode 100644
index 00000000..c119c768
--- /dev/null
+++ b/docs/contribuir/guia-documentador.md
@@ -0,0 +1,74 @@
+# Guía para documentar CfdiUtils
+
+En esta guía conocerás lo básico para crear y modificar la documentación para la librería
+
+
+## Ubicación de la documentación
+
+La documentación se encuentra publicada en .
+
+Los archivos fuente de la documentación están en la carpeta `docs/` y además se apoya
+de los archivos `mkdocs.yml` y `.markdownlint.json`.
+
+La documentación es compilada (o transformada, o como le quieras decir) utilizando
+la herramienta [`mkdocs`](https://www.mkdocs.org/).
+
+Si deseas realizar un cambio en la documentación realiza el proceso normal de cualquier cambio
+en GitHub (fork, pull, push & pull-request).
+
+No somos expertos ni en ReadTheDocs ni en mkdocs, así que si tienes experiencia cuéntanos cómo
+podemos mejorar el proyecto y su integración.
+
+
+## Reglas
+
+- La documentación se debe escribir en español con excepción del archivo `CHANGELOG.md`
+- Términos como XML, XSD, XSLT se escriben en mayúsculas.
+- Los archivos van escritos en minúsculas y estructurados en grupo, a excepción de `TODO.md` y `CHANGELOG.md`
+- Todos los nombres de funciones, clases, propiedades, métodos, etc. deben escribirse con ` (acento grave)
+- Se debe cumplir con la sintaxis de markdown aceptada por `markdownlint`, excepto:
+ - Se puede usar la longitud de línea que sea
+ - Se admiten hasta dos `NEW_LINE` seguidos
+ - Los encabezados (*headings*) pueden acabar con signo de admiración e interrogación
+ - Mira el archivo `.markdownlint.json`
+
+
+## Flujo de trabajo
+
+Estas herramientas te ayudarán para realizar la documentación y no tener problemas de construcción:
+
+- [`mkdocs`](https://www.mkdocs.org/)`: Usada para previsualizar los cambios.
+- [`markdownlint`](https://github.com/DavidAnson/markdownlint): Revisión de la sintaxis.
+- `git`: Control de cambios.
+
+Descargar el proyecto
+
+```shell
+git clone https://github.com/eclipxe13/cfdiutils
+```
+
+Ver los cambios en el navegador mientras suceden, esto abre un puerto en tu equipo
+que puedes consultar en el navegador, por ejemplo: `http://127.0.0.1:8000/`
+
+```shell
+mkdocs serve
+```
+
+Realiza tus cambios, te recomiendo usar alguno de los editores que tienen soporte para
+`markdownlink`, puedes encontrar una lista en
+
+Antes de publicar, verifica tus cambios
+
+```shell
+node_modules/markdownlint-cli/markdownlint.js *.md docs/
+```
+
+
+## Instalación de `markdownlint`
+
+El proyecto cuenta con un archivo `package.json` que contiene la dependencia de `markdownlint-cli`,
+por lo que si no lo tienes instalado globalmente lo único que tendrías que hacer para instalarlo en el proyecto es:
+
+```shell
+npm install
+```
diff --git a/docs/crear/complementos-aun-no-implementados.md b/docs/crear/complementos-aun-no-implementados.md
new file mode 100644
index 00000000..7d18e292
--- /dev/null
+++ b/docs/crear/complementos-aun-no-implementados.md
@@ -0,0 +1,74 @@
+# Complementos que no están implementados
+
+No todos los complementos están disponibles para utilizarse con las clases
+de ayuda `CfdiUtils\Elements`. Sin embargo, este no es motivo para no poder
+agregar el nodo a la estructura del CFDI.
+
+Recuerda que en realidad, la forma en como esta librería almacena la
+información es utilizando [nodos](../componentes/nodes.md) `CfdiUtils\Nodes\Node`.
+Por lo que usando esta estructura será muy fácil agregar la información.
+
+Nodos de `` y ``:
+
+
+En el siguiente ejemplo voy a agregar la información necesaria del complemento de
+[leyenda fiscal](http://www.sat.gob.mx/informacion_fiscal/factura_electronica/Documents/Complementoscfdi/leyendasFisc.pdf)
+
+Y voy a partir de la **suposición** (**no real**) de que al facturar consultoría en
+desarrollo de software tengo que poner una leyenda fiscal con la licencia
+del software desarrollado.
+
+```php
+comprobante();
+$comprobante->addAttributes([
+ // ... atributos del comprobante
+]);
+// ... llenar la información del comprobante
+
+// Creación del nodo de LeyendasFiscales
+$leyendasFisc = new \CfdiUtils\Nodes\Node(
+ 'leyendasFisc:LeyendasFiscales', // nombre del elemento raíz
+ [ // nodos obligatorios de XML y del nodo
+ 'xmlns:leyendasFisc' => 'http://www.sat.gob.mx/leyendasFiscales',
+ 'xsi:schemaLocation' => 'http://www.sat.gob.mx/leyendasFiscales'
+ . ' http://www.sat.gob.mx/sitio_internet/cfd/leyendasFiscales/leyendasFisc.xsd',
+ 'version' => '1.0',
+ ]
+);
+
+$leyendasFisc->addChild(new \CfdiUtils\Nodes\Node('leyendasFisc:Leyenda', [
+ 'disposicionFiscal' => 'RESDERAUTH',
+ 'norma' => 'Artíclo 2. Fracción IV.',
+ 'textoLeyenda' => 'El software desarrollado se entrega con licencia MIT'
+]));
+
+// Agregar el nodo $leyendasFisc a los complementos del CFDI
+$comprobante->addComplemento($leyendasFisc);
+
+// ... más instrucciones
+
+$creator->saveXml('archivo_con_complemento.xml');
+```
+
+Dado el ejemplo anterior, el comprobante contendrá la siguiente información:
+
+```xml
+
+
+
+
+
+
+
+
+
+```
diff --git a/docs/crear/crear-cfdi.md b/docs/crear/crear-cfdi.md
new file mode 100644
index 00000000..ea957982
--- /dev/null
+++ b/docs/crear/crear-cfdi.md
@@ -0,0 +1,161 @@
+# Creación de CFDI 3.3
+
+Para crear un CFDI versión 3.3 se ofrece el objeto `CfdiUtils\CfdiCreator33`.
+
+Este objeto trabaja directamente con la estructura `CfdiUtils\Elements\Cfdi33\Complemento`
+para facilitar la manipulación de la estructura y los datos, y contiene métodos que ayudan
+a establecer el certificado, generar el sello, generar o almacenar el XML, y validar la estructura recién creada.
+
+Esta clase es una especie de pegamento de todas las pequeñas utilerías y estructuras de datos.
+
+## Métodos de ayuda
+
+- `comprobante(): Comprobante`: Obtiene el nodo raíz `Comprobante`. Todos los métodos utilizan este objeto.
+
+- `putCertificado(Certificado $certificado, bool $putEmisorRfcNombre = true)`: Establece el valor de los atributos
+ `NoCertificado` y `Certificado`, y si `$putEmisorRfcNombre` es verdadero entonces también establece el valor
+ de `Rfc` y `Nombre` en el nodo `Emisor`.
+
+- `asXml(): string`: Genera y devuelve el contenido XML de la exportación del nodo `Comprobante`.
+
+- `saveXml(string $filename): bool`: Genera y almacena el contenido XML.
+
+- `buildCadenaDeOrigen(): string`: Construye la cadena de origen siempre que exista un resolvedor de recursos XML.
+
+- `buildSumasConceptos(int $precision = 2): SumasConceptos`: Genera un objeto de tipo `SumasConceptos` según los datos de los `Conceptos`.
+
+- `addSumasConceptos(SumasConceptos $sumasConceptos = null, int $precision = 2)`: Establece los valores de `$sumasConceptos`
+ en el comprobante, si no se pasó el objeto entonces lo fabrica con `buildSumasConceptos()`.
+
+- `addSello(string $key, string $passPhrase = '')`: Realiza el procedimiento de firma con la llave primaria y
+ almacena el valor de dicha llave en base64 en el atributo `Sello`.
+ Si el certificado existe como un objeto `Certificado` entonces este método también valida que la llave primaria
+ pertenece al certificado y genera una excepción si no es así.
+
+- `validate(): Asserts`: Crea un validador que verifica la estructura XML contra su archivo XSD
+ y realiza validaciones adicionales.
+ Consulta la [documentación de validaciones](../validar/validacion-cfdi.md) para más información.
+
+
+## Pasos básicos de creación de un CFDI
+
+No hay una sola forma de hacer las cosas, pero la receta de creación sería algo como:
+
+```php
+ 'XXX',
+ 'Folio' => '0000123456',
+ // y otros atributos más...
+];
+$creator = new \CfdiUtils\CfdiCreator33($comprobanteAtributos, $certificado);
+
+$comprobante = $creator->comprobante();
+
+// No agrego (aunque puedo) el Rfc y Nombre porque uso los que están establecidos en el certificado
+$comprobante->addEmisor([
+ 'RegimenFiscal' => '601', // General de Ley Personas Morales
+]);
+
+$comprobante->addReceptor([/* Atributos del receptor */]);
+
+$comprobante->addConcepto([
+ /* Atributos del concepto */
+])->addTraslado([
+ /* Atributos del impuesto trasladado */
+]);
+
+// método de ayuda para establecer las sumas del comprobante e impuestos
+// con base en la suma de los conceptos y la agrupación de sus impuestos
+$creator->addSumasConceptos(null, 2);
+
+// método de ayuda para generar el sello (obtener la cadena de origen y firmar con la llave privada)
+$creator->addSello('file:// ... ruta para mi archivo key convertido a PEM ...', 'contraseña de la llave');
+
+// método de ayuda para validar usando las validaciones estándar de creación de la librería
+$asserts = $creator->validate();
+$asserts->hasErrors(); // contiene si hay o no errores
+
+// método de ayuda para generar el xml y guardar los contenidos en un archivo
+$creator->saveXml('... lugar para almacenar el cfdi ...');
+
+// método de ayuda para generar el xml y retornarlo como un string
+$creator->asXml();
+```
+
+En el ejemplo anterior en la línea que dice `$comprobante = $creator->comprobante();`
+se está obteniendo el **elemento** `CfdiUtils\Elements\Cfdi33\Comprobante`.
+
+Todos los [elementos](../componentes/elements.md) son una especialización de los [nodos](../componentes/nodes.md).
+A diferencia de los nodos, los elementos contienen métodos de ayuda que pemiten entender los hijos que manejan,
+por ejemplo `CfdiUtils\Elements\Cfdi33\Comprobante` contiene un método llamado `addReceptor()`
+con el que se puede insertar en el lugar correcto el nodo "Receptor" incluyendo un arreglo de atributos.
+
+
+## Formación de el texto de los códigos QR
+
+La formación del texto que se incluye en los códigos QR tiene reglas específicas
+y puede utilizarse el objeto `\CfdiUtils\ConsultaCfdiSat\RequestParameters`
+para obtener el texto contenido en el código QR.
+
+Este es un ejemplo para la obtener la URL directamente de un contenido XML.
+
+```php
+$xmlContents = '...';
+$cfdi = \CfdiUtils\Cfdi::newFromString($xmlContents);
+$comprobante = $cfdi->getNode(); // Nodo de trabajo del nodo cfdi:Comprobante
+
+$parameters = new RequestParameters(
+ $comprobante['Version'],
+ $comprobante->searchAttribute('cfdi:Emisor', 'Rfc'),
+ $comprobante->searchAttribute('cfdi:Receptor', 'Rfc'),
+ $comprobante['Total'],
+ $comprobante->searchAttribute('cfdi:Complemento', 'tfd:TimbreFiscalDigital', 'UUID'),
+ $comprobante['Sello']
+);
+
+echo $parameters->expression(); // https://verificacfdi.facturaelectronica.sat.gob.mx/...
+```
+
+
+## Orden de los nodos de un CFDI
+
+A pesar de tratarse de una estructura XML el SAT por las reglas impuestas en los
+archivos XSD ha puesto reglas de orden de aparición de nodos.
+
+Por lo anterior **esta estructura presentará error** porque el nodo `Receptor`
+debe ir después del nodo `Emisor`:
+
+```xml
+
+
+
+
+```
+
+Cuando se está usando el espacio de nombres `CfdiUtils\Elements` las estructuras conocen el
+orden en el que deben existir los nodos, por lo que no es necesario preocuparse por el orden de aparición.
+Esta mejora fue introducida en la versión 2.4.0.
+
+Si se está utilizando `CfdiUtils\Nodes` de forma independiente a `CfdiUtils\Elements` entonces será necesario
+establecer el orden de los nodos con el método `CfdiUtils\Nodes\Nodes::setOrder(array $order)`.
+O simplemente insertar los nodos en el orden correcto.
+
+
+## Resolvedor de recursos XML
+
+Los archivos XSD necesarios para validar la estructura XML de un CFDI y
+los archivos XSLT necesarios para generar la cadena de origen
+son almacenados localmente y reutilizados cada vez que se require.
+
+Para establecer dicha configuración diferente a la predeterminada establezca el objeto `XmlResolver`
+usando el método `CfdiCreator33::setXmlResolver(XmlResolver $xmlResolver = null)`.
+
+Si establece el valor a nulo (`CfdiCreator33::hasXmlResolver()` es `false`) entonces no se podrá
+crear la cadena de origen (necesario para obtener la ruta de los archivos XSLT) y tampoco se podrá abastecer
+a los objetos de validación que requieran de un resolvedor con el objeto apropiado resultando en varias revisiones
+sin ejecutar.
+
+Si lo que desea es no almacenar localmente los recursos entonces lo que debe hacer es establecer
+una cadena de caracteres vacía mediante el método `XmlResolver::setLocalPath`.
diff --git a/docs/crear/elements-cfdi33.md b/docs/crear/elements-cfdi33.md
new file mode 100644
index 00000000..428b4f4b
--- /dev/null
+++ b/docs/crear/elements-cfdi33.md
@@ -0,0 +1,143 @@
+# Elementos de Cfdi versión 33
+
+El espacio de nombres de `CfdiUtils\Elements\Cfdi33` permite trabajar en forma más fácil
+con los nodos con nombres y acciones específicas y es la base de la creación de un CFDI 3.3.
+
+Es la implementación de [elementos](../componentes/elements.md),
+que son [nodos](../componentes/nodes.md) con métodos de ayuda.
+
+## Comprobante `cfdi:Comprobante`
+
+Representa el nodo raiz Comprobante.
+Contiene los siguientes métodos de ayuda:
+
+- `getCfdiRelacionados(): CfdiRelacionados`: Crea (si no existe) y obtiene el nodo único CfdiRelacionados
+- `addCfdiRelacionados(array $attributes = []): CfdiRelacionados`: Agrega y devuelve el único nodo CfdiRelacionados
+- `addCfdiRelacionado(array $attributes = []): CfdiRelacionado`: Agrega y devuelve un nuevo nodo CfdiRelacionado
+- `addCfdiRelacionados(array ...$attributes): Comprobante`: Agrega nuevos nodos CfdiRelacionado, es una forma rápida de llamar al método `addCfdiRelacionado` múltiples veces
+- `getEmisor(): Emisor`: Crea (si no existe) y obtiene el nodo único Emisor
+- `addEmisor(array $attributes = []): Emisor`: Agrega y devuelve el único nodo Emisor
+- `getReceptor(): Receptor`: Crea (si no existe) y obtiene el nodo único Receptor
+- `addReceptor(array $attributes = []): Receptor`: Agrega y devuelve el único nodo Receptor
+- `getConceptos(): Conceptos`: Crea (si no existe) y obtiene el nodo único Conceptos
+- `addConcepto(array $attributes = [], array $children = []): Concepto`: Agrega y devuelve un nuevo nodo Concepto
+- `getImpuestos(): Impuestos`: Crea (si no existe) y obtiene el nodo único Impuestos
+- `addTraslado(array $attributes = []): Traslado`: Agrega y devuelve un nuevo nodo Traslado (en Impuestos / Traslados)
+- `multiTraslado(array ...$elementAttributes): Comprobante`: Agrega nuevos nodo Traslado, es una forma rápida de llamar al método `addTraslado` múltiples veces
+- `addRetencion(array $attributes = []): Retencion`: Agrega y devuelve un nuevo nodo Retencion (en Impuestos / Retenciones)
+- `multiRetencion(array ...$elementAttributes): Comprobante`: Agrega nuevos nodo Retencion, es una forma rápida de llamar al método `addRetencion` múltiples veces
+- `getComplemento(): Complemento`: Crea (si no existe) y obtiene el nodo único Complemento
+- `addComplemento(NodeInterface $children): Comprobante`: Agrega el nodo $children dentro del único nodo Complemento
+- `getAddenda(): Addenda`: Crea (si no existe) y obtiene el nodo único Addenda
+- `addAddenda(NodeInterface $children): Comprobante`: Agrega el nodo $children dentro del único nodo Addenda
+
+
+## CfdiRelacionados `cfdi:CfdiRelacionados`
+
+Representa el nodo Comprobante / CfdiRelacionados.
+
+- `addCfdiRelacionado(array $attributes = []): CfdiRelacionado`: Agrega y devuelve un nuevo nodo CfdiRelacionado
+
+
+## CfdiRelacionado `cfdi:CfdiRelacionado`
+
+Representa el nodo Comprobante / CfdiRelacionados / CfdiRelacionado.
+
+
+## Emisor `cfdi:Emisor`
+
+Representa el nodo Comprobante / Emisor.
+
+
+## Receptor `cfdi:Receptor`
+
+Representa el nodo Comprobante / Receptor.
+
+
+## Conceptos `cfdi:Conceptos`
+
+Representa el nodo Comprobante / Conceptos.
+
+- `addConcepto(array $attributes = []): Concepto`: Agrega y devuelve un nuevo nodo Conceptos
+
+
+## Conceptos `cfdi:Concepto`
+
+Representa el nodo Comprobante / Conceptos / Concepto.
+
+- `getImpuestos(): Impuestos`: Crea (si no existe) y obtiene el nodo único Impuestos
+- `addTraslado(array $attributes = []): Traslado`: Agrega y devuelve un nuevo nodo Traslado (en Impuestos / Traslados)
+- `multiTraslado(array ...$elementAttributes): Concepto`: Agrega nuevos nodo Traslado, es una forma rápida de llamar al método `addTraslado` múltiples veces
+- `addRetencion(array $attributes = []): Retencion`: Agrega y devuelve un nuevo nodo Retencion (en Impuestos / Retenciones)
+- `multiRetencion(array ...$elementAttributes): Concepto`: Agrega nuevos nodo Retencion, es una forma rápida de llamar al método `addRetencion` múltiples veces
+- `addInformacionAduanera(array $attributes = []): InformacionAduanera`: Agrega y devuelve un nuevo nodo InformacionAduanera
+- `multiInformacionAduanera(array ...$elementAttributes): Concepto`: Agrega nuevos nodo InformacionAduanera, es una forma rápida de llamar al método `addInformacionAduanera` múltiples veces
+- `addCuentaPredial(array $attributes = []): CuentaPredial`: Agrega y devuelve el único nodo CuentaPredial
+- `getComplementoConcepto(): ComplementoConcepto`: Crea (si no existe) y obtiene el nodo único ComplementoConcepto
+- `addComplementoConcepto(array $attributes = [], array $children = []): ComplementoConcepto`: Agrega y devuelve el único nodo Complementoconcepto
+- `addParte(array $attributes = [], array $children = []): Parte`: Agrega y devuelve un nuevo nodo Parte
+- `multiParte(array ...$elementAttributes)`: Agrega nuevos nodo Parte, es una forma rápida de llamar al método `addParte` múltiples veces
+
+
+## InformacionAduanera `cfdi:InformacionAduanera`
+
+Representa el nodo Comprobante / Conceptos / Concepto / InformacionAduanera
+y Comprobante / Conceptos / Concepto / Parte / InformacionAduanera.
+
+
+## CuentaPredial `cfdi:CuentaPredial`
+
+Representa el nodo Comprobante / Conceptos / Concepto / CuentaPredial.
+
+
+## ComplementoConcepto `cfdi:ComplementoConcepto`
+
+Representa el nodo Comprobante / Conceptos / Concepto / ComplementoConcepto.
+
+
+## Parte `cfdi:Parte`
+
+Representa el nodo Comprobante / Conceptos / Concepto / Parte.
+
+- `addInformacionAduanera(array $attributes = []): InformacionAduanera`: Agrega y devuelve un nuevo nodo InformacionAduanera
+- `multiInformacionAduanera(array ...$elementAttributes): Parte`: Agrega nuevos nodo InformacionAduanera, es una forma rápida de
+ llamar al método `addInformacionAduanera` múltiples veces
+
+
+## Impuestos `cfdi:Impuestos`
+
+Representa el nodo Comprobante / Impuestos y también Comprobante / Conceptos / Concepto / Impuestos.
+
+- `getTraslados(): Traslados`: Crea (si no existe) y obtiene el nodo único Traslados.
+- `getRetenciones(): Retenciones`: Crea (si no existe) y obtiene el nodo único Retenciones.
+
+Aunque el nodo impuestos (hijo de comprobante) es diferente que el nodo impuestos (hijo de concepto)
+se puede utilizar la misma estructura de datos porque los cambios se dan a nivel de atributos y no de hijos.
+
+
+## Traslados `cfdi:Traslados`
+
+Representa el nodo Comprobante / Impuestos / Traslados y Comprobante / Conceptos / Concepto / Impuestos / Traslados.
+
+- `addTraslado(array $attributes = []): Traslado`: Agrega y devuelve un nuevo nodo Traslado.
+- `multiTraslado(array ...$elementAttributes): Traslados`: Agrega nuevos nodo Traslado, es una forma rápida de llamar al método `addTraslado` múltiples veces
+
+
+## Retenciones `cfdi:Retenciones`
+
+Representa el nodo Comprobante / Impuestos / Retenciones y Comprobante / Conceptos / Concepto / Impuestos / Retenciones.
+
+- `addRetencion(array $attributes = []): Retencion`: Agrega y devuelve un nuevo nodo Retencion.
+- `multiRetencion(array ...$elementAttributes): Retenciones`: Agrega nuevos nodo Retencion, es una forma rápida de llamar al método `addRetencion` múltiples veces
+
+
+## Traslado `cfdi:Traslado`
+
+Representa el nodo Comprobante / Impuestos / Retenciones / Traslado
+y Conceptos / Concepto / Impuestos / Retenciones / Traslado.
+
+
+## Retencion `cfdi:Retencion`
+
+Representa el nodo Comprobante / Impuestos / Retenciones / Retencion
+y Conceptos / Concepto / Impuestos / Retenciones / Retencion.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..db239553
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,80 @@
+# CfdiUtils
+
+[`eclipxe/CfdiUtils`](https://github.com/eclipxe13/CfdiUtils)
+es una librería de PHP para leer, validar y crear CFDI 3.3.
+
+Mira el archivo [README][] para información rápida (en inglés).
+
+**Este proyecto se migrará eventualmente a `phpcfdi/cfdiutils`, aun no hay fecha planeada.**
+
+La motivación de crear esta librería es contar con una herramienta flexible, rápida y
+confiable para trabajar con CFDI. Se pretende que sea utilizada por la comunidad de PHP
+México, en proyectos privados o proyectos libres como el futuro "BuzonCFDI".
+
+Esta librería se ha liberado como software libre para ayudar a otros desarrolladores a
+trabajar con CFDI y también para obtener su ayuda, todo lo que la comunidad pueda
+contribuir será bien apreciado. Tenemos una comunidad activa y dinámica, nos puedes
+encontrar en [el canal de gitter](https://gitter.im/eclipxe13/php-cfdi).
+
+## Lectura de CFDI
+
+La librería ofrece métodos para leer CFDI versión 3.2 y 3.3.
+
+- [Lectura formal de un CFDI](leer/leer-cfdi.md)
+- [Lectura rápida de un CFDI](leer/quickreader.md)
+- [Limpieza de un CFDI](leer/limpieza-cfdi.md)
+
+
+## Validación de CFDI
+
+Solo hay validadores para CFDI 3.3.
+
+- [Validar un CFDI 3.3](validar/validacion-cfdi.md)
+- [Validaciones estándar](validar/validaciones-estandar.md)
+
+
+## Escritura de CFDI
+
+Solo hay métodos específicos para CFDI 3.3.
+
+- [Crear un CFDI 3.3](crear/crear-cfdi.md)
+- [Elementos de CFDI](crear/elements-cfdi33.md)
+- [Agregar complementos](crear/complementos-aun-no-implementados.md)
+
+
+## Componentes comunes
+
+- [Estructura de datos `Nodes`](componentes/nodes.md)
+- [Estructura de datos `Elements`](componentes/elements.md)
+- [Almacenamiento local de recursos del SAT](componentes/xmlresolver.md)
+- [Certificados](componentes/certificado.md)
+- [Consultar estado de un CFDI](componentes/estado-sat.md)
+- [Generación de cadena original](componentes/cadena-de-origen.md)
+
+
+## Contribuciones
+
+- [Listado de tareas pendientes e ideas](TODO.md)
+- [Guía de contribución para desarrolladores](contribuir/guia-desarrollador.md)
+- [Guía de contribución para documentadores](contribuir/guia-documentador.md)
+- Reportar un problema
+
+
+## Recursos útiles
+
+- [Listado de cambios](CHANGELOG.md) (en inglés)
+- [Página del SAT de CFDI](http://omawww.sat.gob.mx/informacion_fiscal/factura_electronica/Paginas/Anexo_20_version3.3.aspx)
+
+
+## Copyright and License
+
+The `eclipxe/CfdiUtils` library is copyright © [Carlos C Soto](http://eclipxe.com.mx/) and
+licensed for use under the MIT License (MIT). Please see [LICENSE][] for more information.
+
+La librería `eclipxe/CfdiUtils` tiene copyright © [Carlos C Soto](http://eclipxe.com.mx/)
+y se encuentra amparada por la Licencia MIT (MIT).
+Consulte el archivo [LICENSE][] para más información.
+
+
+[license]: https://github.com/eclipxe13/CfdiUtils/blob/master/LICENSE
+[readme]: https://github.com/eclipxe13/CfdiUtils/blob/master/README.md
diff --git a/docs/leer/leer-cfdi.md b/docs/leer/leer-cfdi.md
new file mode 100644
index 00000000..d801a51e
--- /dev/null
+++ b/docs/leer/leer-cfdi.md
@@ -0,0 +1,170 @@
+# Leer un comprobante fiscal digital
+
+El problema de leer un CFDI es que la información entre versiones 3.3, 3.2
+y previas no es compatible. Por ello es necesario necesario primero
+averiguar la versión del archivo que deseamos interpretar.
+
+En esta sección se describe la lectura formal de la librería para leer nodos.
+Si tu intensión es solamente leer CFDI entonces te convendría brincar a la
+[lectura rápida usando `QuickReader`](quickreader.md).
+
+
+## Procesar un CFDI
+
+Esta librería almacena la información de un CFDI en una estructura interna llamada
+[`Nodes`](../componentes/nodes.md). Por lo que, al leer un CFDI lo que en realidad sucede es
+una conversión del contenido XML a esta estructura interna.
+
+
+### El objeto `CfdiUtils\Cfdi`
+
+Este es un ejemplo básico de lectura de un contenido XML a un objeto de
+tipo `CfdiUtils\Cfdi`. Por lo general se utiliza el método estático
+`CfdiUtils\Cfdi::newFromString`.
+
+La clase ofrece cuatro principales *getters* para trabajo, siendo el más importante
+el método `CfdiUtils\Cfdi::getNode` que devuelve la instancia del objeto de tipo
+`NodeInterface` del elemento principal ``.
+
+```php
+...';
+$cfdi = \CfdiUtils\Cfdi::newFromString($xmlContents);
+$cfdi->getVersion(); // (string) 3.3
+$cfdi->getDocument(); // clon del objeto DOMDocument
+$cfdi->getSource(); // (string) getNode(); // Nodo de trabajo del nodo cfdi:Comprobante
+```
+
+
+#### Uso de `CfdiUtils\Cfdi::newFromString`
+
+El método estático `CfdiUtils\Cfdi::newFromString` verifica que el contenido XML
+no esté vacío y no contenga errores (se pueda crear un `DOMDocument` a partir
+de este contenido).
+Posteriormente invoca la creación de un objeto de tipo `CfdiUtils\Cfdi` pasando
+el objeto `DOMDocument` como parámetro.
+
+
+#### Constructor de `CfdiUtils\Cfdi`
+
+Al crear un objeto de tipo `CfdiUtils\Cfdi` se verifican las siguientes reglas
+del objeto `DOMDocument`:
+
+1. el documento implementa el espacio de nombres del cfdi `http://www.sat.gob.mx/cfd/3`
+1. con el prefijo `cfdi`
+1. en el elemento raíz ``
+
+No realiza ninguna validación. La validación de un CFDI está fuera de los límites de esta clase.
+
+
+#### Ejemplos básicos de uso de `NodeInterface`
+
+Para obtener el atributo `Serie` de un complemento, sin importar que el atributo fuera definido
+originalmente, si no se definió entonces devolverá una cadena de caracteres vacía.
+
+```php
+getNode();
+echo $complemento['Serie'];
+```
+
+Para verificar si está especificado el atributo `MetodoPago`
+
+```php
+getNode();
+if (isset($complemento['MetodoPago']) {
+ // ...
+}
+```
+
+Para recorrer todos los nodos `cfdi:Concepto` (que está dentro de `cfdi:Conceptos`)
+se puede utilizar el método `CfdiUtils\Nodes\NodeInterface::searchNodes` que devuelve
+una colección de nodos iterable (que se puede utilizar dentro de un `foreach`).
+
+```php
+getNode();
+$conceptos = $complemento->searchNodes('cfdi:Conceptos', 'cfdi:Concepto');
+foreach ($conceptos as $concepto) {
+ echo $concepto['Unidad'];
+}
+```
+
+Para obtener la primer ocurrencia de un nodo de determinado nombre se puede usar
+el método `CfdiUtils\Nodes\NodeInterface::searchNode`. Este método devolverá el nodo
+si fue encontrado o devolverá `null` si no se encontró.
+
+No confundir con el método anterior que devuelve una colección de nodos.
+
+```php
+getNode();
+$tfd = $complemento->searchNodes('cfdi:Complemento', 'tfd:TimbreFiscalDigital');
+if (null === $tfd) {
+ echo 'No existe el timbre fiscal digital';
+} else {
+ echo 'UUID: ', $tfd['UUID'], PHP_EOL;
+}
+```
+
+Recuerde consultar la entrada completa relacionada con la [Estructura de datos `Nodes`](../componentes/nodes.md).
+
+
+## Obteniendo la versión de un CFDI sin la clase `CfdiUtils\Cfdi`
+
+Obtener la versión de un CFDI es sencillo con la clase `CfdiUtils\CfdiVersion`.
+
+El método que usarás para obtener la versión depende de la información que ya
+tengas instanciada:
+
+- `getFromXmlString()`: Cuando ya tienes el contenido del XML en una variable
+- `getFromNode()`: Cuando tienes el nodo principal en un objeto de tipo `CfdiUtils\Nodes\NodeInterface`
+- `getFromDOMDocument()` y `getFromDOMElement()`: Cuando tienes el contenido XML
+ instanciado en un objeto de tipo DOM.
+
+El resultado de estos métodos será un string con el número de versión y vacío en
+caso de no encontrarse un número de versión compatible.
+
+```php
+getFromXmlString($xmlContents);
+```
+
+Nota: la clase `CfdiUtils\Cfdi` ya realiza este proceso por lo que no es recomendado
+duplicar el trabajo de averiguar la versión.
+
+
+## Limpieza de CFDI
+
+Es frecuente que los archivos CFDI contengan errores.
+Para entender más el tema vea el artículo de [Limpieza de un CFDI](limpieza-cfdi-md).
+
+Si está leyendo un CFDI recibido o no confiable este es un ejemplo de cómo limpiar y crear el objeto CFDI:
+
+```php
+getQuickReader();
+```
+
+
+## Acceder a los atributos
+
+Utiliza el objeto como un arreglo usando la notación de corchetes.
+
+Puedes averiguar si un atributo existe usando `isset()`.
+
+```php
+getQuickReader();
+
+echo $comprobante['version']; // (string) "3.3"
+echo $comprobante['Version']; // (string) "3.3"
+echo $comprobante['vErSiOn']; // (string) "3.3"
+var_dump(isset($comprobante['version'])); // (bool) true
+
+
+var_dump($comprobante['no-existe']); // (string) ""
+var_dump(isset($comprobante['no-existe'])); // (bool) false
+```
+
+
+## Acceder al primer elemento hijo
+
+Puedes acceder al primer elemento hijo (exista o no) usando la notación de propiedades.
+
+Al acceder por medio de propiedades siempre devuelve un objeto de tipo `QuickReader`
+aun cuando no exista. Si existe devolverá el primero.
+
+Puedes averiguar si existe al menos un elemento usando `isset()`.
+
+```php
+getQuickReader();
+
+/*
+ *
+ * ...
+ *
+ */
+echo $comprobante->impuestos['totalImpuestosTrasladados']; // (string) "123.45"
+
+/*
+ *
+ *
+ *
+ *
+ *
+ */
+echo $comprobante->complemento->timbreFiscalDigital['fechatimbrado']; // 2017-03-21T08:18:08
+
+// aun si no existe un elemento no generará una excepción
+var_dump(isset($comprobante->foo)); // (bool) false
+echo $comprobante->foo->bar->baz->xee['info']; // (string) ""
+```
+
+
+## Acceder a todos los hijos
+
+Entrar a todos los hijos requiere de una sintaxis especial que consiste en llamar al objeto
+como una función. Al realizar la llamada lo que se devuelve es un arreglo de objetos `QuickReader` con los hijos.
+
+```php
+getQuickReader();
+
+// $hijos es un arreglo de QuickReader
+$hijos = $comprobante();
+
+foreach($hijos as $hijo) {
+ echo $hijo; // Emisor, Receptor, Conceptos, Impuestos, etc...
+}
+```
+
+La sintaxis de esta operación al principio puede ser un poco complicada, pero una vez que te acostumbras es
+bastante entendible.
+
+Por ejemplo, para acceder a todos los hijos del nodo `Conceptos` **no se puede hacer**:
+`$comprobante->conceptos()` porque esto significa invocar al método `conceptos` del objeto `$comprobante`.
+
+Hay dos alternativas para poder hacer esta llamada:
+
+* Asignar a una variable y luego hacer la invocación:
+
+ `$conceptos = $comprobante->conceptos; $conceptos();`
+
+* Usar paréntesis para separar la propiedad, y luego invocarla:
+
+ `($comprobante->conceptos)()`
+
+Si uso dependerá de tu preferencia, aquí dos formas que ejemplifican lo mismo,
+primero una variable para acceder a los hijos (conceptos), luego se hace una invocación de la propiedad (traslado).
+
+```php
+getQuickReader();
+
+// usando asignación de variable
+$conceptos = $comprobante->conceptos;
+foreach($conceptos() as $concepto) {
+ // usando propiedad
+ foreach(($concepto->impuestos->traslados)() as $traslado) {
+ echo $traslado['impuesto'];
+ }
+}
+```
+
+
+## Acceder a hijos con un mismo nombre
+
+Si requieres todos los nodos hijos a los que les corresponda un mismo nombre
+simplemente pasa el nombre como argumento de la ejecución del objeto.
+
+```php
+getQuickReader();
+
+// $hijos es un arreglo que contiene solo aquellos hijos llamados "concepto"
+$hijos = ($comprobante->conceptos)('concepto');
+```
+
+
+## Nombre del nodo
+
+Difícilmente lo utilizarás, pero si necesitas saber el nombre del nodo entonces puedes
+convertir el nodo a cadena de caracteres y te devolverá su nombre.
+
+```php
+getQuickReader();
+
+echo (string) $comprobante; // (string) "Comprobante"
+```
diff --git a/docs/validar/validacion-cfdi.md b/docs/validar/validacion-cfdi.md
new file mode 100644
index 00000000..754adf2d
--- /dev/null
+++ b/docs/validar/validacion-cfdi.md
@@ -0,0 +1,97 @@
+# Validaciones de CFDI version 3.3
+
+Esta librería provee recursos para realizar validaciones en el espacio de nombres `CfdiUtils\Validate`.
+
+Se busca que al validar no solo se reporten las validaciones con error. También se reportan aquellas
+exitosas, las que tienen una advertencia y las correctas, incluso algunas podrían contener una explicación.
+
+A diferencia de los mensajes de error de toda la librería, todos los mensajes de las validaciones están en español.
+
+El espacio de nombres contiene un validador `MultiValidator`
+que comúnmente se genera con una fábrica `MultiValidatorFactory`.
+Gracias a este proceso validar documentos creados o recibidos se simplifica.
+
+
+## Validación de documentos creados
+
+Si se está creando un documento usando la clase `CfdiUtils\CfdiCreator33`
+entonces se puede validar usando el método `validate(): Asserts`, por ejemplo:
+
+```php
+validate();
+$asserts->hasErrors(); // devuelve verdadero en caso de error
+```
+
+
+## Validación de documentos recibidos
+
+Para esta validar un documento recibido se puede utilizar la clase `CfdiUtils\CfdiValidator33`, por ejemplo:
+
+```php
+validateXml(file_get_contents($cfdiFile));
+$asserts->hasErrors(); // devuelve verdadero en caso de error
+```
+
+Un objeto de tipo `CfdiValidator33` contiene un `XmlResolver`.
+Si se elimina entonces algunos validadores no realizarán el proceso o bien saldrán a internet a encontrar
+los recursos que necesitan. Por omisión se crea un nuevo `XmlResolver` pero puede ser establecido
+desde su contructor o bien con el método `setXmlResolver`.
+
+Recuerda que la validación trabajará con la información tal comno es presentada, por lo que tal vez
+desees usar el método rápido de limpieza `CfdiUtils\Cleaner\Cleaner::staticClean()`.
+
+
+## ValidatorInterface
+
+Para que un validador funcione necesita ser de tipo `ValidatorInterface` e implementar:
+
+- `validate(NodeInterface $comprobante, Asserts $asserts): void`: Método que se llama para validar.
+- `canValidateCfdiVersion(string $version): bool`: Devuelve si el validador es compatible con una versión dada.
+
+
+## Assert
+
+Cada validador debe inyectar uno o más objetos de tipo `Assert` en la colección `Asserts`.
+Se puede considerar que un `Assert` es una prueba o un aseguramiento, y cada `Assert` tiene un estado dado por `Status`.
+
+Gracias al registro de todos los `Assert` en una validación se puede saber no solo lo que falló o generó
+una advertencia; también se puede saber lo que estuvo bien o no se pudo comprobar.
+
+El `Assert` es un "aseguramiento", se trata de un enunciado afirmativo, no un enunciado de error, por ello,
+un ejemplo de título del aseguramiento podría ser: *El CFDI tiene una moneda definida y que pertenece al catálogo de monedas*.
+
+
+## Status
+
+Esta es una clase de tipo "value object" por lo que solamente se puede instanciar con un valor y no modificar.
+
+Un objeto `Status` puede contener uno de cuatro valores:
+
+- error: Existe un fallo y se debe considerar que el CFDI es inválido y debería ser rechazado.
+- warning: Existe un fallo pero se desconoce si esto es correcto o incorrecto.
+- ok: Se realizó la prueba y no se encontró fallo
+- none: Ninguno de los estados anteriores, úsese para describir que la prueba no se realizó.
+
+
+## Asserts
+
+`Asserts` es una colección de objetos de tipo `Assert`.
+Esta colección no permite que existan dos `Assert` con el mismo código, cuando se encuentra que se quiere
+escribir un `Assert` con el mismo código entonces el previo es sobre escrito.
+
+```php
+validate();
+foreach ($asserts as $assert) {
+ echo $assert, "\n";
+}
+```
diff --git a/docs/validar/validaciones-estandar.md b/docs/validar/validaciones-estandar.md
new file mode 100644
index 00000000..fa48c82f
--- /dev/null
+++ b/docs/validar/validaciones-estandar.md
@@ -0,0 +1,183 @@
+# Validaciones estándar para CFDI 3.3
+
+Las validaciones estándar se deben realizar tanto para CFDI crceados como para CFDI recibidos.
+
+
+## XmlFollowSchema
+
+Valida que el archivo XML sigue con los esquemas que tiene declarados contra los
+archivos XSD que tenga declarados en los campos `xsi:schemaLocation`.
+Este es uno de los validadores más útiles porque revisa la estructura contra el SAT, incluyendo los catálogos.
+Cuando este validador falla regresa un estado `mustStop` que previene la ejecución de futuros
+validadores dentro de un objeto `MultiValidator`.
+
+- XDS01: El contenido XML sigue los esquemas XSD
+
+
+## ComprobanteDecimalesMoneda
+
+Valida que los atributos no tengan más del máximo número de decimales que permite la moneda,
+esto incluye los ceros a la izquierda. Si la moneda no es USD, EUR, MXN o XXX entonces todos los estados son NONE.
+
+- MONDEC01: El subtotal del comprobante no contiene más de los decimales de la moneda (CFDI33106)
+- MONDEC02: El descuento del comprobante no contiene más de los decimales de la moneda (CFDI33111)
+- MONDEC03: El total del comprobante no contiene más de los decimales de la moneda
+- MONDEC04: El total de impuestos trasladados no contiene más de los decimales de la moneda (CFDI33182)
+- MONDEC05: El total de impuestos retenidos no contiene más de los decimales de la moneda (CFDI33180)
+
+
+## ComprobanteFormaPago
+
+Valida que exista si no existe el complemento de pagos y que no exista si existe el complemento de pagos
+
+- FORMAPAGO01: El campo forma de pago no debe existir cuando existe el complemento para recepción de pagos (CFDI33103)
+
+
+## ComprobanteImpuestos
+
+Valida el nodo impuestos del comprobante
+
+- COMPIMPUESTOSC01: Si existe el nodo impuestos entonces debe incluir el total de traslados y/o el total de retenciones
+- COMPIMPUESTOSC02: Si existe al menos un traslado entonces debe existir el total de traslados
+- COMPIMPUESTOSC03: Si existe al menos una retención entonces debe existir el total de retenciones
+
+
+## ComprobanteMetodoPago
+
+Validaciones específicas relacionadas con el método de pago
+
+- METPAG01: Si el tipo de documento es T, P ó N, entonces el metodo de pago no debe existir (CFDI33123, CFDI33124)
+- METPAG02: Si el tipo de documento es I ó E, entonces el metodo de pago debe ser "PUE" o "PPD" (CFDI33121, CFDI33122)
+
+
+## ComprobanteTipoCambio
+
+- TIPOCAMBIO01: La moneda exista y no tenga un valor vacío
+- TIPOCAMBIO02: Si la moneda es "MXN", entonces el tipo de cambio debe ser "1" o no debe existir (CFDI33113)
+- TIPOCAMBIO03: Si la moneda es "XXX", entonces el tipo de cambio no debe existir (CFDI33115)
+- TIPOCAMBIO04: Si la moneda no es "MXN" ni "XXX", entonces el tipo de cambio entonces
+ el tipo de cambio debe seguir el patrón [0-9]{1,18}(.[0-9]{1,6})? (CFDI33114, CFDI33117)
+
+
+## ComprobanteTipoDeComprobante
+
+Realiza diferentes validaciones relacionadas con el tipo de comprobante:
+
+- TIPOCOMP01: Si el tipo de comprobante es T, P ó N, entonces no debe existir las condiciones de pago
+- TIPOCOMP02: Si el tipo de comprobante es T, P ó N, entonces no debe existir la definición de impuestos (CFDI33179)
+- TIPOCOMP03: Si el tipo de comprobante es T, P ó N, entonces no debe existir la forma de pago
+- TIPOCOMP04: Si el tipo de comprobante es T, P ó N, entonces no debe existir el método de pago (CFDI33123)
+- TIPOCOMP05: Si el tipo de comprobante es T ó P, entonces no debe existir el descuento del comprobante (CFDI33110)
+- TIPOCOMP06: Si el tipo de comprobante es T ó P, entonces no debe existir el descuento de los conceptos (CFDI33179)
+- TIPOCOMP07: Si el tipo de comprobante es T ó P, entonces el subtotal debe ser cero (CFDI33108)
+- TIPOCOMP08: Si el tipo de comprobante es T ó P, entonces el total debe ser cero
+- TIPOCOMP09: Si el tipo de comprobante es I, E ó N, entonces el valor unitario de todos los conceptos debe ser mayor que cero
+- TIPOCOMP010: Si el tipo de comprobante es N, entonces la moneda debe ser MXN
+
+
+## ComprobanteTotal
+
+- TOTAL01: El atributo Total existe, no está vacío y cumple con el patrón [0-9]+(.[0-9]+)?
+
+
+## ConceptoDescuento
+
+Estas validaciones son exclusivas del atributo descuento del concepto:
+
+- CONCEPDESC01: Si existe el atributo descuento, entonces debe ser menor o igual que el subtotal y mayor o igual que cero (CFDI33109)
+
+
+## ConceptoImpuestos
+
+Estas validaciones son exclusivas del nodo impuestos del concepto:
+
+- CONCEPIMPC01: El nodo impuestos de un concepto debe incluir traslados y/o retenciones (CFDI33152)
+- CONCEPIMPC02: Los traslados de los impuestos de un concepto deben tener una base y ser mayor a cero (CFDI33154)
+- CONCEPIMPC03: No se debe registrar la tasa o cuota ni el importe cuando el tipo de factor de traslado es exento (CFDI33157)
+- CONCEPIMPC04: Se debe registrar la tasa o cuota y el importe cuando el tipo de factor de traslado es tasa o cuota (CFDI33158)
+- CONCEPIMPC05: Las retenciones de los impuestos de un concepto deben tener una base y ser mayor a cero (CFDI33154)
+- CONCEPIMPC06: Las retenciones de los impuestos de un concepto deben tener un tipo de factor diferente de exento (CFDI33166)
+
+
+## EmisorRegimenFiscal
+
+- REGFIS01: El régimen fiscal contenga un valor apropiado según el tipo de RFC emisor (CFDI33130 y CFDI33131)
+
+
+## FechaComprobante
+
+Valida que la fecha del comprobante:
+
+- FECHA01: La fecha del comprobante cumple con el formato
+- FECHA02: La fecha existe en el comprobante y es mayor que 2017-07-01 y menor que el futuro
+ - La fecha en el futuro se puede configurar a un valor determinado
+ - La fecha en el futuro es por defecto el momento de validación más una tolerancia
+ - La tolerancia puede ser configurada y es por defecto 300 segundos
+
+
+## ReceptorResidenciaFiscal
+
+- RESFISC01: Si el RFC no es XEXX010101000, entonces la residencia fiscal no debe existir (CFDI33134)
+- RESFISC02: Si el RFC sí es XEXX010101000 y existe el complemento de comercio exterior, entonces la residencia fiscal debe establecerse y no puede ser "MEX" (CFDI33135 y CFDI33136)
+- RESFISC03: Si el RFC sí es XEXX010101000 y se registró el número de registro de identificación fiscal, entonces la residencia fiscal debe establecerse y no puede ser "MEX" (CFDI33135 y CFDI33136)
+
+
+## SelloDigitalCertificado
+
+Valida el Sello del comprobante y el Certificado
+
+- SELLO01: Se puede obtener el certificado del comprobante
+- SELLO02: El número de certificado del comprobante igual al encontrado en el certificado
+- SELLO03: El RFC del comprobante igual al encontrado en el certificado
+- SELLO04: El nombre del emisor del comprobante igual al encontrado en el certificado
+- SELLO05: La fecha del documento es mayor o igual a la fecha de inicio de vigencia del certificado
+- SELLO06: La fecha del documento menor o igual a la fecha de fin de vigencia del certificado
+- SELLO07: El sello del comprobante está en base 64
+- SELLO08: El sello del comprobante coincide con el certificado y la cadena de origen generada
+
+
+## SumasConceptosComprobanteImpuestos
+
+Obtiene las sumas de los importes de los conceptos y las sumas agrupadas de los impuestos y las valida contra la información del comprobante y el nodo principal de impuestos.
+
+- SUMAS01: La suma de los importes de conceptos es igual a el subtotal del comprobante
+- SUMAS02: La suma de los descuentos es igual a el descuento del comprobante
+- SUMAS03: El cálculo del total es igual a el total del comprobante
+- SUMAS04: El cálculo de impuestos trasladados es igual a el total de impuestos trasladados
+- SUMAS05: Todos los impuestos trasladados existen en el comprobante
+- SUMAS06: Todos los valores de los impuestos trasladados conciden con el comprobante
+- SUMAS07: No existen más nodos de impuestos trasladados en el comprobante de los que se han calculado
+- SUMAS08: El cálculo de impuestos retenidos es igual a el total de impuestos retenidos
+- SUMAS09: Todos los impuestos retenidos existen en el comprobante
+- SUMAS10: Todos los valores de los impuestos retenidos conciden con el comprobante
+- SUMAS11: No existen más nodos de impuestos trasladados en el comprobante de los que se han calculado
+- SUMAS12: El cálculo del descuento debe ser menor o igual al cálculo del subtotal
+
+
+## TimbreFiscalDigitalSello
+
+Posiblemente este es el **validador más importante** porque se encarga de comprobar que
+el CFDI no fue modificado después de haber sido sellado.
+
+- TFDSELLO01: El Sello SAT del Timbre Fiscal Digital corresponde al certificado SAT
+
+Esto lo hace de la siguiente forma:
+
+1. Obtiene el TimbreFiscalDigital, si no existe entonces no hay qué validar.
+1. Corrobora que sea versión 1.1, si no lo es entonces no hay qué validar
+1. Se asegura que cuente con SelloCFD y que coincida con el Sello del comprobante.
+1. Se asegura que NoCertificadoSAT contenga un número válido.
+1. Obtiene el certificado con el que fue sellado desde el sitio del SAT `https://rdc.sat.gob.mx/`.
+ Si no se pudo obtener entonces el resultado será de error.
+1. Fabrica la cadena de origen del TimbreFiscalDigital.
+1. Verifica que el sello corresponde con la cadena de origen usando el certificado.
+
+Es posible que un emisor intente modificar el comprobante, simplemente debe alterar el contenido
+sin modificar el TimbreFiscalDigital ni el atributo Sello del comprobante.
+En ese caso este validador no marcará error, pero sí lo hará el validador `SelloDigitalCertificado`
+al encontrar que el Sello del comprobante no coincide con la cadena de origen.
+
+
+## TimbreFiscalDigitalVersion
+
+- TFDVERSION01: Si existe el complemento timbre fiscal digital, entonces su versión debe ser 1.1
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..738fcadd
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,14 @@
+site_name: CfdiUtils
+site_description: Librería de utilerías comunes para trabajar con PHP y CFDI 3.2 & 3.3 (México)
+
+theme: readthedocs
+
+markdown_extensions:
+ - toc:
+ permalink:
+ - admonition
+ - def_list
+ - fenced_code
+
+copyright: Copyright © MIT License 2017 - 2018 Carlos C Soto & contibutors
+
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..6c9d2d1c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "devDependencies": {
+ "markdownlint-cli": "^0.10.0"
+ }
+}
diff --git a/src/CfdiUtils/Elements/Cfdi33/CfdiRelacionados.php b/src/CfdiUtils/Elements/Cfdi33/CfdiRelacionados.php
index f2da1e38..0c627082 100644
--- a/src/CfdiUtils/Elements/Cfdi33/CfdiRelacionados.php
+++ b/src/CfdiUtils/Elements/Cfdi33/CfdiRelacionados.php
@@ -16,4 +16,12 @@ public function addCfdiRelacionado(array $attributes = []): CfdiRelacionado
$this->addChild($cfdiRelacionado);
return $cfdiRelacionado;
}
+
+ public function multiCfdiRelacionado(array $elementAttributes = []): self
+ {
+ foreach ($elementAttributes as $attributes) {
+ $this->addCfdiRelacionado($attributes);
+ }
+ return $this;
+ }
}
diff --git a/src/CfdiUtils/Elements/Cfdi33/Comprobante.php b/src/CfdiUtils/Elements/Cfdi33/Comprobante.php
index 1270a2b8..58fac88e 100644
--- a/src/CfdiUtils/Elements/Cfdi33/Comprobante.php
+++ b/src/CfdiUtils/Elements/Cfdi33/Comprobante.php
@@ -14,9 +14,28 @@ public function getElementName(): string
return 'cfdi:Comprobante';
}
- public function getCfdiRelacionados(array $attributes = []): CfdiRelacionados
+ /**
+ * @todo Remove this deprecation error on version 3.0.0
+ * @return CfdiRelacionados
+ */
+ public function getCfdiRelacionados(): CfdiRelacionados
{
- return $this->helperGetOrAdd(new CfdiRelacionados($attributes));
+ $arguments = func_get_args();
+ if (count($arguments) > 0) {
+ trigger_error(
+ 'El método getCfdiRelacionados ya no admite atributos, use addCfdiRelacionados en su lugar',
+ E_USER_NOTICE
+ );
+ return $this->addCfdiRelacionados($arguments[0]);
+ }
+ return $this->helperGetOrAdd(new CfdiRelacionados());
+ }
+
+ public function addCfdiRelacionados(array $attributes = []): CfdiRelacionados
+ {
+ $cfdiRelacionados = $this->getCfdiRelacionados();
+ $cfdiRelacionados->addAttributes($attributes);
+ return $cfdiRelacionados;
}
public function addCfdiRelacionado(array $attributes = []): CfdiRelacionado
@@ -24,6 +43,12 @@ public function addCfdiRelacionado(array $attributes = []): CfdiRelacionado
return $this->getCfdiRelacionados()->addCfdiRelacionado($attributes);
}
+ public function multiCfdiRelacionado(array ...$elementAttributes): self
+ {
+ $this->getCfdiRelacionados()->multiCfdiRelacionado($elementAttributes);
+ return $this;
+ }
+
public function getEmisor(): Emisor
{
return $this->helperGetOrAdd(new Emisor());
diff --git a/src/CfdiUtils/Utils/Rfc.php b/src/CfdiUtils/Utils/Rfc.php
index f25ce9a7..a7319c48 100644
--- a/src/CfdiUtils/Utils/Rfc.php
+++ b/src/CfdiUtils/Utils/Rfc.php
@@ -15,11 +15,19 @@ class Rfc
/** @var int */
private $length;
+ /** @var string contains calculated checksum */
+ private $checkSum;
+
+ /** @var bool */
+ private $checkSumMatch;
+
public function __construct(string $rfc, int $flags = 0)
{
$this->checkIsValid($rfc, $flags);
$this->rfc = $rfc;
- $this->length = mb_strlen($this->rfc);
+ $this->length = mb_strlen($rfc);
+ $this->checkSum = static::obtainCheckSum($rfc);
+ $this->checkSumMatch = ($this->checkSum === (string) substr($rfc, -1));
}
public function rfc(): string
@@ -47,6 +55,16 @@ public function isForeign(): bool
return (static::RFC_FOREIGN === $this->rfc);
}
+ public function checkSum(): string
+ {
+ return $this->checkSum;
+ }
+
+ public function checkSumMatch(): bool
+ {
+ return $this->checkSumMatch;
+ }
+
public function __toString(): string
{
return $this->rfc();
@@ -91,13 +109,6 @@ public static function checkIsValid(string $value, int $flags = 0)
if (0 === static::obtainDate($value)) {
throw new \UnexpectedValueException('La fecha obtenida no es lógica');
}
- if (! in_array($value, [static::RFC_FOREIGN, static::RFC_GENERIC], true)) {
- $last = substr($value, -1);
- $expected = static::obtainCheckSum($value);
- if ($last !== $expected) {
- throw new \UnexpectedValueException("El dígito verificador no coincide, debería ser $expected");
- }
- }
}
public static function obtainCheckSum(string $rfc): string
diff --git a/tests/CfdiUtilsTests/CreateComprobanteCaseTest.php b/tests/CfdiUtilsTests/CreateComprobanteCaseTest.php
index 5b3ff0b5..9a32bd03 100644
--- a/tests/CfdiUtilsTests/CreateComprobanteCaseTest.php
+++ b/tests/CfdiUtilsTests/CreateComprobanteCaseTest.php
@@ -111,9 +111,6 @@ public function testCreateCfdiUsingComprobanteElement()
// validate the comprobante and check it has no errors or warnings
$asserts = $creator->validate();
- // EMISORRFC01 - The RFC AAA010101AAA used for testing is not valid !
- $asserts->removeByCode('EMISORRFC01');
-
$this->assertFalse($asserts->hasErrors());
$this->assertFalse($asserts->hasStatus(Status::warn()));
diff --git a/tests/CfdiUtilsTests/Elements/Cfdi33/ComprobanteGetCfdiRelacionadosTest.php b/tests/CfdiUtilsTests/Elements/Cfdi33/ComprobanteGetCfdiRelacionadosTest.php
new file mode 100644
index 00000000..26ba9425
--- /dev/null
+++ b/tests/CfdiUtilsTests/Elements/Cfdi33/ComprobanteGetCfdiRelacionadosTest.php
@@ -0,0 +1,60 @@
+{'getCfdiRelacionados'}([]); to avoid phpstan errors
+ *
+ */
+
+class ComprobanteGetCfdiRelacionadosTest extends TestCase
+{
+ private $errors = [];
+
+ protected function setUp()
+ {
+ parent::setUp();
+ set_error_handler([$this, 'errorHandler'], E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE | E_USER_DEPRECATED);
+ }
+
+ public function errorHandler($errno, $errstr, $errfile, $errline, $errcontext)
+ {
+ $this->errors[] = compact('errno', 'errstr', 'errfile', 'errline', 'errcontext');
+ }
+
+ public function testGetCfdiRelacionadoDontTriggerErrorsWhenCallWithoutArgument()
+ {
+ $comprobante = new Comprobante();
+ $comprobante->getCfdiRelacionados();
+ $this->assertCount(0, $this->errors);
+ }
+
+ public function testErrorWhenPassAnArrayAsArgument()
+ {
+ $comprobante = new Comprobante();
+ $comprobante->{'getCfdiRelacionados'}([]);
+ $this->assertCount(1, $this->errors);
+
+ $expectedError = [
+ 'errno' => E_USER_NOTICE,
+ 'errstr' => 'El método getCfdiRelacionados ya no admite atributos, use addCfdiRelacionados en su lugar',
+ ];
+
+ $this->assertArraySubset($expectedError, $this->errors[0]);
+ }
+
+ public function testStillIsWorkingWhenPassAnArrayAsArgument()
+ {
+ $comprobante = new Comprobante();
+ $cfdiRelacionados = $comprobante->{'getCfdiRelacionados'}(['foo' => 'bar']);
+ $this->assertSame('bar', $cfdiRelacionados['foo']);
+ }
+}
diff --git a/tests/CfdiUtilsTests/Elements/Cfdi33/Helpers/SumasConceptosWriterTest.php b/tests/CfdiUtilsTests/Elements/Cfdi33/Helpers/SumasConceptosWriterTest.php
index 73ae3da9..2cb349c5 100644
--- a/tests/CfdiUtilsTests/Elements/Cfdi33/Helpers/SumasConceptosWriterTest.php
+++ b/tests/CfdiUtilsTests/Elements/Cfdi33/Helpers/SumasConceptosWriterTest.php
@@ -164,14 +164,37 @@ public function testDescuentoWithValueZeroExistsIfAConceptoHasDescuento()
public function testDescuentoNotSetIfAllConceptosDoesNotHaveDescuento()
{
$comprobante = new Comprobante(['Descuento' => '']); // set value with discount
- $comprobante->addConcepto([]); // first concepto does not have Descuento
- $comprobante->addConcepto(); // second concepto has Descuento
+ $comprobante->addConcepto(); // first concepto does not have Descuento
+ $comprobante->addConcepto(); // second concepto does not have Descuento neither
$precision = 2;
$sumasConceptos = new SumasConceptos($comprobante, $precision);
$writer = new SumasConceptosWriter($comprobante, $sumasConceptos, $precision);
$writer->put();
+ // the Comprobante@Descuento attribute must not exists since there is no Descuento in concepts
$this->assertFalse(isset($comprobante['Descuento']));
}
+
+ public function testOnComplementoImpuestosImporteSumIsRounded()
+ {
+ $comprobante = new Comprobante();
+ $comprobante->addConcepto()->addTraslado(
+ ['Importe' => '7.777777', 'Impuesto' => '002', 'TipoFactor' => 'Tasa', 'TasaOCuota' => '0.160000']
+ );
+ $comprobante->addConcepto()->addTraslado(
+ ['Importe' => '2.222222', 'Impuesto' => '002', 'TipoFactor' => 'Tasa', 'TasaOCuota' => '0.160000']
+ );
+
+ $precision = 3;
+ $sumasConceptos = new SumasConceptos($comprobante, $precision);
+ $writer = new SumasConceptosWriter($comprobante, $sumasConceptos, $precision);
+ $writer->put();
+
+ $this->assertSame('10.000', $comprobante->searchAttribute('cfdi:Impuestos', 'TotalImpuestosTrasladados'));
+ $this->assertSame(
+ '10.000',
+ $comprobante->searchAttribute('cfdi:Impuestos', 'cfdi:Traslados', 'cfdi:Traslado', 'Importe')
+ );
+ }
}
diff --git a/tests/CfdiUtilsTests/SumasConceptos/SumasConceptosTest.php b/tests/CfdiUtilsTests/SumasConceptos/SumasConceptosTest.php
index 4accbadd..e1706610 100644
--- a/tests/CfdiUtilsTests/SumasConceptos/SumasConceptosTest.php
+++ b/tests/CfdiUtilsTests/SumasConceptos/SumasConceptosTest.php
@@ -137,4 +137,21 @@ public function testFoundAnyConceptWithDiscount()
$comprobante->addConcepto(['Importe' => '333.33', 'Descuento' => '']);
$this->assertTrue((new SumasConceptos($comprobante))->foundAnyConceptWithDiscount());
}
+
+ public function testImpuestoImporteWithMoreDecimalsThanThePrecisionIsRounded()
+ {
+ $comprobante = new Comprobante();
+ $comprobante->addConcepto()->addTraslado(
+ ['Importe' => '7.777777', 'Impuesto' => '002', 'TipoFactor' => 'Tasa', 'TasaOCuota' => '0.160000']
+ );
+ $comprobante->addConcepto()->addTraslado(
+ ['Importe' => '2.222222', 'Impuesto' => '002', 'TipoFactor' => 'Tasa', 'TasaOCuota' => '0.160000']
+ );
+
+ $sumas = new SumasConceptos($comprobante, 3);
+
+ $this->assertTrue($sumas->hasTraslados());
+ $this->assertEquals(10.0, $sumas->getImpuestosTrasladados(), '', 0.0001);
+ $this->assertEquals(10.0, $sumas->getTraslados()['002:Tasa:0.160000']['Importe'], '', 0.0000001);
+ }
}
diff --git a/tests/CfdiUtilsTests/Utils/RfcTest.php b/tests/CfdiUtilsTests/Utils/RfcTest.php
index f1ede247..80cf186d 100644
--- a/tests/CfdiUtilsTests/Utils/RfcTest.php
+++ b/tests/CfdiUtilsTests/Utils/RfcTest.php
@@ -15,6 +15,8 @@ public function testCreateRfcPerson()
$this->assertFalse($rfc->isForeign());
$this->assertFalse($rfc->isMoral());
$this->assertTrue($rfc->isPerson());
+ $this->assertSame('A', $rfc->checkSum());
+ $this->assertTrue($rfc->checkSumMatch());
}
public function testCreateRfcMoral()
@@ -76,9 +78,9 @@ public function testCreateBadDate()
public function testCreateBadDigit()
{
- $this->expectException(\UnexpectedValueException::class);
- $this->expectExceptionMessage('dígito verificador');
- new Rfc('COSC8001137N9');
+ $rfc = new Rfc('COSC8001137N9');
+ $this->assertSame('A', $rfc->checkSum());
+ $this->assertFalse($rfc->checkSumMatch());
}
public function testIsValid()
@@ -88,7 +90,7 @@ public function testIsValid()
public function testIsNotValid()
{
- $this->assertFalse(Rfc::isValid('COSC8001137N9'));
+ $this->assertFalse(Rfc::isValid('COSC8099137NA'));
}
public function testWithMultiByte()
diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/CfdiRelacionadosTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/CfdiRelacionadosTest.php
index ae113f7c..7498468b 100644
--- a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/CfdiRelacionadosTest.php
+++ b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/CfdiRelacionadosTest.php
@@ -18,7 +18,7 @@ protected function setUp()
public function testValidTipoRelacion()
{
$comprobante = $this->getComprobante();
- $comprobante->getCfdiRelacionados(['TipoRelacion' => '04']);
+ $comprobante->addCfdiRelacionados(['TipoRelacion' => '04']);
$this->runValidate();
@@ -28,7 +28,7 @@ public function testValidTipoRelacion()
public function testInvalidTipoRelacion()
{
$comprobante = $this->getComprobante();
- $comprobante->getCfdiRelacionados(['TipoRelacion' => 'XX']);
+ $comprobante->addCfdiRelacionados(['TipoRelacion' => 'XX']);
$this->runValidate();
diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoBeneficiarioRfcCorrectoTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoBeneficiarioRfcCorrectoTest.php
index 4d69dd02..032e09be 100644
--- a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoBeneficiarioRfcCorrectoTest.php
+++ b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoBeneficiarioRfcCorrectoTest.php
@@ -26,7 +26,7 @@ public function testValid($rfc)
/**
* @param string|null $rfc
- * @testWith ["COSC8001137N1"]
+ * @testWith ["COSC8099137N1"]
* ["XAXX010101000"]
* [""]
*/
diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoOrdenanteRfcCorrectoTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoOrdenanteRfcCorrectoTest.php
index b7c52b83..9a415582 100644
--- a/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoOrdenanteRfcCorrectoTest.php
+++ b/tests/CfdiUtilsTests/Validate/Cfdi33/RecepcionPagos/Pagos/BancoOrdenanteRfcCorrectoTest.php
@@ -26,7 +26,7 @@ public function testValid($rfc)
/**
* @param string|null $rfc
- * @testWith ["COSC8001137N1"]
+ * @testWith ["COSC8099137N1"]
* ["XAXX010101000"]
* [""]
*/
diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/EmisorRfcTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/EmisorRfcTest.php
index 050f54ac..9337c066 100644
--- a/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/EmisorRfcTest.php
+++ b/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/EmisorRfcTest.php
@@ -45,10 +45,9 @@ public function providerInvalidCases()
return [
'none' => [null],
'empty' => [''],
- 'wrong' => ['COSC8001137N0'],
+ 'wrong' => ['COSC8099137NA'],
'generic' => [Rfc::RFC_GENERIC],
'foreign' => [Rfc::RFC_FOREIGN],
- 'testing' => ['AAA010101AAA'],
];
}
diff --git a/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/ReceptorRfcTest.php b/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/ReceptorRfcTest.php
index 3bcec490..835952a9 100644
--- a/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/ReceptorRfcTest.php
+++ b/tests/CfdiUtilsTests/Validate/Cfdi33/Standard/ReceptorRfcTest.php
@@ -47,7 +47,7 @@ public function providerInvalidCases()
return [
'none' => [null],
'empty' => [''],
- 'wrong' => ['COSC8001137N0'],
+ 'wrong' => ['COSC8099137NA'],
];
}