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;
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)
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."
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.
->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
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,
);
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;
}
}
// 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');
$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);
$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
// Da transacção para o modelo
$tx->billable; // → Invoice #42
// Do modelo para as transacções
$invoice->mwsTransactions; // → Collection de Transaction
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,
);
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
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),
);
}
}
$gw = app(GatewayManager::class)->resolveForWallet('e-mola');
$gw->queryAccountBalance(); // saldo do parceiro
$gw->queryBeneficiaryName('+258843000000'); // nome mascarado do beneficiário
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
// 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);
'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'
],
'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',
],
],
$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)
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);
}
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 { /* ... */ }
}
// 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', [])
);
});
}