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 @@
[](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
+
+
+