Skip to content

Commit

Permalink
Merge pull request #131 from josemmo/develop
Browse files Browse the repository at this point in the history
v1.7.7
  • Loading branch information
josemmo authored Jul 2, 2023
2 parents fb876dd + 0e42dcc commit 00c093b
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 40 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.php]
[*.{md,php}]
insert_final_newline = true
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
4 changes: 2 additions & 2 deletions doc/ejemplos/factura-simple.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ $fac->addItem("Lámpara de pie", 20.14, 3, Facturae::TAX_IVA, 21);

// Ya solo queda firmar la factura ...
$fac->sign(
"ruta/hacia/clave_publica.pem",
"ruta/hacia/clave_privada.pem",
"ruta/hacia/banco-de-certificados.p12",
null,
"passphrase"
);

Expand Down
4 changes: 2 additions & 2 deletions doc/ejemplos/sin-composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ $fac->addItem("Lámpara de pie", 20.14, 3, Facturae::TAX_IVA, 21);

// Ya solo queda firmar la factura ...
$fac->sign(
"ruta/hacia/clave_publica.pem",
"ruta/hacia/clave_privada.pem",
"ruta/hacia/banco-de-certificados.p12",
null,
"passphrase"
);

Expand Down
33 changes: 33 additions & 0 deletions doc/entidades/otros-paises.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: Otros países
parent: Entidades
nav_order: 5
permalink: /entidades/otros-paises.html
---

# Otros países
Por defecto, Facturae-PHP asume que las entidades residen en España.
Para establecer el código de país de una entidad, se usa la propiedad "countryCode":
```php
$entity = new FacturaeParty([
"countryCode" => "FRA",
"taxNumber" => "12345678901",
"name" => "Una empresa de Francia",
// [...]
]);
```

El valor del campo XML `<ResidenceTypeCode />` se calcula automáticamente en función del país de acuerdo a la especificación.
Es decir, toma los siguientes valores dependiendo del país:

- `R`: Para España
- `U`: Para países de la Unión Europea
- `E`: Resto de países

Se puede forzar que una entidad se considere (o no) de la Unión Europea usando la propiedad "isEuropeanUnionResident":
```php
$entity = new FacturaeParty([
"isEuropeanUnionResident" => true,
// [...]
]);
```
52 changes: 27 additions & 25 deletions doc/firma-electronica/firma-electronica.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,56 @@ permalink: /firma-electronica/
Aunque es posible exportar las facturas sin firmarlas, es un paso obligatorio para prácticamente cualquier trámite relacionado con la Administración Pública.
Para firmar facturas se necesita un certificado electrónico (generalmente expedido por la FNMT) del que extraer su clave pública y su clave privada.

## Firmado con clave pública y privada X.509
Si se tiene la clave pública (un certificado) y la clave privada en archivos independientes, se debe utilizar este método con los siguientes argumentos:
## Firmado con PKCS#12 (recomendado)
Desde la versión 1.0.5 de Facturae-PHP ya es posible cargar un banco de certificados desde un archivo `.pfx` o `.p12`:
```php
$fac->sign("clave_publica.pem", "clave_privada.pem", "passphrase");
$fac->sign("certificado.pfx", null, "passphrase");
```

También se pueden pasar como parámetros los bytes de ambos ficheros en vez de sus rutas, o instancias de `OpenSSLCertificate` y `OpenSSLAsymmetricKey`, respectivamente:
También se pueden pasar como parámetro los bytes del banco PKCS#12:
```php
$publicKey = openssl_x509_read("clave_publica.pem");
$encryptedPrivateKey = file_get_contents("clave_privada.pem");
$fac->sign($publicKey, $encryptedPrivateKey, "passphrase");
$encryptedStore = file_get_contents("certificado.pfx");
$fac->sign($encryptedStore, null, "passphrase");
```

> #### NOTA
> Los siguientes comandos permiten extraer el certificado (clave pública) y la clave privada de un archivo PFX:
> Al utilizar un banco PKCS#12, Facturae-PHP incluirá la cadena completa de certificados en la factura al firmarla.
>
> Aunque en la mayoría de los casos esto no supone ninguna diferencia con respecto a firmar desde ficheros PEM, los validadores presentan problemas **con algunos certificados expedidos recientemente por la FNMT**.
> Dicho problema se soluciona cuando se incluyen los certificados raíz e intermedios de la Entidad de Certificación, por lo que es recomendable usar este método de firma con Facturae-PHP.
> #### NOTA
> A partir de OpenSSL v3.0.0, algunos algoritmos de digest como RC4 fueron [marcados como obsoletos](https://www.openssl.org/docs/man3.0/man7/migration_guide.html#Deprecated-low-level-encryption-functions).
> Esto puede suponer un problema para bancos de certificados exportados desde el Gestor de Certificados de Windows.
> Se recomienda validar estos ficheros antes de usarlos en la librería:
>
> ```
> openssl pkcs12 -in certificado_de_entrada.pfx -clcerts -nokeys -out clave_publica.pem
> openssl pkcs12 -in certificado_de_entrada.pfx -nocerts -out clave_privada.pem
> openssl pkcs12 -in certificado.pfx -info -nokeys -nocerts
> ```
---
## Firmado con PKCS#12
Desde la versión 1.0.5 de Facturae-PHP ya es posible cargar un banco de certificados desde un archivo `.pfx` o `.p12` sin necesidad de convertirlo previamente a X.509:
## Firmado con clave pública y privada X.509
Si se tiene la clave pública (un certificado) y la clave privada en archivos independientes, se debe utilizar este método con los siguientes argumentos:
```php
$fac->sign("certificado.pfx", null, "passphrase");
$fac->sign("clave_publica.pem", "clave_privada.pem", "passphrase");
```
También se pueden pasar como parámetro los bytes del banco PKCS#12:
También se pueden pasar como parámetros los bytes de ambos ficheros en vez de sus rutas, o instancias de `OpenSSLCertificate` y `OpenSSLAsymmetricKey`, respectivamente:
```php
$encryptedStore = file_get_contents("certificado.pfx");
$fac->sign($encryptedStore, null, "passphrase");
$publicKey = openssl_x509_read("clave_publica.pem");
$encryptedPrivateKey = file_get_contents("clave_privada.pem");
$fac->sign($publicKey, $encryptedPrivateKey, "passphrase");
```

> #### NOTA
> Al utilizar un banco PKCS#12, Facturae-PHP incluirá la cadena completa de certificados en la factura al firmarla.
>
> Aunque en la mayoría de los casos esto no supone ninguna diferencia con respecto a firmar desde ficheros PEM, el validador del Gobierno de España **presenta problemas para verificar firmas de certificados expedidos recientemente por la FNMT**.
> Dicho problema se soluciona cuando se incluyen los certificados raíz e intermedios de la Entidad de Certificación, por lo que es recomendable usar este método de firma con Facturae-PHP.
Este método de firma no añade la cadena completa de certificados a la factura y, por tanto, no se recomienda.

> #### NOTA
> A partir de OpenSSL v3.0.0, algunos algoritmos de digest como RC4 fueron [marcados como obsoletos](https://www.openssl.org/docs/man3.0/man7/migration_guide.html#Deprecated-low-level-encryption-functions).
> Esto puede suponer un problema para bancos de certificados exportados desde el Gestor de Certificados de Windows.
> Se recomienda validar estos ficheros antes de usarlos en la librería:
> Los siguientes comandos permiten extraer el certificado (clave pública) y la clave privada de un archivo PFX:
>
> ```
> openssl pkcs12 -in certificado.pfx -info -nokeys -nocerts
> openssl pkcs12 -in certificado_de_entrada.pfx -clcerts -nokeys -out clave_publica.pem
> openssl pkcs12 -in certificado_de_entrada.pfx -nocerts -out clave_privada.pem
> ```
---
Expand Down
2 changes: 1 addition & 1 deletion doc/propiedades/suplidos.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Para ello, se debe hacer uso de la clase `ReimbursableExpense`:
```php
$fac->addReimbursableExpense(new ReimbursableExpense([
"seller" => new FacturaeParty(["taxNumber" => "00000000A"]),
"buyer" => new FacturaeParty(["taxNumber" => "12-3456789", "isEuropeanUnionResident" => false]),
"buyer" => new FacturaeParty(["taxNumber" => "12-3456789", "countryCode" => "PRT"]),
"issueDate" => "2017-11-27",
"invoiceNumber" => "EX-19912",
"invoiceSeriesCode" => "156A",
Expand Down
5 changes: 2 additions & 3 deletions src/Common/FacturaeSigner.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,8 @@ public function sign($xml) {

// Build <ds:KeyInfo /> element
$privateData = openssl_pkey_get_details($this->privateKey);
$modulus = chunk_split(base64_encode($privateData['rsa']['n']), 76);
$modulus = str_replace("\r", '', $modulus);
$exponent = base64_encode($privateData['rsa']['e']);
$modulus = XmlTools::toBase64($privateData['rsa']['n'], true);
$exponent = XmlTools::toBase64($privateData['rsa']['e']);
$dsKeyInfo = '<ds:KeyInfo Id="' . $this->certificateId . '">' . "\n" . '<ds:X509Data>' . "\n";
foreach ($this->publicChain as $pemCertificate) {
$dsKeyInfo .= '<ds:X509Certificate>' . "\n" . XmlTools::getCert($pemCertificate) . '</ds:X509Certificate>' . "\n";
Expand Down
2 changes: 1 addition & 1 deletion src/Common/XmlTools.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public static function getDigest($input, $pretty=false) {
public static function getCert($pem, $pretty=true) {
$pem = str_replace("-----BEGIN CERTIFICATE-----", "", $pem);
$pem = str_replace("-----END CERTIFICATE-----", "", $pem);
$pem = str_replace("\n", "", str_replace("\r", "", $pem));
$pem = str_replace(["\r", "\n"], ['', ''], $pem);
if ($pretty) $pem = self::prettify($pem);
return $pem;
}
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.6";
const VERSION = "1.7.7";
const USER_AGENT = "FacturaePHP/" . self::VERSION;

const SCHEMA_3_2 = "3.2";
Expand Down
35 changes: 32 additions & 3 deletions src/FacturaeParty.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
*/
class FacturaeParty {

const EU_COUNTRY_CODES = [
'AUT', 'BEL', 'BGR', 'CYP', 'CZE', 'DEU', 'DNK', 'ESP', 'EST', 'FIN', 'FRA', 'GRC', 'HRV', 'HUN',
'IRL', 'ITA', 'LTU', 'LUX', 'LVA', 'MLT', 'NLD', 'POL', 'PRT', 'ROU', 'SVK', 'SVN', 'SWE'
];

public $isLegalEntity = true; // By default is a company and not a person
public $isEuropeanUnionResident = true; // By default resides in the EU
public $taxNumber = null;
public $name = null;

Expand All @@ -33,6 +37,8 @@ class FacturaeParty {
public $town = null;
public $province = null;
public $countryCode = "ESP";
/** @var boolean|null */
public $isEuropeanUnionResident = null; // By default is calculated based on the country code

public $email = null;
public $phone = null;
Expand Down Expand Up @@ -68,7 +74,7 @@ public function getXML($includeAdministrativeCentres) {
// Add tax identification
$xml = '<TaxIdentification>' .
'<PersonTypeCode>' . ($this->isLegalEntity ? 'J' : 'F') . '</PersonTypeCode>' .
'<ResidenceTypeCode>R</ResidenceTypeCode>' .
'<ResidenceTypeCode>' . $this->getResidenceTypeCode() . '</ResidenceTypeCode>' .
'<TaxIdentificationNumber>' . XmlTools::escape($this->taxNumber) . '</TaxIdentificationNumber>' .
'</TaxIdentification>';

Expand Down Expand Up @@ -180,6 +186,29 @@ public function getXML($includeAdministrativeCentres) {
}


/**
* Get residence type code
*
* @return string Residence type code
*/
public function getResidenceTypeCode() {
if ($this->countryCode === "ESP") {
return "R";
}

// Handle overrides
if ($this->isEuropeanUnionResident === true) {
return "U";
}
if ($this->isEuropeanUnionResident === false) {
return "E";
}

// Handle European countries
return in_array($this->countryCode, self::EU_COUNTRY_CODES, true) ? "U" : "E";
}


/**
* Get contact details XML
*
Expand Down Expand Up @@ -227,7 +256,7 @@ private function getContactDetailsXML() {
*/
public function getReimbursableExpenseXML() {
$xml = '<PersonTypeCode>' . ($this->isLegalEntity ? 'J' : 'F') . '</PersonTypeCode>';
$xml .= '<ResidenceTypeCode>' . ($this->isEuropeanUnionResident ? 'R' : 'E') . '</ResidenceTypeCode>';
$xml .= '<ResidenceTypeCode>' . $this->getResidenceTypeCode() . '</ResidenceTypeCode>';
$xml .= '<TaxIdentificationNumber>' . XmlTools::escape($this->taxNumber) . '</TaxIdentificationNumber>';
return $xml;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/InvoiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ public function testCreateInvoice($schemaVersion, $isPfx) {
// Añadimos un suplido
$fac->addReimbursableExpense(new ReimbursableExpense([
"seller" => new FacturaeParty(["taxNumber" => "00000000A"]),
"buyer" => new FacturaeParty(["taxNumber" => "12-3456789", "isEuropeanUnionResident" => false]),
"buyer" => new FacturaeParty(["taxNumber" => "12-3456789", "countryCode" => "PRT"]),
"issueDate" => "2017-11-27",
"invoiceNumber" => "EX-19912",
"invoiceSeriesCode" => "156A",
Expand Down
16 changes: 16 additions & 0 deletions tests/OverseasTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,24 @@ public function testOverseasAddress() {
$fac->getBuyer()->countryCode = "PRT";
$fac->addItem("Línea de producto", 100, 1, Facturae::TAX_IVA, 21);

// Validate invoice as-is
$fac->export(self::FILE_PATH);
$this->validateInvoiceXML(self::FILE_PATH);
$this->assertEquals("R", $fac->getSeller()->getResidenceTypeCode());
$this->assertEquals("U", $fac->getBuyer()->getResidenceTypeCode());

// Switch buyer to United States
$fac->getBuyer()->countryCode = "USA";
$this->assertEquals("E", $fac->getBuyer()->getResidenceTypeCode());

// Force European-resident type code
$fac->getBuyer()->isEuropeanUnionResident = true;
$this->assertEquals("U", $fac->getBuyer()->getResidenceTypeCode());

// Force non-European-resident type code
$fac->getBuyer()->countryCode = "PRT";
$fac->getBuyer()->isEuropeanUnionResident = false;
$this->assertEquals("E", $fac->getBuyer()->getResidenceTypeCode());
}

}

0 comments on commit 00c093b

Please sign in to comment.