brilliantmind / mkesh
PHP package for the MKESH (PagamKesh) mobile money integration over the Ericsson EWP Aggregator (XML over HTTP), with first-class Laravel support.
Requires
- php: ^8.1
- ext-dom: *
- ext-libxml: *
- guzzlehttp/guzzle: ^7.14.2
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- illuminate/bus: ^10.0 || ^11.0 || ^12.0
- illuminate/config: ^10.0 || ^11.0 || ^12.0
- illuminate/database: ^10.0 || ^11.0 || ^12.0
- illuminate/events: ^10.0 || ^11.0 || ^12.0
- illuminate/queue: ^10.0 || ^11.0 || ^12.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0
- phpunit/phpunit: ^10.5 || ^11.0
- ramsey/uuid: ^4.7
- vlucas/phpdotenv: ^5.6
Suggests
- illuminate/database: Required to use the MkeshTransaction / MkeshResponse Eloquent models and the shipped migrations.
- illuminate/support: Required to use the Laravel ServiceProvider and Mkesh facade.
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
- Requisitos
- Instalação
- Configuração — inclui Armadilhas e As duas FRIs de um débito
- Como funciona o fluxo C2B
- Guia rápido Laravel — do zero ao primeiro pagamento
- Usar numa classe Laravel — controller, service, job, command
- Operações em detalhe — payloads completos
- Enums
- Erros
- Base de dados
- TLS, IP de origem e cliente HTTP
- 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-domeext-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/referenceidtem 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 activesendCallbackUrl: 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_FRI — fixo 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:
debit()devolvePENDINGe umapprovalid. Grave os ids e pare.- O cliente aprova no telemóvel. Não há sinal síncrono deste passo.
- O agregador faz POST do
debitcompletedrequestpara o seu endpoint. Tem de responder<ResponseCode>SUCCESS</ResponseCode>— um200vazio não é aceite e o callback será reenviado. - 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 comcurl) - A fila está a correr (
php artisan queue:work) — sem ela o job de reconciliação nunca corre -
MKESH_VERIFY_SSL=truecom 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:
- interpreta o corpo — se não der, grava-o na mesma e devolve
400 - bloqueia a linha correspondente com
lockForUpdate - grava o payload cru em
mkesh_responses, mesmo que não tenha correspondência - liquida a transacção e dispara
MkeshTransactionSettleduma única vez - 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
FAILEDque chegue depois de umSUCCESSFULé 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:worka correr, uma transacção cujo callback se perca ficaPENDINGpara 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 aexternaltransactionid/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>
Resposta → SpTransferResponse:
<?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 |
Só 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