Pular para o conteúdo
PHP

PHP: faturando compras no Igest, em Portugal

Ilustração de um unicórnio com varinha lançando um feixe sobre um pergaminho de fatura com selo, ao lado de uma balança pesando moedas e um castelo português com azulejos

Olá meus Unicórnios! 🦄✨

Sabe quando você olha para a tarefa e pensa "é só mandar um JSON com o valor da compra e pronto"? 😅 Pois é. Faturar em Portugal não é mandar um valor: é montar um documento fiscal, e documento fiscal tem regras que não perdoam um cêntimo de diferença.

Eu precisei integrar um sistema de vendas com o Igest, que é um dos serviços de faturação certificada usados por lá. E o caminho até a primeira fatura sair certa me ensinou quatro coisas que eu não sabia quando comecei: o preço que você cobra não é o valor que vai na linha, o IVA é um código de tabela e não uma percentagem, o NIF precisa chegar limpo, e — a mais assustadora — uma fatura enviada não se reenvia. 🤯

Neste artigo eu vou do zero até a fatura emitida, com um arquivo PHP único, linear, sem classe nem Composer. Só a extensão SOAP que já vem no PHP.

🧩 Por que SOAP, e por que sem WSDL

A primeira surpresa foi o protocolo. O webservice do Igest é SOAP — daqueles com envelope XML — e ele não publica um WSDL. Isso muda como você abre a conexão.

Normalmente você passaria a URL do WSDL para o SoapClient e ele descobriria sozinho os tipos e os métodos. Aqui não dá: o primeiro argumento vai null, e aí você tem de dizer à mão como o envelope deve ser montado.

// Sem WSDL o SoapClient recebe null no 1o argumento, e o envelope
// precisa ser descrito a mao aqui.
function abrirCliente($endpoint)
{
    return new SoapClient(null, array(
        'location'           => $endpoint,
        'uri'                => $endpoint,
        'connection_timeout' => 30,
        'trace'              => true,
        'exceptions'         => true,
        'style'              => SOAP_RPC,      // sem estes dois o servidor
        'use'                => SOAP_ENCODED,  // nao entende o envelope
    ));
}

function chamar($soap, $endpoint, $funcao, $parametros)
{
    // Sem soapaction/uri a resposta e "funcao desconhecida",
    // mesmo com o nome absolutamente correto.
    $retorno = $soap->__soapCall($funcao, $parametros, array(
        'soapaction' => $endpoint . '#' . $funcao,
        'uri'        => $endpoint,
    ));

    // A resposta volta como objeto aninhado; isto achata tudo em array.
    return json_decode(json_encode($retorno), true);
}

Repare nos dois pares que costumam ser esquecidos:

  • style e use — sem SOAP_RPC e SOAP_ENCODED, o PHP monta um envelope de outro formato e o servidor simplesmente não entende o que você mandou.
  • soapaction e uri na chamada — sem eles a resposta que volta é "função desconhecida", mesmo com o nome da função absolutamente correto. É um erro que engana, porque parece que você escreveu o nome errado. 😤

E o último detalhe: a resposta volta como objeto do PHP, aninhado. O json_decode(json_encode(...), true) ali no fim é o truque preguiçoso — e eficaz — para transformar tudo em array de uma vez.

🔐 A assinatura: duas datas que têm de ser a mesma

A autenticação é simples de descrever e fácil de errar. Você manda uma chave calculada assim:

// A data e gerada UMA vez: ela assina e viaja no envelope.
// Duas chamadas a date() podem cair em segundos diferentes.
$data = date('Y-m-d H:i:s');
$chave_assinada = sha1($CHAVE . $data);

Ou seja: sha1 da sua chave secreta concatenada com a data. Só que essa mesma data também viaja no envelope, num campo próprio. E é aí que mora a armadilha.

Se você chamar date('Y-m-d H:i:s') duas vezes — uma para assinar e outra para preencher o campo —, existe uma chance real de as duas caírem em segundos diferentes. A requisição sai, o servidor recalcula o sha1 com a data que recebeu, dá diferente da que você assinou, e a resposta é um educado "autenticação inválida". 😳

Foi exatamente o que aconteceu comigo, e eu perdi um tempo bom achando que a chave estava errada. A chave estava certa. Era o relógio.

E é a mesma variável que vai nos dois parâmetros do envelope:

new SoapParam(new SoapVar($chave_assinada, XSD_STRING), 'keyCliente'),
new SoapParam(new SoapVar($data, XSD_STRING), 'data'),

💶 O IVA: o valor da linha não é o preço que você cobrou

Se você só for ler um pedaço deste artigo, leia este. 🙏

Em Portugal a taxa normal de IVA no continente é de 23%. Quando o cliente paga 20,00 €, esses 20,00 € já têm o IVA dentro. Mas o Igest, do jeito que a gente monta aqui, quer o valor da linha sem IVA — e ele mesmo calcula o imposto e o total por cima.

Quem decide isso é uma flag chamada bIvaInc:

function montarArtigo($endpoint, $descricao, $valor_com_iva, $quantidade, $percentagem_iva, $tipo_iva)
{
    $artigo = array(
        'codigo'         => '',
        'tipo'           => 'S',
        'nome'           => $descricao,
        'valor_unitario' => valorSemIva($valor_com_iva, $percentagem_iva),
        'bIvaInc'        => 0,   // 0 = o valor acima esta limpo, sem imposto
        'quantidade'     => (float) $quantidade,
        'iva'            => montarIva($endpoint, $percentagem_iva, $tipo_iva),
        'desconto'       => (float) 0,
    );

    return new SoapVar($artigo, SOAP_ENC_OBJECT, 'Artigo', $endpoint);
}

Com bIvaInc = 0 você está dizendo "o valor unitário que eu mandei está limpo, sem imposto". Então o valor tem de ser dividido antes de sair:

// O valor cobrado ja tem IVA dentro. A linha vai sem IVA, e com
// QUATRO casas: o servico multiplica pela quantidade e recalcula o total.
function valorSemIva($valor_com_iva, $percentagem_iva)
{
    return round($valor_com_iva / (1 + ($percentagem_iva / 100)), 4);
}

E agora o detalhe que parece paranoia e não é: quatro casas decimais, não duas.

🧮 Por que quatro casas decimais (e não duas)

Meu primeiro instinto foi arredondar para dois — é dinheiro, dinheiro tem dois. Mas o serviço não usa o seu valor como total: ele recalcula. Ele pega o seu valor unitário, multiplica pela quantidade, aplica o IVA e chega ao total dele. Se esse total não bater com o que o cliente pagou, você emitiu uma fatura errada — e fatura errada em documento fiscal é chatice de verdade.

E repare em como ele recalcula: o serviço arredonda a incidência e o IVA separadamente, e só então soma os dois. Não é o total que ele arredonda no fim — são as duas parcelas, cada uma por si. Essa diferença muda o resultado da conta, e é ela que a varredura abaixo reproduz.

Eu quis saber o tamanho do estrago antes de decidir, então varri 2.000 combinações de valor e quantidade comparando as duas precisões, sempre contra o total esperado:

$u2 = round($valor / 1.23, 2);
$i2 = round($u2 * $q, 2);
$t2 = round($i2 + round($i2 * 0.23, 2), 2);

$u4 = round($valor / 1.23, 4);
$i4 = round($u4 * $q, 2);
$t4 = round($i4 + round($i4 * 0.23, 2), 2);
casos testados: 2000
erram com 2 casas: 1529
erram com 4 casas: 375

4 casas salva (2 erra, 4 acerta):
  0.5 x2 esperado 1 / 2casas 1.01 / 4casas 1
  0.5 x3 esperado 1.5 / 2casas 1.51 / 4casas 1.5
  0.5 x4 esperado 2 / 2casas 2.02 / 4casas 2
  0.5 x5 esperado 2.5 / 2casas 2.52 / 4casas 2.5
  0.5 x6 esperado 3 / 2casas 3.03 / 4casas 3
  0.5 x8 esperado 4 / 2casas 4.03 / 4casas 4

2 casas salva (4 erra, 2 acerta):

Olhe a última lista: está vazia. Não existe um caso sequer em que duas casas acertem e quatro errem. Quatro casas é estritamente melhor — e a diferença não é pequena: 1.529 desvios contra 375.

Repare também que o erro aparece quando a quantidade é maior que 1. Com quantidade 1 as duas precisões dão no mesmo, e é por isso que esse bug consegue passar despercebido por meses: os seus testes com um item só passam lindamente, e a fatura torta só aparece quando alguém compra três. 😅

🏷️ O IVA é um código, não um número

Este me pegou de jeito. Dentro da linha vai um objeto de IVA com três campos, e o campo tipo não é a percentagem nem uma letra qualquer que você invente:

// O campo "tipo" e um codigo de tabela, nao a percentagem nem uma letra.
function montarIva($endpoint, $percentagem, $tipo)
{
    $iva = array(
        'percentagem' => (float) $percentagem,
        'tipo'        => $tipo,   // NOR (23%), INT (13%), RED (6%), ISE (isenta)
        'regiao'      => 'PT',
    );

    return new SoapVar($iva, SOAP_ENC_OBJECT, 'IVA', $endpoint);
}

Eu tinha escrito "I", de "IVA", achando que era só um rótulo. Não é: é um código de tabela. Os válidos são NOR (normal, 23%), INT (intermédia, 13%), RED (reduzida, 6%) e ISE (isenta). Qualquer outra coisa e o documento é recusado.

E repare que a percentagem vai junto com o código. Os dois têm de combinar — mandar NOR com percentagem 6 é pedir problema.

🇵🇹 O NIF precisa chegar limpo

O NIF é o número de contribuinte português: nove dígitos. Só que o que chega do formulário raramente é nove dígitos limpos — vem com espaço, com ponto, às vezes com o prefixo PT na frente.

// O NIF portugues tem 9 digitos. Tudo que nao for digito cai fora,
// inclusive o prefixo "PT" que muita gente digita na frente.
function limparNif($nif)
{
    $so_digitos = preg_replace('/\D/', '', (string) $nif);

    // Nove digitos ou nada: NIF pela metade viraria contribuinte inexistente.
    if (strlen($so_digitos) !== 9) {
        return '';
    }

    return $so_digitos;
}

Rodando isso com as entradas que eu realmente via chegar:

"123 456 789"  -> [123456789]
"123.456.789"  -> [123456789]
"PT123456789"  -> [123456789]
"12345678"     -> []
""             -> []

A decisão de projeto que importa está nas duas últimas linhas: um NIF com o número errado de dígitos vira string vazia, não um erro. Isso é de propósito — em Portugal a fatura sem contribuinte é perfeitamente válida (é a de consumidor final). O que não pode é ir um NIF pela metade, porque aí o documento sai com um contribuinte que não existe. Vazio é honesto; torto não é.

A morada segue a mesma lógica, e por isso os três campos vão juntos ou não vão:

function montarCliente($endpoint, $cliente)
{
    $dados = array(
        'codigo'    => (string) $cliente['codigo'],
        'nome'      => $cliente['nome'],
        'nif'       => limparNif($cliente['nif']),
        'pais_code' => 'PT',
        'email'     => $cliente['email'],
    );

    // Morada incompleta e pior que morada nenhuma: os tres campos
    // vao juntos ou nao vao.
    if ($cliente['cod_postal'] !== '' && $cliente['morada'] !== '' && $cliente['cidade'] !== '') {
        $dados['cod_postal'] = $cliente['cod_postal'];
        $dados['morada']     = $cliente['morada'];
        $dados['cidade']     = $cliente['cidade'];
    }

    return new SoapVar($dados, SOAP_ENC_OBJECT, 'Cliente', $endpoint);
}

📅 Duas datas, dois formatos

Um detalhe cruel e silencioso: o documento carrega duas datas, e elas não têm o mesmo formato.

  • data (a da autenticação, lá em cima) é data e hora: Y-m-d H:i:s.
  • data_emissao, dentro do documento, é só a data: Y-m-d.

Mandar a hora junto no data_emissao é rejeição na hora. E é o tipo de coisa que você copia de uma variável para a outra sem pensar, porque as duas "são a data".

🚫 A fatura que não se reenvia

Agora a parte que muda como você escreve o tratamento de erro — e a razão de eu ter tomado tanto cuidado com o catch.

Uma fatura é um documento fiscal. Ela não é um registro no seu banco que você apaga e recria: ela entra na contabilidade, ela é comunicada, ela tem número numa série. Emitir a mesma compra duas vezes não é um bug de dados — é um documento duplicado que alguém vai ter que anular formalmente depois. 😬

Só que a rede não colabora. Existe um cenário perfeitamente possível: a requisição chega ao Igest, a fatura é criada, e a resposta se perde no caminho de volta. Do lado do seu código, o que você vê é uma exceção — exatamente igual à de "não consegui conectar". Mas os dois casos são opostos: num não existe fatura nenhuma, no outro existe uma fatura que você não sabe que existe.

Por isso o tratamento separa as duas situações, e a diferença fica num campo explícito:

    try {
        $soap = abrirCliente($ENDPOINT);
        $resposta = chamar($soap, $ENDPOINT, 'novo_documento', $parametros);
    } catch (Exception $e) {
        // Nao sabemos o que aconteceu do outro lado: a fatura pode
        // ter sido criada e so a resposta ter se perdido.
        return array(
            'ok'        => false,
            'reenviar'  => false,   // conferir a mao, NUNCA repetir sozinho
            'mensagem'  => 'Falha de comunicacao: ' . $e->getMessage(),
        );
    }

    if (empty($resposta['bSucesso'])) {
        // Aqui ha resposta dizendo que nada foi criado: pode corrigir e reenviar.
        return array(
            'ok'       => false,
            'reenviar' => true,
            'mensagem' => 'O Igest recusou o documento: ' . $resposta['descricaoErro'],
        );
    }

Repare no 'reenviar':

  • Exceção (rede, timeout) → false. Você não sabe o que aconteceu do outro lado. Marque para conferir à mão e siga.
  • Recusa (bSucesso vazio) → true. Aqui você tem uma resposta dizendo que nada foi criado. Pode corrigir o que o serviço reclamou e mandar de novo, tranquila.

É uma distinção de uma linha que evita o pior estrago possível desta integração.

🛡️ E a rede de segurança: o número do documento

Mesmo com todo o cuidado acima, o que de fato impede a duplicata é uma decisão de modelagem: o número da compra vira o número do documento.

        'data_emissao'        => date('Y-m-d'),  // so a data, sem hora
        // O numero da compra vira o numero do documento: o proprio Igest
        // recusa um segundo documento com o mesmo numero na serie.
        'numero_seguranca'    => (string) $compra['numero'],
        'numero_fatura'       => (string) $compra['numero'],

Como o Igest não aceita dois documentos com o mesmo número na mesma série, uma segunda tentativa da mesma compra bate numa parede do lado dele — e não depende de você lembrar de checar. É a proteção que continua funcionando mesmo quando o seu código falha.

Foi o que eu quis ver acontecer antes de confiar. Mandando a mesma compra duas vezes seguidas:

Fatura emitida: FR AB/4021
Sem IVA: 16.26 EUR
IVA:     3.74 EUR
Total:   20 EUR
PDF:     index.php?mod=fact&op=viewPDF&id=1001

Erro: O Igest recusou o documento: Documento com este numero ja existe na serie
Pode reenviar: sim

A primeira passa, a segunda bate na parede. 🎉 E repare na aritmética da primeira: 16,26 + 3,74 = 20,00, exatamente o que o cliente pagou. É esse fechamento ao cêntimo que a divisão por 1,23 com quatro casas está protegendo.

🔗 O link do PDF vem em base64

Uma última pegadinha, pequena e simpática: o linkDocumento da resposta não é uma URL. É o caminho do documento, codificado em base64.

Então para chegar ao PDF você decodifica e junta com o domínio do serviço:

echo base64_decode($resultado['link']);
// index.php?mod=fact&op=viewPDF&id=1001

Guarde esse link junto do número do documento na sua base. É o comprovativo que o cliente vai pedir.

🔑 Onde ficam as credenciais

Nada de chave escrita dentro do arquivo. Tudo por variável de ambiente:

// Credenciais so por variavel de ambiente - nada escrito aqui dentro.

$ENDPOINT = getenv('IGEST_ENDPOINT');  // https://<servidor>/webservice.php
$ID_ENTIDADE = (int) getenv('IGEST_ID_ENTIDADE');
$CHAVE = getenv('IGEST_CHAVE');
$EMAIL = getenv('IGEST_EMAIL');
$SERIE = getenv('IGEST_SERIE');

São cinco: o endereço do webservice, o identificador da sua entidade, a chave secreta, o e-mail da conta e a série do documento. A série é aquele prefixo que aparece no número da fatura — no exemplo acima, AB/4021.

📄 O arquivo inteiro

É este, de cima a baixo. Uma função por assunto, a chamada no fim, e comentário curto em cada linha onde mora uma armadilha:

<?php
// Emissao de fatura no Igest via SOAP sem WSDL.
// Credenciais so por variavel de ambiente - nada escrito aqui dentro.

$ENDPOINT = getenv('IGEST_ENDPOINT');  // https://<servidor>/webservice.php
$ID_ENTIDADE = (int) getenv('IGEST_ID_ENTIDADE');
$CHAVE = getenv('IGEST_CHAVE');
$EMAIL = getenv('IGEST_EMAIL');
$SERIE = getenv('IGEST_SERIE');

// O NIF portugues tem 9 digitos. Tudo que nao for digito cai fora,
// inclusive o prefixo "PT" que muita gente digita na frente.
function limparNif($nif)
{
    $so_digitos = preg_replace('/\D/', '', (string) $nif);

    // Nove digitos ou nada: NIF pela metade viraria contribuinte inexistente.
    if (strlen($so_digitos) !== 9) {
        return '';
    }

    return $so_digitos;
}

// O valor cobrado ja tem IVA dentro. A linha vai sem IVA, e com
// QUATRO casas: o servico multiplica pela quantidade e recalcula o total.
function valorSemIva($valor_com_iva, $percentagem_iva)
{
    return round($valor_com_iva / (1 + ($percentagem_iva / 100)), 4);
}

// O campo "tipo" e um codigo de tabela, nao a percentagem nem uma letra.
function montarIva($endpoint, $percentagem, $tipo)
{
    $iva = array(
        'percentagem' => (float) $percentagem,
        'tipo'        => $tipo,   // NOR (23%), INT (13%), RED (6%), ISE (isenta)
        'regiao'      => 'PT',
    );

    return new SoapVar($iva, SOAP_ENC_OBJECT, 'IVA', $endpoint);
}

function montarArtigo($endpoint, $descricao, $valor_com_iva, $quantidade, $percentagem_iva, $tipo_iva)
{
    $artigo = array(
        'codigo'         => '',
        'tipo'           => 'S',
        'nome'           => $descricao,
        'valor_unitario' => valorSemIva($valor_com_iva, $percentagem_iva),
        'bIvaInc'        => 0,   // 0 = o valor acima esta limpo, sem imposto
        'quantidade'     => (float) $quantidade,
        'iva'            => montarIva($endpoint, $percentagem_iva, $tipo_iva),
        'desconto'       => (float) 0,
    );

    return new SoapVar($artigo, SOAP_ENC_OBJECT, 'Artigo', $endpoint);
}

function montarCliente($endpoint, $cliente)
{
    $dados = array(
        'codigo'    => (string) $cliente['codigo'],
        'nome'      => $cliente['nome'],
        'nif'       => limparNif($cliente['nif']),
        'pais_code' => 'PT',
        'email'     => $cliente['email'],
    );

    // Morada incompleta e pior que morada nenhuma: os tres campos
    // vao juntos ou nao vao.
    if ($cliente['cod_postal'] !== '' && $cliente['morada'] !== '' && $cliente['cidade'] !== '') {
        $dados['cod_postal'] = $cliente['cod_postal'];
        $dados['morada']     = $cliente['morada'];
        $dados['cidade']     = $cliente['cidade'];
    }

    return new SoapVar($dados, SOAP_ENC_OBJECT, 'Cliente', $endpoint);
}

// Sem WSDL o SoapClient recebe null no 1o argumento, e o envelope
// precisa ser descrito a mao aqui.
function abrirCliente($endpoint)
{
    return new SoapClient(null, array(
        'location'           => $endpoint,
        'uri'                => $endpoint,
        'connection_timeout' => 30,
        'trace'              => true,
        'exceptions'         => true,
        'style'              => SOAP_RPC,      // sem estes dois o servidor
        'use'                => SOAP_ENCODED,  // nao entende o envelope
    ));
}

function chamar($soap, $endpoint, $funcao, $parametros)
{
    // Sem soapaction/uri a resposta e "funcao desconhecida",
    // mesmo com o nome absolutamente correto.
    $retorno = $soap->__soapCall($funcao, $parametros, array(
        'soapaction' => $endpoint . '#' . $funcao,
        'uri'        => $endpoint,
    ));

    // A resposta volta como objeto aninhado; isto achata tudo em array.
    return json_decode(json_encode($retorno), true);
}

function emitirFatura($compra, $cliente)
{
    global $ENDPOINT, $ID_ENTIDADE, $CHAVE, $EMAIL, $SERIE;

    // A data e gerada UMA vez: ela assina e viaja no envelope.
    // Duas chamadas a date() podem cair em segundos diferentes.
    $data = date('Y-m-d H:i:s');
    $chave_assinada = sha1($CHAVE . $data);

    $artigo = montarArtigo(
        $ENDPOINT,
        $compra['descricao'],
        $compra['valor'],
        $compra['quantidade'],
        23,
        'NOR'
    );

    $documento = array(
        'serie'               => $SERIE,
        'tipo'                => 'FT',
        'data_emissao'        => date('Y-m-d'),  // so a data, sem hora
        // O numero da compra vira o numero do documento: o proprio Igest
        // recusa um segundo documento com o mesmo numero na serie.
        'numero_seguranca'    => (string) $compra['numero'],
        'numero_fatura'       => (string) $compra['numero'],
        'tipo_pagamento_code' => '0',
        'cliente'             => montarCliente($ENDPOINT, $cliente),
        'artigos'             => new SoapVar(array($artigo), SOAP_ENC_ARRAY, 'ArrayOfArtigo', $ENDPOINT),
        'email_send'          => $cliente['email'],
    );

    $parametros = array(
        new SoapParam(new SoapVar($ID_ENTIDADE, XSD_INT), 'idEntidade'),
        new SoapParam(new SoapVar($chave_assinada, XSD_STRING), 'keyCliente'),
        new SoapParam(new SoapVar($data, XSD_STRING), 'data'),
        new SoapParam(new SoapVar($EMAIL, XSD_STRING), 'email'),
        new SoapParam(new SoapVar($documento, SOAP_ENC_OBJECT, 'Documento', $ENDPOINT), 'documento'),
        new SoapParam(new SoapVar('1.0', XSD_STRING), 'versao'),
    );

    try {
        $soap = abrirCliente($ENDPOINT);
        $resposta = chamar($soap, $ENDPOINT, 'novo_documento', $parametros);
    } catch (Exception $e) {
        // Nao sabemos o que aconteceu do outro lado: a fatura pode
        // ter sido criada e so a resposta ter se perdido.
        return array(
            'ok'        => false,
            'reenviar'  => false,   // conferir a mao, NUNCA repetir sozinho
            'mensagem'  => 'Falha de comunicacao: ' . $e->getMessage(),
        );
    }

    if (empty($resposta['bSucesso'])) {
        // Aqui ha resposta dizendo que nada foi criado: pode corrigir e reenviar.
        return array(
            'ok'       => false,
            'reenviar' => true,
            'mensagem' => 'O Igest recusou o documento: ' . $resposta['descricaoErro'],
        );
    }

    return array(
        'ok'        => true,
        'documento' => $resposta['numeroDocumentoCompleto'],
        'sem_iva'   => $resposta['valorIncidencia'],
        'iva'       => $resposta['valorIva'],
        'total'     => $resposta['valorTotal'],
        'link'      => $resposta['linkDocumento'],   // caminho em base64
    );
}

// -------------------------------------------------------------------
// Uso

$compra = array(
    'numero'     => 4021,
    'descricao'  => 'COMPRA #4021 - PLANO MENSAL',
    'valor'      => 20.00,
    'quantidade' => 1,
);

$cliente = array(
    'codigo'     => 77,
    'nome'       => 'Maria Silva',
    'nif'        => 'PT123456789',
    'email'      => '[email protected]',
    'cod_postal' => '1000-001',
    'morada'     => 'Rua das Flores, 10',
    'cidade'     => 'Lisboa',
);

$resultado = emitirFatura($compra, $cliente);

if ($resultado['ok']) {
    echo "Fatura emitida: " . $resultado['documento'] . "\n";
    echo "Sem IVA: " . $resultado['sem_iva'] . " EUR\n";
    echo "IVA:     " . $resultado['iva'] . " EUR\n";
    echo "Total:   " . $resultado['total'] . " EUR\n";
    echo "PDF:     " . base64_decode($resultado['link']) . "\n";
} else {
    echo "Erro: " . $resultado['mensagem'] . "\n";
    echo "Pode reenviar: " . ($resultado['reenviar'] ? 'sim' : 'nao') . "\n";
}

🎁 O que eu levo desta integração

Faturação certificada é um daqueles temas em que o código é fácil e as regras é que são difíceis. O SOAP sem WSDL assusta na primeira meia hora e depois nunca mais; o que continua exigindo atenção é o resto: a data que precisa ser a mesma nos dois lugares, o valor que vai limpo com quatro casas, o código de IVA que é de tabela, o NIF que é nove dígitos ou nada.

E, acima de tudo, aquela distinção entre "foi recusado" e "não sei o que aconteceu". Num sistema comum, repetir a requisição é a coisa mais natural do mundo. Aqui, repetir é criar um documento fiscal a mais. 🦄

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

Leia também