Skip to content

isDuplicateExternalId() depende de match em texto livre; pedido de código de erro estável #37

Description

@andrenfe

Resumo

ServiceInvoicesResource::isDuplicateExternalId() identifica a rejeição de externalId duplicado por match em texto livre da mensagem de erro. Isso funciona hoje, mas quebra silenciosamente se a API mudar a redação, a capitalização ou o idioma da mensagem — e o modo de falha é perigoso, porque o método passa a devolver false e o integrador conclui que nenhuma nota foi criada.

Verificado na v3.5.0.

Onde

src/Resource/ServiceInvoicesResource.php:

public static function isDuplicateExternalId(ApiErrorException $e): bool
{
    if ($e->statusCode !== 400) {
        return false;
    }

    $haystack = strtolower(($e->responseBody ?? '') . ' ' . $e->getMessage());

    return str_contains($haystack, 'external id') && str_contains($haystack, 'already exists');
}

O próprio docblock já reconhece a fragilidade ("A mensagem pode chegar no corpo cru ou (futuramente) no campo message").

O comportamento confirmado ao vivo

Vale registrar porque diverge da documentação da API. Testado em 2026-09-02 contra conta de desenvolvimento, empresa com environment = Development, rota POST /v1/companies/{id}/serviceinvoices:

  1. Emitida uma NFS-e com externalId: "WOO-NFE-101"202, nota criada e emitida.
  2. Nota cancelada via cancel()flowStatus: "Cancelled".
  3. Reenviada a criação com o mesmo externalId: "WOO-NFE-101".

Resultado: 400, Nfe\Exception\InvalidRequestException, e isDuplicateExternalId() retornou true.

A documentação da NFe.io sobre idempotência descreve o comportamento oposto — "em vez de criarmos uma nova nota, retornaremos os dados da nota fiscal original". Nesta rota o que ocorre é rejeição, não replay.

Isso não é um defeito do SDK — o SDK está certo e a documentação é que está desatualizada — mas reforça que integradores vão depender desse método, e que ele precisa ser confiável.

Pedido

Um código de erro estável para essa rejeição, exposto em ApiErrorException::$errorCode (o campo já existe), de modo que isDuplicateExternalId() possa comparar um identificador em vez do texto. Algo como external_id_already_exists.

Enquanto o código não existir, uma melhoria barata no SDK seria tornar o match mais tolerante — hoje str_contains($haystack, 'external id') não casa com externalId, external_id nem com a mensagem traduzida.

Sugestão adicional para a documentação

Alinhar a página de idempotência com o comportamento real da rota, ou explicitar em quais rotas/layouts vale replay e em quais vale rejeição. A diferença é relevante: quem assume replay conclui que reemitir é seguro e idempotente; na prática a segunda emissão falha.

Contexto

Encontrado ao migrar o plugin nfe/woo-nfe para o nfe/nfe 3.5. O plugin enviava externalId fixo por pedido, o que tornava impossível reemitir após cancelamento — corrigido para externalId por tentativa.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions