diff --git a/README.md b/README.md index b3b78fa7..3e5c0809 100644 --- a/README.md +++ b/README.md @@ -59,14 +59,14 @@ composer require eclipxe/cfdiutils - 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 8.0. - - It could be possible to migrate to phpcfdi/cfdi-utils under [phpCfdi][] organization. + - It could be possible to migrate to `phpcfdi/cfdi-utils` under [phpCfdi][] organization. ## PHP Support This library is compatible with **PHP 7.3 and above**. Please, try to use the language's full potential. -The intended support is to be aligned with oldest *Active support* PHP Branch. +The intended support is to be aligned with the oldest *Active support* PHP Branch. See for more details. | CfdiUtils | PHP Supported versions | Since | diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f08ee22e..f1a6081c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,7 +11,7 @@ - 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. -- Change signature of `CfdiUtils\Elements\Cfdi33\CfdiRelacionados::multiCfdiRelacionado` to receive as paremers +- Change signature of `CfdiUtils\Elements\Cfdi33\CfdiRelacionados::multiCfdiRelacionado` to receive as parameters `array ...$elementAttributes` instead of `array $elementAttributes`. - Refactor `\CfdiUtils\Certificado\SerialNumber` to be immutable, this change will remove `loadHexadecimal`, `loadDecimal` and `loadAscii`. @@ -30,6 +30,20 @@ - Remove `CfdiUtils\Validate\Cfdi33\Xml\XmlFollowSchema`. - Remove classes `CfdiUtils\Elements\Cfdi33\Helpers\SumasConceptosWriter` and `CfdiUtils\Elements\Cfdi40\Helpers\SumasConceptosWriter`. - Merge methods from `\CfdiUtils\Nodes\NodeHasValueInterface` into `\CfdiUtils\Nodes\NodeInterface`. +- Remove deprecated constant `CfdiUtils\Retenciones\Retenciones::RET_NAMESPACE`. + +## Version 2.22.0 2022-05-15 + +Add support to read and create a RET 1.0 (*Retenciones e información de pagos 2.0*) document. + +- Add helper elements on namespace `CfdiUtils\Elements\Retenciones20`. +- Add `CfdiUtils\Retenciones\RetencionVersion`. +- Add `CfdiUtils\Retenciones\RetencionesCreator20`. +- Move shared methods from `CfdiUtils\Retenciones\RetencionesCreator10` to `CfdiUtils\Retenciones\RetencionesCreatorTrait`. +- Refactor `CfdiUtils\Retenciones\Retenciones` to read versions 1.0 and 2.0. +- Improve documentation about RET 2.0. + +Thanks `@gam04` for your contribution. ## Version 2.21.0 2022-04-29 @@ -39,7 +53,7 @@ ## Version 2.20.2 2022-04-05 -Allow installing Genkgo/Xsl version 1.1.0; used for PHP >= 7.4. +Allow installing `Genkgo/Xsl` version 1.1.0; used for PHP >= 7.4. Test: Fix test that was overriding `retenciones/sample-before-tfd.xml` file. @@ -81,7 +95,7 @@ Thanks @EmmanuelJCS. ## Version 2.19.1 2022-02-09 -Fix `EmisorRegimenFiscal` validation. Add `626 - RESICO`. Thanks @celli33. +Fix `EmisorRegimenFiscal` validation. Add `626 - RESICO`. Thanks `@celli33`. The following changes apply only to development and has been applied to main branch. @@ -141,7 +155,7 @@ Other changes: ## Version 2.18.3 2022-01-15 -Fix *Carta Porte 1.0* add missing element `Notificado`. Thanks @celli33. +Fix *Carta Porte 1.0* add missing element `Notificado`. Thanks `@celli33`. ## Version 2.18.2 2021-12-17 @@ -166,14 +180,14 @@ Fix `dev:coverage` composer script. ## Version 2.17.0 2021-12-10 The helper object `SumasConceptosWriter` also writes the sum of *impuestos locales* when they are present. -Thanks, @ccelli33 and @luffinando for your help. +Thanks, `@celli33` and `@luffinando` for your help. ## Version 2.16.1 2021-12-08 Fix bug when create expression to query for the SAT status and the RFC (*emisor* or *receptor*) contains the characters `&` or `Ñ`. The service requires that the expression is XML "encoded". -Thanks, @ramboram and @TheSpectroMX for your help. +Thanks, `@ramboram` and `@TheSpectroMX` for your help. Refactor test script `tests/estadosat.php`. @@ -220,22 +234,22 @@ General: - Upgrade to PHPUnit 9.5 and upgrade test suite. - Test classes are declared as final. - Remove support for PHP 7.0, PHP 7.1 and PHP 7.2. -- Compatilize with PHP 8.0 / OpenSSL: +- Compatibilize with PHP 8.0 / OpenSSL: - openssl functions does not return resources but objects. - On deprecated functions run only if PHP version is lower than 8.0 and put annotations for `phpcs`. Bugfixes: -- Validation `SELLO04` fails when there are special caracters like `é` and `LC_CTYPE` is not setup. +- Validation `SELLO04` fails when there are special characters like `é` and `LC_CTYPE` is not setup. - Fix `COMPIMPUESTOSC01` description typo. There are some soft backwards incompatibility changes: -- Method __construct() of class CfdiUtils\Validate\Cfdi33\Standard\FechaComprobante became final -- Method __construct() of class CfdiUtils\Validate\Cfdi33\RecepcionPagos\Pago became final -- The return type of CfdiUtils\Validate\Cfdi33\RecepcionPagos\Pago#getValidators() changed from no type to array -- The parameter $decimals of CfdiUtils\Utils\Format::number() changed from no type to a non-contravariant int -- The parameter $content of CfdiUtils\Cleaner\Cleaner::staticClean() changed from no type to a non-contravariant string. +- Method `__construct()` of class `CfdiUtils\Validate\Cfdi33\Standard\FechaComprobante` became final +- Method `__construct()` of class `CfdiUtils\Validate\Cfdi33\RecepcionPagos\Pago` became final +- The return type of `CfdiUtils\Validate\Cfdi33\RecepcionPagos\Pago#getValidators()` changed from no type to array +- The parameter $decimals of `CfdiUtils\Utils\Format::number()` changed from no type to a non-contravariant int +- The parameter $content of `CfdiUtils\Cleaner\Cleaner::staticClean()` changed from no type to a non-contravariant string. Development environment: @@ -256,7 +270,7 @@ Development environment: ### `MetodoPago` on `N - Nómina` -- Remove redundant valiations `METPAG01` and `METPAG02`. +- Remove redundant validations `METPAG01` and `METPAG02`. - Validation `TIPOCOMP04` does not apply on documents type `N - Nómina`. ### Download certificates (unreleased 2020-10-08) @@ -296,7 +310,7 @@ Development environment: ## Version 2.12.11 2020-08-16 - Fix TimbreFiscalDigital XSLT URL locations, updated from SAT documentation. - For more information check [phpcfdi/sat-ns-registry](https://github.com/phpcfdi/sat-ns-registry) project. + For more information check [`phpcfdi/sat-ns-registry`](https://github.com/phpcfdi/sat-ns-registry) project. ## Version 2.12.10 2020-07-18 @@ -326,7 +340,7 @@ with `http://www.sat.gob.mx/sitio_internet/cfd/TimbreFiscalDigital/TimbreFiscalD - Improve explanation on `TFDSELLO01` when unable to get certificate. -The assert `TFDSELLO01` *El Sello SAT del Timbre Fiscal Digital corresponde al certificado SAT*, now includes the +The assertion `TFDSELLO01` *El Sello SAT del Timbre Fiscal Digital corresponde al certificado SAT*, now includes the exception message when unable to obtain a certificate. - Remove insecure downloader from testing. @@ -338,8 +352,8 @@ This problem does not exist anymore (since 2019-10-24). ## Version 2.12.9 2020-04-25 - Review and fix `CreateComprobantePagosCaseTest`. -- Add docblocks on `StatusResponse` and fix script `tests/estadosat.php`. -- Remove `overtrue/phplint` from development dependences. +- Add doc-blocks on `StatusResponse` and fix script `tests/estadosat.php`. +- Remove `overtrue/phplint` from development dependencies. ## Version 2.12.8 2020-01-07 @@ -406,7 +420,7 @@ This problem does not exist anymore (since 2019-10-24). ## Version 2.12.2 2019-09-24 -- When cannot load an Xml string include `LibXMLError` information into exception, like: +- When cannot load a Xml string include `LibXMLError` information into exception, like: ```text Cannot create a DOM Document from xml string @@ -425,7 +439,7 @@ This problem does not exist anymore (since 2019-10-24). - Remove several development files from final package - Development: - Fix `.editorconfig` - - Integrate codeclimate, evaluate for a while to consider a replacement for scrutinizer + - Integrate `codeclimate`, evaluate for a while to consider a replacement for `scrutinizer` - Add PHP 7.4snapshot - Remove Symfony Insight config file - On `composer dev:build` it also calls `composer dev:check-style` @@ -452,19 +466,19 @@ This problem does not exist anymore (since 2019-10-24). and that collapsing removes the issues and do not change the *"cadena de origen"*. - Document SAT issue with multiple `cfdi:Complemento` (problems and clean). - Travis: since `mkdocs` version is newer, there is no need to change `nav` to `pages` to compile docs. -- phpstan: create `phpstan.neon.dist` with `inferPrivatePropertyTypeFromConstructor`. +- PHPStan: create `phpstan.neon.dist` with `inferPrivatePropertyTypeFromConstructor`. ## Version 2.10.4 2019-06-27 -- Add `Xml::createElement` and `Xml::createElementNS` to deal with non scaped ampersand `&` +- Add `Xml::createElement` and `Xml::createElementNS` to deal with non escaped ampersand `&` on `DOMDocument::createElement` and `DOMDocument::createElementNS`. - Improve `Rfc::obtainDate` with invalid length dates and tests ## Version 2.10.3 2019-05-29 -- Add static methods to `CfdiUtils\Utils\Xml`, this methods are created to help fixing issues found by `phpstan`: +- Add static methods to `CfdiUtils\Utils\Xml`, these methods are created to help to fix issues found by `phpstan`: - `Xml::documentElement(DOMDocument $document): DOMElement`: Safe helper to get `$document->documentElement` - `Xml::ownerDocument(DOMNode $node): DOMDocument`: Safe helper to get `$node->ownerDocument` - Fix [`phpstan`](https://github.com/phpstan/phpstan) 0.11.6 issues, this must solve all travis build @@ -473,7 +487,7 @@ This problem does not exist anymore (since 2019-10-24). ## Version 2.10.2 2019-04-08 - Fix bug on `QuickReader` getting the content of falsy values (like `"0"`) return an empty string. - Thanks @jaimeres. (Closes #48) + Thanks `@jaimeres`. (Closes #48) ## Version 2.10.1 2019-04-02 @@ -517,14 +531,14 @@ This problem does not exist anymore (since 2019-10-24). - Cover `SumasConceptosWriter::getComprobante()`. - Cover `CfdiUtils\Certificado\SerialNumber::loadHexadecimal` when throw exception. - Cover `CfdiUtils\Nodes\Attributes::import` (and constructor) when throw exception. -- Genkgo/Xsl upgrated to 0.6 (compatible with PHP 7.0), also fix siggestion on `composer.json` file. +- `Genkgo/Xsl` upgraded to 0.6 (compatible with PHP 7.0), also fix suggestion on `composer.json` file. - Internal: `TemporaryFile` now is able to cast itself to string returning the path to file, retrieve contents, store contents and remove file after run some function even if exception was thrown. - Internal: Add `ShellExec` class that works around with `symfony/process` component. Also added: - `ShellExecResponse`: contains the response of ShellExec::run(). - `ShellExecTemplate`: basic command array creation from a string template. - Internal: Move internal to `CfdiUtils\Internal`. Check `@internal` annotation on all elements. Add README.md -- CI: AppVeyor complete refactory, now uses correctly caches and upgrade php if required. +- CI: `AppVeyor` complete refactor, now uses correctly caches and upgrade php if required. - CI: Only run `phpstan` on PHP 7.3. - Dev: `composer dev:build` now runs `phpunit --testdox --verbose --stop-on-failure`. @@ -533,12 +547,12 @@ This problem does not exist anymore (since 2019-10-24). - Extract base convert logic from `CfdiUtils\Certificado\SerialNumber::baseConvert` to new internal classes: - `CfdiUtils\Utils\Internal\BaseConverterSequence` Value object to store the character maps. - - `CfdiUtils\Utils\Internal\BaseConverter` Object that perform the convertion. + - `CfdiUtils\Utils\Internal\BaseConverter` Object that perform the conversion. - Fix possible bug converting from an inferior to a superior base thanks to new test on `BaseConverter`. - Classes inside `CfdiUtils\Utils\Internal\` namespace should not be used outside the library. Changing this will not be considered a backward compatibility break. - Deprecate `CfdiUtils\Certificado\SerialNumber::baseConvert`. -- Create `CfdiUtils\Utils\Internal\TemporaryFile` to aviod using directly `\tempnam` and throw `\RuntimeException` +- Create `CfdiUtils\Utils\Internal\TemporaryFile` to avoid using directly `\tempnam` and throw `\RuntimeException` - Replace usages of `\tempnam` with `TemporaryFile::create()` on: - `CfdiUtils\CadenaOrigen\SaxonbCliBuilder` - `CfdiUtils\Certificado\NodeCertificado` @@ -546,7 +560,7 @@ This problem does not exist anymore (since 2019-10-24). - `CfdiUtilsTests\Certificado\NodeCertificadoTest` - Fix possible bug on `CfdiUtils\Cleaner\Cleaner` when making an XPath query. - Fix docblock on `CfdiUtils\QuickReader\QuickReader` on magic method `__get`. -- Fix issues on functions expecting a variable of certain type but receiving false instead. Thanks phpstan! +- Fix issues on functions expecting a variable of certain type but receiving false instead. Thanks `phpstan`! - Call `NodeInderface::offsetExists($name)` instead of `isset(NodeInderface[$name])`. The reasons behind this change are: - `isset` is not a *function* but a *keyword*, making `phpstan` or other tools to fail on this. @@ -555,12 +569,12 @@ This problem does not exist anymore (since 2019-10-24). The previous change does not have to be replicated in the users of this library. It is internal. In future version (when BCB are allowed) will introduce a better method for this operation `NodeInderface::exists(string $name): bool` and will fix documentation to better use this method instead of `isset`. -- Fix documentation on `docs/leer/leer-cfdi.md` about method `NodeInterface::getNode()`. Thanks @ReynerHL. +- Fix documentation on `docs/leer/leer-cfdi.md` about method `NodeInterface::getNode()`. Thanks `@ReynerHL`. ## Version 2.8.0 2019-01-29 -- Initial attempt to create a *CFDI de retenciones e información de pagos*: +- Initial attempt to create a RET 1.0 (*CFDI de retenciones e información de pagos*): - Add namespace `\CfdiUtils\Retenciones`. - Add class `\CfdiUtils\Retenciones\RetencionesCreator10`. - Add test for green path on creating a CFDI without TFD. @@ -638,24 +652,24 @@ This problem does not exist anymore (since 2019-10-24). - Honor status from `ValidatePagoException` or `ValidateDoctoException` - Tests use XmlResolver from `CfdiUtilsTests\TestCase` instead of creating a new one - Fix docblock `CfdiUtils\Nodes\Nodes::searchNodes` -- Improve docblocks on `CfdiUtils\Certificado\Certificado` +- Improve doc-blocks on `CfdiUtils\Certificado\Certificado` - Documentation: - Create `docs/problemas/contradicciones-pagos.md` - Create `docs/problemas/descarga-certificados.md` to document error `TFDSELLO01` - Create examples on `docs/componentes/certificado.md` on object creation - Change tests to not ssl verify peer due SAT web server configuration errors (expired certificate) - Add `CfdiUtilsTests\TestCase::newInsecurePhpDownloader(): DownloaderInterface` - - Use insecure downlader in `CfdiUtilsTests\CfdiValidator33Test` - - Use insecure downlader in `CfdiUtilsTests\Validate\Cfdi33\Standard\TimbreFiscalDigitalSelloTest` - - Use insecure downlader in `CfdiUtilsTests\Certificado\CerRetrieverTest` + - Use insecure downloader in `CfdiUtilsTests\CfdiValidator33Test` + - Use insecure downloader in `CfdiUtilsTests\Validate\Cfdi33\Standard\TimbreFiscalDigitalSelloTest` + - Use insecure downloader in `CfdiUtilsTests\Certificado\CerRetrieverTest` - Also add note to `docs/TODO.md` to remove this insecure downloader when SAT server is fine - Change composer scripts and prefix `dev:`, commands are now: - `dev:build`: run dev:fix-style dev:tests and dev:docs, run before pull request - `dev:check-style`: search for code style errors using php-cs-fixer and phpcs - `dev:fix-style`: fix code style errors using php-cs-fixer and phpcbf - - `dev:docs`: search for code style errors unsing markdownlint and build docs using mkdocs - - `dev:test`: run phplint, phpunit and phpstan - - `dev:coverage`: run phpunit with xdebug and storage coverage in build/coverage/html + - `dev:docs`: search for code style errors using `markdownlint` and build docs using `mkdocs` + - `dev:test`: run `phplint`, `phpunit` and `phpstan` + - `dev:coverage`: run `phpunit` with `xdebug` and storage coverage in `build/coverage/html` ## Version 2.6.6 2018-10-04 @@ -684,25 +698,25 @@ This problem does not exist anymore (since 2019-10-24). Now it allows any value equals to `1` considering 6 decimal numbers, so the following values are valid: `"1"`, `"1.00"`, `"1.000000"` - Change description from `... debe ser "1"...` to `... debe tener el valor "1"...` -- Fix scrutinizer issue in `Validate/Cfdi33/Standard/ComprobanteImpuestos.php`: +- Fix Scrutinizer issue in `Validate/Cfdi33/Standard/ComprobanteImpuestos.php`: *Using logical operators such as and instead of && is generally not recommended* -- `CfdiUtils\ConsultaCfdiSat\WebService` is using SAT Web Service but since 2018-08 it is ramdomly failing. +- `CfdiUtils\ConsultaCfdiSat\WebService` is using SAT Web Service but since 2018-08 it is randomly failing. The test that are consuming was moved to a different test case class `WebServiceConsumingTest` and are marked as skipped when `\SoapFault` is thrown instead of fail - Fix `xmlns:xsi` definition case to `XMLSchema` -- Allow install phpunit 7 if php >= 7.1 +- Allow to install phpunit 7 if php >= 7.1 - Fix `phpunit.xml.dist` configuration file removing redundant options and setting missing options -- Solve phpstan 0.10.x issues, not yet upgraded since it contains several bugfixes +- Solve PHPStan 0.10.x issues, not yet upgraded since it contains several bugfixes ## Version 2.6.3 2018-08-21 - Fix validations `COMPIMPUESTOSC02` and `COMPIMPUESTOSC03` - Previously both or any should exists (`xnor`): nodes `Traslado|Retencion` and attributes `TotalImpuestosTrasladados|TotalImpuestosRetenidos` - Now it allows to have `Impuestos` totals even when related nodes does not exists + Previously both or any should exist (`xnor`): nodes `Traslado|Retencion` and attributes `TotalImpuestosTrasladados|TotalImpuestosRetenidos` + Now it allows to have `Impuestos` totals even when related nodes does not exist This is because is not mandatory by Anexo 20 - What is mandatory is that if nodes exists then totals must exists + What is mandatory is that if nodes exists then totals must exist - Add helper method to create a `RequestParameters` from a `Cfdi` - Fix: add missing dependence `ext-iconv` into `composer.json` - Testing: add helper development script `tests/estadosat.php` @@ -713,24 +727,24 @@ This problem does not exist anymore (since 2019-10-24). - Dependence on has been set to `^2.0.1` to fix validation using XSD local repository on MS Windows. -- Improve docblocks in property traits +- Improve doc-blocks in property traits - Restore previous error handler on `ComprobanteGetCfdiRelacionadosTest` - Make sure that input file on `PemPrivateKey` is not a directory and is readable - On MS Windows send to `NUL` instead of `/dev/null` - Convert from `UTF-8` to `ASCII//TRANSLIT` can add single quotes, remove it. - Add [AppVeyor](https://ci.appveyor.com/project/eclipxe13/cfdiutils) continuous integration -- Add documentation about developing this library on windows +- Add documentation about developing this library on Windows - Allow to set `saxonb` path using environment variable `saxonb-path` ## Version 2.6.1 2018-07-16 -- Fix order of `Impuestos` children (thanks @aldolinares): +- Fix order of `Impuestos` children (thanks `@aldolinares`): - When is inside `Comprobante` the order is `Retenciones` then `Traslados` - When is inside `Concepto` the order is `Traslados` then `Retenciones` - Add `testMultiRelacionado` in `ComprobanteTest` -- Fix markdown syntax errors in a lot of documents -- Use self instead of static in docblocks, static is not standard +- Fix Markdown syntax errors in a lot of documents +- Use self instead of static in doc-blocks, static is not standard - Add badges to `docs/index.md` @@ -742,17 +756,17 @@ This problem does not exist anymore (since 2019-10-24). - 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 + - For backwards compatibility when it receives a parameter do the same thing but trigger a `E_USER_NOTICE` + - Create a special test case `ComprobanteGetCfdiRelacionadosTest` that catch 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 +- Minor fix at doc-blocks 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 + - Fix markdown files according to `markdownlint` - Add `composer docs` and append to general `composer build` @@ -770,39 +784,39 @@ This problem does not exist anymore (since 2019-10-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". + It implements almost all the validations from the SAT "Matriz de errores". - Append it to `\CfdiUtils\Validate\MultiValidatorFactory` -- Remove non existent validators discovery `Cfdi33/Timbre` +- 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 -- Fix deprecation notices existent docblocks +- Fix deprecation notices existent doc-blocks - Update deprecation notice to README - Replace TODO with a more explained version ## Version 2.4.6 2018-05-24 -- Fix validation of TIPOCOMP06, it was not checking correctly. +- 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` - 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 + to a protected method, this allows better testing of the class by mocking the call - In `CfdiUtils\PemPrivateKey\PemPrivateKey` deprecate `isOpened` and add `isOpen` - In `CfdiUtils\Cfdi::getNode` use `XmlNodeUtils` instead of `XmlNodeImporter` - In `CfdiUtils\Cfdi::newFromString` create `new self` instead of `new static`. - If using `new static` the constructor might be different and it would fail. + If using `new static` the constructor might be different, and it would fail. - In `CfdiUtils\CfdiVersion::fromXmlString` it no longer create a Cfdi object, it will just create a `DOMObject` and delegate to `fromDOMDocument` as in `TfdVersion`. - Remove `CfdiUtils\Elements\Pagos10\Pago::multiImpuestos`, - it should never exists and must not have any use case. + it should never exist and must not have any use case. - Improve testing on: - `CfdiUtils\Elements\Pagos10\Pagos` - `CfdiUtils\Validate\Cfdi33\Standard\ConceptoImpuestos` -- Improve docblocks and fix typos in several files +- Improve doc-blocks 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 @@ -822,15 +836,15 @@ This problem does not exist anymore (since 2019-10-24). - 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 -- Add docblocks to `CfdiUtils\Cfdi` +- Add doc-blocks to `CfdiUtils\Cfdi` - Building: - - Add .phplint.yml to export-ignore (standard line) + - 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` +- Add more dependencies: `ext-dom`, `ext-xsl`, `ext-simplexml`, `ext-mbstring` ## Version 2.4.4 2018-05-11 -- FIX: Unable to load a PEM file using filename on windows (Closes #33) +- 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 @@ -887,9 +901,9 @@ This problem does not exist anymore (since 2019-10-24). - Add a client `\CfdiUtils\ConsultaCfdiSat\WebService` for the SAT WebService `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 + Did not appear 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 a lot of fixes in doc-blocks 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` - @@ -900,8 +914,8 @@ This problem does not exist anymore (since 2019-10-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 + - `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`. @@ -925,14 +939,14 @@ This problem does not exist anymore (since 2019-10-24). a certificate from the SAT repository - Add a new validator `CfdiUtils\Validate\Cfdi33\Standard\TimbreFiscalDigitalSello` to validate that the SelloSAT is actually the signature of the Timbre Fiscal Digital. If not then the CFDI was modified -- Add a new real and valid CFDI to test, this allow `TimbreFiscalDigitalSello` to check real data and pass +- Add a new real and valid CFDI to test, this allows `TimbreFiscalDigitalSello` to check real data and pass - 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 -- Small bugfixes thanks to scrutinizer-ci.com -- Fix some docblocks +- Small bugfixes thanks to Scrutinizer +- Fix some doc-blocks - Travis: Build also with PHP 7.2 ## Version 2.0.0 2018-01-01 @@ -957,16 +971,16 @@ This problem does not exist anymore (since 2019-10-24). - 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. + throw an exception, but it was not clear in the flow of `build` method. Now it returns a \RuntimeException and that is thrown. So it is easy for an analysis tool to know that the flow has been stopped. - Also fix case of calls `XSLTProcessor::importStylesheet` and `XSLTProcessor::transformToXml` -- Check with `isset` that `LibXMLError::$message` exists, phpstan was failing for this. +- Check with `isset` that `LibXMLError::$message` exists, `phpstan` was failing for this. ## Version 1.0.1 2017-09-27 -- Remove Travis CI PHP nightly builds, it fail with require-dev dependencies. +- Remove Travis CI PHP nightly builds, it fails with require-dev dependencies. ## Version 1.0.0 2017-09-27 diff --git a/docs/crear/cfdi-de-retenciones-e-informacion-de-pagos.md b/docs/crear/cfdi-de-retenciones-e-informacion-de-pagos.md index 82b9b524..29ba75a4 100644 --- a/docs/crear/cfdi-de-retenciones-e-informacion-de-pagos.md +++ b/docs/crear/cfdi-de-retenciones-e-informacion-de-pagos.md @@ -9,11 +9,16 @@ La estrategia para crear este un CFDI de retenciones es la misma que para [crear Consulta la información relacionada con el uso de [elementos](../componentes/elements.md), [nodos](../componentes/nodes.md) y [complementos no implementados](../crear/complementos-aun-no-implementados.md). -## Ejemplo de creación de un CFDI de retenciones e información de pagos +## Validaciones + +Al momento de validar un CFDI de retenciones e información de pagos solamente se está comprobando que efectivamente +se siga el esquema XSD. A diferencia de la creación de CFDI Regulares donde se incluyen múltiples validadores. + +## Ejemplo de creación de un CFDI de retenciones e información de pagos versión 1.0 ```php '2019-01-23T08:00:00-06:00', 'CveRetenc' => '14', // Dividendos o utilidades distribuidos @@ -67,3 +72,69 @@ $asserts = $creator->validate(); // guardar el precfdi file_put_contents('precfdi.xml', $creator->asXml()); ``` + +## Ejemplo de creación de un CFDI de retenciones e información de pagos versión 2.0 + +```php +// creator es un objeto de ayuda, similar a CfdiCreator40 +$creator = new RetencionesCreator20([ + 'FechaExp' => '2022-01-13T14:15:16', + 'CveRetenc' => '14', // Dividendos o utilidades distribuidos + 'LugarExpRetenc' => '91778', +]); + +// retenciones es un objeto de ayuda, similar a Comprobante +$retenciones = $creator->retenciones(); + +$retenciones->addCfdiRetenRelacionados([ + 'TipoRelacion' => '01', + 'UUID' => '1474b7d3-61fc-41c4-a8b8-3f22e1161bb4', +]); +$retenciones->addEmisor([ + 'RfcE' => 'EKU9003173C9', + 'NomDenRazSocE' => 'ESCUELA KEMPER URGATE', + 'RegimenFiscalE' => '601', +]); +$retenciones->getReceptor()->addExtranjero([ + 'NumRegIdTribR' => '998877665544332211', + 'NomDenRazSocR' => 'WORLD WIDE COMPANY INC', +]); +$retenciones->addPeriodo(['MesIni' => '05', 'MesFin' => '05', 'Ejercicio' => '2021']); +$retenciones->addTotales([ + 'MontoTotOperacion' => '55578643', + 'MontoTotGrav' => '0', + 'MontoTotExent' => '55578643', + 'MontoTotRet' => '0', + 'UtilidadBimestral' => '0.1', + 'ISRCorrespondiente' => '0.1', +]); +$retenciones->addImpRetenidos([ + 'BaseRet' => '0', + 'ImpuestoRet' => '001', // same as CFDI + 'TipoPagoRet' => '01', + 'MontoRet' => '200.00', +]); + +$dividendos = new \CfdiUtils\Elements\Dividendos10\Dividendos(); +$dividendos->addDividOUtil([ + 'CveTipDivOUtil' => '06', // 06 - Proviene de CUFIN al 31 de diciembre 2013 + 'MontISRAcredRetMexico' => '0', + 'MontISRAcredRetExtranjero' => '0', + 'MontRetExtDivExt' => '0', + 'TipoSocDistrDiv' => 'Sociedad Nacional', + 'MontISRAcredNal' => '0', + 'MontDivAcumNal' => '0', + 'MontDivAcumExt' => '0', +]); +$retenciones->addComplemento($dividendos); + +// poner certificado y sellar el precfdi, después de sellar no debes hacer cambios +$creator->putCertificado(new \CfdiUtils\Certificado\Certificado('archivo.cer')); +$creator->addSello('file://archivo.key.pem', 'la contraseña'); + +// Asserts contendrá el resultado de la validación +$asserts = $creator->validate(); + +// guardar el precfdi +file_put_contents('precfdi.xml', $creator->asXml()); +``` diff --git a/docs/leer/leer-cfdi-retenciones.md b/docs/leer/leer-cfdi-retenciones.md index 77a6abfa..768b7ac2 100644 --- a/docs/leer/leer-cfdi-retenciones.md +++ b/docs/leer/leer-cfdi-retenciones.md @@ -85,10 +85,14 @@ no tendrán un impacto en el contenedor. ### Versión -El CFDI de retenciones solo tiene la versión 1.0, en caso de que se fabrique un objeto que contiene un número de versión +El CFDI de retenciones abarca las versiones 1.0 y 2.0, en caso de que se fabrique un objeto que contiene un número de versión diferente, entonces el método `Retenciones::getVersion()` devolverá una cadena vacía. Esto es por homogeneidad con la lectura de CFDI regulares. +Es importante notar cambios en la estructura y que muchos de los atributos usados en la versión 1.0 +cambian de nombre o contenido en la versión 2.0, por lo que se recomienda utilizar +diferentes lectores según la versión del comprobante. + ### Lectura formal La lectura formal utiliza [nodos](../componentes/nodes.md), que es una representación en memoria del contenido @@ -117,5 +121,31 @@ echo $nodeRetenciones->searchAttribute('retenciones:Emisor', 'RFCEmisor'); // EK // obtener el QuickReader para lectura rápida $qrRetenciones = $reader->getQuickReader(); -echo $qrRetenciones->emisor['rfcemisor']; // EKU9003173C9 +echo $qrRetenciones->emisor['NomDenRazSocE']; // Nombre del emisor +``` + + +## Obteniendo la versión de un CFDI de Retenciones sin la clase `CfdiUtils\Retenciones\Retenciones` + +Obtener la versión de un CFDI de Retenciones es sencillo con la clase `CfdiUtils\Retenciones\RetencionVersion`. + +El método que usarás para obtener la versión depende de la información que ya +tengas cargada: + +- `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 cargado 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\Retenciones\Retenciones` ya realiza este proceso por lo que no es recomendado +duplicar el trabajo de averiguar la versión. diff --git a/src/CfdiUtils/Elements/Retenciones20/Addenda.php b/src/CfdiUtils/Elements/Retenciones20/Addenda.php new file mode 100644 index 00000000..60db2d46 --- /dev/null +++ b/src/CfdiUtils/Elements/Retenciones20/Addenda.php @@ -0,0 +1,20 @@ +children()->add($child); + return $this; + } +} diff --git a/src/CfdiUtils/Elements/Retenciones20/CfdiRetenRelacionados.php b/src/CfdiUtils/Elements/Retenciones20/CfdiRetenRelacionados.php new file mode 100644 index 00000000..53bd2abb --- /dev/null +++ b/src/CfdiUtils/Elements/Retenciones20/CfdiRetenRelacionados.php @@ -0,0 +1,13 @@ +children()->add($child); + return $this; + } +} diff --git a/src/CfdiUtils/Elements/Retenciones20/Emisor.php b/src/CfdiUtils/Elements/Retenciones20/Emisor.php new file mode 100644 index 00000000..9886f35e --- /dev/null +++ b/src/CfdiUtils/Elements/Retenciones20/Emisor.php @@ -0,0 +1,13 @@ +helperGetOrAdd(new Nacional()); + $this->children()->removeAll(); + $this->addChild($nacional); + $this['NacionalidadR'] = 'Nacional'; + return $nacional; + } + + public function addNacional(array $attributes = []): Nacional + { + $nacional = $this->getNacional(); + $nacional->addAttributes($attributes); + return $nacional; + } + + public function getExtranjero(): Extranjero + { + $nacional = $this->helperGetOrAdd(new Extranjero()); + $this->children()->removeAll(); + $this->addChild($nacional); + $this['NacionalidadR'] = 'Extranjero'; + return $nacional; + } + + public function addExtranjero(array $attributes = []): Extranjero + { + $extranjero = $this->getExtranjero(); + $extranjero->addAttributes($attributes); + return $extranjero; + } +} diff --git a/src/CfdiUtils/Elements/Retenciones20/Retenciones.php b/src/CfdiUtils/Elements/Retenciones20/Retenciones.php new file mode 100644 index 00000000..9d3aa76f --- /dev/null +++ b/src/CfdiUtils/Elements/Retenciones20/Retenciones.php @@ -0,0 +1,133 @@ +helperGetOrAdd(new CfdiRetenRelacionados()); + } + + public function addCfdiRetenRelacionados(array $attributes = []): CfdiRetenRelacionados + { + $cfdiRelacionado = $this->getCfdiRetenRelacionados(); + $cfdiRelacionado->addAttributes($attributes); + return $cfdiRelacionado; + } + + public function getEmisor(): Emisor + { + return $this->helperGetOrAdd(new Emisor()); + } + + public function addEmisor(array $attributes = []): Emisor + { + $emisor = $this->getEmisor(); + $emisor->addAttributes($attributes); + return $emisor; + } + + public function getReceptor(): Receptor + { + return $this->helperGetOrAdd(new Receptor()); + } + + public function addReceptor(array $attributes = []): Receptor + { + $receptor = $this->getReceptor(); + $receptor->addAttributes($attributes); + return $receptor; + } + + public function getPeriodo(): Periodo + { + return $this->helperGetOrAdd(new Periodo()); + } + + public function addPeriodo(array $attributes = []): Periodo + { + $periodo = $this->getPeriodo(); + $periodo->addAttributes($attributes); + return $periodo; + } + + public function getTotales(): Totales + { + return $this->helperGetOrAdd(new Totales()); + } + + public function addTotales(array $attributes = []): Totales + { + $totales = $this->getTotales(); + $totales->addAttributes($attributes); + return $totales; + } + + public function addImpRetenidos(array $attributes = []): ImpRetenidos + { + return $this->getTotales()->addImpRetenidos($attributes); + } + + public function multiImpRetenidos(array ...$elementAttributes): self + { + $this->getTotales()->multiImpRetenidos(...$elementAttributes); + return $this; + } + + public function getComplemento(): Complemento + { + return $this->helperGetOrAdd(new Complemento()); + } + + public function addComplemento(NodeInterface $children): self + { + $this->getComplemento()->add($children); + return $this; + } + + public function getAddenda(): Addenda + { + return $this->helperGetOrAdd(new Addenda()); + } + + public function addAddenda(NodeInterface $children): self + { + $this->getAddenda()->add($children); + return $this; + } + + public function getChildrenOrder(): array + { + return [ + 'retenciones:CfdiRetenRelacionados', + 'retenciones:Emisor', + 'retenciones:Receptor', + 'retenciones:Periodo', + 'retenciones:Totales', + 'retenciones:Complemento', + 'retenciones:Addenda', + ]; + } + + public function getFixedAttributes(): array + { + return [ + 'xmlns:retenciones' => 'http://www.sat.gob.mx/esquemas/retencionpago/2', + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation' => vsprintf('%s %s', [ + 'http://www.sat.gob.mx/esquemas/retencionpago/2', + 'http://www.sat.gob.mx/esquemas/retencionpago/2/retencionpagov2.xsd', + ]), + 'Version' => '2.0', + ]; + } +} diff --git a/src/CfdiUtils/Elements/Retenciones20/Totales.php b/src/CfdiUtils/Elements/Retenciones20/Totales.php new file mode 100644 index 00000000..9187dd27 --- /dev/null +++ b/src/CfdiUtils/Elements/Retenciones20/Totales.php @@ -0,0 +1,28 @@ +addChild($impRetenidos); + return $impRetenidos; + } + + public function multiImpRetenidos(array ...$elementAttributes): self + { + foreach ($elementAttributes as $attributes) { + $this->addImpRetenidos($attributes); + } + return $this; + } +} diff --git a/src/CfdiUtils/Retenciones/RetencionVersion.php b/src/CfdiUtils/Retenciones/RetencionVersion.php new file mode 100644 index 00000000..19beb1f6 --- /dev/null +++ b/src/CfdiUtils/Retenciones/RetencionVersion.php @@ -0,0 +1,33 @@ + 'Version', + '1.0' => 'Version', + ]; + } +} diff --git a/src/CfdiUtils/Retenciones/Retenciones.php b/src/CfdiUtils/Retenciones/Retenciones.php index 3be1e7ff..918e2d81 100644 --- a/src/CfdiUtils/Retenciones/Retenciones.php +++ b/src/CfdiUtils/Retenciones/Retenciones.php @@ -2,15 +2,17 @@ namespace CfdiUtils\Retenciones; +use CfdiUtils\CfdiCreateObjectException; use CfdiUtils\Internals\XmlReaderTrait; use DOMDocument; +use UnexpectedValueException; /** * This class contains minimum helpers to read CFDI Retenciones based on DOMDocument * * When the object is instantiated it checks that: * implements the namespace static::RET_NAMESPACE using a prefix "retenciones" - * the root node is retenciones:Retenciones + * the root node is prefix:Retenciones * * This class also provides conversion to Node for easy access and manipulation, * changes made in Node structure are not reflected into the DOMDocument, @@ -22,13 +24,44 @@ class Retenciones { use XmlReaderTrait; + /** + * @var string Retenciones 1.0 namespace definition + * @deprecated :3.0.0 + * @internal Preserve this constant to not break compatibility + */ const RET_NAMESPACE = 'http://www.sat.gob.mx/esquemas/retencionpago/1'; + /** @var array Dictionary of versions and namespaces */ + private const RET_SPECS = [ + '2.0' => 'http://www.sat.gob.mx/esquemas/retencionpago/2', + '1.0' => 'http://www.sat.gob.mx/esquemas/retencionpago/1', + ]; + public function __construct(DOMDocument $document) { - $rootElement = self::checkRootElement($document, self::RET_NAMESPACE, 'retenciones', 'Retenciones'); + $retVersion = new RetencionVersion(); + /** @var array $exceptions */ + $exceptions = []; + foreach (self::RET_SPECS as $version => $namespace) { + try { + $this->loadDocumentWithNamespace($retVersion, $document, $namespace); + return; + } catch (UnexpectedValueException $exception) { + $exceptions[$version] = $exception; + } + } + + throw CfdiCreateObjectException::withVersionExceptions($exceptions); + } + + /** @throws UnexpectedValueException */ + private function loadDocumentWithNamespace( + RetencionVersion $retVersion, + DOMDocument $document, + string $namespace + ): void { + $rootElement = self::checkRootElement($document, $namespace, 'retenciones', 'Retenciones'); + $this->version = $retVersion->getFromDOMElement($rootElement); $this->document = clone $document; - $version = $rootElement->getAttribute('Version'); - $this->version = ('1.0' === $version) ? $version : ''; } } diff --git a/src/CfdiUtils/Retenciones/RetencionesCreator10.php b/src/CfdiUtils/Retenciones/RetencionesCreator10.php index 988915d5..7dcb388a 100644 --- a/src/CfdiUtils/Retenciones/RetencionesCreator10.php +++ b/src/CfdiUtils/Retenciones/RetencionesCreator10.php @@ -2,7 +2,6 @@ namespace CfdiUtils\Retenciones; -use CfdiUtils\CadenaOrigen\DOMBuilder; use CfdiUtils\CadenaOrigen\XsltBuilderInterface; use CfdiUtils\CadenaOrigen\XsltBuilderPropertyInterface; use CfdiUtils\CadenaOrigen\XsltBuilderPropertyTrait; @@ -10,10 +9,6 @@ use CfdiUtils\Certificado\CertificadoPropertyInterface; use CfdiUtils\Certificado\CertificadoPropertyTrait; use CfdiUtils\Elements\Retenciones10\Retenciones; -use CfdiUtils\Nodes\XmlNodeUtils; -use CfdiUtils\PemPrivateKey\PemPrivateKey; -use CfdiUtils\Validate\Asserts; -use CfdiUtils\Validate\Xml\XmlFollowSchema; use CfdiUtils\XmlResolver\XmlResolver; use CfdiUtils\XmlResolver\XmlResolverPropertyInterface; use CfdiUtils\XmlResolver\XmlResolverPropertyTrait; @@ -23,6 +18,7 @@ class RetencionesCreator10 implements XmlResolverPropertyInterface, XsltBuilderPropertyInterface { + use RetencionesCreatorTrait; use CertificadoPropertyTrait; use XmlResolverPropertyTrait; use XsltBuilderPropertyTrait; @@ -31,13 +27,13 @@ class RetencionesCreator10 implements private $retenciones; public function __construct( - array $comprobanteAttributes = [], + array $retencionesAttributes = [], XmlResolver $xmlResolver = null, - XsltBuilderInterface $xsltBuilder = null + XsltBuilderInterface $xsltBuilder = null, + Certificado $certificado = null ) { - $this->retenciones = new Retenciones($comprobanteAttributes); - $this->setXmlResolver($xmlResolver ? : new XmlResolver()); - $this->setXsltBuilder($xsltBuilder ? : new DOMBuilder()); + $this->retenciones = new Retenciones(); + $this->retencionesCreatorConstructor($retencionesAttributes, $certificado, $xmlResolver, $xsltBuilder); } public function retenciones(): Retenciones @@ -65,38 +61,9 @@ public function buildCadenaDeOrigen(): string return $this->getXsltBuilder()->build($this->asXml(), $xsltLocation); } - public function addSello(string $key, string $passPhrase = '') + /** @internal This function is required by RetencionesCreatorTrait::addSello */ + private function getSelloAlgorithm(): int { - // create private key - $privateKey = new PemPrivateKey($key); - if (! $privateKey->open($passPhrase)) { - throw new \RuntimeException('Cannot open the private key'); - } - - // check privatekey belongs to certificado - if ($this->hasCertificado()) { - if (! $privateKey->belongsTo($this->getCertificado()->getPemContents())) { - throw new \RuntimeException('The private key does not belong to the current certificate'); - } - } - - // create sign and set into Sello attribute - $this->retenciones['Sello'] = base64_encode( - $privateKey->sign($this->buildCadenaDeOrigen(), OPENSSL_ALGO_SHA1) - ); - } - - public function validate(): Asserts - { - $validator = new XmlFollowSchema(); - $validator->setXmlResolver($this->getXmlResolver()); - $asserts = new Asserts(); - $validator->validate($this->retenciones, $asserts); - return $asserts; - } - - public function asXml(): string - { - return XmlNodeUtils::nodeToXmlString($this->retenciones, true); + return OPENSSL_ALGO_SHA1; } } diff --git a/src/CfdiUtils/Retenciones/RetencionesCreator20.php b/src/CfdiUtils/Retenciones/RetencionesCreator20.php new file mode 100644 index 00000000..88918fe5 --- /dev/null +++ b/src/CfdiUtils/Retenciones/RetencionesCreator20.php @@ -0,0 +1,70 @@ +retenciones = new Retenciones(); + $this->retencionesCreatorConstructor($retencionesAttributes, $certificado, $xmlResolver, $xsltBuilder); + } + + public function retenciones(): Retenciones + { + return $this->retenciones; + } + + public function putCertificado(Certificado $certificado) + { + $this->setCertificado($certificado); + $this->retenciones['NoCertificado'] = $certificado->getSerial(); + $this->retenciones['Certificado'] = $certificado->getPemContentsOneLine(); + // maybe put Emisor values from Certificate, as in CfdiCreatorTrait + } + + public function buildCadenaDeOrigen(): string + { + if (! $this->hasXmlResolver()) { + throw new \LogicException('Cannot build the cadena de origen since there is no xml resolver'); + } + $xmlResolver = $this->getXmlResolver(); + $xsltLocation = $xmlResolver->resolve( + 'http://www.sat.gob.mx/esquemas/retencionpago/2/retenciones.xslt', + $xmlResolver::TYPE_XSLT + ); + return $this->getXsltBuilder()->build($this->asXml(), $xsltLocation); + } + + /** @internal This function is required by RetencionesCreatorTrait::addSello */ + private function getSelloAlgorithm(): int + { + return OPENSSL_ALGO_SHA256; + } +} diff --git a/src/CfdiUtils/Retenciones/RetencionesCreatorTrait.php b/src/CfdiUtils/Retenciones/RetencionesCreatorTrait.php new file mode 100644 index 00000000..736d0e96 --- /dev/null +++ b/src/CfdiUtils/Retenciones/RetencionesCreatorTrait.php @@ -0,0 +1,77 @@ +retenciones->addAttributes($retencionesAttributes); + $this->setXmlResolver($xmlResolver ? : new XmlResolver()); + if (null !== $certificado) { + $this->putCertificado($certificado); + } + $this->setXsltBuilder($xsltBuilder ? : new DOMBuilder()); + } + + public function addSello(string $key, string $passPhrase = '') + { + // create private key + $privateKey = new PemPrivateKey($key); + if (! $privateKey->open($passPhrase)) { + throw new \RuntimeException('Cannot open the private key'); + } + + // check privatekey belongs to certificado + if ($this->hasCertificado()) { + if (! $privateKey->belongsTo($this->getCertificado()->getPemContents())) { + throw new \RuntimeException('The private key does not belong to the current certificate'); + } + } + + // create sign and set into Sello attribute + $this->retenciones['Sello'] = base64_encode( + $privateKey->sign($this->buildCadenaDeOrigen(), $this->getSelloAlgorithm()) + ); + } + + public function validate(): Asserts + { + $validator = new XmlFollowSchema(); + $validator->setXmlResolver($this->getXmlResolver()); + $asserts = new Asserts(); + $validator->validate($this->retenciones, $asserts); + return $asserts; + } + + public function asXml(): string + { + return XmlNodeUtils::nodeToXmlString($this->retenciones, true); + } +} diff --git a/tests/CfdiUtilsTests/Elements/Retenciones20/CfdiRetenRelacionadosTest.php b/tests/CfdiUtilsTests/Elements/Retenciones20/CfdiRetenRelacionadosTest.php new file mode 100644 index 00000000..1d27bbed --- /dev/null +++ b/tests/CfdiUtilsTests/Elements/Retenciones20/CfdiRetenRelacionadosTest.php @@ -0,0 +1,23 @@ +element = new CfdiRetenRelacionados(); + } + + public function testGetElementName() + { + $this->assertSame('retenciones:CfdiRetenRelacionados', $this->element->getElementName()); + } +} diff --git a/tests/CfdiUtilsTests/Elements/Retenciones20/RetencionesTest.php b/tests/CfdiUtilsTests/Elements/Retenciones20/RetencionesTest.php new file mode 100644 index 00000000..6ac2f153 --- /dev/null +++ b/tests/CfdiUtilsTests/Elements/Retenciones20/RetencionesTest.php @@ -0,0 +1,145 @@ +assertElementHasName($element, 'retenciones:Retenciones'); + $this->assertElementHasFixedAttributes($element, [ + 'xmlns:retenciones' => 'http://www.sat.gob.mx/esquemas/retencionpago/2', + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation' => vsprintf('%s %s', [ + 'http://www.sat.gob.mx/esquemas/retencionpago/2', + 'http://www.sat.gob.mx/esquemas/retencionpago/2/retencionpagov2.xsd', + ]), + 'Version' => '2.0', + ]); + $this->assertElementHasChildSingle($element, CfdiRetenRelacionados::class); + $this->assertElementHasChildSingle($element, Emisor::class); + $this->assertElementHasChildSingle($element, Receptor::class); + $this->assertElementHasChildSingle($element, Periodo::class); + $this->assertElementHasChildSingle($element, Totales::class); + $this->assertElementHasChildSingleAddChild($element, Complemento::class); + $this->assertElementHasChildSingleAddChild($element, Addenda::class); + $this->assertElementHasOrder($element, [ + 'retenciones:CfdiRetenRelacionados', + 'retenciones:Emisor', + 'retenciones:Receptor', + 'retenciones:Periodo', + 'retenciones:Totales', + 'retenciones:Complemento', + 'retenciones:Addenda', + ]); + } + + public function testCfdiRetenRelacionados() + { + $element = new CfdiRetenRelacionados(); + $this->assertElementHasName($element, 'retenciones:CfdiRetenRelacionados'); + } + + public function testEmisor() + { + $element = new Emisor(); + $this->assertElementHasName($element, 'retenciones:Emisor'); + } + + public function testReceptor() + { + $element = new Receptor(); + $this->assertElementHasName($element, 'retenciones:Receptor'); + $this->assertElementHasChildSingle($element, Nacional::class); + $this->assertElementHasChildSingle($element, Extranjero::class); + } + + public function testNacional() + { + $element = new Nacional(); + $this->assertElementHasName($element, 'retenciones:Nacional'); + } + + public function testExtranjero() + { + $element = new Extranjero(); + $this->assertElementHasName($element, 'retenciones:Extranjero'); + } + + public function testPeriodo() + { + $element = new Periodo(); + $this->assertElementHasName($element, 'retenciones:Periodo'); + } + + public function testTotales() + { + $element = new Totales(); + $this->assertElementHasName($element, 'retenciones:Totales'); + $this->assertElementHasChildMultiple($element, ImpRetenidos::class); + } + + public function testImpRetenidos() + { + $element = new ImpRetenidos(); + $this->assertElementHasName($element, 'retenciones:ImpRetenidos'); + } + + public function testComplemento() + { + $element = new Complemento(); + $this->assertElementHasName($element, 'retenciones:Complemento'); + } + + public function testAddenda() + { + $element = new Addenda(); + $this->assertElementHasName($element, 'retenciones:Addenda'); + } + + public function testShortcutRetencionImpRetenidos() + { + $element = new Retenciones(); + + $first = $element->addImpRetenidos(['id' => '1']); + $this->assertCount(1, $element->getTotales()->children()); + $this->assertTrue($element->getTotales()->children()->exists($first)); + + $second = $element->addImpRetenidos(['id' => '2']); + $this->assertCount(2, $element->getTotales()->children()); + $this->assertTrue($element->getTotales()->children()->exists($second)); + + $this->assertSame( + $element, + $element->multiImpRetenidos(['id' => '3'], ['id' => '4']), + 'Method Retenciones::multiImpRetenidos should return retenciones self instance' + ); + $this->assertCount(4, $element->getTotales()->children()); + + $this->assertSame( + ['1', '2', '3', '4'], + array_map( + function (NodeInterface $element): string { + return $element['id']; + }, + iterator_to_array($element->getTotales()->children()) + ), + 'All elements added should exists with expected values' + ); + } +} diff --git a/tests/CfdiUtilsTests/Retenciones/RetencionVersionTest.php b/tests/CfdiUtilsTests/Retenciones/RetencionVersionTest.php new file mode 100644 index 00000000..5573318b --- /dev/null +++ b/tests/CfdiUtilsTests/Retenciones/RetencionVersionTest.php @@ -0,0 +1,54 @@ +assertInstanceOf(RetencionVersion::class, $extended::exposeCreateDiscoverer()); + } + + public function providerRetencionVersion(): array + { + return [ + '2.0' => ['2.0', 'Version', '2.0'], + '1.0' => ['1.0', 'Version', '1.0'], + '2.0 bad case' => ['', 'version', '2.0'], + '1.0 bad case' => ['', 'version', '1.0'], + '2.0 non set' => ['', 'Version', null], + '1.0 non set' => ['', 'Version', null], + '2.0 empty' => ['', 'Version', ''], + '1.0 empty' => ['', 'Version', ''], + '2.0 wrong number' => ['', 'Version', '2.1'], + '1.0 wrong number' => ['', 'Version', '2.1'], + ]; + } + + /** + * @param string $expected + * @param string $attribute + * @param string|null $value + * @dataProvider providerRetencionVersion + */ + public function testRetencionVersion(string $expected, string $attribute, ?string $value): void + { + $node = new Node('retenciones', [$attribute => $value]); + $cfdiVersion = new RetencionVersion(); + $this->assertSame($expected, $cfdiVersion->getFromNode($node)); + $this->assertSame($expected, $cfdiVersion->getFromXmlString(XmlNodeUtils::nodeToXmlString($node))); + } +} diff --git a/tests/CfdiUtilsTests/Retenciones/RetencionesCreator10Test.php b/tests/CfdiUtilsTests/Retenciones/RetencionesCreator10Test.php index e2fda262..ed333fac 100644 --- a/tests/CfdiUtilsTests/Retenciones/RetencionesCreator10Test.php +++ b/tests/CfdiUtilsTests/Retenciones/RetencionesCreator10Test.php @@ -80,7 +80,7 @@ public function testCreatePreCfdiWithAllCorrectValues() $this->assertFalse($asserts->hasErrors()); // check against known content - $this->assertXmlStringEqualsXmlFile($this->utilAsset('retenciones/sample-before-tfd.xml'), $creator->asXml()); + $this->assertXmlStringEqualsXmlFile($this->utilAsset('retenciones/retenciones10.xml'), $creator->asXml()); } public function testValidateIsCheckingAgainstXsdViolations() diff --git a/tests/CfdiUtilsTests/Retenciones/RetencionesCreator20Test.php b/tests/CfdiUtilsTests/Retenciones/RetencionesCreator20Test.php new file mode 100644 index 00000000..91ca1736 --- /dev/null +++ b/tests/CfdiUtilsTests/Retenciones/RetencionesCreator20Test.php @@ -0,0 +1,133 @@ +utilAsset('certs/EKU9003173C9.cer'); + $pemFile = $this->utilAsset('certs/EKU9003173C9.key.pem'); + $passPhrase = ''; + $certificado = new Certificado($cerFile); + $xmlResolver = $this->newResolver(); + $xsltBuilder = new DOMBuilder(); + + // create object + $creator = new RetencionesCreator20([ + 'FechaExp' => '2022-01-13T14:15:16', + 'CveRetenc' => '14', // Dividendos o utilidades distribuidos + 'LugarExpRetenc' => '91778', + ], $xmlResolver, $xsltBuilder); + $retenciones = $creator->retenciones(); + + // available on RET 2.0 + $retenciones->addCfdiRetenRelacionados([ + 'TipoRelacion' => '01', + 'UUID' => '1474b7d3-61fc-41c4-a8b8-3f22e1161bb4', + ]); + $retenciones->addEmisor([ + 'RfcE' => 'EKU9003173C9', + 'NomDenRazSocE' => 'ESCUELA KEMPER URGATE', + 'RegimenFiscalE' => '601', + ]); + $retenciones->getReceptor()->addExtranjero([ + 'NumRegIdTribR' => '998877665544332211', + 'NomDenRazSocR' => 'WORLD WIDE COMPANY INC', + ]); + $retenciones->addPeriodo(['MesIni' => '05', 'MesFin' => '05', 'Ejercicio' => '2021']); + $retenciones->addTotales([ + 'MontoTotOperacion' => '55578643', + 'MontoTotGrav' => '0', + 'MontoTotExent' => '55578643', + 'MontoTotRet' => '0', + 'UtilidadBimestral' => '0.1', + 'ISRCorrespondiente' => '0.1', + ]); + $retenciones->addImpRetenidos([ + 'BaseRet' => '0', + 'ImpuestoRet' => '001', // same as CFDI + 'TipoPagoRet' => '01', + 'MontoRet' => '200.00', + ]); + + $dividendos = new Dividendos(); + $dividendos->addDividOUtil([ + 'CveTipDivOUtil' => '06', // 06 - Proviene de CUFIN al 31 de diciembre 2013 + 'MontISRAcredRetMexico' => '0', + 'MontISRAcredRetExtranjero' => '0', + 'MontRetExtDivExt' => '0', + 'TipoSocDistrDiv' => 'Sociedad Nacional', + 'MontISRAcredNal' => '0', + 'MontDivAcumNal' => '0', + 'MontDivAcumExt' => '0', + ]); + $retenciones->addComplemento($dividendos); + + // verify properties + $this->assertSame($xmlResolver, $creator->getXmlResolver()); + $this->assertSame($xsltBuilder, $creator->getXsltBuilder()); + + // verify root node + $root = $creator->retenciones(); + $this->assertSame('2.0', $root['Version']); + + // put additional content using helpers + $creator->putCertificado($certificado); + $creator->addSello('file://' . $pemFile, $passPhrase); + + // validate + $asserts = $creator->validate(); + $this->assertGreaterThanOrEqual(1, $asserts->count()); + $this->assertTrue($asserts->exists('XSD01')); + $this->assertSame('', $asserts->get('XSD01')->getExplanation()); + $this->assertFalse($asserts->hasErrors()); + + // check against known content + $this->assertXmlStringEqualsXmlFile($this->utilAsset('retenciones/retenciones20.xml'), $creator->asXml()); + } + + public function testValidateIsCheckingAgainstXsdViolations() + { + $retencion = new RetencionesCreator20(); + $retencion->setXmlResolver($this->newResolver()); + $assert = $retencion->validate()->get('XSD01'); + $this->assertTrue($assert->getStatus()->isError()); + } + + public function testAddSelloFailsWithWrongPassPrase() + { + $pemFile = $this->utilAsset('certs/EKU9003173C9_password.key.pem'); + $passPhrase = '_worng_passphrase_'; + + $retencion = new RetencionesCreator20(); + $retencion->setXmlResolver($this->newResolver()); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot open the private key'); + $retencion->addSello('file://' . $pemFile, $passPhrase); + } + + public function testAddSelloFailsWithWrongCertificado() + { + $cerFile = $this->utilAsset('certs/CSD09_AAA010101AAA.cer'); + $pemFile = $this->utilAsset('certs/EKU9003173C9.key.pem'); + $passPhrase = ''; + $certificado = new Certificado($cerFile); + + $retencion = new RetencionesCreator20(); + $retencion->setXmlResolver($this->newResolver()); + + $retencion->putCertificado($certificado); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The private key does not belong to the current certificate'); + $retencion->addSello('file://' . $pemFile, $passPhrase); + } +} diff --git a/tests/CfdiUtilsTests/Retenciones/RetencionesTest.php b/tests/CfdiUtilsTests/Retenciones/RetencionesTest.php index f2fef56f..1cfbc0a5 100644 --- a/tests/CfdiUtilsTests/Retenciones/RetencionesTest.php +++ b/tests/CfdiUtilsTests/Retenciones/RetencionesTest.php @@ -2,6 +2,7 @@ namespace CfdiUtilsTests\Retenciones; +use CfdiUtils\CfdiCreateObjectException; use CfdiUtils\Retenciones\Retenciones; use CfdiUtils\Utils\Xml; use CfdiUtilsTests\TestCase; @@ -12,6 +13,18 @@ final class RetencionesTest extends TestCase XML; + const XML_20_MINIMAL_DEFINITION = << +XML; + + public function providerRetencionesVersionNamespace(): array + { + return [ + '2.0' => ['2.0', 'http://www.sat.gob.mx/esquemas/retencionpago/2'], + '1.0' => ['1.0', 'http://www.sat.gob.mx/esquemas/retencionpago/1'], + ]; + } + public function testNewFromStringWithEmptyXml() { $this->expectException(\UnexpectedValueException::class); @@ -26,53 +39,73 @@ public function testNewFromStringWithInvalidXml() Retenciones::newFromString(' '); } - public function testConstructWithoutNamespace() + /** @dataProvider providerRetencionesVersionNamespace */ + public function testConstructWithoutNamespace(string $version, string $namespace) { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('namespace http://www.sat.gob.mx/esquemas/retencionpago/1'); - Retenciones::newFromString(''); + $exception = $this->captureException(function () { + Retenciones::newFromString(''); + }); + $this->assertInstanceOf(CfdiCreateObjectException::class, $exception); + /** @var CfdiCreateObjectException $exception */ + $this->assertStringContainsString( + 'namespace ' . $namespace, + $exception->getExceptionByVersion($version)->getMessage() + ); } - public function testConstructWithEmptyDomDocument() + /** @dataProvider providerRetencionesVersionNamespace */ + public function testConstructWithEmptyDomDocument(string $version) { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('DOM Document does not have root element'); - new Retenciones(new \DOMDocument()); + $exception = $this->captureException(function () { + new Retenciones(new \DOMDocument()); + }); + $this->assertInstanceOf(CfdiCreateObjectException::class, $exception); + /** @var CfdiCreateObjectException $exception */ + $this->assertStringContainsString( + 'DOM Document does not have root element', + $exception->getExceptionByVersion($version)->getMessage() + ); } - public function testInvalidCfdiRootIsNotComprobante() + /** @dataProvider providerRetencionesVersionNamespace */ + public function testInvalidCfdiRootIsNotComprobante(string $version, string $namespace) { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Root element is not retenciones:Retenciones'); - - Retenciones::newFromString( - str_replace('captureException(function () use ($namespace) { + Retenciones::newFromString(sprintf('', $namespace)); + }); + $this->assertInstanceOf(CfdiCreateObjectException::class, $exception); + /** @var CfdiCreateObjectException $exception */ + $this->assertStringContainsString( + 'Root element is not retenciones:Retenciones', + $exception->getExceptionByVersion($version)->getMessage() ); } - public function testInvalidCfdiRootIsPrefixedWithUnexpectedName() + /** @dataProvider providerRetencionesVersionNamespace */ + public function testInvalidCfdiRootIsPrefixedWithUnexpectedName(string $version, string $namespace) { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage( - 'Prefix for namespace http://www.sat.gob.mx/esquemas/retencionpago/1 is not "retenciones"' - ); - - Retenciones::newFromString( - str_replace( - ['captureException(function () use ($namespace) { + Retenciones::newFromString(sprintf('', $namespace)); + }); + $this->assertInstanceOf(CfdiCreateObjectException::class, $exception); + /** @var CfdiCreateObjectException $exception */ + $this->assertStringContainsString( + "Prefix for namespace $namespace is not \"retenciones\"", + $exception->getExceptionByVersion($version)->getMessage() ); } - public function testInvalidCfdiRootPrefixDoesNotMatchWithNamespaceDeclaration() + /** @dataProvider providerRetencionesVersionNamespace */ + public function testInvalidCfdiRootPrefixDoesNotMatchWithNamespaceDeclaration(string $version, string $namespace) { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Root element is not retenciones:Retenciones'); - - Retenciones::newFromString( - str_replace('captureException(function () use ($namespace) { + Retenciones::newFromString(sprintf('', $namespace)); + }); + $this->assertInstanceOf(CfdiCreateObjectException::class, $exception); + /** @var CfdiCreateObjectException $exception */ + $this->assertStringContainsString( + 'Root element is not retenciones:Retenciones', + $exception->getExceptionByVersion($version)->getMessage() ); } @@ -83,6 +116,13 @@ public function testValid10() $this->assertEquals('1.0', $retencion->getVersion()); } + public function testValida20() + { + $retencion = Retenciones::newFromString(self::XML_20_MINIMAL_DEFINITION); + + $this->assertEquals('2.0', $retencion->getVersion()); + } + public function testValid10WithXmlHeader() { $retencion = Retenciones::newFromString( diff --git a/tests/assets/retenciones/sample-before-tfd.xml b/tests/assets/retenciones/retenciones10.xml similarity index 100% rename from tests/assets/retenciones/sample-before-tfd.xml rename to tests/assets/retenciones/retenciones10.xml diff --git a/tests/assets/retenciones/retenciones20.xml b/tests/assets/retenciones/retenciones20.xml new file mode 100644 index 00000000..c437360f --- /dev/null +++ b/tests/assets/retenciones/retenciones20.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + +