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

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. techsolutions-projects/mobile-wallet-sdk

ActiveLibrary[Payment Processing](/categories/payments)

techsolutions-projects/mobile-wallet-sdk
========================================

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

v1.0.0(3mo ago)00MITPHPPHP ^8.2

Since Apr 6Pushed 1mo agoCompare

[ Source](https://github.com/TECHSOLUTIONS-PROJECTS/mobile-wallet-sdk)[ Packagist](https://packagist.org/packages/techsolutions-projects/mobile-wallet-sdk)[ Docs](https://github.com/techsolutions-projects/mobile-wallet-sdk)[ RSS](/packages/techsolutions-projects-mobile-wallet-sdk/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (10)Versions (2)Used By (0)

Mobile Wallet SDK — Documentação Oficial
========================================

[](#mobile-wallet-sdk--documentação-oficial)

**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
------

[](#índice)

1. [Introdução](#1-introdu%C3%A7%C3%A3o)
2. [Instalação e Configuração](#2-instala%C3%A7%C3%A3o-e-configura%C3%A7%C3%A3o)
3. [Arquitectura](#3-arquitectura)
4. [Conceitos Principais](#4-conceitos-principais)
5. [Utilização Básica](#5-utiliza%C3%A7%C3%A3o-b%C3%A1sica)
6. [API Fluente (Transaction Builder)](#6-api-fluente-transaction-builder)
7. [Trait Billable](#7-trait-billable)
8. [Facade](#8-facade)
9. [Enumerações](#9-enumera%C3%A7%C3%B5es)
10. [Códigos de Resposta](#10-c%C3%B3digos-de-resposta)
11. [Sistema de Retry](#11-sistema-de-retry)
12. [Eventos](#12-eventos)
13. [Webhooks](#13-webhooks)
14. [WebSockets / Broadcasting](#14-websockets--broadcasting)
15. [Dashboard](#15-dashboard)
16. [Drivers](#16-drivers)
17. [Gateways Externos](#17-gateways-externos)
18. [Internacionalização](#18-internacionaliza%C3%A7%C3%A3o)
19. [Testes](#19-testes)
20. [Análise Estática e Qualidade de Código](#20-an%C3%A1lise-est%C3%A1tica-e-qualidade-de-c%C3%B3digo)
21. [Extensão — Criar um Driver Customizado](#21-extens%C3%A3o--criar-um-driver-customizado)
22. [Referência de Configuração](#22-refer%C3%AAncia-de-configura%C3%A7%C3%A3o)
23. [FAQ](#23-faq)

---

1. Introdução
-------------

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

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

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

[](#2-instalação-e-configuração)

### Requisitos

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

[](#instalar-via-composer)

```
composer require techsolutions-projects/mobile-wallet-sdk
```

### Publicar ficheiros

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

[](#executar-as-migrations)

```
php artisan migrate
```

Isto cria duas tabelas:

TabelaDescrição`mws_wallets`Carteiras registadas (M-Pesa, E-Mola)`mws_transactions`Transacções com estado, tentativas, resposta do gateway### Registar as carteiras

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

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

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

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

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

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

[](#4-conceitos-principais)

### Wallet

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

CampoTipoDescriçã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

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

CampoTipoDescriçã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

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

[](#5-utilização-básica)

### Transacção C2B (Cliente para Empresa)

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

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

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

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

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

MétodoDescriçãoObrigatório`forBillable(Billable $model)`Associa um modelo pagável (Invoice, Order)Não`toSocket(string $channel)`Activa broadcasting num canal WebSocketNão`onSuccess(Closure $fn)`Callback para sucessoNão`onFail(Closure $fn)`Callback para falha finalNão`shouldRetry(Closure $fn)`Override da lógica de retryNão`withMetadata(array $data)`Anexa dados arbitráriosNão### Métodos terminais (executam a transacção)

[](#métodos-terminais-executam-a-transacção)

MétodoTipoDescrição`c2b(msisdn, amount, attempts)`Customer → BusinessCobrança ao cliente`b2c(msisdn, amount, attempts)`Business → CustomerDesembolso`b2b(msisdn, amount, attempts)`Business → BusinessTransferência entre empresas`c2c(msisdn, amount, attempts)`Customer → CustomerTransferê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éricoEspecificar o tipo como parâmetro---

7. Trait Billable
-----------------

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

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

[](#interface-billable)

MétodoRetornoDescriçã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

[](#métodos-disponibilizados-pelo-trait)

#### Pagamento directo

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

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

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

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

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

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

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

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

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

[](#gatewayresponsecode)

Detalhado na secção seguinte.

---

10. Códigos de Resposta
-----------------------

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

[](#códigos-m-pesa)

Caso do EnumValorDescriçãoRetentá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

[](#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** (``, prefixo `EMOLA-`) e o **nível gateway** BCCS (``, prefixo `EMOLA-GW-`). Abaixo um subconjunto dos mais relevantes — a tabela completa está em `src/Enums/GatewayResponseCode.php`.

**Nível transacção:**

Caso do EnumValorDescriçãoRetentá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 EnumValorDescriçãoRetentá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 EnumValorDescriçãoRetentável?`E_MOLA_SOAP_FAULT``EMOLA-SOAP`Erro SOAP✅`E_MOLA_CONNECTION_ERROR``EMOLA-CONN`Erro de ligação✅### Utilizaçã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  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
--------------------

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

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

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

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

[](#12-eventos)

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

EventoQuando é disparadoPropriedades`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

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

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

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

[](#endpoints)

URLProvedor`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

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

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

[](#14-websockets--broadcasting)

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

### Activar

[](#activar)

```
MWS_BROADCASTING_ENABLED=true
```

### Utilizar

[](#utilizar)

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

### Ouvir no frontend (Laravel Echo)

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

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

[](#15-dashboard)

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

### Activar

[](#activar-1)

```
MWS_DASHBOARD_ENABLED=true
```

### Endpoints

[](#endpoints-1)

MétodoURLDescrição`GET``/mws/dashboard`Resumo: carteiras, estatísticas, transacções recentes`GET``/mws/dashboard/transaction/{uuid}`Detalhe de uma transacçãoO middleware por defeito é `['web', 'auth']` — configurável em `config/mobile-wallet.php`.

---

16. Drivers
-----------

[](#16-drivers)

### M-Pesa (Vodacom Moçambique)

[](#m-pesa-vodacom-moçambique)

PropriedadeValor**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](https://developer.mpesa.vm.co.mz)**Endpoints utilizados:**

TipoURLC2B`/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)

[](#e-mola-movitel-moçambique)

PropriedadeValor**Protocolo**SOAP — Viettel BCCS Gateway**Endpoint**`https://:/BCCSGateway/BCCSGateway?wsdl`**Autenticação**`username`/`password` **dentro do ``** + 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árioA resposta tem **dois níveis**: a tag `` no `` reflecte o estado do gateway BCCS (→ `EMOLA-GW-*`) e o `` real da transacção fica dentro do CDATA `` (→ `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

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

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

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

[](#presets-disponíveis)

PresetPlataformaDescrição`eacacia`EACACIA / Vitae ERPGateway REST unificado para M-Pesa, E-Mola e M-Kesh`e2payments`e2Payments / ExplicadorGateway REST com OAuth2 para E-Mola`custom`QualquerConfiguração manual completa de endpoints e mapeamento### Activar para uma wallet

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

[](#configuração-por-preset)

#### EACACIA (Vitae ERP)

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

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

[](#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ódigos-de-resposta-de-gateways-externos)

CódigoConstanteRetentá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

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

[](#18-internacionalização)

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

### Ficheiros de língua

[](#ficheiros-de-língua)

FicheiroConteú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

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

[](#publicar-para-customização)

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

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

---

19. Testes
----------

[](#19-testes)

O pacote inclui testes unitários e de integração utilizando o [Orchestra Testbench](https://github.com/orchestral/testbench).

### Executar

[](#executar)

```
composer test
# ou
./vendor/bin/phpunit
```

### Cobertura

[](#cobertura)

SuiteFicheiroCoberturaUnit`CoreTest.php`AmountParser, GatewayResponseCode, SandboxGateway, TransactionBuilder, Wallet scopesFeature`FlowTest.php`Fluxo completo de transacção, retry, callbacks, facade, webhooks, trait Billable### Testar nos teus projectos

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

[](#20-análise-estática-e-qualidade-de-código)

O pacote inclui configuração para [Larastan](https://github.com/larastan/larastan) (PHPStan para Laravel) e [Rector](https://getrector.com).

### Larastan (PHPStan nível 8)

[](#larastan-phpstan-nível-8)

```
composer analyse
```

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

### Rector

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

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

[](#1-criar-o-driver)

```
