Skip to content

Commit

Permalink
Merge pull request #159 from josemmo/develop
Browse files Browse the repository at this point in the history
v1.8.0
  • Loading branch information
josemmo authored May 11, 2024
2 parents 6d5fb5f + 6a6f9f5 commit 102edbb
Show file tree
Hide file tree
Showing 19 changed files with 259 additions and 95 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental || false }}
strategy:
fail-fast: false
matrix:
php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
include:
- php-version: '8.2'
- php-version: '8.3'
test-ws: true
send-coverage: true
- php-version: '8.3'
- php-version: '8.4'
experimental: true
steps:
# Download code from repository
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

# Setup PHP and Composer
- name: Setup PHP
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,13 @@ file_put_contents(__DIR__ . "/factura.xsig", $signedXml);
- Sellado de tiempo según el [RFC3161](https://www.ietf.org/rfc/rfc3161.txt)
- Envío automatizado de facturas a **FACe y FACeB2B** 🔥

## Usan Facturae-PHP
Estas son algunas de las organizaciones y soluciones software que usan Facturae-PHP o mantienen un fork interno basado en el código de la librería:

<a href="https://www.holded.com/" target="_blank"><img height="50" alt="Holded" src="https://i.imgur.com/zqdQsPA.png"></a>
<a href="https://hotelgest.com/" target="_blank"><img height="50" alt="hotelgest" src="https://i.imgur.com/hyuKAOt.png"></a>
<a href="https://invoiceninja.com/" target="_blank"><img height="50" alt="InvoiceNinja" src="https://i.imgur.com/ySryAUA.png"></a>
<a href="https://facturascripts.com/" target="_blank"><img height="50" alt="FacturaScripts" src="https://i.imgur.com/UPnUVCD.png"></a>

## Licencia
Facturae-PHP se encuentra bajo [licencia MIT](LICENSE). Eso implica que puedes utilizar este paquete en cualquier proyecto (incluso con fines comerciales), siempre y cuando hagas referencia al uso y autoría de la misma.
11 changes: 11 additions & 0 deletions doc/envio-y-recepcion/face.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ $endpointUrl = "https://w390w.gipuzkoa.net/WAS/HACI/HFAServiciosProveedoresWEB/s
$face = new CustomFaceClient($endpointUrl, "certificado.pfx", null, "passphrase");
```

Si al intentar conectarte al punto de recepción obtienes un error similar a "A bad canonicalization algorithm was specified", es posible que el servidor de destino no soporte canonicalización de XML exclusiva ([EXC-C14N](https://www.w3.org/TR/xml-exc-c14n/)).
Para usar C14N en vez de EXC-C14N, utiliza este método del cliente:
```php
$face->setExclusiveC14n(false);
```

Otro error típico es "Document element namespace mismatch expected". En ese caso, el punto de entrada necesita usar un namespace personalizado, que puedes especificar usando este método:
```php
$face->setWebNamespace('https://webservice.efact.es/sspp'); // Ejemplo para e-FACT
```

---

## Listado de métodos
Expand Down
16 changes: 10 additions & 6 deletions src/Common/FacturaeSigner.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,18 @@ public function sign($xml) {
$signingTime = ($this->signingTime === null) ? time() : $this->signingTime;
$certData = openssl_x509_parse($this->publicChain[0]);
$certIssuer = [];
foreach ($certData['issuer'] as $item=>$rawValues) {
if (!array_key_exists($item, self::ALLOWED_OID_TYPES)) {
continue;
}
$item = self::ALLOWED_OID_TYPES[$item];
foreach ($certData['issuer'] as $rawType=>$rawValues) {
$values = is_array($rawValues) ? $rawValues : [$rawValues];
foreach ($values as $value) {
$certIssuer[] = "$item=$value";
if ($rawType === "UNDEF" && preg_match('/^VAT[A-Z]{2}-/', $value) === 1) {
$type = "OID.2.5.4.97"; // Fix for OpenSSL <3.0.0
} else {
if (!array_key_exists($rawType, self::ALLOWED_OID_TYPES)) {
continue; // Skip unknown OID types
}
$type = self::ALLOWED_OID_TYPES[$rawType];
}
$certIssuer[] = "$type=$value";
}
}
$certIssuer = implode(', ', array_reverse($certIssuer));
Expand Down
1 change: 1 addition & 0 deletions src/Extensions/FacturaeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use josemmo\Facturae\Facturae;

abstract class FacturaeExtension {
/** @var Facturae */
private $fac;

/**
Expand Down
4 changes: 4 additions & 0 deletions src/Extensions/Fb2bExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
use josemmo\Facturae\FacturaeCentre;

class Fb2bExtension extends FacturaeExtension {
/** @var array<string,string> */
private $publicSectorInfo = array();
/** @var FacturaeCentre|null */
private $receiver = null;
/** @var FacturaeCentre[] */
private $sellerCentres = array();
/** @var FacturaeCentre[] */
private $buyerCentres = array();

/**
Expand Down
18 changes: 18 additions & 0 deletions src/Face/CustomFaceClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

class CustomFaceClient extends SoapClient {
private $endpointUrl;
private $webNamespace = "https://webservice.face.gob.es";

use FaceTrait;

Expand All @@ -22,6 +23,23 @@ public function __construct($endpointUrl, $publicPath, $privatePath=null, $passp
}


/**
* Set custom web service namespace
* @param string $webNamespace Web service namespace to override the default one
*/
public function setWebNamespace($webNamespace) {
$this->webNamespace = $webNamespace;
}


/**
* @inheritdoc
*/
protected function getWebNamespace() {
return $this->webNamespace;
}


/**
* Get endpoint URL
* @return string Endpoint URL
Expand Down
16 changes: 14 additions & 2 deletions src/Face/SoapClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
abstract class SoapClient {

const REQUEST_EXPIRATION = 60; // In seconds
private $useExcC14n = true;

use KeyPairReaderTrait;

Expand All @@ -28,6 +29,15 @@ public function __construct($storeOrCertificate, $privateKey=null, $passphrase='
}


/**
* Set exclusive canonicalization mode
* @param boolean $enabled Whether to use EXC-C14N (`true`) or C14N (`false`)
*/
public function setExclusiveC14n($enabled) {
$this->useExcC14n = $enabled;
}


/**
* Get endpoint URL
* @return string Endpoint URL
Expand Down Expand Up @@ -91,8 +101,10 @@ protected function request($body) {
'</wsse:BinarySecurityToken>';

// Generate signed info
$c14nNamespace = $this->useExcC14n ? "http://www.w3.org/2001/10/xml-exc-c14n#" : "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
$signedInfoNs = $this->useExcC14n ? ['xmlns:ds' => $ns['xmlns:ds']] : $ns;
$signedInfo = '<ds:SignedInfo>' .
'<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">' .
'<ds:CanonicalizationMethod Algorithm="' . $c14nNamespace . '">' .
'</ds:CanonicalizationMethod>' .
'<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"></ds:SignatureMethod>' .
'<ds:Reference URI="#' . $timestampId . '">' .
Expand All @@ -104,7 +116,7 @@ protected function request($body) {
'<ds:DigestValue>' . $bodyDigest . '</ds:DigestValue>' .
'</ds:Reference>' .
'</ds:SignedInfo>';
$signedInfoPayload = XmlTools::injectNamespaces($signedInfo, $ns);
$signedInfoPayload = XmlTools::injectNamespaces($signedInfo, $signedInfoNs);

// Add signature and KeyInfo to header
$reqHeader .= '<ds:Signature Id="' . $sigId . '">' .
Expand Down
2 changes: 1 addition & 1 deletion src/Facturae.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* Class for creating electronic invoices that comply with the Spanish FacturaE format.
*/
class Facturae {
const VERSION = "1.7.9";
const VERSION = "1.8.0";
const USER_AGENT = "FacturaePHP/" . self::VERSION;

const SCHEMA_3_2 = "3.2";
Expand Down
11 changes: 11 additions & 0 deletions src/FacturaeCentre.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,29 @@ class FacturaeCentre {
const ROLE_B2B_COLLECTION_RECEIVER = "Collection receiver";
const ROLE_B2B_ISSUER = "Issuer";

/** @var string|null */
public $code = null;
/** @var string|null */
public $role = null;

/** @var string|null */
public $name = null;
/** @var string|null */
public $firstSurname = null;
/** @var string|null */
public $lastSurname = null;
/** @var string|null */
public $description = null;

/** @var string|null */
public $address = null;
/** @var string|null */
public $postCode = null;
/** @var string|null */
public $town = null;
/** @var string|null */
public $province = null;
/** @var string */
public $countryCode = "ESP";


Expand Down
22 changes: 22 additions & 0 deletions src/FacturaeItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,54 @@ class FacturaeItem {
/** Non-subject operation */
const SPECIAL_TAXABLE_EVENT_NON_SUBJECT = "02";

/** @var int|string|null */
private $articleCode = null;
/** @var string|null */
private $name = null;
/** @var string|null */
private $description = null;
/** @var int|float */
private $quantity = 1;
/** @var string */
private $unitOfMeasure = Facturae::UNIT_DEFAULT;
/** @var int|float|null */
private $unitPrice = null;
/** @var int|float|null */
private $unitPriceWithoutTax = null;
private $discounts = array();
private $charges = array();
private $taxesOutputs = array();
private $taxesWithheld = array();
/** @var string|null */
private $specialTaxableEventCode = null;
/** @var string|null */
private $specialTaxableEventReason = null;

/** @var string|null */
private $issuerContractReference = null;
/** @var string|null */
private $issuerContractDate = null;
/** @var string|null */
private $issuerTransactionReference = null;
/** @var string|null */
private $issuerTransactionDate = null;
/** @var string|null */
private $receiverContractReference = null;
/** @var string|null */
private $receiverContractDate = null;
/** @var string|null */
private $receiverTransactionReference = null;
/** @var string|null */
private $receiverTransactionDate = null;
/** @var string|null */
private $fileReference = null;
/** @var string|null */
private $fileDate = null;
/** @var string|null */
private $sequenceNumber = null;
/** @var string|null */
private $periodStart = null;
/** @var string|null */
private $periodEnd = null;


Expand Down
69 changes: 59 additions & 10 deletions src/FacturaeParty.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,87 @@ class FacturaeParty {
'IRL', 'ITA', 'LTU', 'LUX', 'LVA', 'MLT', 'NLD', 'POL', 'PRT', 'ROU', 'SVK', 'SVN', 'SWE'
];

/** @var boolean */
public $isLegalEntity = true; // By default is a company and not a person
/** @var string|null */
public $taxNumber = null;
/** @var string|null */
public $name = null;

// This block is only used for legal entities
public $book = null; // "Libro"
public $registerOfCompaniesLocation = null; // "Registro mercantil"
public $sheet = null; // "Hoja"
public $folio = null; // "Folio"
public $section = null; // "Sección"
public $volume = null; // "Tomo"
/**
* Libro (only for legal entities)
* @var string|null
*/
public $book = null;
/**
* Registro mercantil (only for legal entities)
* @var string|null
*/
public $registerOfCompaniesLocation = null;
/**
* Hoja (only for legal entities)
* @var string|null
*/
public $sheet = null;
/**
* Folio (only for legal entities)
* @var string|null
*/
public $folio = null;
/**
* Sección (only for legal entities)
* @var string|null
*/
public $section = null;
/**
* Tomo (only for legal entities)
* @var string|null
*/
public $volume = null;

// This block is only required for individuals
/**
* First surname (required for individuals)
* @var string|null
*/
public $firstSurname = null;
/**
* Last surname (required for individuals)
* @var string|null
*/
public $lastSurname = null;

/** @var string|null */
public $address = null;
/** @var string|null */
public $postCode = null;
/** @var string|null */
public $town = null;
/** @var string|null */
public $province = null;
/** @var string */
public $countryCode = "ESP";
/** @var boolean|null */
public $isEuropeanUnionResident = null; // By default is calculated based on the country code
/**
* NOTE: By default (when `null`) is calculated based on the country code
* @var boolean|null
*/
public $isEuropeanUnionResident = null;

/** @var string|null */
public $email = null;
/** @var string|null */
public $phone = null;
/** @var string|null */
public $fax = null;
/** @var string|null */
public $website = null;

/** @var string|null */
public $contactPeople = null;
/** @var string|null */
public $cnoCnae = null;
/** @var string|null */
public $ineTownCode = null;
/** @var FacturaeCentre[] */
public $centres = array();


Expand Down
1 change: 1 addition & 0 deletions src/FacturaeTraits/UtilsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* @var Facturae $this
*/
trait UtilsTrait {
/** @var FacturaeExtension[] */
protected $extensions = array();

/**
Expand Down
1 change: 0 additions & 1 deletion tests/AbstractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ abstract class AbstractTest extends TestCase {
const OUTPUT_DIR = __DIR__ . "/output";
const CERTS_DIR = __DIR__ . "/certs";
const FACTURAE_CERT_PASS = "1234";
const WEBSERVICES_CERT_PASS = "IZProd2021";
const NOTIFICATIONS_EMAIL = "[email protected]";
const COOKIES_PATH = self::OUTPUT_DIR . "/cookies.txt";

Expand Down
Loading

0 comments on commit 102edbb

Please sign in to comment.