Incarcare factura XML in platforma SPV ANAF folosind autentificarea OAuth in PHP

Intr-un articol separat (Autentificare OAuth si obtinere token JWT de la ANAF folosind PHP) am descris modul de autentificare prin OAuth in platforma ANAF si obtinerea tokenului JWT care autorizeaza o aplicatie terta sa comunice cu platforma ANAF in numele unui client. Scopul acestei comunicari este transmiterea sau descarcarea facturilor electronice in SPV (Spatiul Privat Virtual) in format XML in contextul implementarii proiectului e-Factura de catre Ministerul de Finante.

Resurse importante pentru crearea fisierului XML si pentru transmiterea facturilor catre SPV pot fi gasite aici:

In contextul incarcarii facturii xml in SPV, ANAF pune la dispozitie servicii web pentru:

  • validarea fisierului xml
  • upload-ul fisierului xml
  • verificarea starii unei facturi incarcate anterior
  • descarcarea unui raspuns

Mai jos vom crea metode pentru fiecare din aceste servicii web. Metodele fac parte din aceeasi clasa ANAF pe care am creat-o in primul articol (Autentificare OAuth si obtinere token JWT de la ANAF folosind PHP).

1. Validarea fisierului XML

Pentru validarea fisierului xml (serviciu care nu necesita autentificare) vom crea metoda validare_factura_xml().

public function validare_factura_xml($xml_filename){
	//VALIDARE
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, "https://webservicesp.anaf.ro/prod/FCTEL/rest/validare/FACT1");
	curl_setopt($ch, CURLOPT_POST, 1);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
	curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
	curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents($xml_filename));
	curl_setopt($ch, CURLOPT_HTTPHEADER,
		array(
			'Content-Type: text/plain'
		)
	);		
	$resp = curl_exec($ch);
	$resp = json_decode($resp, true);
	
	if ($resp['stare'] == 'ok'){
		return ['result' => 0];
	}
	else{
		return ['result' => 1, 'errors' => $resp['Messages']];
	}
}

Asa cum aminteste si documentatia, daca raspunsul returnat este de forma:

The requested URL was rejected. Please consult with your administrator.
Your support ID is: 15320385530209486105
[Go Back]

atunci, recomandarea ANAF este sa se elimine din continutul fisierului xml urmatoarea secventa:

xsi:schemaLocation="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2 ../../UBL-2.1(1)/xsd/maindoc/UBL
Invoice-2.1.xsd"

2. Upload fisier XML

In continuare, daca fisierul xml a fost validat, urmeaza upload-ul acestuia in platforma SPV. Pentru aceasta vom crea metoda upload_factura_xml(). Accesarea acestui serviciu web necesita autentificare. Parametrul cif va contine partea numerica a codului fiscal al entitatii in numele careia se incarca fisierul xml.

public function upload_factura_xml($xml_filename){
	global $db;
	
	//verificare existenta token valid pentru ANAF
	if (!$this->get_last_valid_token()){
		return ['result' => 1, 'error' => 'Nu exista token valabil.'];
	}
	
	//POSTARE (upload)
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, 'https://api.anaf.ro/prod/FCTEL/rest/upload?standard=UBL&cif=COD_FISCAL');
	curl_setopt($ch, CURLOPT_POST, 1);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
	curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
	curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents($xml_filename));
	curl_setopt($ch, CURLOPT_HTTPHEADER,
		array(
			"Authorization: Bearer {$this->token_info['access_token']}",
			"Content-Type: text/plain",
			"Accept-Encoding: gzip, deflate, br",
			"Connection: keep-alive"
		)
	);		
	
	$resp = curl_exec($ch);
	
	$dateResponse = $ExecutionStatus = $index_incarcare = null;
	if (preg_match("/dateResponse=\"(\d*)\"/", $resp, $matches)) $dateResponse = $matches[1];
	if (preg_match("/ExecutionStatus=\"(\d*)\"/", $resp, $matches)) $ExecutionStatus = $matches[1];
	if (preg_match("/index_incarcare=\"(\d*)\"/", $resp, $matches)) $index_incarcare = $matches[1];
	
	if (!is_null($dateResponse) && !is_null($ExecutionStatus) && !is_null($index_incarcare)){
		return ['result' => 0, 'dateResponse' => $dateResponse, 'ExecutionStatus' => $ExecutionStatus, 'index_incarcare' => $index_incarcare];
	}
	else{
		return ['result' => 1, 'errors' => $resp];
	}
}

Din raspunsul returnat de serviciul web ANAF am extras 3 parametri de care am avut nevoie si am considerat ca daca niciunul din acestia nu este null, atunci inseamna ca apelul a fost cu succes.

Informatiile extrase din raspunsul ANAF le salvam intr-o tabela in care pastram relatia dintre numarul facturii si indexul de incarcare de care vom avea nevoie in continuare (pentru ca in ANAF nu se pot face cautari dupa numarul de factura):

$r = $db->query("INSERT INTO anaf_spv
				SET nr_factura = '$row[nr_factura]',
					dateResponse = '$dateResponse',
					ExecutionStatus = '$res[ExecutionStatus]',
					index_incarcare = '$res[index_incarcare]';");

3. Verificare stare mesaj

La scurt timp dupa incarcarea facturii, aceasta este procesata, iar ANAF furnizeaza ca raspuns un mesaj. Urmatorul pas este verificarea starii mesajului corespunzator facturii incarcate in SPV. Functia propusa pentru aceasta interogare este verificare_stare_mesaj(). Apelul acestui serviciu necesita autentificare si se face pe baza indexului de incarcare obtinut in pasul anterior.

public function verificare_stare_mesaj($nr_factura){
	global $db;
	
	//verificare existenta token valid pentru ANAF
	if (!$this->get_last_valid_token()){
		return ['result' => 1, 'error' => 'Nu exista token valabil.'];
	}
	
	//citire detalii factura incarcata anterior
	$r = $db->query("SELECT * FROM anaf_spv WHERE nr_factura = '$nr_factura';");
	$f = $r[0];
	
	//interogare
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, 'https://api.anaf.ro/prod/FCTEL/rest/stareMesaj?id_incarcare=' . $f['index_incarcare']);
	curl_setopt($ch, CURLOPT_POST, 0);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
	curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
	curl_setopt($ch, CURLOPT_HTTPHEADER,
		array(
			"Authorization: Bearer {$anaf->token_info['access_token']}",
			"Content-Type: text/plain",
			"Accept-Encoding: gzip, deflate, br",
			"Connection: keep-alive"
		)
	);		
	
	//verbose output
	curl_setopt($ch, CURLOPT_VERBOSE, true);
	$streamVerboseHandle = fopen('php://temp', 'w+');
	curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);

	$resp = curl_exec($ch);
	
	rewind($streamVerboseHandle);
	$verboseLog = stream_get_contents($streamVerboseHandle);

	//mesaje: ok (prelucrare cu succes), nok (prelucrare esuata), in prelucrare, XML cu erori nepreluat in sistem
	$stare = $id_descarcare = null;
	
	//extragere a erorilor din formatul XML
	$errors = [];
	$xml = simplexml_load_string($resp);
	if (isset($xml->Errors) && count($xml->Errors->attributes())){
		foreach($xml->Errors->attributes() AS $msg){
			$errors[] = $msg;
		}
	}
	
	if (trim($resp) == ''){
		error_log("Interogare raspuns ANAF: $nr_factura");
		error_log($verboseLog);
		return ['result' => 1, 'error' => 'Raspuns vid la interogarea ANAF (verifica error_log)'];
	}

	//stare
	if (preg_match("/header xmlns=\".*stareMesajFactura.*\" stare=\"([a-z\s\d]*)\"/", $resp, $matches)){
		$stare = $matches[1];
	}
	elseif (preg_match("/Errors errorMessage=\".*stareMesajFactura.*\" stare=\"([a-z\s\d]*)\"/", $resp, $matches)){
		$stare = $matches[1];
	}

	//id_descarcare
	if (preg_match("/id_descarcare=\"(\d*)\"/", $resp, $matches)) $id_descarcare = $matches[1];
	
	if (!is_null($stare)){
		//succes
		return ['result' => 0, 'stare' => $stare, 'id_descarcare' => $id_descarcare, 'resp' => $resp];
	}
	else{
		//eroare
		if (count($errors))	return ['result' => 1, 'error' => implode(', ', $errors)];
		else				return ['result' => 1, 'error' => $resp];
	}
}

Daca raspunsul returnat este cu succes, atunci actualizam informatia legata de starea mesajului in aceeasi tabela in care am salvat indexul de incarcare:

$db->query("UPDATE anaf_spv
			SET stare = '$res[stare]',
				id_descarcare = '$res[id_descarcare]'
				stare_verificata_la = NOW()
			WHERE nr_factura = '$row[nr_factura]';");

4. Descarcare raspuns

Descarcarea unui raspuns se face pe baza parametrului id_descarcare obtinut mai sus si care este asociat facturii. Apelul acestui serviciu necesita autentificare.

Functia de mai jos efectueaza apelul serviciului web, apoi verifica daca informatia returnata este de tip fisier zip, iar apoi salveaza fisierul local, pe server.

Raspunsul returnat de servicul web, in caz de succes, trebuie sa fie un fisier zip care contine:

  • factura in format xml (cea care a fost uploadata initial)
  • factura in format xml semnata de ANAF (practic este acelasi fisier xml ca si cel uploadat initial, in care a fost adaugata semnatura cu certificatul ANAF)

Acest fisier zip este considerat ca fiind factura originala.

public function descarcare_raspuns($nr_factura){
	global $db;
	
	//verificare existenta token valid pentru ANAF
	if (!$this->get_last_valid_token()){
		return ['result' => 1, 'error' => 'Nu exista token valabil.'];
	}
	
	//citire detalii factura incarcata anterior
	$r = $db->query("SELECT * FROM anaf_spv WHERE nr_factura = '$nr_factura';");
	$f = $r[0];
	$id_descarcare = $f['id_descarcare'];
	
	//interogare
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, 'https://api.anaf.ro/prod/FCTEL/rest/descarcare?id=' . $id_descarcare);
	curl_setopt($ch, CURLOPT_POST, 0);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
	curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
	curl_setopt($ch, CURLOPT_HTTPHEADER,
		array(
			"Authorization: Bearer {$anaf->token_info['access_token']}",
			"Content-Type: text/plain",
			"Accept-Encoding: gzip, deflate, br",
			"Connection: keep-alive"
		)
	);		
	
	//verbose output
	curl_setopt($ch, CURLOPT_VERBOSE, true);
	$streamVerboseHandle = fopen('php://temp', 'w+');
	curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);
	
	$resp = curl_exec($ch);
	
	rewind($streamVerboseHandle);
	$verboseLog = stream_get_contents($streamVerboseHandle);
	
	//verific daca raspunsul returnat este fisier zip
	if (substr($resp, 0, 4) == "\x50\x4b\x03\x04"){
		//salvez fisierul zip
		$filepath = LOCAL_PATH . "/documente-facturi/$nr_factura";
		$filename = "{$id_descarcare}.zip";
		
		//daca nu exista folderul destinatie il creez
		if (!is_dir($filepath)) mkdir($filepath);
		
		//scriere continut fisier zip
		$f = fopen($filepath . '/' . $filename, 'w');
		fwrite($f, $resp);
		fclose($f);
		chmod($filepath . '/' . $filename, 0755);
		
		//returnare rezultat
		return ['result' => 0, 'filename' => $filepath . '/' . $filename];
	}
	else{
		//NU este continut fisier zip
		return ['result' => 1, 'error' => $resp . "\n" . $verboseLog];
	}
}