PHP package for the MKESH (PagamKesh) mobile money integration over the Ericsson EWP Aggregator (XML over HTTP), with first-class Laravel support.

Maintainers

Package info

github.com/osvaldogeraldo/mkesh

pkg:composer/brilliantmind/mkesh

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.3.0 2026-07-24 16:04 UTC

This package is auto-updated.

Last update: 2026-07-24 16:10:49 UTC


README

Pacote PHP para a integração MKESH / PagamKesh através do Agregador Ericsson EWP (API "XML over HTTP"), com suporte nativo para Laravel.

O pacote constrói e interpreta todo o XML por si e expõe objectos tipados de pedido/resposta. Autenticação HTTP Basic, transporte PSR-18 (Guzzle por omissão).

Operação Método Fluxo Endpoint (por omissão)
Debit request debit() C2B – cobrar um cliente /DebitServlet/DebitSvlt
SP transfer transfer() B2C – pagar a um cliente /sptransfer/sptransfer
Get transaction status getTransactionStatus() recuperar um resultado /GetTransactionStatus/GetStatusSvlt
Debit completed parseDebitCompleted() callback assíncrono C2B (o seu webhook)
Transfer completed parseInitiateTransferCompleted() callback assíncrono B2C (o seu webhook)

Acabou de instalar? Comece aqui

Não há ficheiros para copiar. Models, webhook, job de reconciliação e a própria rota vêm registados no pacote. Falta-lhe fazer três coisas:

1. Instalar e migrar

composer require brilliantmind/mkesh
php artisan vendor:publish --tag=mkesh-config
php artisan migrate

2. Preencher o .env (detalhe em 3.1)

MKESH_USERNAME=o-seu-utilizador
MKESH_PASSWORD=a-sua-senha
MKESH_SP_FRI=FRI:pagamKesh/USER
MKESH_TRANSACTION_PREFIX=ACME

3. Reagir ao pagamento — em AppServiceProvider::boot():

use BrilliantMind\Mkesh\Laravel\Events\MkeshTransactionSettled;
use Illuminate\Support\Facades\Event;

Event::listen(function (MkeshTransactionSettled $event) {
    if ($event->isSuccessful()) {
        $event->payable()?->marcarComoPaga();   // a sua encomenda
    }
});

E já pode cobrar, de qualquer sítio:

use BrilliantMind\Mkesh\Laravel\Facades\Mkesh;

$transaccao = Mkesh::charge('258821234567', '25.00', $encomenda);

O pacote grava o registo, agenda a reconciliação, recebe o callback, responde ao agregador e dispara o evento acima quando o pagamento fecha.

Falta ainda dar ao provedor o URL do webhook — POST /api/mkesh/callback, já registado — e garantir que tem php artisan queue:work a correr.

Três coisas que poupam horas se as ler primeiro:

💸 charge() devolve PENDING, não "pago". O dinheiro só se move quando o cliente aprova no telemóvel. Entregue o produto no evento MkeshTransactionSettled, nunca a seguir ao charge().
🔑 MKESH_SP_FRI é uma FRI, não um URL nem o número do cliente. É a carteira do provedor que recebe. Ver Armadilhas.
⚙️ Sem queue:work a correr, uma transacção cujo callback se perca fica PENDING para sempre.

Índice

  1. Requisitos
  2. Instalação
  3. Configuração — inclui Armadilhas e As duas FRIs de um débito
  4. Como funciona o fluxo C2B
  5. Guia rápido Laravel — do zero ao primeiro pagamento
  6. Usar numa classe Laravel — controller, service, job, command
  7. Operações em detalhe — payloads completos
  8. Enums
  9. Erros
  10. Base de dados
  11. TLS, IP de origem e cliente HTTP
  12. Testes e resolução de problemas

examples/usage.php é um guia anotado com tudo isto num só ficheiro de código.

1. Requisitos

  • PHP 8.1+
  • Extensões ext-dom e ext-libxml
  • Um cliente HTTP PSR-18 (o Guzzle vem incluído)
  • Laravel 10, 11 ou 12 (opcional — o pacote funciona em PHP puro)

2. Instalação

composer require brilliantmind/mkesh

Em Laravel o MkeshServiceProvider e a facade Mkesh são registados automaticamente. Publique a configuração:

php artisan vendor:publish --tag=mkesh-config
php artisan migrate

As migrations vêm dentro do pacote e correm directamente com migrate. Só precisa de as publicar se quiser alterar o schema:

php artisan vendor:publish --tag=mkesh-migrations

3. Configuração

3.1 Variáveis de ambiente

# Credenciais HTTP Basic (dadas pelo provedor)
MKESH_USERNAME=o-seu-utilizador
MKESH_PASSWORD=

# FRI creditada quando cobra um cliente (C2B)
MKESH_SP_FRI=FRI:pagamKesh/USER

# Carteira debitada num pagamento B2C. Vazio = usa a MKESH_SP_FRI.
MKESH_SP_TRANSFER_FRI=FRI:47225552/MM

# Prefixo obrigatório nos ids. O pacote aplica-o sozinho.
MKESH_TRANSACTION_PREFIX=ACME

# O seu endpoint de callback — registe este URL junto do provedor
MKESH_CALLBACK_URL=https://a-sua-app.co.mz/api/mkesh/callback

MKESH_BASE_URL=https://41.220.193.151
MKESH_CURRENCY=MZN
MKESH_TIMEOUT=30

MKESH_VERIFY_SSL=true
MKESH_SSL_CA_BUNDLE=

Referência completa (ficheiro pronto a copiar em .env.example):

Variável Omissão Descrição
MKESH_USERNAME Utilizador HTTP Basic
MKESH_PASSWORD Senha HTTP Basic
MKESH_SP_FRI FRI:pagamKesh/USER FRI creditada num débito (C2B)
MKESH_SP_TRANSFER_FRI (usa MKESH_SP_FRI) Carteira debitada num pagamento (B2C)
MKESH_BASE_URL https://41.220.193.151 Host do agregador
MKESH_CURRENCY MZN Moeda por omissão
MKESH_TRANSACTION_PREFIX Prefixo forçado nos ids (ex.: ACME)
MKESH_CALLBACK_URL O seu endpoint de callback
MKESH_SEND_CALLBACK_URL false Emitir <callbackurl> dentro do débito
MKESH_VERIFY_SSL true Verificar o certificado TLS
MKESH_SSL_CA_BUNDLE Caminho para o CA de verificação
MKESH_TIMEOUT 30 Timeout por pedido (segundos)
MKESH_DEBIT_PATH /DebitServlet/DebitSvlt Path do débito
MKESH_SP_TRANSFER_PATH /sptransfer/sptransfer Path da transferência
MKESH_STATUS_PATH /GetTransactionStatus/GetStatusSvlt Path da consulta

3.2 Três regras rígidas do agregador

  • O prefixo é obrigatório. Todo o externaltransactionid / referenceid tem de começar pelo seu token de parceiro (ex.: ACME). Defina-o uma vez na configuração e passe ids simples — o pacote prefixa-os, de forma idempotente.
  • Os ids têm de ser únicos por service provider. Reutilizar um dá REFERENCE_ID_ALREADY_IN_USE. Use $config->newTransactionId() e grave o valor antes de enviar o pedido.
  • O endpoint do callback é registado do lado do provedor, não vai em cada pedido. Dê-lhes o URL; o pacote não coloca <callbackurl> no payload do débito a não ser que active sendCallbackUrl: true.

3.3 Armadilhas na configuração

Estas seis já partiram integrações reais. Vale a pena lê-las antes de preencher o .env.

1. Uma FRI nunca é um URL

Todos os campos *_FRI levam sempre a forma FRI:<valor>/<TIPO>. Nunca um endereço. Os únicos campos que levam https:// são MKESH_BASE_URL e MKESH_CALLBACK_URL.

MKESH_SP_FRI=FRI:pagamKesh/USER            #
MKESH_SP_FRI=https://41.220.193.151        # ❌ é um endereço, não uma conta

Sintoma:

FRI "https://41.220.193.151" is a URL. The *_FRI settings take an account
identifier in the form "FRI:<value>/<TYPE>" (e.g. "FRI:pagamKesh/USER"),
never a URL — only the base URL and the callback URL are addresses.

2. MKESH_SP_FRI é fixo, é do provedor, e não é o cliente

É a carteira que recebe o dinheiro — o <tofri> do débito. Vem do provedor e não muda de transacção para transacção. Quem se engana aqui costuma pôr lá o número do cliente, e depois todas as cobranças vão parar à conta errada (ou falham com ACCOUNTHOLDER_WITH_FRI_NOT_FOUND).

3. O cliente não vai à configuração

O <fromfri> — quem paga — é construído automaticamente a partir do MSISDN que passa ao pedido. Nunca está no .env:

// O pacote monta FRI:258821234567/MSISDN sozinho:
DebitRequest::charge('258821234567', Money::of(25), $id);

// Equivalente explícito:
new DebitRequest(fromFri: Fri::msisdn('258821234567'), ...);

4. Os *_PATH são caminhos, não URLs

MKESH_DEBIT_PATH=/DebitServlet/DebitSvlt                      #
MKESH_DEBIT_PATH=https://41.220.193.151/DebitServlet/DebitSvlt  #

Deixe-os vazios para usar os valores por omissão — só os altere se o seu agregador diferir.

5. O id tem de ser único por tentativa, não por encomenda

Se derivar o external_transaction_id de uma chave estável (nº de encomenda, id de factura), a segunda tentativa de pagamento da mesma encomenda é rejeitada com REFERENCE_ID_ALREADY_IN_USE. Junte sempre um sufixo aleatório:

// ❌ a segunda tentativa desta encomenda falha
$id = $config->applyPrefix("ENC-{$encomenda->id}");

// ✅ único por tentativa
$id = $config->newTransactionId("ENC-{$encomenda->id}-" . Str::random(6));

// ✅ ou simplesmente, deixando o pacote gerar
$id = $config->newTransactionId();

6. Números mKesh começam por 82 ou 83

Em formato internacional: 2588 2… ou 2588 3…. Um número de outra operadora dá ACCOUNTHOLDER_WITH_FRI_NOT_FOUND — valide antes de cobrar:

if (!preg_match('/^2588[23]\d{7}$/', $msisdn)) {
    // não é um número mKesh
}

3.4 As duas FRIs de um débito

Um débito envolve duas contas, e trocá-las é o erro mais caro da lista acima. Só uma delas está na configuração:

Elemento Quem é De onde vem
<fromfri> o cliente que paga do MSISDN passado ao pedido — automático
<tofri> a sua carteira, que recebe MKESH_SP_FRIfixo na configuração
<ns0:debitrequest xmlns:ns0="http://www.ericsson.com/em/emm/financial/v1_1">
  <fromfri>FRI:258821234567/MSISDN</fromfri>   <!-- cliente: do MSISDN -->
  <tofri>FRI:pagamKesh/USER</tofri>            <!-- você: do MKESH_SP_FRI -->
  ...
</ns0:debitrequest>
// Só passa o número do cliente. O tofri sai da configuração.
$mkesh->debit(DebitRequest::charge('258821234567', Money::of(25), $id));

Num pagamento B2C (sptransfer) o sentido inverte-se, e a carteira de origem é outra configuração ainda — MKESH_SP_TRANSFER_FRI, que na folha do provedor é uma FRI diferente da creditada num débito:

Elemento Quem é De onde vem
<sendingfri> a sua carteira, que paga MKESH_SP_TRANSFER_FRI (ou MKESH_SP_FRI se vazia)
<receivingfri> o cliente que recebe do MSISDN passado ao pedido — automático

3.5 PHP puro (sem Laravel)

use BrilliantMind\Mkesh\Config\MkeshConfig;
use BrilliantMind\Mkesh\MkeshClient;

$config = new MkeshConfig(
    username: 'o-seu-utilizador',
    password: 'a-sua-senha',
    serviceProviderFri: 'FRI:pagamKesh/USER',   // creditada no débito (C2B)
    transactionPrefix: 'ACME',
    callbackUrl: 'https://a-sua-app.co.mz/api/mkesh/callback',
    spTransferSendingFri: 'FRI:47225552/MM',    // debitada no pagamento (B2C)
);

$mkesh = MkeshClient::create($config);

Ou a partir de um array, com o mesmo formato do config/mkesh.php:

$config = MkeshConfig::fromArray([
    'username' => 'o-seu-utilizador',
    'password' => 'a-sua-senha',
    'service_provider_fri' => 'FRI:pagamKesh/USER',
    'sp_transfer_sending_fri' => 'FRI:47225552/MM',
    'transaction_prefix' => 'ACME',
]);

3.6 Checklist de onboarding

A folha do provedor deixa o bloco por ambiente em branco. Estes valores têm de ser acordados com eles separadamente para teste e produção:

Valor Direcção Corresponde a
Endereço IP de origem você → provedor o IP de saída que eles autorizam
URL de callback você → provedor MKESH_CALLBACK_URL
Utilizador / senha provedor → você MKESH_USERNAME / MKESH_PASSWORD
Nr. de conta / MSISDN provedor → você MKESH_SP_FRI / MKESH_SP_TRANSFER_FRI
Prefixo de transacção provedor → você MKESH_TRANSACTION_PREFIX
URL base provedor → você MKESH_BASE_URL

4. Como funciona o fluxo C2B

Cobrar um cliente é assíncrono. A resposta do débito só diz que o pedido foi aceite — o dinheiro ainda não se moveu.

  Parceiro                         MKESH                        Cliente
     │                               │                              │
     │  1. debitrequest v1_1         │                              │
     ├──────────────────────────────►│                              │
     │  2. debitresponse PENDING     │                              │
     │◄──────────────────────────────┤   SMS: aprovação pendente    │
     │                               ├─────────────────────────────►│
     │                               │   aprova antes de expirar    │
     │                               │◄─────────────────────────────┤
     │  3. debitcompletedrequest v1_2│                              │
     │◄──────────────────────────────┤                              │
     │     <ResponseCode>SUCCESS</…> │   SMS: débito concluído      │
     ├──────────────────────────────►├─────────────────────────────►│
     │                               │                              │
     │  ─ se o passo 3 nunca chegar ─│                              │
     │  4. gettransactionstatus v1_3 │                              │
     ├──────────────────────────────►│                              │
     │     SUCCESSFUL / FAILED       │                              │
     │◄──────────────────────────────┤                              │

Na prática:

  1. debit() devolve PENDING e um approvalid. Grave os ids e pare.
  2. O cliente aprova no telemóvel. Não há sinal síncrono deste passo.
  3. O agregador faz POST do debitcompletedrequest para o seu endpoint. Tem de responder <ResponseCode>SUCCESS</ResponseCode> — um 200 vazio não é aceite e o callback será reenviado.
  4. Se o callback nunca chegar, consulte getTransactionStatus($referenceId) até o estado ficar liquidado.

Os pagamentos B2C (transfer()) são mais simples: uma sptransferresponse com sucesso significa que o dinheiro foi transferido.

5. Guia rápido Laravel

O que o pacote já faz por si

Não há ficheiros para copiar. Isto vem registado assim que instala:

Models MkeshTransaction e MkeshResponse, com as migrations
Rota do webhook POST /api/mkesh/callback, sem middleware — o agregador não tem sessão nem token CSRF
Controller do webhook valida, grava, liquida a transacção e responde <ResponseCode>SUCCESS</ResponseCode>
Job de reconciliação agendado sozinho depois de cada débito
Eventos MkeshTransactionSettled e MkeshCallbackReceived
Facade Mkesh::charge() / Mkesh::payout()

Sobram-lhe quatro passos.

Passo 1 — instalar e configurar

composer require brilliantmind/mkesh
php artisan vendor:publish --tag=mkesh-config
php artisan migrate

Preencha o .env conforme a secção 3.1.

Passo 2 — dar o URL do webhook ao provedor

https://a-sua-app.co.mz/api/mkesh/callback — já está registado, confirme com php artisan route:list --name=mkesh. Eles configuram-no do lado deles. Confirme também que o IP de saída do seu servidor está autorizado.

Para mudar o caminho, ou registar o seu próprio endpoint:

// config/mkesh.php
'route' => [
    'enabled' => true,                    // false = registo o meu
    'path' => 'api/mkesh/callback',
    'middleware' => [],                   // deixe vazio: sem sessão, sem CSRF
],

Passo 3 — cobrar

use BrilliantMind\Mkesh\Laravel\Facades\Mkesh;

$transaccao = Mkesh::charge(
    msisdn:  '258821234567',
    amount:  '25.00',
    payable: $encomenda,     // opcional: liga o pagamento ao seu registo
);

$transaccao->status;                    // TransactionStatus::PENDING
$transaccao->external_transaction_id;   // "ACME9F2C…" — guarde para suporte

Ou injectando, se preferir não usar facades:

public function __construct(private readonly MkeshPayments $mkesh) {}

Passo 4 — saber que foi pago

Este é o passo que se esquece. charge() devolve PENDING: o dinheiro ainda não se moveu e não há resposta síncrona a dizer que sim. O cliente ainda tem de aprovar no telemóvel.

Ponha a lógica de "foi pago" num listener, não à volta do charge():

// app/Providers/AppServiceProvider.php, no boot()
use BrilliantMind\Mkesh\Laravel\Events\MkeshTransactionSettled;
use Illuminate\Support\Facades\Event;

Event::listen(function (MkeshTransactionSettled $event) {
    if ($event->isSuccessful()) {
        $event->payable()?->marcarComoPaga();
    } else {
        $event->payable()?->marcarComoFalhada($event->transaction->error_message);
    }
});

O evento dispara uma única vez por transacção, venha o resultado do callback ou do job de reconciliação. Um callback reenviado não volta a disparar — é essa a rede de segurança contra entregar a encomenda duas vezes.

Passo 5 — mostrar o estado ao utilizador

O browser não recebe nada do MKESH. Devolva 202 com a referência e deixe o frontend consultar:

// POST /pagamentos → inicia
return response()->json([
    'referencia' => $transaccao->external_transaction_id,
    'mensagem'   => 'Confirme o pagamento no seu telemóvel.',
], 202);

// GET /pagamentos/{referencia} → o frontend consulta este de 3 em 3 segundos
$t = MkeshTransaction::query()->forExternalId($referencia)->firstOrFail();

return response()->json([
    'estado'   => $t->status->value,          // PENDING | SUCCESSFUL | FAILED
    'concluido'=> $t->isSettled(),
    'erro'     => $t->error_message,
]);

Isto consulta a sua base de dados, não o MKESH — o callback e o job já a mantêm actualizada. Não chame getTransactionStatus() a cada polling do browser.

Resumo do ciclo de vida

Momento O que a sua aplicação faz
Utilizador carrega em pagar charge() → grava linha PENDING, devolve 202
Cliente recebe SMS e aprova nada — não há sinal síncrono
Chega o debitcompletedrequest webhook liquida a linha e entrega o produto
O callback nunca chega o job de reconciliação apanha o estado por polling
Frontend pergunta "já?" lê a sua tabela, não o MKESH

Antes de ir para produção

  • MKESH_SP_FRI é uma FRI, não um URL nem o número do cliente (3.3)
  • O IP de saída do servidor está autorizado pelo provedor
  • O URL de callback está registado do lado deles e é acessível de fora
  • O webhook devolve <ResponseCode>SUCCESS</ResponseCode> (teste com curl)
  • A fila está a correr (php artisan queue:work) — sem ela o job de reconciliação nunca corre
  • MKESH_VERIFY_SSL=true com o CA do provedor
  • Os ids são únicos por tentativa, não por encomenda (3.3)

6. Usar numa classe Laravel

6.1 Injecção no construtor (recomendado)

MkeshClient está registado no container como singleton — basta declarar o tipo:

namespace App\Services;

use BrilliantMind\Mkesh\MkeshClient;
use BrilliantMind\Mkesh\Request\DebitRequest;
use BrilliantMind\Mkesh\ValueObject\Money;

final class CheckoutService
{
    public function __construct(
        private readonly MkeshClient $mkesh,
    ) {
    }

    public function pagar(string $msisdn, string $valor): void
    {
        $id = $this->mkesh->config()->newTransactionId();

        // grave $id na sua base de dados AQUI, antes de enviar

        $resposta = $this->mkesh->debit(
            DebitRequest::charge($msisdn, Money::of($valor), $id),
        );
    }
}

6.2 Facade

use BrilliantMind\Mkesh\Laravel\Facades\Mkesh;

$id       = Mkesh::config()->newTransactionId();
$debito   = Mkesh::debit(DebitRequest::charge('258823040400', Money::of(25), $id));
$estado   = Mkesh::getTransactionStatus($id);
$callback = Mkesh::parseDebitCompleted($request->getContent());

Métodos disponíveis na facade:

Método Devolve
Mkesh::debit($request) DebitResponse
Mkesh::transfer($request) SpTransferResponse
Mkesh::getTransactionStatus($ref) TransactionStatusResponse
Mkesh::parseDebitCompleted($xml) DebitCompletedNotification
Mkesh::parseInitiateTransferCompleted($xml) InitiateTransferCompletedNotification
Mkesh::acknowledgeCallback() CallbackResponse
Mkesh::config() MkeshConfig

6.3 Controller que inicia um pagamento

namespace App\Http\Controllers;

use BrilliantMind\Mkesh\Laravel\MkeshPayments;
use BrilliantMind\Mkesh\Enum\ErrorCode;
use BrilliantMind\Mkesh\Exception\ErrorResponseException;
use BrilliantMind\Mkesh\Exception\TransportException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class PagamentoController extends Controller
{
    public function __construct(
        private readonly MkeshPayments $pagamentos,
    ) {
    }

    public function store(Request $request): JsonResponse
    {
        $dados = $request->validate([
            'msisdn' => ['required', 'regex:/^258[0-9]{9}$/'],
            'valor'  => ['required', 'numeric', 'min:1'],
        ]);

        try {
            $transaccao = $this->pagamentos->charge(
                msisdn: $dados['msisdn'],
                amount: (string) $dados['valor'],
            );
        } catch (ErrorResponseException $e) {
            $codigo = $e->code();

            // Erros que o cliente consegue resolver: mostre a mensagem.
            if ($codigo->isCustomerFault()) {
                return response()->json([
                    'mensagem' => match ($codigo) {
                        ErrorCode::AUTHORIZATION_CURRENT_BALANCE_TOO_LOW => 'Saldo insuficiente.',
                        ErrorCode::ACCOUNTHOLDER_NOT_ACTIVE => 'Conta mKesh inactiva.',
                        default => 'Não foi possível processar o pagamento.',
                    },
                ], 422);
            }

            report($e);

            return response()->json(['mensagem' => 'Serviço indisponível.'], 502);
        } catch (TransportException $e) {
            // CUIDADO: pode ter passado do lado deles. Não reenvie às cegas —
            // o job de reconciliação vai apurar o estado real.
            report($e);

            return response()->json(['mensagem' => 'Sem resposta do MKESH.'], 504);
        }

        return response()->json([
            'referencia' => $transaccao->external_transaction_id,
            'estado'     => $transaccao->status->value,
            'mensagem'   => 'Confirme o pagamento no seu telemóvel.',
        ], 202);
    }
}

6.4 O webhook (já feito)

Não escreva um. O pacote regista o controller e a rota. Ele faz, dentro de uma transacção de base de dados:

  1. interpreta o corpo — se não der, grava-o na mesma e devolve 400
  2. bloqueia a linha correspondente com lockForUpdate
  3. grava o payload cru em mkesh_responses, mesmo que não tenha correspondência
  4. liquida a transacção e dispara MkeshTransactionSettled uma única vez
  5. devolve <ResponseCode>SUCCESS</ResponseCode>

As três propriedades que isto garante, e que são fáceis de errar à mão:

  • Reentrega não duplica. Duas entregas do mesmo callback dão dois registos de auditoria mas um só evento de negócio — a sua encomenda não é entregue duas vezes.
  • Callback tardio não inverte o resultado. Um FAILED que chegue depois de um SUCCESSFUL é registado e ignorado.
  • Acusa sempre a recepção quando consegue ler o corpo, mesmo sem correspondência local. Caso contrário o agregador reenvia para sempre.

Se precisar mesmo do seu próprio endpoint, desligue o do pacote e reutilize o handler — assim mantém as garantias acima:

// config/mkesh.php → 'route' => ['enabled' => false]

use BrilliantMind\Mkesh\Laravel\CallbackHandler;

public function __invoke(Request $request, CallbackHandler $handler): Response
{
    $resultado = $handler->handle($request->getContent());

    return new Response(
        $resultado->body(),
        $resultado->httpStatus,
        ['Content-Type' => $resultado->contentType()],
    );
}

6.5 Job de reconciliação (já agendado)

Cobre o ramo "sem resposta do MKESH". O Mkesh::charge() despacha-o sozinho — não tem de fazer nada:

// config/mkesh.php
'reconcile' => [
    'enabled' => true,
    'delay' => 120,      // segundos até à primeira verificação
],

Faz polling ao gettransactionstatus com backoff progressivo (1, 2, 5, 10, 15, 30, 60 minutos) e pára assim que a linha estiver liquidada — seja pelo callback, seja pelo próprio polling. Códigos retentáveis (ErrorCode::isRetryable(), sobretudo TRANSACTION_NOT_FOUND, que aqui significa "ainda não registado") libertam o job para nova tentativa.

Precisa de um worker. Sem php artisan queue:work a correr, uma transacção cujo callback se perca fica PENDING para sempre.

6.6 Command Artisan para reconciliar em lote

namespace App\Console\Commands;

use BrilliantMind\Mkesh\Laravel\Jobs\ReconcileMkeshTransaction;
use BrilliantMind\Mkesh\Laravel\Models\MkeshTransaction;
use BrilliantMind\Mkesh\Enum\TransactionStatus;
use Illuminate\Console\Command;

final class ReconciliarMkesh extends Command
{
    protected $signature = 'mkesh:reconciliar {--minutos=10}';
    protected $description = 'Consulta o estado dos débitos ainda pendentes';

    public function handle(): int
    {
        $pendentes = MkeshTransaction::query()
            ->where('type', MkeshTransaction::TYPE_DEBIT)
            ->where('status', TransactionStatus::PENDING)
            ->where('created_at', '<', now()->subMinutes((int) $this->option('minutos')))
            ->get();

        foreach ($pendentes as $transaccao) {
            ReconcileMkeshTransaction::dispatch($transaccao->getKey());
        }

        $this->info("{$pendentes->count()} transacções enviadas para reconciliação.");

        return self::SUCCESS;
    }
}

Agende-o em routes/console.php:

Schedule::command('mkesh:reconciliar')->everyFifteenMinutes();

6.7 Testar sem tocar na rede

Injecte um cliente PSR-18 falso — não é preciso mais nada:

use BrilliantMind\Mkesh\Config\MkeshConfig;
use BrilliantMind\Mkesh\MkeshClient;
use GuzzleHttp\Psr7\HttpFactory;
use GuzzleHttp\Psr7\Response;

$http = new class (new Response(200, [], $xmlDeResposta)) implements \Psr\Http\Client\ClientInterface {
    public function __construct(private $resposta) {}
    public function sendRequest(\Psr\Http\Message\RequestInterface $r): \Psr\Http\Message\ResponseInterface
    {
        return $this->resposta;
    }
};

$factory = new HttpFactory();
$mkesh = new MkeshClient($config, $http, $factory, $factory);

// No teste, substitua o singleton do container:
$this->app->instance(MkeshClient::class, $mkesh);

7. Operações em detalhe

Todos os ids mostrados já incluem o prefixo ACME, que o pacote aplica automaticamente a externaltransactionid / providertransactionid / referenceid.

7.1 Debit request — C2B (cobrar um cliente)

use BrilliantMind\Mkesh\Request\DebitRequest;
use BrilliantMind\Mkesh\ValueObject\Fri;
use BrilliantMind\Mkesh\ValueObject\Money;

$resposta = $mkesh->debit(DebitRequest::charge(
    customerMsisdn: '258823040400',
    amount: Money::of(25),             // 25 MZN
    externalTransactionId: '000001',
));

// Forma completa, com todos os parâmetros:
$pedido = new DebitRequest(
    fromFri: Fri::msisdn('258823040400'),   // quem paga
    amount: Money::of(25, 'MZN'),
    externalTransactionId: '000001',
    toFri: null,          // omissão: serviceProviderFri da config
    referenceId: null,    // omissão: igual ao externalTransactionId
    fromMessage: null,
    toMessage: null,
);

Pedido enviado (Content-Type: text/xml):

<?xml version="1.0" encoding="UTF-8"?>
<ns0:debitrequest xmlns:ns0="http://www.ericsson.com/em/emm/financial/v1_1">
  <fromfri>FRI:258823040400/MSISDN</fromfri>
  <tofri>FRI:pagamKesh/USER</tofri>
  <amount>
    <amount>25</amount>
    <currency>MZN</currency>
  </amount>
  <externaltransactionid>ACME000001</externaltransactionid>
  <referenceid>ACME000001</referenceid>
</ns0:debitrequest>

O referenceid assume por omissão o valor do id externo (como na folha do provedor) e é por ele que o getTransactionStatus() procura a transacção.

Resposta (PENDING)DebitResponse:

<?xml version="1.0" encoding="UTF-8"?>
<ns0:debitresponse xmlns:ns0="http://www.ericsson.com/em/emm/financial/v1_1">
  <transactionid>3171312</transactionid>
  <status>PENDING</status>
  <approvalid>470390</approvalid>
</ns0:debitresponse>
$resposta->transactionId;          // "3171312"
$resposta->status;                 // TransactionStatus::PENDING
$resposta->approvalId;             // "470390"
$resposta->isPending();            // true

7.2 SP transfer — B2C (pagar a um cliente)

A carteira de origem vem de spTransferSendingFri, que é muitas vezes uma FRI diferente da creditada num débito (FRI:47225552/MM vs FRI:pagamKesh/USER); se não estiver definida, usa a serviceProviderFri.

use BrilliantMind\Mkesh\Request\SpTransferRequest;

$resposta = $mkesh->transfer(SpTransferRequest::payout(
    customerMsisdn: '258823040400',
    amount: Money::of(25),
    providerTransactionId: 'XXXXX',
));

Pedido enviado:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:sptransferrequest xmlns:ns2="http://www.ericsson.com/em/emm/serviceprovider/v1_2/backend">
  <sendingfri>FRI:47225552/MM</sendingfri>
  <receivingfri>FRI:258823040400/MSISDN</receivingfri>
  <amount>
    <amount>25</amount>
    <currency>MZN</currency>
  </amount>
  <providertransactionid>ACMEXXXXX</providertransactionid>
  <referenceid>ACMEXXXXX</referenceid>
</ns2:sptransferrequest>

RespostaSpTransferResponse:

<?xml version="1.0" encoding="UTF-8"?>
<ns0:sptransferresponse xmlns:ns0="http://www.ericsson.com/em/emm/serviceprovider/v1_2/backend">
  <transactionid>3282002</transactionid>
  <providertransactionid>ACME-XXXXXX</providertransactionid>
</ns0:sptransferresponse>
$resposta->transactionId;            // "3282002"
$resposta->providerTransactionId;    // "ACME-XXXXXX"

7.3 Get transaction status (recuperar um resultado)

Use o referenceid de uma operação anterior quando não houve resposta nem callback.

$estado = $mkesh->getTransactionStatus('000001');   // prefixo aplicado

Pedido enviado:

<?xml version="1.0" encoding="UTF-8"?>
<ns0:gettransactionstatusrequest xmlns:ns0="http://www.ericsson.com/em/emm/financial/v1_3">
  <referenceid>ACME000001</referenceid>
</ns0:gettransactionstatusrequest>

Resposta (SUCESSO):

<?xml version="1.0" encoding="UTF-8"?>
<ns0:gettransactionstatusresponse xmlns:ns0="http://www.ericsson.com/em/emm/financial/v1_3">
  <financialtransactionid>3171312</financialtransactionid>
  <status>SUCCESSFUL</status>
  <providertransactionid>ACME000001</providertransactionid>
</ns0:gettransactionstatusresponse>

Resposta (FALHA) — repare que não traz providertransactionid:

<?xml version="1.0" encoding="UTF-8"?>
<ns0:gettransactionstatusresponse xmlns:ns0="http://www.ericsson.com/em/emm/financial/v1_3">
  <financialtransactionid>3221328</financialtransactionid>
  <status>FAILED</status>
</ns0:gettransactionstatusresponse>
$estado->financialTransactionId;   // "3171312"
$estado->status;                   // TransactionStatus::SUCCESSFUL
$estado->providerTransactionId;    // null quando FAILED
$estado->status->isSettled();      // true — pare de consultar

7.4 Callback debit-completed

O agregador faz POST disto para o endpoint que registou junto do provedor.

Pedido recebido:

<?xml version="1.0" encoding="UTF-8"?>
<ns0:debitcompletedrequest xmlns:ns0="http://www.ericsson.com/em/emm/callback/v1_2">
  <transactionid>3171312</transactionid>
  <externaltransactionid>ACME000001</externaltransactionid>
  <receiverinfo>
    <fri>FRI:1360073/MM</fri>
    <msisdn>8230X04XX</msisdn>
    <language>en</language>
  </receiverinfo>
  <status>SUCCESSFUL</status>
  <communicationchannel>http-sp</communicationchannel>
  <referenceid>ACME000001</referenceid>
</ns0:debitcompletedrequest>
$callback = $mkesh->parseDebitCompleted($corpoDoPedido);

$callback->transactionId;            // "3171312"
$callback->externalTransactionId;    // "ACME000001" — o NOSSO id
$callback->referenceId;
$callback->status;                   // TransactionStatus::SUCCESSFUL
$callback->communicationChannel;     // "http-sp"
$callback->receiver->fri;            // "FRI:1360073/MM"
$callback->receiver->msisdn;
$callback->receiver->language;       // "en"
$callback->isSuccessful();

A resposta obrigatória. Um 200 vazio não é aceite e o callback será reenviado:

<?xml version="1.0" encoding="utf-8"?><ResponseCode>SUCCESS</ResponseCode>
use BrilliantMind\Mkesh\Callback\CallbackResponse;

$ack = CallbackResponse::success();
$ack->toXml();                   // o corpo a devolver
CallbackResponse::CONTENT_TYPE;  // "text/xml; charset=utf-8"

7.5 Callback transfer-completed (B2C)

O equivalente B2C é entregue da mesma forma:

$callback = $mkesh->parseInitiateTransferCompleted($corpoDoPedido);

$callback->financialTransactionId;
$callback->externalTransactionId;
$callback->status->isSuccessful();
$callback->receiver->msisdn;
// responda com o mesmo <ResponseCode>SUCCESS</ResponseCode>

8. Enums

Enum Casos Serve para
Enum\TransactionStatus PENDING SUCCESSFUL FAILED UNKNOWN o estado de um débito/transferência
Enum\ErrorCode 22 códigos + UNKNOWN decidir o que fazer perante uma falha
Enum\CallbackResponseCode SUCCESS FAILURE o corpo devolvido pelo seu webhook
Enum\FriType MSISDN USER MM construir FRIs

Todos interpretam defensivamente: um valor desconhecido dá UNKNOWN em vez de lançar excepção, para que um valor novo da plataforma nunca parta o parsing.

8.1 TransactionStatus

use BrilliantMind\Mkesh\Enum\TransactionStatus;

TransactionStatus::PENDING;      // à espera da aprovação do cliente
TransactionStatus::SUCCESSFUL;   // dinheiro movido
TransactionStatus::FAILED;
TransactionStatus::UNKNOWN;      // valor não reconhecido

// O diagrama do provedor escreve SUCCESS/FAILURE, os payloads escrevem
// SUCCESSFUL/FAILED — ambos são aceites.
TransactionStatus::fromWire('SUCCESS');    // SUCCESSFUL
TransactionStatus::fromWire('failure');    // FAILED
TransactionStatus::fromWire('seja o que for');   // UNKNOWN

$status->isPending();
$status->isSuccessful();
$status->isFailed();
$status->isSettled();   // true se SUCCESSFUL ou FAILED — pare de consultar

Em Eloquent, faça cast directo para o enum:

protected $casts = ['status' => TransactionStatus::class];

8.2 ErrorCode

Cobre os códigos que mudam o que a aplicação faz, com a decisão já embutida em vez de ficar à mercê de comparações de strings:

use BrilliantMind\Mkesh\Enum\ErrorCode;

$codigo = $e->code();        // ErrorCode; $e->getErrorCode() dá a string crua

$codigo->isRetryable();      // transitório — repita o mesmo pedido mais tarde
$codigo->isDuplicate();      // id já usado; consulte o estado, NÃO reenvie
$codigo->isCustomerFault();  // sem saldo / inactivo / PIN errado / expirou
$codigo->isExpired();        // a janela de aprovação passou
$codigo->isAuthFailure();    // credenciais erradas ou IP não autorizado
$codigo->description();      // do catálogo completo de 670 códigos

Casos disponíveis, por família:

Família Casos
Pesquisa / idempotência TRANSACTION_NOT_FOUND REFERENCE_ID_ALREADY_IN_USE AMBIGUOUS_REFERENCE_ID EXPIRED_OR_INVALID_TRANSACTION_ID
Contraparte ACCOUNTHOLDER_WITH_FRI_NOT_FOUND ACCOUNTHOLDER_WITH_MSISDN_NOT_FOUND ACCOUNTHOLDER_NOT_ACTIVE AUTHORIZATION_ACCOUNTHOLDER_NOT_ACTIVE ACCOUNT_NOT_FOUND
Dinheiro AUTHORIZATION_CURRENT_BALANCE_TOO_LOW AUTHORIZATION_MAX_TRANSFER_AMOUNT AUTHORIZATION_MAXIMUM_AMOUNT_ALLOWED_TO_SEND AUTHORIZATION_MAXIMUM_AMOUNT_ALLOWED_TO_RECEIVE AMOUNT_INVALID INVALID_CURRENCY CURRENCY_NOT_SUPPORTED
Aprovação TRANSACTION_REQUEST_EXPIRED INCORRECT_PIN QUEUED_FOR_APPROVAL INVALID_APPROVAL_TRANSACTION_STATUS RETRY_FROM_BEGINNING
Acesso AUTHORIZATION_FAILED

O enum não lista os 670 códigos de propósito — seria uma segunda cópia do ErrorCodes sem ganho nenhum. Qualquer código fora dele dá ErrorCode::UNKNOWN, enquanto getErrorCode() e ErrorCodes::description() continuam a funcionar sobre a string crua.

8.3 CallbackResponseCode

use BrilliantMind\Mkesh\Enum\CallbackResponseCode;

CallbackResponseCode::SUCCESS;   // o único documentado pelo provedor
CallbackResponseCode::FAILURE;

CallbackResponse::of(CallbackResponseCode::SUCCESS)->toXml();

Na dúvida responda SUCCESS e reconcilie fora de banda com o gettransactionstatus — o tratamento de FAILURE do lado da plataforma não está especificado.

8.4 FriType e os value objects

use BrilliantMind\Mkesh\Enum\FriType;
use BrilliantMind\Mkesh\ValueObject\Fri;
use BrilliantMind\Mkesh\ValueObject\Money;

FriType::MSISDN;         // FRI:258823040400/MSISDN — número de telemóvel
FriType::USER;           // FRI:pagamKesh/USER      — conta de service provider
FriType::MOBILE_MONEY;   // FRI:1360073/MM          — conta mobile money

Fri::msisdn('258823040400');
Fri::user('pagamKesh');
Fri::mobileMoney('1360073');
Fri::fromString('FRI:258823040400/MSISDN');
(string) $fri;                  // "FRI:258823040400/MSISDN"

// O valor é guardado como string normalizada, para não apanhar erros de
// vírgula flutuante ao serializar o XML.
Money::of(25);                  // 25 MZN
Money::of('25.50', 'MZN');
Money::of(25.5)->amount;        // "25.5"

9. Erros

Erros de negócio chegam como um envelope errorResponse e são lançados como ErrorResponseException.

<?xml version="1.0" encoding="UTF-8"?>
<ns0:errorResponse xmlns:ns0="http://www.ericsson.com/lwac" errorcode="ACCOUNTHOLDER_WITH_FRI_NOT_FOUND">
  <arguments name="fri" value="FRI:258823040420/MSISDN"/>
</ns0:errorResponse>
use BrilliantMind\Mkesh\Enum\ErrorCode;
use BrilliantMind\Mkesh\Exception\ErrorResponseException;
use BrilliantMind\Mkesh\Exception\MkeshException;

try {
    $mkesh->debit($pedido);
} catch (ErrorResponseException $e) {
    $e->getErrorCode();       // "ACCOUNTHOLDER_WITH_FRI_NOT_FOUND"
    $e->getDescription();     // "Account holder with given FRI could not be found"
    $e->getArguments();       // ["fri" => "FRI:258823040420/MSISDN"]
    $e->getArgument('fri');
    $e->getRawXml();          // corpo original, para auditoria
    $e->code();               // ErrorCode (ver secção 8.2)

    // is() aceita enum ou string, indiferentemente
    $e->is(ErrorCode::TRANSACTION_NOT_FOUND);
    $e->is('TRANSACTION_NOT_FOUND');
    $e->is(ErrorCode::AMOUNT_INVALID, ErrorCode::INVALID_CURRENCY);
} catch (MkeshException $e) {
    // qualquer outra falha do pacote (transporte, config, parsing…)
}

Hierarquia — todas implementam a interface marcadora MkeshException:

Excepção Lançada quando
ErrorResponseException a plataforma devolveu um <errorResponse>
TransportException falha de ligação/TLS/timeout, corpo vazio ou XML inválido
ConfigurationException configuração em falta ou inválida
InvalidArgumentException value object ou input de pedido inválido

Catálogo completo de códigos

Os 670 códigos da referência da plataforma estão em BrilliantMind\Mkesh\Error\ErrorCodes, e a descrição é acrescentada automaticamente à mensagem da excepção:

use BrilliantMind\Mkesh\Error\ErrorCodes;

ErrorCodes::description('TRANSACTION_NOT_FOUND');
ErrorCodes::has('REFERENCE_ID_ALREADY_IN_USE');
ErrorCodes::all();     // array<string, string>

Os três erros que vai mesmo encontrar

Código Significado O que fazer
REFERENCE_ID_ALREADY_IN_USE id repetido O original quase de certeza passou. Consulte o estado — não reenvie com id novo.
TRANSACTION_NOT_FOUND ainda não registado Consultou cedo demais. Repita mais tarde.
ACCOUNTHOLDER_WITH_FRI_NOT_FOUND número não é mKesh Valide o MSISDN antes de cobrar.

10. Base de dados

Duas migrations e dois models acompanham o pacote. As migrations correm com php artisan migrate; os models estão em BrilliantMind\Mkesh\Laravel\Models\ e não precisam de ser copiados.

Chaves primárias são UUID

Ambas as tabelas usam UUID ordenado (HasUuids) em vez de auto-incremento. Estes ids saem da base de dados — vão em payloads de fila, logs e tickets de suporte — e um inteiro sequencial revelaria o volume de transacções e convidaria à enumeração. Sendo ordenados (ordenáveis no tempo), o índice não fragmenta como aconteceria com UUID v4 puro.

Consequência prática: onde passar a chave, é string:

ReconcileMkeshTransaction::dispatch($transaccao->getKey());   // string, não int

payable_id é string, de propósito

A ligação polimórfica usa colunas string, não nullableMorphs():

$table->string('payable_type')->nullable();
$table->string('payable_id')->nullable();
$table->index(['payable_type', 'payable_id']);

Um pacote não deve impor o tipo de chave aos models da aplicação. nullableMorphs() forçaria unsignedBigInteger e partiria quem usa UUID ou ULID — com o erro Data truncated for column 'payable_id' assim que tentasse guardar uma chave não numérica. nullableUuidMorphs() teria o problema simétrico: partiria quem usa auto-incremento. string aceita int, UUID e ULID, sem o pacote precisar de saber qual deles usa.

// Na sua encomenda:
public function mkeshTransactions(): MorphMany
{
    return $this->morphMany(MkeshTransaction::class, 'payable');
}

// E do outro lado:
$transaccao->payable;        // a sua Encomenda / Factura / Subscrição
$transaccao->payable()->associate($encomenda)->save();

Nomes de tabela e ligação

Migrations e models lêem os mesmos valores da config, por isso renomear num sítio chega:

// config/mkesh.php
'database' => [
    'connection' => env('MKESH_DB_CONNECTION'),   // null = ligação por omissão
    'tables' => [
        'transactions' => 'mkesh_transactions',
        'responses' => 'mkesh_responses',
    ],
],

mkesh_transactions — o livro-razão

A linha é criada antes do pedido sair, para reservar o id localmente.

Coluna Notas
id UUID ordenado
type debit (C2B) ou transfer (B2C)
external_transaction_id enviado no débito, único por tipo
provider_transaction_id enviado na transferência, único por tipo
reference_id o que o getTransactionStatus() procura
financial_transaction_id devolvido pela plataforma
approval_id de um débito pendente
msisdn, amount, currency contraparte e dinheiro
status PENDING / SUCCESSFUL / FAILED / UNKNOWN
error_code, error_message código da plataforma + descrição do catálogo
payable_type, payable_id ligação polimórfica — payable_id é string, aceita qualquer chave (int, UUID, ULID)
completed_at quando liquidou

Métodos e scopes do model:

// Transições que respeitam a idempotência
$transaccao->settle(TransactionStatus::SUCCESSFUL, '3171312');  // false se já liquidada
$transaccao->fail($errorResponseException);                     // grava código + descrição

// Estado
$transaccao->isPending();  $transaccao->isSettled();
$transaccao->isDebit();    $transaccao->isTransfer();

// Scopes
MkeshTransaction::query()->debits()->pending()->get();
MkeshTransaction::query()->stale(10)->get();          // pendentes há mais de 10 min
MkeshTransaction::query()->forExternalId('ACME000001')->first();

settle() devolver false é a rede de segurança contra callbacks reenviados: o primeiro resultado nunca é sobrescrito.

mkesh_responses — a auditoria

Propositadamente não é única por transacção, porque os callbacks são reenviados. É precisamente isso que a torna útil: prova o que chegou, quando, e o que respondeu.

Coluna Notas
id UUID ordenado
direction inbound (callback recebido) / outbound (resposta a pedido nosso)
operation debitcompletedrequest, sptransferresponse, errorResponse
mkesh_transaction_id ligação ao livro-razão, quando houve correspondência
external_transaction_id, reference_id, financial_transaction_id desnormalizados para pesquisa
status, error_code extraídos para consulta
response_code o SUCCESS / FAILURE que devolvemos
http_status código HTTP devolvido ou recebido
payload o XML intacto
$transaccao->responses;              // tudo o que o MKESH disse sobre ela
$resposta->transaction;              // relação inversa

// Registar, sem montar o array à mão
MkeshResponse::logCallback($callback, $corpoBruto, $transaccao);
MkeshResponse::logUnparsable($corpoBruto);            // corpo que não deu para ler
MkeshResponse::logError($excepcao, 'debitresponse', $transaccao);

MkeshResponse::query()->inbound()->latest()->get();

Ao apagar uma transacção, o mkesh_transaction_id fica a null mas o registo sobrevive — a prova do que chegou não se apaga com o livro-razão.

11. TLS, IP de origem e cliente HTTP

TLS

O agregador está publicado em HTTPS sobre um IP nu, por isso o certificado não corresponde ao hostname e a verificação por omissão falha. A verificação está ligada por omissão — aponte MKESH_SSL_CA_BUNDLE para o certificado que o provedor fornecer e só desligue (MKESH_VERIFY_SSL=false) contra o ambiente de testes.

IP de origem

O provedor faz whitelist do IP de origem: as chamadas têm de sair do host que registou junto deles. Uma máquina local ou um IP de saída diferente é rejeitado ao nível da rede, antes de qualquer XML ser interpretado.

Cliente HTTP personalizado

MkeshClient::create() liga o Guzzle por si. Para usar outro cliente PSR-18, injecte-o (com as factories PSR-17) pelo construtor:

$client = new MkeshClient($config, $psr18Client, $requestFactory, $streamFactory);

12. Testes e resolução de problemas

composer test

Teste manual pela linha de comandos

php examples/mkesh-test.php debit    258823040400 25
php examples/mkesh-test.php transfer 258823040400 10
php examples/mkesh-test.php status   000001

Imprime o XML enviado e a resposta interpretada — é a forma mais rápida de resolver um desacordo com o provedor.

Problemas comuns

Sintoma / erro Causa provável Como resolver
MKESH_SP_FRI … is not set Variável vazia ou em falta Peça a FRI ao provedor. É FRI:<valor>/<TIPO>, não um URL — ver 3.3
FRI "https://…" is a URL Pôs um endereço num campo *_FRI MKESH_BASE_URL e MKESH_CALLBACK_URL levam https://
FRI "…" must start with "FRI:" Falta o prefixo FRI: FRI:pagamKesh/USER, não pagamKesh/USER
FRI "…" is missing the "/<TYPE>" Falta o sufixo de tipo /USER, /MSISDN ou /MM
Data truncated for column 'payable_id' Migration antiga com payable_id em bigint e um payable com chave UUID/ULID Actualize as migrations — desde a 0.2.0 a coluna é string
REFERENCE_ID_ALREADY_IN_USE Id reutilizado (típico: derivado do nº de encomenda, sem sufixo por tentativa) newTransactionId(), ou junte um sufixo aleatório. Grave-o antes de enviar
TRANSACTION_NOT_FOUND ao consultar Consultou cedo demais, ou usou o id errado A procura é pelo referenceid. Repita mais tarde
ACCOUNTHOLDER_WITH_FRI_NOT_FOUND Número não é mKesh, ou pôs o número do cliente no MKESH_SP_FRI Números mKesh começam por 82 ou 83. O <fromfri> sai do MSISDN, não da config
AUTHORIZATION_FAILED Credenciais erradas ou IP de origem não autorizado Confirme as duas coisas com o provedor
Erro de certificado TLS Host publicado num IP nu, logo o certificado não bate com o hostname MKESH_SSL_CA_BUNDLE com o CA do provedor; MKESH_VERIFY_SSL=false só em testes
O callback chega repetidamente Não está a devolver <ResponseCode>SUCCESS</ResponseCode> Um 200 vazio não é aceite
Timeout sem resposta Pode ter passado do lado deles Consulte o estado antes de reenviar
Débito fica sempre PENDING O cliente não aprovou O pedido expira; veja TRANSACTION_REQUEST_EXPIRED

Licença

MIT — © 2026 BrilliantMind. Ver LICENSE.

Suporte: it@brilliantmind.co.mz