techsolutions-projects/mobile-wallet-sdk

A Mozambican mobile wallet (M-Pesa, E-Mola, mKesh) integration SDK for Laravel applications.

Maintainers

Package info

github.com/TECHSOLUTIONS-PROJECTS/mobile-wallet-sdk

Homepage

Issues

pkg:composer/techsolutions-projects/mobile-wallet-sdk

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-04-06 15:22 UTC

This package is auto-updated.

Last update: 2026-05-26 11:55:00 UTC


README

Pacote: techsolutions-projects/mobile-wallet-sdk Namespace: Techsolutions\Sdk\MobileWallets Compatibilidade: PHP 8.2–8.3 · Laravel 11–13 Licença: MIT — Techsolutions Lda

Índice

  1. Introdução
  2. Instalação e Configuração
  3. Arquitectura
  4. Conceitos Principais
  5. Utilização Básica
  6. API Fluente (Transaction Builder)
  7. Trait Billable
  8. Facade
  9. Enumerações
  10. Códigos de Resposta
  11. Sistema de Retry
  12. Eventos
  13. Webhooks
  14. WebSockets / Broadcasting
  15. Dashboard
  16. Drivers
  17. Gateways Externos
  18. Internacionalização
  19. Testes
  20. Análise Estática e Qualidade de Código
  21. Extensão — Criar um Driver Customizado
  22. Referência de Configuração
  23. FAQ

1. Introdução

O Mobile Wallet Gateway é um pacote Laravel que permite integrar múltiplas carteiras móveis moçambicanas através de uma única interface consistente.

O objectivo é simplificar a integração com APIs de pagamento como M-Pesa e E-Mola, garantindo ao mesmo tempo robustez, extensibilidade e qualidade de código.

Porquê este pacote?

Integrar directamente com cada provedor de pagamento móvel em Moçambique apresenta desafios:

  • O M-Pesa (Vodacom) utiliza uma API REST com autenticação RSA.
  • O E-Mola (Movitel) utiliza um WebService SOAP/WSDL — um protocolo completamente diferente.
  • Cada provedor tem os seus próprios códigos de resposta, formatos de payload e fluxos de callback.
  • Não existe padronização entre eles.

Este pacote resolve estes problemas ao fornecer:

  • Uma interface única para todos os provedores.
  • Um sistema de drivers que abstrai as diferenças de protocolo.
  • Tipagem forte (declare(strict_types=1)) em todos os ficheiros.
  • Enumerações para estados, tipos de transacção e códigos de resposta.
  • Retry inteligente que distingue erros transitórios de erros terminais.
  • Internacionalização completa (PT pré-reforma e EN).

Princípios de design

  • Fortemente tipado — Todos os ficheiros utilizam declare(strict_types=1). Parâmetros, retornos e propriedades são tipados.
  • Laravel-native — Service Container, Service Provider, Facades, Jobs, Events, Broadcasting, Queues, Eloquent.
  • SOLID — Cada classe tem uma única responsabilidade. Os drivers são substituíveis. O sistema é extensível sem modificação.
  • Execução lazy — Nenhuma operação é executada até se invocar begin(), c2b(), b2c(), etc.

2. Instalação e Configuração

Requisitos

  • PHP 8.1 ou superior
  • Laravel 10, 11, 12 ou 13
  • Extensão PHP ext-openssl (para M-Pesa)
  • Extensão PHP ext-soap (para E-Mola)

Instalar via Composer

composer require techsolutions-projects/mobile-wallet-sdk

Publicar ficheiros

Publicar tudo de uma vez:

php artisan vendor:publish --tag=mobile-wallet

Ou separadamente:

php artisan vendor:publish --tag=mobile-wallet-config      # config/mobile-wallet.php
php artisan vendor:publish --tag=mobile-wallet-migrations   # database/migrations/
php artisan vendor:publish --tag=mobile-wallet-lang         # lang/vendor/mobile-wallet/

Executar as migrations

php artisan migrate

Isto cria duas tabelas:

Tabela Descrição
mws_wallets Carteiras registadas (M-Pesa, E-Mola)
mws_transactions Transacções com estado, tentativas, resposta do gateway

Registar as carteiras

Antes de processar transacções, é necessário inserir as carteiras na base de dados:

use Techsolutions\Sdk\MobileWallets\Models\Wallet;

Wallet::create([
    'name'                  => 'm-pesa',
    'display_name'          => 'M-Pesa',
    'service_provider_code' => '171717',
    'is_active'             => true,
]);

Wallet::create([
    'name'         => 'e-mola',
    'display_name' => 'E-Mola',
    'is_active'    => true,
]);

Nota: Recomenda-se criar um seeder (MwgWalletSeeder) para automatizar este processo.

Configuração do ambiente (.env)

# Modo sandbox (todas as transacções são simuladas)
MWS_SANDBOX=true

# M-Pesa (REST API — developer.mpesa.vm.co.mz)
MWS_MPESA_BASE_URL=https://api.sandbox.vm.co.mz
MWS_MPESA_API_KEY=a-tua-api-key
MWS_MPESA_PUBLIC_KEY=a-tua-public-key
MWS_MPESA_SERVICE_PROVIDER_CODE=171717
MWS_MPESA_ORIGIN=*

# E-Mola (SOAP/WSDL — credenciais via parceria Movitel)
MWS_EMOLA_WSDL_URL=https://o-teu-wsdl.movitel.co.mz/service?wsdl
MWS_EMOLA_USERNAME=o-teu-username
MWS_EMOLA_PASSWORD=a-tua-password
MWS_EMOLA_API_KEY=a-tua-key
MWS_EMOLA_PARTNER_CODE=o-teu-partner-code
MWS_EMOLA_LANGUAGE=pt

# Fila
MWS_QUEUE_ENABLED=true
MWS_QUEUE_NAME=mws

# Webhook
MWS_WEBHOOK_SECRET=uma-chave-secreta-forte

# Broadcasting (Laravel Echo)
MWS_BROADCASTING_ENABLED=false

# Logging
MWS_LOG_ENABLED=true
MWS_LOG_CHANNEL=mobile-wallet

3. Arquitectura

O pacote segue uma arquitectura baseada em drivers, inspirada no padrão Manager do Laravel (à semelhança do DatabaseManager, MailManager, FilesystemManager).

Diagrama de camadas

┌─────────────────────────────────────────────────────────┐
│  Camada de Acesso                                       │
│  MobileWallet (Facade) · MobileWalletServiceProvider    │
└──────────────────────────┬──────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────┐
│  API Fluente                                            │
│  TransactionBuilder                                     │
│  onWallet() → toSocket() → onSuccess() → c2b()         │
└──────────────────────────┬──────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────┐
│  Gateway Manager (driver resolver)                      │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐ │
│  │ MPesaGateway│  │ EMolaGateway│  │ SandboxGateway  │ │
│  │   (REST)    │  │   (SOAP)    │  │    (mock)       │ │
│  └─────────────┘  └─────────────┘  └─────────────────┘ │
│  ┌─────────────────────────────────────────────────────┐│
│  │ ExternalGateway (REST genérico configurável)        ││
│  │ Presets: EACACIA · e2Payments · custom              ││
│  └─────────────────────────────────────────────────────┘│
└──────────────────────────┬──────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────┐
│  Processamento                                          │
│  ProcessTransactionJob · Retry · Eventos · Broadcasting │
└──────────────────────────┬──────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────┐
│  Camada de Dados                                        │
│  Wallet (mws_wallets) · Transaction (mws_transactions)  │
└─────────────────────────────────────────────────────────┘

Estrutura do pacote

src/
├── Providers/MobileWalletServiceProvider.php
├── Facades/MobileWallet.php
├── Contracts/
│   ├── MobileWalletGateway.php       # Classe abstracta (contracto para drivers)
│   └── Billable.php                  # Interface para modelos pagáveis
├── Traits/MwgBillable.php            # pay(), payWith(), mwsTransactions()
├── Drivers/
│   ├── MPesaGateway.php              # REST/JSON (Vodacom)
│   ├── EMolaGateway.php              # SOAP/WSDL (Movitel)
│   ├── SandboxGateway.php            # Mock para testes
│   └── ExternalGateway.php          # REST genérico para gateways intermediários
├── Models/
│   ├── Wallet.php                    # Scopes: mpesa(), emola()
│   └── Transaction.php              # Ponto de entrada: onWallet()
├── Builders/TransactionBuilder.php   # API fluente
├── Enums/
│   ├── TransactionStatus.php         # pending, processing, succeeded, ...
│   ├── TransactionType.php           # b2b, b2c, c2b, c2c
│   ├── WalletDriver.php             # m-pesa, e-mola, external, sandbox
│   ├── GatewayMode.php              # direct, external
│   ├── ExternalGatewayPreset.php    # eacacia, e2payments, custom
│   └── GatewayResponseCode.php      # INS-0, INS-5, EMOLA-0, EMOLA-07, EMOLA-GW-1000, ...
├── DTOs/GatewayResponse.php          # Resposta imutável do gateway
├── Jobs/ProcessTransactionJob.php    # Processamento + retry
├── Events/                           # Started, Succeeded, Failed, Retried
├── Http/
│   ├── Controllers/
│   │   ├── WebhookController.php
│   │   └── DashboardController.php
│   └── Middleware/VerifyWebhookSignature.php
├── Broadcasting/TransactionChannel.php
├── Support/
│   ├── GatewayManager.php            # Driver resolver
│   └── AmountParser.php              # "250,00MT" → 250.00
└── Exceptions/
    ├── GatewayException.php
    ├── InvalidWalletException.php
    └── TransactionFailedException.php

Fluxo de uma transacção

  1. O programador invoca Transaction::onWallet($wallet)->c2b(...).
  2. O TransactionBuilder acumula configurações (callbacks, canal WebSocket, metadata).
  3. Ao invocar c2b() / b2c() / begin(), o builder cria um registo Transaction na base de dados com estado PENDING.
  4. O builder despacha um ProcessTransactionJob (síncrono ou via fila).
  5. O job resolve o driver correcto via GatewayManager.
  6. O driver chama a API do provedor (HTTP para M-Pesa, SOAP para E-Mola).
  7. Com base na resposta:
    • Sucesso → marca como SUCCEEDED, dispara TransactionSucceeded, executa callback.
    • Falha retentável → agenda retry com backoff exponencial, dispara TransactionRetried.
    • Falha terminal → marca como FAILED, dispara TransactionFailed, executa callback.

4. Conceitos Principais

Wallet

Representa um provedor de pagamento registado na base de dados. Cada carteira tem um name que corresponde a um driver.

use Techsolutions\Sdk\MobileWallets\Models\Wallet;

// Obter a carteira M-Pesa activa
$wallet = Wallet::mpesa()->first();

// Obter a carteira E-Mola activa
$wallet = Wallet::emola()->first();

// Listar todas as carteiras activas
$wallets = Wallet::active()->get();

// Aceder às transacções de uma carteira
$wallet->transactions;

Campos:

Campo Tipo Descrição
name string Chave do driver: m-pesa, e-mola
display_name string|null Nome para exibição: "M-Pesa"
service_provider_code string|null Código do provedor (M-Pesa)
is_active bool Se a carteira está activa
metadata array|null Dados extra em JSON

Transaction

Representa uma transacção de pagamento com o seu estado completo.

use Techsolutions\Sdk\MobileWallets\Models\Transaction;
use Techsolutions\Sdk\MobileWallets\Models\Wallet;

// Obter a carteira M-Pesa activa
$wallet = Wallet::mpesa()->first();

// Criar solicitação de débito na carteira móvel
$tx = Transaction::onWallet($wallet)->c2b('+258840000000', '250.00');

// Consultar
$tx->status;           // TransactionStatus::SUCCEEDED
$tx->transaction_type; // TransactionType::CUSTOMER_TO_BUSINESS
$tx->amount;           // 250.00
$tx->isSucceeded();    // true
$tx->isFailed();       // false
$tx->canRetry();       // false (já não tem tentativas)

// Scopes
Transaction::pending()->get();
Transaction::succeeded()->get();
Transaction::failed()->get();
Transaction::retryable()->get();

// Relações
$tx->wallet;    // Wallet
$tx->billable;  // Invoice, Order, etc. (polimórfico)

Campos principais:

Campo Tipo Descrição
uuid string Identificador único UUID v4
mws_wallet_id int FK para a carteira
billable_type / billable_id string|int Relação polimórfica (o que está a ser pago)
msisdn string Número de telefone do cliente
amount float Valor da transacção
transaction_type TransactionType Tipo: c2b, b2c, b2b, c2c
status TransactionStatus Estado actual
status_code string|null Código de resposta do gateway (INS-0, EMOLA-07, EMOLA-GW-1000, etc.)
max_attempts int Número máximo de tentativas
current_attempt int Tentativa actual
gateway_transaction_id string|null ID da transacção no provedor
gateway_raw_response array|null Resposta bruta do provedor
streaming_channel string|null Canal WebSocket (se configurado)
metadata array|null Dados arbitrários

GatewayResponse

DTO (Data Transfer Object) imutável que encapsula a resposta de um gateway.

use Techsolutions\Sdk\MobileWallets\DTOs\GatewayResponse;

$response->isSuccessful();       // bool
$response->isFailed();           // bool
$response->transactionId;        // "f449abol7j38"
$response->statusCode;           // "INS-0"
$response->statusDescription;    // "Request processed successfully"
$response->rawResponse;          // array completo do provedor

// Resolver para o enum de códigos
$code = $response->resolveCode();  // GatewayResponseCode::M_PESA_SUCCESS
$code->isRetryable();              // false
$code->description();              // "Transacção processada com sucesso."

5. Utilização Básica

Transacção C2B (Cliente para Empresa)

O caso mais comum — cobrar um pagamento ao cliente:

use Techsolutions\Sdk\MobileWallets\Models\{Wallet, Transaction};

$wallet = Wallet::mpesa()->first();

$tx = Transaction::onWallet($wallet)
    ->c2b(
        msisdn: '+258840000000',
        amount: '250.00',
        attempts: 3,
    );

// $tx é uma instância de Transaction já persistida na base de dados.
// O ProcessTransactionJob já foi despachado.

Transacção B2C (Empresa para Cliente)

Desembolsar fundos para um cliente:

$tx = Transaction::onWallet($wallet)
    ->b2c(
        msisdn: '+258840000000',
        amount: 500.00,
        attempts: 2,
    );

Formatos de valor aceites

O AmountParser aceita múltiplos formatos:

->c2b('+258840000000', 250)          // inteiro
->c2b('+258840000000', 250.00)       // float
->c2b('+258840000000', '250.00')     // string EN
->c2b('+258840000000', '250,00')     // string PT
->c2b('+258840000000', '250,00MT')   // com sufixo de moeda
->c2b('+258840000000', '1.250,50 MZN') // milhar PT com moeda

6. API Fluente (Transaction Builder)

O TransactionBuilder é o coração da API fluente. Todos os métodos de configuração são opcionais e encadeáveis. A transacção só é criada quando se invoca um método terminal (begin(), c2b(), b2c(), etc.).

Exemplo completo

use Techsolutions\Sdk\MobileWallets\Models\{Wallet, Transaction};
use Techsolutions\Sdk\MobileWallets\DTOs\GatewayResponse;
use Techsolutions\Sdk\MobileWallets\Enums\GatewayResponseCode;

$wallet = Wallet::mpesa()->firstOrFail();

$tx = Transaction::onWallet($wallet)

    // Opcional: canal WebSocket para notificações em tempo real
    ->toSocket('payments_channel_' . $userId)

    // Opcional: callback executado quando a transacção é bem-sucedida
    ->onSuccess(function (Transaction $tx, GatewayResponse $resp): void {
        $tx->billable?->update(['paid' => true]);
        // Enviar SMS de confirmação, gerar recibo, etc.
    })

    // Opcional: callback executado quando a transacção falha definitivamente
    ->onFail(function (Transaction $tx, GatewayResponse $resp): void {
        $code = $resp->resolveCode();
        if ($code === GatewayResponseCode::M_PESA_INSUFFICIENT_BALANCE) {
            // Notificar o cliente que não tem saldo
        }
    })

    // Opcional: lógica customizada para decidir se re-tenta
    ->shouldRetry(function (Transaction $tx, GatewayResponse $resp): ?bool {
        $code = $resp->resolveCode();

        // Se o código é terminal, não re-tentar
        if ($code?->isTerminal()) {
            return false;
        }

        // null = usar lógica padrão do pacote
        return null;
    })

    // Opcional: metadata arbitrária
    ->withMetadata([
        'order_id' => 42,
        'source'   => 'api',
    ])

    // Método terminal: cria a transacção e despacha o job
    ->c2b(
        msisdn: '+258840000000',
        amount: '250,00MT',
        attempts: 3,
    );

Métodos de configuração

Método Descrição Obrigatório
forBillable(Billable $model) Associa um modelo pagável (Invoice, Order) Não
toSocket(string $channel) Activa broadcasting num canal WebSocket Não
onSuccess(Closure $fn) Callback para sucesso Não
onFail(Closure $fn) Callback para falha final Não
shouldRetry(Closure $fn) Override da lógica de retry Não
withMetadata(array $data) Anexa dados arbitrários Não

Métodos terminais (executam a transacção)

Método Tipo Descrição
c2b(msisdn, amount, attempts) Customer → Business Cobrança ao cliente
b2c(msisdn, amount, attempts) Business → Customer Desembolso
b2b(msisdn, amount, attempts) Business → Business Transferência entre empresas
c2c(msisdn, amount, attempts) Customer → Customer Transferência entre clientes
customerToBusiness(...) Alias de c2b()
businessToCustomer(...) Alias de b2c()
businessToBusiness(...) Alias de b2b()
customerToCustomer(...) Alias de c2c()
begin(msisdn, amount, attempts, type) Genérico Especificar o tipo como parâmetro

7. Trait Billable

O trait MwgBillable permite que qualquer modelo Eloquent se torne "pagável" — criando uma ligação directa entre o que está a ser pago e a transacção.

Implementar no modelo

O modelo deve implementar a interface Billable e usar o trait MwgBillable:

use Illuminate\Database\Eloquent\Model;
use Techsolutions\Sdk\MobileWallets\Contracts\Billable;
use Techsolutions\Sdk\MobileWallets\Traits\MwgBillable;

class Invoice extends Model implements Billable
{
    use MwgBillable;

    public function getMwgAmount(): float
    {
        return (float) $this->total;
    }

    public function getMwgDescription(): string
    {
        return "Factura #{$this->number}";
    }

    public function getMwgReference(): string
    {
        return $this->uuid;
    }

    public function getMwgMsisdn(): ?string
    {
        // Retorna null se o MSISDN deve ser fornecido no momento do pagamento
        return $this->customer_phone;
    }
}

Interface Billable

Método Retorno Descrição
getMwgAmount() float Valor a cobrar
getMwgDescription() string Descrição legível
getMwgReference() string Referência única
getMwgMsisdn() ?string Número do cliente (pode ser null)

Métodos disponibilizados pelo trait

Pagamento directo

// Com instância da Wallet
$invoice->pay($wallet, '+258840000000');
$invoice->pay($wallet, '+258840000000', TransactionType::BUSINESS_TO_CUSTOMER);
$invoice->pay($wallet, '+258840000000', TransactionType::CUSTOMER_TO_BUSINESS, attempts: 3);

// Por nome da carteira (resolve automaticamente)
$invoice->payWith('m-pesa', '+258840000000');

// Atalhos por tipo
$invoice->payC2B('m-pesa', '+258840000000');
$invoice->payB2C('e-mola', '+258860000000');

Builder fluente com billable

$invoice->mwsPayment($wallet)
    ->toSocket('channel_123')
    ->onSuccess(fn($tx, $r) => $invoice->update(['paid' => true]))
    ->c2b(msisdn: '+258840000000', amount: 250.00, attempts: 3);

// Ou por nome
$invoice->mwsPaymentWith('m-pesa')
    ->onFail(fn($tx, $r) => Log::error('Pagamento falhou'))
    ->c2b(msisdn: '+258840000000', amount: 250.00);

Consultas

$invoice->isPaid();                // bool — tem pelo menos uma transacção com sucesso?
$invoice->hasPaymentInProgress();  // bool — tem transacção pendente/em processamento?
$invoice->totalPaid();             // float — soma de todas as transacções com sucesso
$invoice->mwsTransactions;         // Collection de Transaction
$invoice->latestMwgTransaction();  // Transaction|null

Como funciona a ligação polimórfica

Quando se utiliza o trait, as colunas billable_type e billable_id na tabela mws_transactions são preenchidas automaticamente:

billable_type = "App\Models\Invoice"
billable_id   = 42

Isto permite:

// Da transacção para o modelo
$tx->billable; // → Invoice #42

// Do modelo para as transacções
$invoice->mwsTransactions; // → Collection de Transaction

8. Facade

A facade MobileWallet fornece um ponto de acesso estático ao GatewayManager:

use Techsolutions\Sdk\MobileWallets\Facades\MobileWallet;

// Atalho: resolve a wallet e retorna um TransactionBuilder
MobileWallet::wallet('m-pesa')
    ->c2b(msisdn: '+258840000000', amount: 500.00);

MobileWallet::wallet('e-mola')
    ->b2c(msisdn: '+258860000000', amount: 100.00);

// Acesso directo ao driver (sem builder)
$driver = MobileWallet::resolve('m-pesa');
$response = $driver->initiateTransaction(
    msisdn: '+258840000000',
    amount: 250.00,
    reference: 'REF-001',
    type: TransactionType::CUSTOMER_TO_BUSINESS,
);

A facade está registada como alias MWS no composer.json:

// Ambos funcionam:
MobileWallet::wallet('m-pesa');
MWS::wallet('m-pesa');

9. Enumerações

Todos os valores discretos do pacote são representados por enumerações PHP 8.1, garantindo segurança de tipo em tempo de compilação.

TransactionStatus

Estados possíveis de uma transacção:

use Techsolutions\Sdk\MobileWallets\Enums\TransactionStatus;

TransactionStatus::PENDING      // 'pending'    — criada, ainda não processada
TransactionStatus::PROCESSING   // 'processing' — em processamento (tentativa em curso)
TransactionStatus::SUCCEEDED    // 'succeeded'  — concluída com sucesso
TransactionStatus::FAILED       // 'failed'     — falhou (tentativas esgotadas)
TransactionStatus::CANCELLED    // 'cancelled'  — cancelada manualmente
TransactionStatus::REFUNDED     // 'refunded'   — reembolsada

$status->label();       // "Pendente" (traduzido)
$status->isTerminal();  // true para succeeded, failed, cancelled, refunded
$status->isInProgress();// true para pending, processing

TransactionType

Tipos de transacção suportados:

use Techsolutions\Sdk\MobileWallets\Enums\TransactionType;

TransactionType::CUSTOMER_TO_BUSINESS  // 'c2b'
TransactionType::BUSINESS_TO_CUSTOMER  // 'b2c'
TransactionType::BUSINESS_TO_BUSINESS  // 'b2b'
TransactionType::CUSTOMER_TO_CUSTOMER  // 'c2c'

$type->label(); // "Cliente para Empresa" (traduzido)

WalletDriver

Drivers disponíveis com resolução de aliases:

use Techsolutions\Sdk\MobileWallets\Enums\WalletDriver;

WalletDriver::M_PESA   // 'm-pesa'
WalletDriver::E_MOLA   // 'e-mola'
WalletDriver::SANDBOX  // 'sandbox'

// Resolve aliases
WalletDriver::resolve('mpesa');   // WalletDriver::M_PESA
WalletDriver::resolve('m-peza');  // WalletDriver::M_PESA
WalletDriver::resolve('emola');   // WalletDriver::E_MOLA

GatewayResponseCode

Detalhado na secção seguinte.

10. Códigos de Resposta

O enum GatewayResponseCode unifica os códigos de resposta de todos os provedores num único enum. Cada caso é prefixado com M_PESA_* ou E_MOLA_*.

Códigos M-Pesa

Caso do Enum Valor Descrição Retentável?
M_PESA_SUCCESS INS-0 Sucesso
M_PESA_INTERNAL_ERROR INS-1 Erro interno
M_PESA_INVALID_API_KEY INS-2 Chave API inválida
M_PESA_INSUFFICIENT_BALANCE INS-5 Saldo insuficiente
M_PESA_TRANSACTION_CANCELLED INS-6 Cancelada pelo cliente
M_PESA_TRANSACTION_FAILED INS-9 Falha no processamento
M_PESA_UNRESOLVED_ACCOUNT INS-10 MSISDN não registado
M_PESA_INVALID_SHORTCODE INS-12 Shortcode inválido
M_PESA_INVALID_REFERENCE INS-13 Referência inválida
M_PESA_INVALID_AMOUNT INS-14 Valor inválido
M_PESA_DUPLICATE_TRANSACTION INS-15 Transacção duplicada
M_PESA_INVALID_MSISDN INS-16 Número inválido
M_PESA_TRANSACTION_TIMEOUT INS-22 Timeout
M_PESA_LIMIT_EXCEEDED INS-23 Limite excedido
M_PESA_CUSTOMER_NOT_ACTIVE INS-26 Conta não activa
M_PESA_THROTTLE_LIMIT INS-998 Rate limit
M_PESA_SERVER_ERROR INS-999 Erro do servidor

Códigos E-Mola

Conforme a MOVITEL USSD PUSH API SPECIFICATION v1.5, a E-Mola devolve dois níveis de código de resposta: o nível transacção (<errorCode>, prefixo EMOLA-) e o nível gateway BCCS (<error>, prefixo EMOLA-GW-). Abaixo um subconjunto dos mais relevantes — a tabela completa está em src/Enums/GatewayResponseCode.php.

Nível transacção:

Caso do Enum Valor Descrição Retentável?
E_MOLA_SUCCESS EMOLA-0 Sucesso
E_MOLA_TRANSACTION_FAILED EMOLA-03 Transacção falhou
E_MOLA_INVALID_PARTNER EMOLA-05 Código de parceiro inválido
E_MOLA_INVALID_AMOUNT EMOLA-06 Valor inválido
E_MOLA_INVALID_MSISDN EMOLA-07 MSISDN inválido
E_MOLA_PIN_NOT_ENTERED EMOLA-11 Cliente não introduziu PIN (timeout USSD)
E_MOLA_NO_EMOLA_ACCOUNT EMOLA-12 Cliente sem conta E-Mola
E_MOLA_DUPLICATE_TRANSID EMOLA-14 transId já existente
E_MOLA_ISDN_IN_OTHER_PROCESS EMOLA-20 Número noutro processo
E_MOLA_ALREADY_PROCESSED EMOLA-28 Transacção já processada
E_MOLA_PROCESSING_ERROR EMOLA-99 Erro genérico

Nível gateway (BCCS):

Caso do Enum Valor Descrição Retentável?
E_MOLA_GW_USERNAME_INVALID EMOLA-GW-1000 Username inválido
E_MOLA_GW_TIMEOUT EMOLA-GW-2007 Timeout ao chamar a API
E_MOLA_GW_SYSTEM_OVERLOADED EMOLA-GW-6000 Sistema sobrecarregado
E_MOLA_GW_LOGIN_FAILED EMOLA-GW-6003 Falha de autenticação (credenciais/IP)
E_MOLA_GW_IP_INCORRECT EMOLA-GW-8000 IP do cliente incorrecto

Transporte (gerados pelo driver):

Caso do Enum Valor Descrição Retentável?
E_MOLA_SOAP_FAULT EMOLA-SOAP Erro SOAP
E_MOLA_CONNECTION_ERROR EMOLA-CONN Erro de ligação

Utilização

use Techsolutions\Sdk\MobileWallets\Enums\GatewayResponseCode;

// Resolver a partir de um código M-Pesa bruto
$code = GatewayResponseCode::fromMPesaCode('INS-5');
// → GatewayResponseCode::M_PESA_INSUFFICIENT_BALANCE

// Resolver a partir de um código E-Mola bruto de transacção (com ou sem zero à esquerda)
$code = GatewayResponseCode::fromEMolaCode('07');
// → GatewayResponseCode::E_MOLA_INVALID_MSISDN
GatewayResponseCode::fromEMolaCode('7'); // mesmo resultado

// Resolver um código de NÍVEL GATEWAY (tag <error> do BCCS)
$gw = GatewayResponseCode::fromEMolaGatewayCode('1000');
// → GatewayResponseCode::E_MOLA_GW_USERNAME_INVALID

// Propriedades
$code->isSuccess();     // false
$code->isTerminal();    // true  (MSISDN inválido — não retentar)
$code->isRetryable();   // false
$code->isEMola();       // true
$code->description();   // "Número de telefone (MSISDN) inválido." (traduzido)

// A partir do GatewayResponse
$response->resolveCode(); // GatewayResponseCode|null

11. Sistema de Retry

O pacote inclui retry automático com backoff exponencial e decisão inteligente baseada nos códigos de resposta.

Configuração

// config/mobile-wallet.php
'retry' => [
    'max_attempts'       => 3,    // número máximo de tentativas
    'backoff_base'       => 2,    // segundos (base)
    'backoff_multiplier' => 2,    // factor exponencial
],
// Tentativa 1: delay 0 (imediata)
// Tentativa 2: delay 2s  (2 × 2^0)
// Tentativa 3: delay 4s  (2 × 2^1)
// Tentativa 4: delay 8s  (2 × 2^2)

Comportamento padrão

  1. Se a resposta é um sucesso → para. Marca como SUCCEEDED.
  2. Se a resposta é uma falha terminal (ex.: INS-5 saldo insuficiente) → para imediatamente. Marca como FAILED. Não há razão para re-tentar.
  3. Se a resposta é uma falha retentável (ex.: INS-22 timeout) e current_attempt < max_attempts → agenda nova tentativa com backoff exponencial.
  4. Se as tentativas se esgotam → marca como FAILED.

Override via shouldRetry()

Transaction::onWallet($wallet)
    ->shouldRetry(function (Transaction $tx, GatewayResponse $resp): ?bool {
        $code = $resp->resolveCode();

        // Forçar paragem imediata para saldo insuficiente
        if ($code === GatewayResponseCode::M_PESA_INSUFFICIENT_BALANCE) {
            return false;
        }

        // Forçar retry mesmo para códigos que normalmente seriam terminais
        if ($code === GatewayResponseCode::M_PESA_TRANSACTION_FAILED) {
            return true;
        }

        // null = usar lógica padrão do pacote
        return null;
    })
    ->c2b('+258840000000', '250.00', 5);

12. Eventos

O pacote dispara eventos Laravel em cada ponto do ciclo de vida da transacção:

Evento Quando é disparado Propriedades
TransactionStarted Na primeira tentativa $transaction
TransactionSucceeded Quando a transacção é bem-sucedida $transaction, $response
TransactionFailed Quando a transacção falha definitivamente $transaction, $response
TransactionRetried Quando uma nova tentativa é agendada $transaction, $lastResponse, $attemptNumber

Registar listeners

// app/Providers/EventServiceProvider.php
use Techsolutions\Sdk\MobileWallets\Events\TransactionSucceeded;
use Techsolutions\Sdk\MobileWallets\Events\TransactionFailed;

protected $listen = [
    TransactionSucceeded::class => [
        \App\Listeners\SendPaymentConfirmation::class,
        \App\Listeners\GenerateReceipt::class,
    ],
    TransactionFailed::class => [
        \App\Listeners\NotifyPaymentFailure::class,
    ],
];

Exemplo de listener

namespace App\Listeners;

use Techsolutions\Sdk\MobileWallets\Events\TransactionSucceeded;

class SendPaymentConfirmation
{
    public function handle(TransactionSucceeded $event): void
    {
        $tx = $event->transaction;

        // Enviar notificação ao cliente
        Notification::send(
            $tx->billable?->customer,
            new PaymentConfirmedNotification($tx),
        );
    }
}

13. Webhooks

Os provedores de pagamento enviam notificações assíncronas (callbacks) quando o estado de uma transacção muda. O pacote fornece um endpoint pronto a usar.

Endpoints

URL Provedor
POST /mws/webhook/m-pesa M-Pesa (Vodacom)
POST /mws/webhook/e-mola E-Mola (Movitel)

O prefixo pode ser alterado em config/mobile-wallet.php:

'webhook' => [
    'prefix' => 'mws/webhook',  // alterável
],

Segurança

Quando MWS_WEBHOOK_SECRET está definido, o middleware VerifyWebhookSignature valida:

  1. A presença dos cabeçalhos X-MWS-Signature e X-MWS-Timestamp.
  2. A tolerância temporal (por defeito 5 minutos).
  3. A assinatura HMAC-SHA256 do payload.
Assinatura = HMAC-SHA256(timestamp + "." + body, secret)

Configurar no provedor

Ao configurar webhooks no portal M-Pesa ou no acordo E-Mola, forneça:

https://o-teu-dominio.co.mz/mws/webhook/m-pesa
https://o-teu-dominio.co.mz/mws/webhook/e-mola

14. WebSockets / Broadcasting

O pacote suporta notificações em tempo real via Laravel Broadcasting (Echo + Pusher/Soketi/Ably).

Activar

MWS_BROADCASTING_ENABLED=true

Utilizar

Transaction::onWallet($wallet)
    ->toSocket('payments_user_' . $userId)
    ->c2b('+258840000000', '250.00');

Ouvir no frontend (Laravel Echo)

Echo.channel('payments_user_42')
    .listen('.mws.transaction.succeeded', (e) => {
        console.log('Pagamento confirmado!', e.transaction_uuid);
        showSuccessToast(e.response);
    })
    .listen('.mws.transaction.failed', (e) => {
        console.log('Pagamento falhou', e.response);
        showErrorToast(e.response);
    })
    .listen('.mws.transaction.retrying', (e) => {
        console.log('A re-tentar...', e.response);
    });

Payload do broadcast

{
    "event": "succeeded",
    "transaction_uuid": "a1b2c3d4-...",
    "status": "succeeded",
    "response": {
        "success": true,
        "transaction_id": "f449abol7j38",
        "status_code": "INS-0"
    },
    "timestamp": "2025-06-15T10:30:00+02:00"
}

15. Dashboard

O pacote inclui endpoints JSON para um painel de controlo simples.

Activar

MWS_DASHBOARD_ENABLED=true

Endpoints

Método URL Descrição
GET /mws/dashboard Resumo: carteiras, estatísticas, transacções recentes
GET /mws/dashboard/transaction/{uuid} Detalhe de uma transacção

O middleware por defeito é ['web', 'auth'] — configurável em config/mobile-wallet.php.

16. Drivers

M-Pesa (Vodacom Moçambique)

Propriedade Valor
Protocolo REST/JSON
Autenticação Bearer token gerado por encriptação RSA
Tipos suportados C2B, B2C, B2B, C2C
Portal de developers developer.mpesa.vm.co.mz

Endpoints utilizados:

Tipo URL
C2B /ipg/v1x/c2bPayment/singleStage/
B2C /ipg/v1x/b2cPayment/
B2B /ipg/v1x/b2bPayment/
C2C /ipg/v1x/c2cPayment/
Query /ipg/v1x/queryTransactionStatus/

E-Mola (Movitel Moçambique)

Propriedade Valor
Protocolo SOAP — Viettel BCCS Gateway
Endpoint https://<ip>:<porto>/BCCSGateway/BCCSGateway?wsdl
Autenticação username/password dentro do <Input> + params key e partnerCode (NÃO em SoapHeader)
Tipos suportados C2B, B2C (depende do acordo)
Especificação MOVITEL USSD PUSH API SPECIFICATION v1.5
Documentação Não pública — obtida via acordo de parceria com a Movitel

Operações SOAP — uma única operação gwOperation cujo wscode determina a acção:

wscode Tipo
pushUssdMessage C2B (cobrança ao cliente)
pushUssdDisbursementB2C B2C (desembolso)
pushUssdQueryTrans Consulta de estado
queryAccountBalance Saldo do parceiro
queryBeneficiaryName Nome do beneficiário

A resposta tem dois níveis: a tag <error> no <Result> reflecte o estado do gateway BCCS (→ EMOLA-GW-*) e o <errorCode> real da transacção fica dentro do CDATA <original> (→ EMOLA-*). O driver faz o parsing dos dois automaticamente.

Métodos adicionais expostos por EMolaGateway:

$gw = app(GatewayManager::class)->resolveForWallet('e-mola');
$gw->queryAccountBalance();                  // saldo do parceiro
$gw->queryBeneficiaryName('+258843000000');  // nome mascarado do beneficiário

Nota importante: A E-Mola não disponibiliza uma API pública para developers. As credenciais (WSDL URL, username, password, key, partner code) são obtidas exclusivamente através de acordo comercial directo com a Movitel. A extensão PHP ext-soap é necessária para usar este driver directamente.

Sandbox

Driver de simulação para desenvolvimento e testes. Não faz chamadas HTTP/SOAP reais.

use Techsolutions\Sdk\MobileWallets\Drivers\SandboxGateway;

// Simular falhas temporárias (as próximas 2 chamadas falham)
SandboxGateway::failNextCalls(2);

// Simular falha permanente
SandboxGateway::$shouldFail = true;

// Resetar
SandboxGateway::reset();

// Verificar estado
SandboxGateway::$callCount;     // número de chamadas feitas
SandboxGateway::$lastPayload;   // último payload enviado

17. Gateways Externos

O pacote suporta integração com plataformas intermediárias que já implementaram a comunicação directa com os provedores. Em vez de cada developer obter credenciais directas da Vodacom ou fazer acordo de parceria com a Movitel, pode utilizar um intermediário que abstrai tudo numa API REST simples.

Conceito

┌──────────────┐                  ┌──────────────────┐                  ┌──────────┐
│  A tua App   │ ── REST JSON ──▶ │  Gateway Externo  │ ── API nativa ─▶ │ M-Pesa   │
│  (Laravel)   │                  │  (EACACIA, etc.)  │                  │ E-Mola   │
└──────────────┘                  └──────────────────┘                  └──────────┘

O código da tua aplicação não muda — a mesma API fluente funciona independentemente de estar a usar a API directa ou um intermediário. A diferença está apenas na configuração.

Presets disponíveis

Preset Plataforma Descrição
eacacia EACACIA / Vitae ERP Gateway REST unificado para M-Pesa, E-Mola e M-Kesh
e2payments e2Payments / Explicador Gateway REST com OAuth2 para E-Mola
custom Qualquer Configuração manual completa de endpoints e mapeamento

Activar para uma wallet

Para usar a EACACIA em vez da API directa do M-Pesa, basta alterar a configuração:

// config/mobile-wallet.php
'wallets' => [
    'm-pesa' => [
        'driver'   => 'external',       // ← muda de 'm-pesa' para 'external'
        'gateway'  => 'eacacia',         // preset pré-configurado
        'base_url' => env('MWS_EACACIA_BASE_URL', 'https://testewallet.vitae-erp.co.mz'),
        'api_key'  => env('MWS_EACACIA_API_KEY', ''),
        'timeout'  => 30,
    ],
],

O código da aplicação continua exactamente igual:

// Isto funciona tanto com driver directo como externo
Transaction::onWallet(Wallet::mpesa()->first())
    ->c2b(msisdn: '+258852444364', amount: 100.00);

$invoice->payWith('m-pesa', '+258852444364');

MobileWallet::wallet('m-pesa')->c2b('+258852444364', 100.00);

Configuração por preset

EACACIA (Vitae ERP)

'm-pesa' => [
    'driver'   => 'external',
    'gateway'  => 'eacacia',
    'base_url' => env('MWS_EACACIA_BASE_URL', 'https://testewallet.vitae-erp.co.mz'),
    'api_key'  => env('MWS_EACACIA_API_KEY', ''),
],

'e-mola' => [
    'driver'   => 'external',
    'gateway'  => 'eacacia',
    'base_url' => env('MWS_EACACIA_BASE_URL', 'https://testewallet.vitae-erp.co.mz'),
    'api_key'  => env('MWS_EACACIA_API_KEY', ''),
],

O preset eacacia já sabe:

  • Que o endpoint C2B para M-Pesa é /api/eacacia/mpesa/c2b
  • Que o endpoint C2B para E-Mola é /api/eacacia/emola/c2b
  • Que o campo msisdn deve ser enviado como phone_number
  • Que o sucesso é response_code === 'success'
  • Que o ID da transacção está em details.reference

e2Payments (Explicador)

'e-mola' => [
    'driver'    => 'external',
    'gateway'   => 'e2payments',
    'base_url'  => 'https://e2payments.explicador.co.mz',
    'api_key'   => env('MWS_E2PAYMENTS_TOKEN', ''),
    'wallet_id' => env('MWS_E2PAYMENTS_WALLET_ID', ''),
],

Gateway Customizado

Para qualquer API REST que não tenha um preset:

'm-pesa' => [
    'driver'   => 'external',
    'gateway'  => 'custom',
    'base_url' => 'https://outro-gateway.co.mz',
    'api_key'  => env('CUSTOM_API_KEY', ''),

    // Definir os endpoints para cada tipo de transacção
    'endpoints' => [
        'c2b' => '/api/v1/mpesa/c2b',
        'b2c' => '/api/v1/mpesa/b2c',
        'query' => '/api/v1/transaction/{reference}',
    ],

    // Mapear os nossos campos para os campos da API externa
    'payload_map' => [
        'msisdn'    => 'phone_number',    // o nosso 'msisdn' → o 'phone_number' deles
        'amount'    => 'amount',
        'reference' => 'transaction_ref',
    ],

    // Mapear a resposta da API externa para os nossos campos
    // Suporta dot notation para campos aninhados (ex.: 'data.txn_id')
    'response_map' => [
        'success_field'  => 'status',           // campo que indica sucesso
        'success_value'  => 'completed',        // valor que significa sucesso
        'transaction_id' => 'data.txn_id',      // onde está o ID da transacção
        'reference'      => 'data.reference',   // onde está a referência
    ],

    // Campos extras enviados em todos os pedidos
    'extra_payload' => [
        'client_id' => env('CUSTOM_CLIENT_ID', ''),
        'channel'   => 'api',
    ],

    // Tipo de autenticação: 'bearer', 'basic', 'header'
    'auth_type'        => 'bearer',
    'auth_header_name' => 'X-Api-Key',   // usado quando auth_type = 'header'
],

Códigos de resposta de gateways externos

Código Constante Retentável?
EXTERNAL-SUCCESS EXTERNAL_SUCCESS
EXTERNAL-FAIL EXTERNAL_FAIL
EXTERNAL-TIMEOUT EXTERNAL_TIMEOUT
EXTERNAL-AUTH EXTERNAL_AUTH_ERROR
EXTERNAL-PAYLOAD EXTERNAL_INVALID_PAYLOAD
EXTERNAL-CONN EXTERNAL_CONNECTION_ERROR

Misto: wallet directa + wallet externa

Pode-se ter uma wallet com driver directo e outra com externo no mesmo projecto:

'wallets' => [
    'm-pesa' => [
        'driver' => 'm-pesa',     // ligação directa à Vodacom
        // ...credenciais directas...
    ],
    'e-mola' => [
        'driver'  => 'external',  // via EACACIA (sem precisar de SOAP)
        'gateway' => 'eacacia',
        'base_url' => 'https://testewallet.vitae-erp.co.mz',
    ],
],

18. Internacionalização

O pacote inclui ficheiros de tradução em Português Europeu (pré-reforma) e Inglês.

Ficheiros de língua

Ficheiro Conteúdo
messages.php Mensagens do sistema (transacção iniciada, falhou, etc.)
statuses.php Rótulos dos estados de transacção
types.php Rótulos dos tipos de transacção
response_codes.php Descrições dos códigos de resposta
errors.php Mensagens de erro

Utilização automática

Os enums utilizam o sistema de tradução automaticamente:

$status = TransactionStatus::PENDING;
$status->label(); // "Pendente" (pt) / "Pending" (en)

$type = TransactionType::CUSTOMER_TO_BUSINESS;
$type->label(); // "Cliente para Empresa" (pt) / "Customer to Business" (en)

$code = GatewayResponseCode::M_PESA_INSUFFICIENT_BALANCE;
$code->description(); // "Saldo insuficiente na conta do cliente." (pt)

Publicar para customização

php artisan vendor:publish --tag=mobile-wallet-lang

Os ficheiros são copiados para lang/vendor/mobile-wallet/.

19. Testes

O pacote inclui testes unitários e de integração utilizando o Orchestra Testbench.

Executar

composer test
# ou
./vendor/bin/phpunit

Cobertura

Suite Ficheiro Cobertura
Unit CoreTest.php AmountParser, GatewayResponseCode, SandboxGateway, TransactionBuilder, Wallet scopes
Feature FlowTest.php Fluxo completo de transacção, retry, callbacks, facade, webhooks, trait Billable

Testar nos teus projectos

O SandboxGateway é activado automaticamente quando MWS_SANDBOX=true. Nos teus testes:

use Techsolutions\Sdk\MobileWallets\Drivers\SandboxGateway;

protected function setUp(): void
{
    parent::setUp();
    config(['mobile-wallet.sandbox' => true]);
    config(['mobile-wallet.queue.enabled' => false]); // síncrono nos testes
    SandboxGateway::reset();
}

public function test_payment_flow(): void
{
    $invoice = Invoice::factory()->create(['total' => 500]);
    $tx = $invoice->payWith('m-pesa', '+258840000000');

    $this->assertTrue($tx->fresh()->isSucceeded());
    $this->assertTrue($invoice->fresh()->isPaid());
}

public function test_payment_failure_with_retry(): void
{
    SandboxGateway::failNextCalls(2);

    $tx = Transaction::onWallet(Wallet::mpesa()->first())
        ->c2b('+258840000000', '100.00', 3);

    $tx->refresh();
    $this->assertTrue($tx->isSucceeded());
    $this->assertEquals(3, $tx->current_attempt);
}

20. Análise Estática e Qualidade de Código

O pacote inclui configuração para Larastan (PHPStan para Laravel) e Rector.

Larastan (PHPStan nível 8)

composer analyse

Configuração em phpstan.neon — nível 8 (o mais rigoroso).

Rector

composer refactor

Configuração em rector.php — inclui sets de qualidade de código, tipos, early return e dead code.

21. Extensão — Criar um Driver Customizado

Para adicionar suporte a um novo provedor (ex.: mKesh, se a API for disponibilizada no futuro):

1. Criar o driver

<?php

declare(strict_types=1);

namespace App\MobileWallet\Drivers;

use Techsolutions\Sdk\MobileWallets\Contracts\MobileWalletGateway;
use Techsolutions\Sdk\MobileWallets\DTOs\GatewayResponse;
use Techsolutions\Sdk\MobileWallets\Enums\TransactionType;

final class MKeshGateway extends MobileWalletGateway
{
    protected function getBaseUrl(): string { /* ... */ }
    protected function getTransactionUrl(TransactionType $type): string { /* ... */ }
    protected function buildHeaders(): array { /* ... */ }
    protected function buildPayload(string $msisdn, float $amount, string $reference, TransactionType $type, array $extra = []): array { /* ... */ }
    protected function parseResponse(array $body, int $httpStatus): GatewayResponse { /* ... */ }
    protected function getQueryUrl(string $reference): string { /* ... */ }
    protected function parseQueryResponse(array $body, int $httpStatus): GatewayResponse { /* ... */ }
}

2. Registar no GatewayManager

// app/Providers/AppServiceProvider.php
use Techsolutions\Sdk\MobileWallets\Support\GatewayManager;

public function boot(): void
{
    $this->app->make(GatewayManager::class)->extend('m-kesh', function ($app) {
        return new \App\MobileWallet\Drivers\MKeshGateway(
            config('mobile-wallet.wallets.m-kesh', [])
        );
    });
}

3. Adicionar configuração

// config/mobile-wallet.php → wallets
'm-kesh' => [
    'driver'   => 'm-kesh',
    'base_url' => env('MWS_MKESH_BASE_URL'),
    // ...
],

4. Registar a wallet na base de dados

Wallet::create(['name' => 'm-kesh', 'display_name' => 'M-Kesh', 'is_active' => true]);

22. Referência de Configuração

Todas as opções disponíveis em config/mobile-wallet.php:

Chave Tipo Default Descrição
default string 'm-pesa' Carteira padrão
sandbox bool true Modo sandbox (todas as transacções são simuladas)
wallets.m-pesa.base_url string sandbox URL URL base da API M-Pesa
wallets.m-pesa.api_key string '' Chave API M-Pesa
wallets.m-pesa.public_key string '' Chave pública RSA M-Pesa
wallets.m-pesa.service_provider_code string '' Código do provedor M-Pesa
wallets.m-pesa.origin string '*' Cabeçalho Origin
wallets.m-pesa.timeout int 30 Timeout HTTP em segundos
wallets.e-mola.wsdl_url string '' URL do WSDL E-Mola
wallets.e-mola.username string '' Username SOAP E-Mola
wallets.e-mola.password string '' Password SOAP E-Mola
wallets.e-mola.api_key string '' Chave API E-Mola
wallets.e-mola.partner_code string '' Código de parceiro E-Mola
wallets.e-mola.language string 'pt' Língua das respostas (pt|en)
wallets.e-mola.ssl_verify_off bool false Desactivar verificação SSL
retry.max_attempts int 3 Tentativas máximas
retry.backoff_base int 2 Base do backoff (segundos)
retry.backoff_multiplier int 2 Multiplicador exponencial
queue.enabled bool true Processar via fila
queue.connection string|null null Ligação da fila
queue.queue string 'mws' Nome da fila
webhook.prefix string 'mws/webhook' Prefixo das rotas de webhook
webhook.signing_secret string '' Segredo HMAC (vazio = sem verificação)
webhook.tolerance int 300 Tolerância temporal em segundos
broadcasting.enabled bool false Activar broadcasting WebSocket
logging.enabled bool true Activar logging
logging.channel string 'mobile-wallet' Canal de log
dashboard.enabled bool false Activar endpoints do dashboard
dashboard.prefix string 'mws/dashboard' Prefixo das rotas do dashboard
dashboard.middleware array ['web', 'auth'] Middleware do dashboard

23. FAQ

P: Posso usar sem filas (synchronous)?

Sim. Defina MWS_QUEUE_ENABLED=false no .env. O ProcessTransactionJob será executado sincronicamente via dispatch_sync().

P: Os callbacks onSuccess/onFail funcionam com filas?

Sim. Os closures são serializados usando Illuminate\Queue\SerializableClosure. No entanto, tome cuidado para não capturar objectos complexos ou não serializáveis na closure.

P: A E-Mola suporta B2B e C2C?

Não. A API SOAP da E-Mola suporta oficialmente apenas C2B. O suporte a B2C (desembolso) depende do acordo de parceria com a Movitel. B2B e C2C não são suportados — tentar utilizá-los lançará um GatewayException.

P: Como mudo para produção?

  1. Defina MWS_SANDBOX=false no .env.
  2. Altere MWS_MPESA_BASE_URL para a URL de produção: https://api.vm.co.mz.
  3. Use credenciais de produção para M-Pesa e E-Mola.
  4. Configure MWS_WEBHOOK_SECRET com uma chave forte.

P: O pacote cria alguma rota automaticamente?

Sim:

  • POST /mws/webhook/{wallet} — sempre registada (protegida por middleware).
  • GET /mws/dashboard e GET /mws/dashboard/transaction/{uuid} — apenas quando MWS_DASHBOARD_ENABLED=true.

P: Posso adicionar novos provedores no futuro?

Sim. Consulte a secção 20. Extensão. O sistema de drivers permite adicionar novos provedores sem alterar o código do pacote.

P: Como funciona o retry inteligente?

O ProcessTransactionJob consulta o GatewayResponseCode enum para determinar se o erro é retentável ou terminal. Erros como "saldo insuficiente" (INS-5) ou "cancelado pelo cliente" (INS-6) são terminais — o pacote para imediatamente em vez de desperdiçar tentativas. Erros como "timeout" (INS-22) ou "erro do servidor" (INS-999) são retentáveis.

P: Como sei qual versão do PHP/Laravel é suportada?

PHP Laravel
8.1 10, 11
8.2 10, 11, 12
8.3 10, 11, 12, 13

Documentação mantida por Techsolutions Lda.