PHPackages                             brilliantmind/mkesh - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. brilliantmind/mkesh

ActiveLibrary

brilliantmind/mkesh
===================

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

v0.2.0(today)00MITPHPPHP ^8.1CI failing

Since Jul 23Pushed todayCompare

[ Source](https://github.com/osvaldogeraldo/mkesh)[ Packagist](https://packagist.org/packages/brilliantmind/mkesh)[ Docs](https://github.com/osvaldogeraldo/mkesh)[ RSS](/packages/brilliantmind-mkesh/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (11)Versions (3)Used By (0)

brilliantmind/mkesh
===================

[](#brilliantmindmkesh)

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çãoMétodoFluxoEndpoint (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)*Índice
------

[](#índice)

1. [Requisitos](#1-requisitos)
2. [Instalação](#2-instala%C3%A7%C3%A3o)
3. [Configuração](#3-configura%C3%A7%C3%A3o)
4. [Como funciona o fluxo C2B](#4-como-funciona-o-fluxo-c2b)
5. [Guia rápido Laravel](#5-guia-r%C3%A1pido-laravel) — do zero ao primeiro pagamento
6. [Usar numa classe Laravel](#6-usar-numa-classe-laravel) — controller, service, job, command
7. [Operações em detalhe](#7-opera%C3%A7%C3%B5es-em-detalhe) — payloads completos
8. [Enums](#8-enums)
9. [Erros](#9-erros)
10. [Base de dados](#10-base-de-dados)
11. [TLS, IP de origem e cliente HTTP](#11-tls-ip-de-origem-e-cliente-http)
12. [Testes e resolução de problemas](#12-testes-e-resolu%C3%A7%C3%A3o-de-problemas)

> [`examples/usage.php`](examples/usage.php) é um guia anotado com tudo isto num só ficheiro de código.

---

1. Requisitos
-------------

[](#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
-------------

[](#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-configuração)

### 3.1 Variáveis de ambiente

[](#31-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`](.env.example)):

VariávelOmissãoDescriçã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 `` 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

[](#32-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 `` no payload do débito a não ser que active `sendCallbackUrl: true`.

### 3.3 PHP puro (sem Laravel)

[](#33-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.4 Checklist de onboarding

[](#34-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**:

ValorDirecçãoCorresponde aEndereço IP de origemvocê → provedoro IP de saída que eles autorizamURL de callbackvocê → provedor`MKESH_CALLBACK_URL`Utilizador / senhaprovedor → você`MKESH_USERNAME` / `MKESH_PASSWORD`Nr. de conta / MSISDNprovedor → você`MKESH_SP_FRI` / `MKESH_SP_TRANSFER_FRI`Prefixo de transacçãoprovedor → você`MKESH_TRANSACTION_PREFIX`URL baseprovedor → você`MKESH_BASE_URL`---

4. Como funciona o fluxo C2B
----------------------------

[](#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│                              │
     │◄──────────────────────────────┤                              │
     │     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 `SUCCESS` — 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
----------------------

[](#5-guia-rápido-laravel)

Do zero ao primeiro pagamento em cinco 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](#31-vari%C3%A1veis-de-ambiente).

**Passo 2 — copiar os ficheiros de exemplo**

Os **models já vêm no pacote** — não precisa de copiar nada para os ter:

```
use BrilliantMind\Mkesh\Laravel\Models\MkeshTransaction;
use BrilliantMind\Mkesh\Laravel\Models\MkeshResponse;
```

O resto é código da sua aplicação, e por isso fica em [`examples/Laravel/`](examples/Laravel/) para copiar e adaptar:

Ficheiro de exemploDestino`MkeshPaymentService.php``app/Services/``MkeshCallbackController.php``app/Http/Controllers/``ReconcileMkeshTransaction.php``app/Jobs/`**Passo 3 — registar a rota do callback**

Fora do grupo protegido por CSRF, para o agregador conseguir chegar lá sem token:

```
// routes/api.php
use App\Http\Controllers\MkeshCallbackController;

Route::post('/mkesh/callback', MkeshCallbackController::class);
```

**Passo 4 — dar o URL ao provedor**

`https://a-sua-app.co.mz/api/mkesh/callback`. Eles configuram-no do lado deles. Confirme também que o IP de saída do seu servidor está autorizado.

**Passo 5 — cobrar**

```
$transaccao = app(MkeshPaymentService::class)->charge(
    msisdn:  '258823040400',
    amount:  '25.00',
    payable: $encomenda,
);

$transaccao->status;   // TransactionStatus::PENDING
```

E já está. O serviço despacha sozinho o job de reconciliação: ou chega o callback, ou o job apanha o resultado por polling.

---

6. Usar numa classe Laravel
---------------------------

[](#6-usar-numa-classe-laravel)

### 6.1 Injecção no construtor (recomendado)

[](#61-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

[](#62-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étodoDevolve`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

[](#63-controller-que-inicia-um-pagamento)

```
namespace App\Http\Controllers;

use App\Services\MkeshPaymentService;
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 MkeshPaymentService $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 Controller que recebe o callback

[](#64-controller-que-recebe-o-callback)

O ponto crítico: **o corpo da resposta tem de ser o documento `ResponseCode`.**

```
namespace App\Http\Controllers;

use App\Models\MkeshTransaction;
use BrilliantMind\Mkesh\Callback\CallbackResponse;
use BrilliantMind\Mkesh\Exception\MkeshException;
use BrilliantMind\Mkesh\Laravel\Facades\Mkesh;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;

final class MkeshCallbackController extends Controller
{
    public function __invoke(Request $request): Response
    {
        try {
            $callback = Mkesh::parseDebitCompleted($request->getContent());
        } catch (MkeshException) {
            return response('', 400);   // corpo inválido: não reenviem
        }

        DB::transaction(function () use ($callback): void {
            // Bloqueie a linha: os callbacks podem chegar em duplicado.
            $transaccao = MkeshTransaction::query()
                ->where('type', MkeshTransaction::TYPE_DEBIT)
                ->where('external_transaction_id', $callback->externalTransactionId)
                ->lockForUpdate()
                ->first();

            if ($transaccao === null || $transaccao->isSettled()) {
                return;   // reenvio de algo já liquidado — ignorar
            }

            $transaccao->update([
                'financial_transaction_id' => $callback->transactionId,
                'status' => $callback->status,
                'completed_at' => now(),
            ]);

            if ($callback->isSuccessful()) {
                $transaccao->payable?->marcarComoPaga();
            }
        });

        return response(CallbackResponse::success()->toXml(), 200)
            ->header('Content-Type', CallbackResponse::CONTENT_TYPE);
    }
}
```

Regras de ouro para o webhook:

- Responda sempre `SUCCESS` quando conseguir ler o corpo, mesmo que a transacção já esteja liquidada. Caso contrário ficam a reenviar.
- Torne-o idempotente — procure pelo `externalTransactionId` e ignore se já estiver liquidado.
- Bloqueie a linha (`lockForUpdate`) para dois reenvios simultâneos não liquidarem a mesma transacção duas vezes.
- Não faça trabalho demorado aqui. Despache um job.

### 6.5 Job de reconciliação

[](#65-job-de-reconciliação)

Cobre o ramo "sem resposta do MKESH". Ver [`ReconcileMkeshTransaction`](examples/Laravel/ReconcileMkeshTransaction.php):

```
ReconcileMkeshTransaction::dispatch($transaccao->id)->delay(now()->addMinutes(2));
```

Faz polling ao `gettransactionstatus` com backoff progressivo 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.

### 6.6 Command Artisan para reconciliar em lote

[](#66-command-artisan-para-reconciliar-em-lote)

```
namespace App\Console\Commands;

use App\Jobs\ReconcileMkeshTransaction;
use App\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', '
