diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 7de83c06..395d5beb 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -24,8 +24,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - # zip only for linter - extensions: mbstring, soap, :fileinfo, zip + extensions: mbstring, curl, soap, :fileinfo - name: Validate composer.json run: composer validate diff --git a/CHANGELOG.md b/CHANGELOG.md index 19553d62..7472576f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Los cambios notables de cada lanzamiento serán documentados en este archivo. ## Unreleased - #206 Agregar xml para nueva Guia de Remisión. - #209 Cambiar versión mínima de PHP a `7.4` +- #213 Agregar GRE cliente REST. ## 4.3.4 - 2022-11-21 diff --git a/README.md b/README.md index c76c0823..89569824 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Puede ver una demostración en [@greenter/demo](https://github.com/thegreenter/d ## Requerimientos - PHP `7.4` o superior -- Extensiones PHP Activadas: `soap`, `zlib`, `openssl`. +- Extensiones PHP: `soap`, `zlib`, `openssl`, `curl`. ## Documentación - Lee esta [guia](https://fe-primer.greenter.dev/) para conocer mas sobre facturación electrónica. diff --git a/composer.json b/composer.json index 3dd7a8d3..4b3427bd 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "require": { "php": ">=7.4", "bacon/bacon-qr-code": "^2.0", + "greenter/gre-api": "^1.0", "greenter/xmldsig": "^5.0", "mikehaertl/phpwkhtmltopdf": "^2.4", "nelexa/zip": "^4.0", @@ -31,15 +32,16 @@ "require-dev": { "greenter/ubl-validator": "^2.0", "mockery/mockery": "^1.2", - "phpstan/phpstan": "^1.8", - "phpunit/phpunit": "^8", - "vimeo/psalm": "^4.4" + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^9", + "vimeo/psalm": "^5.4" }, "suggest": { "ext-dom": "For xml, xml-parser, cpe-validator, ws package", "ext-soap": "For ws package", "ext-zlib": "For ws package", - "ext-xsl": "For cpe-validator package" + "ext-xsl": "For cpe-validator package", + "ext-curl": "For ws package (GRE)" }, "autoload": { "psr-4": { diff --git a/packages/core/src/Core/Services/Api/BasicToken.php b/packages/core/src/Core/Services/Api/BasicToken.php new file mode 100644 index 00000000..7d2a0447 --- /dev/null +++ b/packages/core/src/Core/Services/Api/BasicToken.php @@ -0,0 +1,59 @@ +value = $value; + $this->expire = $expire; + } + + /** + * @return string|null + */ + public function getValue(): ?string + { + return $this->value; + } + + /** + * @param string|null $value + * @return BasicToken + */ + public function setValue(?string $value): BasicToken + { + $this->value = $value; + return $this; + } + + /** + * @return DateTimeInterface|null + */ + public function getExpire(): ?DateTimeInterface + { + return $this->expire; + } + + /** + * @param DateTimeInterface|null $expire + * @return BasicToken + */ + public function setExpire(?DateTimeInterface $expire): BasicToken + { + $this->expire = $expire; + return $this; + } +} diff --git a/packages/core/src/Core/Services/Api/TokenStoreInterface.php b/packages/core/src/Core/Services/Api/TokenStoreInterface.php new file mode 100644 index 00000000..cfb53790 --- /dev/null +++ b/packages/core/src/Core/Services/Api/TokenStoreInterface.php @@ -0,0 +1,25 @@ + - - - - tests - - - - - src - - ./tests - ./vendor - - - - - - - - \ No newline at end of file + + + + + src + + + ./tests + ./vendor + + + + + + + + tests + + + + + + diff --git a/packages/htmltopdf/composer.json b/packages/htmltopdf/composer.json index 7ec8f460..c82ce32a 100644 --- a/packages/htmltopdf/composer.json +++ b/packages/htmltopdf/composer.json @@ -20,7 +20,7 @@ "mikehaertl/phpwkhtmltopdf": "^2.4" }, "require-dev": { - "phpunit/phpunit": "^8" + "phpunit/phpunit": "^9" }, "autoload": { "psr-4": { diff --git a/packages/htmltopdf/phpunit.xml.dist b/packages/htmltopdf/phpunit.xml.dist index 5d57d93b..43e91e73 100644 --- a/packages/htmltopdf/phpunit.xml.dist +++ b/packages/htmltopdf/phpunit.xml.dist @@ -1,25 +1,23 @@ - - - - - tests - - - - - src - - ./tests - ./vendor - - - - - - - - \ No newline at end of file + + + + + src + + + ./tests + ./vendor + + + + + + + + tests + + + + + + diff --git a/packages/lite/composer.json b/packages/lite/composer.json index c004d235..791fd2cd 100644 --- a/packages/lite/composer.json +++ b/packages/lite/composer.json @@ -23,7 +23,7 @@ "greenter/xml": "^4.4" }, "require-dev": { - "phpunit/phpunit": "^8" + "phpunit/phpunit": "^9" }, "autoload": { "psr-4": { diff --git a/packages/lite/phpunit.xml.dist b/packages/lite/phpunit.xml.dist index 4cf00867..43e91e73 100644 --- a/packages/lite/phpunit.xml.dist +++ b/packages/lite/phpunit.xml.dist @@ -1,25 +1,23 @@ - - - - - tests - - - - - src - - ./tests - ./vendor - - - - - - - - \ No newline at end of file + + + + + src + + + ./tests + ./vendor + + + + + + + + tests + + + + + + diff --git a/packages/lite/src/Greenter/Api.php b/packages/lite/src/Greenter/Api.php new file mode 100644 index 00000000..57565ad9 --- /dev/null +++ b/packages/lite/src/Greenter/Api.php @@ -0,0 +1,164 @@ + 'https://api-seguridad.sunat.gob.pe/v1', + 'cpe' => 'https://api.sunat.gob.pe/v1', + ]; + + /** + * Twig Render Options. + */ + private array $options = ['autoescape' => false]; + + /** + * @param array|null $endpoints + * @param ApiFactory|null $factory + * @param SignedXml|null $signer + */ + public function __construct(?array $endpoints = null, ?ApiFactory $factory = null, ?SignedXml $signer = null) + { + $this->factory = $factory ?? $this->createApiFactory($endpoints ?? $this->defaaultEndpoints); + $this->signer = $signer ?? new SignedXml(); + } + + /** + * Set Xml Builder Options. + * + * @param array $options + * + * @return Api + */ + public function setBuilderOptions(array $options): Api + { + $this->options = array_merge($this->options, $options); + + return $this; + } + + /** + * @param string $clientId + * @param string $secret + * + * @return Api + */ + public function setApiCredentials(string $clientId, string $secret): Api + { + $this->credentials['client_id'] = $clientId; + $this->credentials['client_secret'] = $secret; + + return $this; + } + + /** + * Set Clave SOL de usuario secundario. + * + * @param string $ruc + * @param string $user + * @param string $password + * + * @return Api + */ + public function setClaveSOL(string $ruc, string $user, string $password): Api + { + $this->credentials['username'] = $ruc.$user; + $this->credentials['password'] = $password; + + return $this; + } + + /** + * @param string $certificate + * + * @return Api + */ + public function setCertificate(string $certificate): Api + { + $this->signer->setCertificate($certificate); + + return $this; + } + + /** + * Envia comprobante. + * + * @param DocumentInterface $document + * + * @return BaseResult|null + * @throws ApiException + */ + public function send(DocumentInterface $document): ?BaseResult + { + $buildResolver = new XmlBuilderResolver($this->options); + $builder = $buildResolver->find(get_class($document)); + $sender = $this->createSender(); + + $xml = $builder->build($document); + $xmlSigned = $this->signer->signXml($xml); + return $sender->send($document->getName(), $xmlSigned); + } + + /** + * Consultar el estado del envio. + * + * @param string|null $ticket + * @return StatusResult + * @throws ApiException + */ + public function getStatus(?string $ticket): StatusResult + { + $sender = $this->createSender(); + + return $sender->status($ticket); + } + + /** + * @throws ApiException + */ + private function createSender(): GreSender + { + $api = $this->factory->create( + $this->credentials['client_id'], + $this->credentials['client_secret'], + $this->credentials['username'], + $this->credentials['password'] + ); + + return new GreSender($api); + } + + private function createApiFactory(array $endpoints): ApiFactory + { + $client = new Client(); + $config = new Configuration(); + + return new ApiFactory( + new AuthApi($client, $config->setHost($endpoints['auth'])), + $client, + new InMemoryStore(), + $endpoints['cpe'], + ); + } +} diff --git a/packages/lite/tests/Greenter/ApiTest.php b/packages/lite/tests/Greenter/ApiTest.php new file mode 100644 index 00000000..51c0d31c --- /dev/null +++ b/packages/lite/tests/Greenter/ApiTest.php @@ -0,0 +1,92 @@ +getApi(); + $despatch = $this->createDespatch(); + + /**@var $result SummaryResult */ + $result = $api->send($despatch); + + $this->assertTrue($result->isSuccess()); + $this->assertNotEmpty($result->getTicket()); + + $res = $api->getStatus($result->getTicket()); + $this->assertTrue($res->isSuccess()); + $this->assertEquals('0', $res->getCode()); + $this->assertNotEmpty($res->getCdrZip()); + $this->assertStringContainsString('ACEPTADA', $res->getCdrResponse()->getDescription()); + $this->assertStringStartsWith("http", $res->getCdrResponse()->getReference()); + } + + private function getApi(): Api + { + $api = new Api([ + 'auth' => 'https://gre-test.nubefact.com/v1', + 'cpe' => 'https://gre-test.nubefact.com/v1', + ]); + + return $api->setBuilderOptions([ + 'strict_variables' => true, + 'optimizations' => 0, + 'debug' => true, + 'cache' => false, + ]) + ->setApiCredentials('test-85e5b0ae-255c-4891-a595-0b98c65c9854', 'test-Hty/M6QshYvPgItX2P0+Kw==') + ->setClaveSOL('20161515648', 'MODDATOS', 'MODDATOS') + ->setCertificate(file_get_contents(__DIR__.'/../Resources/SFSCert.pem')); + } + + private function createDespatch(): Despatch + { + $envio = (new Shipment()) + ->setCodTraslado('01') + ->setIndicadores(['SUNAT_Envio_IndicadorTrasladoVehiculoM1L']) + ->setModTraslado('02') + ->setFecTraslado(new DateTime()) + ->setPesoTotal(12.5) + ->setUndPesoTotal('KGM') + ->setLlegada(new Direction('150101', 'AV LIMA')) + ->setPartida(new Direction('150203', 'AV ITALIA')); + + $despatch = new Despatch(); + $despatch->setVersion('2022') + ->setTipoDoc('09') + ->setSerie('T001') + ->setCorrelativo('1') + ->setFechaEmision(new DateTime()) + ->setCompany((new Company()) + ->setRuc('20161515648') + ->setRazonSocial('GREENTER SAC')) + ->setDestinatario((new Client()) + ->setTipoDoc('6') + ->setNumDoc('20000000002') + ->setRznSocial('EMPRESA DEST 1')) + ->setEnvio($envio); + + $detail = new DespatchDetail(); + $detail->setCantidad(2) + ->setUnidad('ZZ') + ->setDescripcion('PROD 1') + ->setCodigo('PROD1'); + + return $despatch->setDetails([$detail]); + } +} \ No newline at end of file diff --git a/packages/lite/tests/Greenter/Factory/CeFactoryBase.php b/packages/lite/tests/Greenter/Factory/CeFactoryBase.php index 1859e95d..127ff2a8 100644 --- a/packages/lite/tests/Greenter/Factory/CeFactoryBase.php +++ b/packages/lite/tests/Greenter/Factory/CeFactoryBase.php @@ -15,10 +15,6 @@ use Greenter\Model\Company\Address; use Greenter\Model\Company\Company; use Greenter\Model\Despatch\Despatch; -use Greenter\Model\Despatch\DespatchDetail; -use Greenter\Model\Despatch\Direction; -use Greenter\Model\Despatch\Shipment; -use Greenter\Model\Despatch\Transportist; use Greenter\Model\DocumentInterface; use Greenter\Model\Perception\Perception; use Greenter\Model\Perception\PerceptionDetail; @@ -27,7 +23,6 @@ use Greenter\Model\Retention\Payment; use Greenter\Model\Retention\Retention; use Greenter\Model\Retention\RetentionDetail; -use Greenter\Model\Sale\Document; use Greenter\Model\Summary\Summary; use Greenter\Model\Voided\Reversion; use Greenter\Model\Voided\VoidedDetail; @@ -82,9 +77,6 @@ protected function setUp(): void protected function getFactoryResult(DocumentInterface $document) { $url = SunatEndpoints::RETENCION_BETA; - if ($document instanceof Despatch) { - $url = SunatEndpoints::GUIA_BETA; - } $sender = $this->getSender(get_class($document), $url); $builder = new $this->builders[get_class($document)](); @@ -272,88 +264,6 @@ protected function getReversion() return $reversion; } - /** - * @return Despatch - */ - protected function getDespatch() - { - list($baja, $rel, $envio) = $this->getExtrasDespatch(); - $despatch = new Despatch(); - $despatch->setTipoDoc('09') - ->setSerie('T001') - ->setCorrelativo('123') - ->setFechaEmision(new \DateTime()) - ->setCompany($this->getCompany()) - ->setDestinatario((new Client()) - ->setTipoDoc('6') - ->setNumDoc('20000000002') - ->setRznSocial('EMPRESA ( />) 1')) - ->setTercero((new Client()) - ->setTipoDoc('6') - ->setNumDoc('20000000003') - ->setRznSocial('EMPRESA SA')) - ->setObservacion('NOTA GUIA') - ->setDocBaja($baja) - ->setRelDoc($rel) - ->setEnvio($envio); - - $detail = new DespatchDetail(); - $detail->setCantidad(2) - ->setUnidad('ZZ') - ->setCodProdSunat('22222') - ->setDescripcion('PROD 1') - ->setCodigo('PROD1'); - - $despatch->setDetails([$detail]); - - return $despatch; - } - - /** - * @return array - */ - private function getExtrasDespatch() - { - $baja = new Document(); - $baja->setTipoDoc('09') - ->setNroDoc('T001-00001'); - - $rel = new Document(); - $rel->setTipoDoc('02') // Tipo: Numero de Orden de Entrega - ->setNroDoc('213123'); - - $envio = new Shipment(); - $envio - ->setCodTraslado('01') - ->setDesTraslado('VENTA') - ->setFecTraslado(new \DateTime()) - ->setCodPuerto('123') - ->setIndTransbordo(false) - ->setPesoTotal(12.5) - ->setUndPesoTotal('KGM') -// ->setNumBultos(2) // Solo en Importación: CodTraslado: 08 - ->setModTraslado('01') - ->setNumContenedor('XD-2232') - ->setLlegada(new Direction('150101', 'AV LIMA')) - ->setPartida(new Direction('150203', 'AV ITALIA')) - ->setTransportista($this->getTransportist()); - - return [$baja, $rel, $envio]; - } - - private function getTransportist() - { - $transp = new Transportist(); - $transp->setTipoDoc('6') - ->setNumDoc('20000000002') - ->setRznSocial('TRANSPORTES S.A.C') - ->setPlaca('ABI-453') - ->setChoferTipoDoc('1') - ->setChoferDoc('40003344'); - - return $transp; - } - /** * @return Company */ diff --git a/packages/lite/tests/Greenter/Factory/CeFactoryTest.php b/packages/lite/tests/Greenter/Factory/CeFactoryTest.php index 5c73ab96..3054d592 100644 --- a/packages/lite/tests/Greenter/Factory/CeFactoryTest.php +++ b/packages/lite/tests/Greenter/Factory/CeFactoryTest.php @@ -19,26 +19,6 @@ */ class CeFactoryTest extends CeFactoryBase { - public function testDespatch() - { - $despatch = $this->getDespatch(); - $result = $this->getFactoryResult($despatch); - - // Enviar a API - if (!$result->isSuccess() && - $result->getError()->getCode() == '1085') { - return; - } - - $this->assertTrue($result->isSuccess()); - $this->assertNotNull($result->getCdrResponse()); - $this->assertEquals( - '0', - $result->getCdrResponse()->getCode() - ); - - } - public function testRetention() { $retention = $this->getRetention(); @@ -54,8 +34,8 @@ public function testRetention() public function testGetXmlSigned() { - $despatch = $this->getDespatch(); - $signXml = $this->getXmlSigned($despatch); + $perception = $this->getPerception(); + $signXml = $this->getXmlSigned($perception); $this->assertNotEmpty($signXml); } diff --git a/packages/lite/tests/Greenter/SeeCeTest.php b/packages/lite/tests/Greenter/SeeCeTest.php index 504da943..4afd9cd5 100644 --- a/packages/lite/tests/Greenter/SeeCeTest.php +++ b/packages/lite/tests/Greenter/SeeCeTest.php @@ -23,18 +23,6 @@ */ class SeeCeTest extends CeFactoryBase { - public function testSendDespatch() - { - $doc = $this->getDespatch(); - - /**@var $result BillResult*/ - $result = $this->getSee(SunatEndpoints::GUIA_BETA)->send($doc); - - // Enviar a API - $this->assertFalse($result->isSuccess()); - $this->assertEquals($result->getError()->getCode(), '1085'); - } - /** * @dataProvider providerBillDocs * @param DocumentInterface $doc diff --git a/packages/report/composer.json b/packages/report/composer.json index d8218262..84cba6b2 100644 --- a/packages/report/composer.json +++ b/packages/report/composer.json @@ -21,7 +21,7 @@ "bacon/bacon-qr-code": "^2.0" }, "require-dev": { - "phpunit/phpunit": "^8", + "phpunit/phpunit": "^9", "greenter/data": "^4.4" }, "autoload": { diff --git a/packages/report/phpunit.xml.dist b/packages/report/phpunit.xml.dist index 4cf00867..43e91e73 100644 --- a/packages/report/phpunit.xml.dist +++ b/packages/report/phpunit.xml.dist @@ -1,25 +1,23 @@ - - - - - tests - - - - - src - - ./tests - ./vendor - - - - - - - - \ No newline at end of file + + + + + src + + + ./tests + ./vendor + + + + + + + + tests + + + + + + diff --git a/packages/report/src/Report/XmlUtils.php b/packages/report/src/Report/XmlUtils.php index 09e970bd..05d317b5 100644 --- a/packages/report/src/Report/XmlUtils.php +++ b/packages/report/src/Report/XmlUtils.php @@ -81,9 +81,9 @@ private function getXpath(DOMDocument $document) /** * @param DOMNodeList $exts * @param DOMXPath $xpt - * @return string + * @return string|null */ - private function getHash(DOMNodeList $exts, DOMXPath $xpt) + private function getHash(DOMNodeList $exts, DOMXPath $xpt): ?string { for ($i = $exts->length; $i-- > 0;) { $nodeSign = $exts->item($i); diff --git a/packages/validator/composer.json b/packages/validator/composer.json index c64aeb7d..e65caf45 100644 --- a/packages/validator/composer.json +++ b/packages/validator/composer.json @@ -20,7 +20,7 @@ "symfony/validator": "^5.0 || ^6.0" }, "require-dev": { - "phpunit/phpunit": "^8" + "phpunit/phpunit": "^9" }, "autoload": { "psr-4": { diff --git a/packages/validator/phpunit.xml.dist b/packages/validator/phpunit.xml.dist index 4cf00867..43e91e73 100644 --- a/packages/validator/phpunit.xml.dist +++ b/packages/validator/phpunit.xml.dist @@ -1,25 +1,23 @@ - - - - - tests - - - - - src - - ./tests - ./vendor - - - - - - - - \ No newline at end of file + + + + + src + + + ./tests + ./vendor + + + + + + + + tests + + + + + + diff --git a/packages/ws/README.md b/packages/ws/README.md index 3312e3ba..4d8a72ea 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -2,7 +2,7 @@ [![src greenter](https://greenter.dev/img/greenter_badge.svg)](https://github.com/thegreenter/greenter) -Conexión con los webservices de SUNAT, para el envío de comprabantes electrónicos. +Conexión con los webservices de SUNAT, para el envío de comprobantes electrónicos. ## Recursos - [Documentación](https://greenter.dev/packages/ws/) diff --git a/packages/ws/composer.json b/packages/ws/composer.json index f4ab1627..bd7086ab 100644 --- a/packages/ws/composer.json +++ b/packages/ws/composer.json @@ -20,10 +20,11 @@ "ext-dom": "*", "ext-soap": "*", "greenter/core": "^4.4", + "greenter/gre-api": "^1.0", "nelexa/zip": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^8", + "phpunit/phpunit": "^9", "mockery/mockery": "^1.2" }, "autoload": { @@ -36,6 +37,9 @@ "Tests\\Greenter\\": "tests/" } }, + "suggest": { + "ext-curl": "Necesario para enviar guia de remision" + }, "extra": { "branch-alias": { "dev-master": "4.4-dev" diff --git a/packages/ws/phpunit.xml.dist b/packages/ws/phpunit.xml.dist index f3422fea..beb3b2c4 100644 --- a/packages/ws/phpunit.xml.dist +++ b/packages/ws/phpunit.xml.dist @@ -1,28 +1,26 @@ - - - - - tests - - - - - src - - ./tests - ./vendor - - - - - - - - - - - \ No newline at end of file + + + + + src + + + ./tests + ./vendor + + + + + + + + tests + + + + + + + + + diff --git a/packages/ws/src/Api/ApiFactory.php b/packages/ws/src/Api/ApiFactory.php new file mode 100644 index 00000000..982469b2 --- /dev/null +++ b/packages/ws/src/Api/ApiFactory.php @@ -0,0 +1,95 @@ +api = $api; + $this->client = $client; + $this->store = $store; + $this->cpeEndpoint = $endpoint; + } + + /** + * @throws ApiException + * @throws Exception + */ + public function create(?string $clientId, ?string $secret, ?string $user, ?string $password): CpeApiInterface + { + $token = $this->getToken($clientId, $secret, $user, $password); + + $config = (new Configuration()) + ->setAccessToken($token); + + return new CpeApi( + $this->client, + $config->setHost($this->cpeEndpoint ?? $config->getHostFromSettings(1)) + ); + } + + /** + * @throws Exception + */ + private function getToken(?string $clientId, ?string $secret, ?string $user, ?string $password): ?string + { + $tokenData = $this->store->get($clientId); + if ($tokenData && $tokenData->getExpire() > $this->addSeconds(new DateTime(), 600)) { + return $tokenData->getValue(); + } + + $result = $this->api->getToken( + 'password', + 'https://api-cpe.sunat.gob.pe', + $clientId, + $secret, + $user, + $password + ); + + $token = $result->getAccessToken(); + $expire = $this->addSeconds(new DateTime(), $result->getExpiresIn()); + $this->store->set($clientId, new BasicToken($token, $expire)); + + return $token; + } + + /** + * @throws Exception + */ + private function addSeconds(DateTime $time, int $seconds): DateTime + { + return $time->add(new DateInterval('PT'.$seconds.'S')); + } +} diff --git a/packages/ws/src/Api/GreSender.php b/packages/ws/src/Api/GreSender.php new file mode 100644 index 00000000..4c58e3d3 --- /dev/null +++ b/packages/ws/src/Api/GreSender.php @@ -0,0 +1,124 @@ +api = $api; + } + + public function send(?string $filename, ?string $content): ?BaseResult + { + $result = new SummaryResult(); + try { + $zipContent = $this->compress($filename.'.xml', $content); + $document = (new CpeDocument()) + ->setArchivo((new CpeDocumentArchivo()) + ->setNomArchivo($filename.'.zip') + ->setArcGreZip(base64_encode($zipContent)) + ->setHashZip(hash('sha256', $zipContent)) + ); + $response = $this->api->enviarCpe($filename, $document); + $result + ->setTicket($response->getNumTicket()) + ->setSuccess(true); + } catch (ApiException $e) { + $result->setError($this->processException($e)); + } + + return $result; + } + + public function status(?string $ticket): StatusResult + { + $result = new StatusResult(); + try { + $response = $this->api->consultarEnvio($ticket); + + return $this->processResponse($response); + } catch (ApiException $e) { + $result->setError($this->processException($e)); + } + + return $result; + } + + /** + * @param StatusResponse $status + * @return StatusResult + */ + private function processResponse(StatusResponse $status): StatusResult + { + $code = $status->getCodRespuesta(); + + $result = new StatusResult(); + $result->setCode($code); + + $isPending = $code === '98'; + if ($isPending) { + $result->setError(new Error($code, 'En proceso')); + + return $result; + } + + $isCompleted = $code === '0' || $code === '99'; + if ($isCompleted) { + if ($status->getError()) { + $result->setError( + new Error( + $status->getError()->getNumError(), + $status->getError()->getDesError() + ) + ); + } + + if ($status->getIndCdrGenerado() === '1') { + $cdrZip = base64_decode($status->getArcCdr()); + $result + ->setSuccess(true) + ->setCdrResponse($this->extractResponse((string)$cdrZip)) + ->setCdrZip($cdrZip); + } + } + + return $result; + } + + private function processException(ApiException $ex): Error + { + if ($ex->getCode() === 422) { + /**@var $resp \Greenter\Sunat\GRE\Model\CpeErrorValidation */ + $resp = $ex->getResponseObject(); + foreach ($resp->getErrors() as $err) { + return new Error($err->getCod(), $err->getMsg()); + } + } elseif ($ex->getCode() === 500) { + /**@var $resp \Greenter\Sunat\GRE\Model\CpeError */ + $resp = $ex->getResponseObject(); + return new Error($resp->getCod(), $resp->getMsg()); + } + + return new Error("API", $ex->getMessage()); + } +} diff --git a/packages/ws/src/Api/InMemoryStore.php b/packages/ws/src/Api/InMemoryStore.php new file mode 100644 index 00000000..bd95e959 --- /dev/null +++ b/packages/ws/src/Api/InMemoryStore.php @@ -0,0 +1,30 @@ +tokens)) { + return $this->tokens[$id]; + } + + return null; + } + + public function set(?string $id, BasicToken $token): void + { + $this->tokens[$id] = $token; + } +} diff --git a/packages/ws/src/Ws/Services/SunatEndpoints.php b/packages/ws/src/Ws/Services/SunatEndpoints.php index 64350123..a0776140 100644 --- a/packages/ws/src/Ws/Services/SunatEndpoints.php +++ b/packages/ws/src/Ws/Services/SunatEndpoints.php @@ -25,8 +25,13 @@ final class SunatEndpoints /** * GUIA DE REMISION SERVICES. + * + * @deprecated use API endpoint */ public const GUIA_BETA = 'https://e-beta.sunat.gob.pe/ol-ti-itemision-guia-gem-beta/billService'; + /** + * @deprecated use API endpoint + */ public const GUIA_PRODUCCION = 'https://e-guiaremision.sunat.gob.pe/ol-ti-itemision-guia-gem/billService'; /** diff --git a/packages/ws/tests/Resources/cdrGRE.zip b/packages/ws/tests/Resources/cdrGRE.zip new file mode 100644 index 00000000..3d9673ab Binary files /dev/null and b/packages/ws/tests/Resources/cdrGRE.zip differ diff --git a/packages/ws/tests/Ws/Api/ApiFactoryTest.php b/packages/ws/tests/Ws/Api/ApiFactoryTest.php new file mode 100644 index 00000000..7d5d4430 --- /dev/null +++ b/packages/ws/tests/Ws/Api/ApiFactoryTest.php @@ -0,0 +1,58 @@ +deps(); + $factory = new ApiFactory($auth, $client, $store, null); + $api = $factory->create('x', 'x', '20123456780MODDATOS', 'moddatos'); + $token = $store->get('x'); + + $this->assertInstanceOf(CpeApi::class, $api); + $this->assertNotNull($token); + + $factory->create('x', 'x', '20123456780MODDATOS', 'moddatos'); + $token2 = $store->get('x'); // no changes + + $this->assertEquals($token, $token2); + + // get token if expireIn < 10min + $store->set('x', $token->setValue('xx')->setExpire(new DateTime('+9 minutes'))); + $factory->create('x', 'x', '20123456780MODDATOS', 'moddatos'); + } + + private function deps(): array + { + $auth = Mockery::mock(AuthApiInterface::class); + $auth->shouldReceive('getToken') + ->twice() + ->andReturn(new ApiToken([ + 'access_token' => 'xxxx.xxxx.xxxx', + 'token_type' => 'JWT', + 'expires_in' => 3600, + ])); + + $client = Mockery::mock(ClientInterface::class); + return [$auth, $client, new InMemoryStore()]; + } + + public function tearDown(): void + { + Mockery::close(); + } +} diff --git a/packages/ws/tests/Ws/Api/GreSenderTest.php b/packages/ws/tests/Ws/Api/GreSenderTest.php new file mode 100644 index 00000000..4252ef96 --- /dev/null +++ b/packages/ws/tests/Ws/Api/GreSenderTest.php @@ -0,0 +1,148 @@ +shouldReceive('enviarCpe') + ->andReturn(new CpeResponse(['num_ticket' => 'a.b.c', 'fec_recepcion' => '2023-01-10T20:10:50'])); + $sender = new GreSender($api); + + $nameXml = '20600055519-01-F001-00000001'; + $xml = file_get_contents(__DIR__."/../../Resources/$nameXml.xml"); + /**@var $result SummaryResult */ + $result = $sender->send($nameXml, $xml); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('a.b.c', $result->getTicket()); + } + + public function testSendInValid(): void + { + $api = Mockery::mock(CpeApiInterface::class); + $api->shouldReceive('enviarCpe') + ->andThrowExceptions([ + $this->createException(422, new CpeErrorValidation([ + 'cod' => '422', + 'msg' => 'Unprocessable Entity', + 'errors' => [new CpeError(['cod' => '501', 'msg' => 'El valor de codCpe no permitido o no valido'])] + ])), + $this->createException(500, new CpeError(['cod' => '500', 'msg' => 'Internal Server Error'])), + $this->createException(400, new CpeError()) + ]); + $sender = new GreSender($api); + + $nameXml = '20600055519-01-F001-00000001'; + $xml = file_get_contents(__DIR__."/../../Resources/$nameXml.xml"); + /**@var $result SummaryResult */ + $result = $sender->send($nameXml, $xml); + + $this->assertFalse($result->isSuccess()); + $this->assertEquals('501', $result->getError()->getCode()); + + /**@var $result SummaryResult */ + $result = $sender->send($nameXml, $xml); + + $this->assertFalse($result->isSuccess()); + $this->assertEquals('500', $result->getError()->getCode()); + + /**@var $result SummaryResult */ + $result = $sender->send($nameXml, $xml); + + $this->assertFalse($result->isSuccess()); + $this->assertEquals('API', $result->getError()->getCode()); + } + + public function testStatusCompleted(): void + { + $api = Mockery::mock(CpeApiInterface::class); + $api->shouldReceive('consultarEnvio') + ->andReturn( + new StatusResponse([ + 'cod_respuesta' => '0', + 'arc_cdr' => base64_encode(file_get_contents(__DIR__."/../../Resources/cdrGRE.zip")), + 'ind_cdr_generado' => '1', + ]) + ); + $sender = new GreSender($api); + $result = $sender->status('a.b.c'); + $cdr = $result->getCdrResponse(); + $this->assertTrue($result->isSuccess()); + $this->assertNotEmpty($result->getCdrZip()); + $this->assertStringStartsWith('https://e-factura.sunat.gob.pe/', $cdr->getReference()); + $this->assertEquals(1, count($cdr->getNotes())); + $this->assertEquals('T001-1', $cdr->getId()); + $this->assertEquals('0', $cdr->getCode()); + } + + public function testStatusPending(): void + { + $api = Mockery::mock(CpeApiInterface::class); + $api->shouldReceive('consultarEnvio') + ->andThrowExceptions([ + $this->createException(500, new CpeError(['cod' => '500', 'msg' => 'Internal Server Error'])), + ]); + $sender = new GreSender($api); + $result = $sender->status('a.b.c'); + + $this->assertFalse($result->isSuccess()); + $this->assertEmpty($result->getCode()); + $this->assertNotNull($result->getError()); + } + + public function testStatusError(): void + { + $api = Mockery::mock(CpeApiInterface::class); + $api->shouldReceive('consultarEnvio') + ->andReturn( + new StatusResponse([ + 'cod_respuesta' => '98', + 'ind_cdr_generado' => '0', + ]), + new StatusResponse([ + 'cod_respuesta' => '99', + 'error' => new StatusResponseError([ + 'num_error' => '1345', + 'des_error' => 'El RUC no valido' + ]), + 'ind_cdr_generado' => '0', + ]) + ); + $sender = new GreSender($api); + $result = $sender->status('a.b.c'); + + $this->assertFalse($result->isSuccess()); + $this->assertEquals('98', $result->getCode()); + + $result = $sender->status('a.b.c'); + + $this->assertFalse($result->isSuccess()); + $this->assertEquals('99', $result->getCode()); + $this->assertEquals('1345', $result->getError()->getCode()); + } + + private function createException(int $code, object $data): ApiException + { + $ex = new ApiException('TEST ERROR', $code); + $ex->setResponseObject($data); + + return $ex; + } +} diff --git a/packages/xcodes/composer.json b/packages/xcodes/composer.json index ef1f634f..4738164c 100644 --- a/packages/xcodes/composer.json +++ b/packages/xcodes/composer.json @@ -19,7 +19,7 @@ "greenter/core": "^4.4" }, "require-dev": { - "phpunit/phpunit": "^8" + "phpunit/phpunit": "^9" }, "autoload": { "psr-4": { diff --git a/packages/xcodes/phpunit.xml.dist b/packages/xcodes/phpunit.xml.dist index 4cf00867..43e91e73 100644 --- a/packages/xcodes/phpunit.xml.dist +++ b/packages/xcodes/phpunit.xml.dist @@ -1,25 +1,23 @@ - - - - - tests - - - - - src - - ./tests - ./vendor - - - - - - - - \ No newline at end of file + + + + + src + + + ./tests + ./vendor + + + + + + + + tests + + + + + + diff --git a/packages/xml-parser/phpunit.xml.dist b/packages/xml-parser/phpunit.xml.dist index 4cf00867..43e91e73 100644 --- a/packages/xml-parser/phpunit.xml.dist +++ b/packages/xml-parser/phpunit.xml.dist @@ -1,25 +1,23 @@ - - - - - tests - - - - - src - - ./tests - ./vendor - - - - - - - - \ No newline at end of file + + + + + src + + + ./tests + ./vendor + + + + + + + + tests + + + + + + diff --git a/packages/xml/composer.json b/packages/xml/composer.json index fd81da23..14ffcdee 100644 --- a/packages/xml/composer.json +++ b/packages/xml/composer.json @@ -22,7 +22,7 @@ "require-dev": { "greenter/data": "^4.4", "greenter/ubl-validator": "^2.0", - "phpunit/phpunit": "^8" + "phpunit/phpunit": "^9" }, "autoload": { "psr-4": { diff --git a/packages/xml/phpunit.xml.dist b/packages/xml/phpunit.xml.dist index 4cf00867..43e91e73 100644 --- a/packages/xml/phpunit.xml.dist +++ b/packages/xml/phpunit.xml.dist @@ -1,25 +1,23 @@ - - - - - tests - - - - - src - - ./tests - ./vendor - - - - - - - - \ No newline at end of file + + + + + src + + + ./tests + ./vendor + + + + + + + + tests + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6b9c13f1..4b9bcedd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,22 +1,16 @@ - - - - packages/**/tests - - - - api - - - - - packages/*/src - - - \ No newline at end of file + + + + + packages/*/src + + + + packages/**/tests + + + + api + + +