Skip to content

Commit

Permalink
[WIP] feat: upload xml (efactura) to ANAF (#17)
Browse files Browse the repository at this point in the history
* feature: upload xml (efactura) to ANAF + tests

* Upload efactura
  • Loading branch information
ciungulete committed Jan 20, 2024
1 parent c8a8d7e commit 1f5695c
Show file tree
Hide file tree
Showing 15 changed files with 228 additions and 37 deletions.
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,22 @@ $entityInfo->toArray(); // ["tax_identification_number" => '', "entity_name" =>
### [eFactura](https://mfinante.gov.ro/web/efactura/informatii-tehnice) Resource

#### [Upload](https://mfinante.gov.ro/static/10/eFactura/upload.html) Resource
TODO: implement `upload` from [here](hhttps://mfinante.gov.ro/static/10/eFactura/upload.html)

Upload an XML (eFactura) file to the SPV

TODO: improve error handling

```php
$upload = $authorizedClient->efactura()->upload(
xml_path: $pathToXmlFile,
taxIdentificationNumber: '12345678',
//standard: UploadStandard::UBL, // default value is UBL
//extern: false, // default value is false
);
$upload->responseDate, // 202401011640
$upload->executionStatus,
$upload->uploadIndex,
```

#### [Status](https://mfinante.gov.ro/static/10/eFactura/upload.html) Resource
TODO: implement `status` from [here](https://mfinante.gov.ro/static/10/eFactura/staremesaj.html)
Expand Down Expand Up @@ -291,8 +306,6 @@ $file = $client->efactura()->xmlToPdf($pathToXmlFile, $xmlStandard);
$file->getContent(); // string - You can save the pdf content to a file
```

TODO: implement `/transformare/{standard}/{novld}` from [here](https://mfinante.gov.ro/static/10/eFactura/xmltopdf.html#/EFacturaXmlToPdf/getPdfNoVld)

---

ANAF PHP is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
Binary file modified art/social.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"require": {
"php": "^8.1.0",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^7.5.0",
"php-http/discovery": "^1.19"
},
Expand All @@ -23,8 +25,7 @@
"pestphp/pest-plugin-arch": "^2.5",
"pestphp/pest-plugin-type-coverage": "^2.7",
"phpstan/phpstan": "^1.10.54",
"rector/rector": "^0.18.13",
"spatie/ray": "^1.40",
"rector/rector": "^0.17.13",
"symfony/var-dumper": "^6.2.2"
},
"autoload": {
Expand Down
16 changes: 16 additions & 0 deletions src/Enums/Efactura/UploadStandard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Anaf\Enums\Efactura;

/**
* @internal
*/
enum UploadStandard: string
{
case UBL = 'UBL';
case CN = 'CN';
case CII = 'CII';
case RASP = 'RASP';
}
11 changes: 8 additions & 3 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Factory
*/
private ?string $baseUri = null;

private bool $staging = false;
private static bool $staging = false;

/**
* The query parameters for the requests.
Expand Down Expand Up @@ -58,11 +58,16 @@ public function withBaseUri(string $baseUri): self
*/
public function staging(): self
{
$this->staging = true;
self::$staging = true;

return $this;
}

public static function isStaging(): bool
{
return self::$staging;
}

/**
* Adds a custom query parameter to the request url.
*/
Expand All @@ -84,7 +89,7 @@ public function make(): Client
$headers = Headers::withAuthorization(ApiKey::from($this->apiKey));
}

$baseUri = BaseUri::from($this->baseUri ?: 'webservicesp.anaf.ro', $this->staging);
$baseUri = BaseUri::from($this->baseUri ?: 'webservicesp.anaf.ro');

$queryParams = QueryParams::create();

Expand Down
27 changes: 27 additions & 0 deletions src/Resources/Efactura.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace Anaf\Resources;

use Anaf\Contracts\FileContract;
use Anaf\Enums\Efactura\UploadStandard;
use Anaf\Responses\Efactura\CreateMessagesResponse;
use Anaf\Responses\Efactura\CreateUploadResponse;
use Anaf\ValueObjects\Transporter\Payload;
use Anaf\ValueObjects\Transporter\Xml;
use Exception;
Expand All @@ -15,6 +17,31 @@ class Efactura
{
use Concerns\Transportable;

/**
* Upload an eFactura XML file to ANAF.
*
* @see https://mfinante.gov.ro/static/10/eFactura/upload.html
*
* @throws Exception
*/
public function upload(string $xml_path, string $tax_identification_number, UploadStandard $standard = UploadStandard::UBL, bool $extern = false): CreateUploadResponse
{
$payload = Payload::upload(
resource: 'prod/FCTEL/rest/upload',
body: Xml::from($xml_path)->toString(),
parameters: [
'cif' => $tax_identification_number,
'standard' => $standard->value,
...($extern ? ['extern' => 'DA'] : []),
],
);

/** @var array<array-key, array{dateResponse: string, ExecutionStatus: string, index_incarcare: string}> $response */
$response = $this->transporter->requestObject($payload);

return CreateUploadResponse::from($response['@attributes']);
}

/**
* Get the list of messages for a given taxpayer.
*
Expand Down
55 changes: 55 additions & 0 deletions src/Responses/Efactura/CreateUploadResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Anaf\Responses\Efactura;

use Anaf\Contracts\Response;
use Anaf\Responses\Concerns\ArrayAccessible;

/**
* @implements Response<array{response_date: string, execution_status: string, upload_index: string}>
*/
class CreateUploadResponse implements Response
{
/**
* @use ArrayAccessible<array{response_date: string, execution_status: string, upload_index: string}>
*/
use ArrayAccessible;

/**
* Creates a new CreateResponse instance.
*/
private function __construct(
public readonly string $responseDate,
public readonly string $executionStatus,
public readonly string $uploadIndex,
) {
}

/**
* Acts as static factory, and returns a new Response instance.
*
* @param array{dateResponse: string, ExecutionStatus: string, index_incarcare: string} $attributes
*/
public static function from(array $attributes): self
{
return new self(
$attributes['dateResponse'],
$attributes['ExecutionStatus'],
$attributes['index_incarcare'],
);
}

/**
* {@inheritDoc}
*/
public function toArray(): array
{
return [
'response_date' => $this->responseDate,
'execution_status' => $this->executionStatus,
'upload_index' => $this->uploadIndex,
];
}
}
25 changes: 22 additions & 3 deletions src/Transporters/HttpTransporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public function __construct(
* {@inheritDoc}
*
* @throws TaxIdentificationNumberNotFoundException
* @throws JsonException
*/
public function requestObject(Payload $payload): array
{
Expand All @@ -54,12 +55,30 @@ public function requestObject(Payload $payload): array
$contents = $response->getBody()->getContents();

try {
/** @var array{notFound?: array<int, int>} $response */
/** @var array<array-key, mixed> $response */
$response = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);

} catch (JsonException $jsonException) {
throw new UnserializableResponse($jsonException);

$xml = simplexml_load_string($contents, 'SimpleXMLElement', LIBXML_NOCDATA);

if ($xml === false) {
throw new UnserializableResponse($jsonException);
}

try {
$jsonResponse = json_encode($xml, JSON_THROW_ON_ERROR);

} catch (JsonException $jsonException) {
throw new UnserializableResponse($jsonException);
}

/** @var array<array-key, mixed> $response */
$response = json_decode($jsonResponse, true, 512, JSON_THROW_ON_ERROR);

}
if (! array_key_exists('notFound', $response)) {

if (! array_key_exists('notFound', $response) && ! array_key_exists('Errors', $response)) {
return $response;
}

Expand Down
22 changes: 4 additions & 18 deletions src/ValueObjects/Transporter/BaseUri.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,16 @@ class BaseUri implements StringableContract
*/
private function __construct(
private readonly string $baseUri,
private readonly bool $staging,
) {
// ..
}

/**
* Creates a new Base URI value object.
*/
public static function from(string $baseUri, bool $staging = false): self
public static function from(string $baseUri): self
{
return new self($baseUri, $staging);
return new self($baseUri);
}

/**
Expand All @@ -36,23 +35,10 @@ public function toString(): string
{
foreach (['http://', 'https://'] as $protocol) {
if (str_starts_with($this->baseUri, $protocol)) {
return $this->setStaging($this->baseUri);
return "{$this->baseUri}/";
}
}

return $this->setStaging("https://{$this->baseUri}");
}

/**
* Sets the staging environment.
*/
private function setStaging(string $baseUri): string
{
// check if base url contains prod string and replace with test if staging is true
if ($this->staging) {
$baseUri = str_replace('/prod/', '/test/', $baseUri);
}

return $baseUri.'/';
return "https://{$this->baseUri}/";
}
}
33 changes: 27 additions & 6 deletions src/ValueObjects/Transporter/Payload.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Anaf\Enums\Transporter\ContentType;
use Anaf\Enums\Transporter\Method;
use Anaf\Factory;
use Anaf\ValueObjects\ResourceUri;
use Http\Discovery\Psr17Factory;
use Psr\Http\Message\RequestInterface;
Expand All @@ -23,6 +24,7 @@ class Payload
*/
private function __construct(
private readonly ContentType $contentType,
private readonly ContentType $acceptContentType,
private readonly Method $method,
private readonly ResourceUri $uri,
private readonly array $parameters = [],
Expand All @@ -39,23 +41,26 @@ private function __construct(
public static function create(string $resource, array $parameters): self
{
$contentType = ContentType::JSON;
$acceptContentType = ContentType::JSON;
$method = Method::POST;
$uri = ResourceUri::create($resource);

return new self($contentType, $method, $uri, $parameters);
return new self($contentType, $acceptContentType, $method, $uri, $parameters);
}

/**
* Creates a new Payload value object from the given parameters.
*
* @param array<array-key, string> $parameters
*/
public static function upload(string $resource, string $body, array $parameters = [], ContentType $contentType = ContentType::TEXT): self
public static function upload(string $resource, string $body, array $parameters = []): self
{
$method = Method::POST;
$uri = ResourceUri::create($resource);
$contentType = ContentType::TEXT;
$acceptContentType = ContentType::ALL;

return new self($contentType, $method, $uri, $parameters, $body);
return new self($contentType, $acceptContentType, $method, $uri, $parameters, $body);
}

/**
Expand All @@ -66,10 +71,11 @@ public static function upload(string $resource, string $body, array $parameters
public static function get(string $resource, array $parameters): self
{
$contentType = ContentType::JSON;
$acceptContentType = ContentType::JSON;
$method = Method::GET;
$uri = ResourceUri::get($resource);

return new self($contentType, $method, $uri, $parameters);
return new self($contentType, $acceptContentType, $method, $uri, $parameters);
}

/**
Expand All @@ -79,21 +85,25 @@ public function toRequest(BaseUri $baseUri, Headers $headers, QueryParams $query
{
$psr17Factory = new Psr17Factory();

$uri = $baseUri->toString().$this->uri->toString();
$uri = $this->buildUri($baseUri);

$queryParams = $queryParams->toArray();

if ($this->method === Method::GET) {
$queryParams = [...$queryParams, ...$this->parameters];
}

if ($this->method === Method::POST && in_array($this->contentType, [ContentType::ALL, ContentType::TEXT])) {
$queryParams = [...$queryParams, ...$this->parameters];
}

if ($queryParams !== []) {
$uri .= '?'.http_build_query($queryParams);
}

$headers = $headers
->withContentType($this->contentType)
->acceptContentType($this->contentType);
->acceptContentType($this->acceptContentType);

$body = match ($this->contentType) {
ContentType::JSON => $this->method === Method::GET ? null : $psr17Factory->createStream(json_encode($this->parameters, JSON_THROW_ON_ERROR)),
Expand All @@ -112,4 +122,15 @@ public function toRequest(BaseUri $baseUri, Headers $headers, QueryParams $query

return $request;
}

private function buildUri(BaseUri $baseUri): string
{
$uri = $baseUri->toString().$this->uri->toString();

if (! Factory::isStaging()) {
return $uri;
}

return str_replace('prod/', 'test/', $uri);
}
}
Loading

0 comments on commit 1f5695c

Please sign in to comment.