techsolutions-projects / mobile-wallet-sdk
A Mozambican mobile wallet (M-Pesa, E-Mola, mKesh) integration SDK for Laravel applications.
Package info
github.com/TECHSOLUTIONS-PROJECTS/mobile-wallet-sdk
pkg:composer/techsolutions-projects/mobile-wallet-sdk
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/events: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/queue: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- larastan/larastan: ^2.0
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
- rector/rector: ^1.0
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
- Introdução
- Instalação e Configuração
- Arquitectura
- Conceitos Principais
- Utilização Básica
- API Fluente (Transaction Builder)
- Trait Billable
- Facade
- Enumerações
- Códigos de Resposta
- Sistema de Retry
- Eventos
- Webhooks
- WebSockets / Broadcasting
- Dashboard
- Drivers
- Gateways Externos
- Internacionalização
- Testes
- Análise Estática e Qualidade de Código
- Extensão — Criar um Driver Customizado
- Referência de Configuração
- 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
- O programador invoca
Transaction::onWallet($wallet)->c2b(...). - O
TransactionBuilderacumula configurações (callbacks, canal WebSocket, metadata). - Ao invocar
c2b()/b2c()/begin(), o builder cria um registoTransactionna base de dados com estadoPENDING. - O builder despacha um
ProcessTransactionJob(síncrono ou via fila). - O job resolve o driver correcto via
GatewayManager. - O driver chama a API do provedor (HTTP para M-Pesa, SOAP para E-Mola).
- Com base na resposta:
- Sucesso → marca como
SUCCEEDED, disparaTransactionSucceeded, executa callback. - Falha retentável → agenda retry com backoff exponencial, dispara
TransactionRetried. - Falha terminal → marca como
FAILED, disparaTransactionFailed, executa callback.
- Sucesso → marca como
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
- Se a resposta é um sucesso → para. Marca como
SUCCEEDED. - Se a resposta é uma falha terminal (ex.:
INS-5saldo insuficiente) → para imediatamente. Marca comoFAILED. Não há razão para re-tentar. - Se a resposta é uma falha retentável (ex.:
INS-22timeout) ecurrent_attempt < max_attempts→ agenda nova tentativa com backoff exponencial. - 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:
- A presença dos cabeçalhos
X-MWS-SignatureeX-MWS-Timestamp. - A tolerância temporal (por defeito 5 minutos).
- 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
msisdndeve ser enviado comophone_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?
- Defina
MWS_SANDBOX=falseno.env. - Altere
MWS_MPESA_BASE_URLpara a URL de produção:https://api.vm.co.mz. - Use credenciais de produção para M-Pesa e E-Mola.
- Configure
MWS_WEBHOOK_SECRETcom uma chave forte.
P: O pacote cria alguma rota automaticamente?
Sim:
POST /mws/webhook/{wallet}— sempre registada (protegida por middleware).GET /mws/dashboardeGET /mws/dashboard/transaction/{uuid}— apenas quandoMWS_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.