diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
index bf44c70..df6d745 100644
--- a/.github/workflows/phpunit.yml
+++ b/.github/workflows/phpunit.yml
@@ -1,13 +1,16 @@
-name: PHPUnit Tests
+name: Tests
on:
+ push:
+ branches:
+ - main
pull_request:
branches:
- main
jobs:
run_tests:
- name: Run PHPUnit Tests
+ name: Run Tests & Static Analysis
runs-on: ubuntu-latest
steps:
@@ -23,5 +26,8 @@ jobs:
- name: Install dependencies
run: composer install --no-progress
+ - name: Run PHPStan
+ run: composer phpstan
+
- name: Run PHPUnit
run: ./vendor/bin/phpunit --configuration phpunit.xml
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 9ffb4d5..eb9e971 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,8 @@ vendor
.fleet
.idea
.vscode
-index.php
\ No newline at end of file
+index.php
+phpstan-baseline.neon
+MtnPaymentMethod.php
+disbursement.yaml
+CLAUDE.md
\ No newline at end of file
diff --git a/README.md b/README.md
index b1d9be1..419b56b 100644
--- a/README.md
+++ b/README.md
@@ -3,186 +3,300 @@
[](https://packagist.org/packages/lepresk/momo-api)

+A powerful and professional PHP wrapper for integrating MTN Mobile Money API. Supports **Collection** (receive payments) and **Disbursement** (send money) operations.
+## Features
-La librairie **lepresk/momo-api** est une surcouche au-dessus de l'API officielle de Momo (Mobile Money). Elle facilite
-l'interaction avec la plateforme Momo et fournit des fonctionnalités supplémentaires pour simplifier l'intégration et la
-gestion des transactions financières.
+| Product | Supported Operations |
+|---------|---------------------|
+| **Collection** | Request payments from customers, Check payment status, Get account balance |
+| **Disbursement** | Transfer money, Deposit funds, Process refunds, Get account balance |
+| **Sandbox** | Create API users, Generate API keys, Test environment support |
-## Fonctionnalités
+## Requirements
-La librairie **lepresk/momo-api** vous permet de :
-
-| Produit | Support |
-|--------------|-------------------------------------------------------------------------------------------------------------------------------|
-| Sandbox | - Créer un api user
- Créer un api key
- Récupérer les informations du compte |
-| Collection | - Récupérer le solde du compte
- Faire un requestToPay
- Vérifier le statut d'une transaction
- Gérer le callback |
-| Disbursement | - *En cours d'implémentation* |
-
-## Configuration requise
-
-- PHP 7.4 ou supérieur.
-- Avoir un compte sur [Momo Developper](https://momodeveloper.mtn.com/) et récupérer la `subscriptionKey` ou avoir les clés d'API fournit par MTN si vous êtes en production.
-
-> 📢 En production la `subscriptionKey`, le `apiUser` et le `apiKey` vous sont directement fourni par MTN
+- PHP 7.4 or higher
+- MTN MoMo Developer Account ([Sign up](https://momodeveloper.mtn.com/))
+- Subscription Key (sandbox or production)
## Installation
-Pour installer la librairie **lepresk/momo-api**, vous pouvez utiliser [Composer](https://getcomposer.org/) :
-
```bash
composer require lepresk/momo-api
```
-## Utilisation
+## Quick Start
-Voici un exemple simple d'utilisation de la librairie :
+### Collection API (Receive Payments)
```php
'sandbox', // or 'mtncongo', 'mtnuganda', etc.
+ 'subscription_key' => 'YOUR_SUBSCRIPTION_KEY',
+ 'api_user' => 'YOUR_API_USER',
+ 'api_key' => 'YOUR_API_KEY',
+ 'callback_url' => 'https://yourdomain.com/callback'
+]);
-require 'vendor/autoload.php';
+// Quick payment - 3 parameters
+$paymentId = $collection->quickPay('1000', '242068511358', 'ORDER-123');
-// Récupérer la subscriptionKey dans son profile ou utiliser celui fournit par MTN si vous êtes en production
-$subscriptionKey = 'SUBSCRIPTION KEY HERE';
+// Check payment status
+$transaction = $collection->getPaymentStatus($paymentId);
-// Récupérer le client Momo
-$momo = MomoApi::create(MomoApi::ENVIRONMENT_SANDBOX);
+if ($transaction->isSuccessful()) {
+ echo "Payment of {$transaction->getAmount()} received!";
+}
```
-> 📢 Assurez-vous de remplacer "SUBSCRIPTION KEY HERE" par votre clé d'abonnement réelle.
-Les environnements possibles
-
-| Constante | Valeur | Default |
-|--------------------------------------|:----------------:|:-------:|
-| `MomoApi::ENVIRONMENT_MTN_CONGO` | mtncongo | |
-| `MomoApi::ENVIRONMENT_MTN_UGANDA` | mtnuganda | |
-| `MomoApi::ENVIRONMENT_MTN_GHANA` | mtnghana | |
-| `MomoApi::ENVIRONMENT_IVORY_COAST` | mtnivorycoast | |
-| `MomoApi::ENVIRONMENT_ZAMBIA` | mtnzambia | |
-| `MomoApi::ENVIRONMENT_CAMEROON` | mtncameroon | |
-| `MomoApi::ENVIRONMENT_BENIN` | mtnbenin | |
-| `MomoApi::ENVIRONMENT_SWAZILAND` | mtnswaziland | |
-| `MomoApi::ENVIRONMENT_GUINEACONAKRY` | mtnguineaconakry | |
-| `MomoApi::ENVIRONMENT_SOUTHAFRICA` | mtnsouthafrica | |
-| `MomoApi::ENVIRONMENT_LIBERIA` | mtnliberia | |
-| `MomoApi::ENVIRONMENT_SANDBOX` | sandbox | **OUI** |
-
-### Intéragir avec la sandbox
-
-#### Créer un api user
+### Disbursement API (Send Money)
```php
-// Créer une api user
-$uuid = Utilities::guidv4(); // Ou tout autre guuidv4 valide
-$callbackHost = 'https://my-domain.com/callback';
-
-$apiUser = $momo->sandbox($subscriptionKey)->createApiUser($uuid, $callbackHost);
-echo "Api user created: $apiUser\n";
-```
-
-#### Récupérer les informations d'un utilisateur
-
-```php
-$data = $momo->sandbox($subscriptionKey)->getApiUser($apiUser);
-print_r($data);
-// [
-// 'providerCallbackHost' => 'https://my-domain.com/callback',
-// 'targetEnvironment' => 'sandbox',
-// ]
+ 'sandbox',
+ 'subscription_key' => 'YOUR_SUBSCRIPTION_KEY',
+ 'api_user' => 'YOUR_API_USER',
+ 'api_key' => 'YOUR_API_KEY',
+ 'callback_url' => 'https://yourdomain.com/callback'
+]);
+
+// Transfer money to a beneficiary
+$transfer = TransferRequest::make('5000', '242068511358', 'SALARY-001');
+$transferId = $disbursement->transfer($transfer);
+
+// Check transfer status
+$result = $disbursement->getTransferStatus($transferId);
```
-#### Créer une api key
+## Sandbox Setup
```php
-$apiKey = $momo->sandbox($subscriptionKey)->createApiKey($apiUser);
-echo "Api token created: $apiKey\n";
-```
+sandbox($subscriptionKey)->createApiUser($uuid, $callbackHost);
-```php
-// Créer un object Config
-$config = new \Lepresk\MomoApi\Config::collection($subscriptionKey, $apiUser, $apiKey, $callbackHost);
+// 2. Create API Key
+$apiKey = $momo->sandbox($subscriptionKey)->createApiKey($apiUser);
-// Définir la configuration sur l'instance de MomoApi
-$momo->setupCollection($config);
+// Now use these credentials for Collection/Disbursement
```
-#### Obtenir un token oauth
+## Advanced Usage
-```php
-$token = $momo->collection()->getAccessToken();
+### Collection API - Full Example
-echo $token->getAccessToken(); // Token
-echo $token->getExpiresIn(); // Date d'expiration du token
+```php
+use Lepresk\MomoApi\MomoApi;
+use Lepresk\MomoApi\Models\PaymentRequest;
+
+$collection = MomoApi::collection([
+ 'environment' => 'mtncongo',
+ 'subscription_key' => env('MOMO_SUBSCRIPTION_KEY'),
+ 'api_user' => env('MOMO_API_USER'),
+ 'api_key' => env('MOMO_API_KEY'),
+ 'callback_url' => 'https://yourdomain.com/webhook/momo'
+]);
+
+// Custom payment request
+$request = new PaymentRequest(
+ amount: '2500',
+ currency: 'XAF',
+ externalId: 'ORDER-456',
+ payer: '242068511358',
+ payerMessage: 'Payment for order #456',
+ payeeNote: 'Thank you for your purchase'
+);
+
+$paymentId = $collection->requestToPay($request);
+
+// Get account balance
+$balance = $collection->getBalance();
+echo "Available: {$balance->getAvailableBalance()} {$balance->getCurrency()}";
```
-> _Pour faire une requête requestToPay ou vérifier le statut de la transaction, vous n'avez pas besoin de demander un token, il est automatiquement généré à chaque transaction_
-
-#### Récupérer le solde du compte
+### Disbursement API - Full Example
```php
-$balance = $momo->collection()->getAccountBalance();
-
-echo $balance->getAvailableBalance(); // Solde du compte
-echo $balance->getCurrency(); // Devise du compte
+use Lepresk\MomoApi\MomoApi;
+use Lepresk\MomoApi\Models\TransferRequest;
+use Lepresk\MomoApi\Models\RefundRequest;
+
+$disbursement = MomoApi::disbursement([...config...]);
+
+// Transfer
+$transfer = new TransferRequest(
+ amount: '10000',
+ currency: 'XAF',
+ externalId: 'PAYOUT-789',
+ payee: '242068511358',
+ payerMessage: 'Monthly salary',
+ payeeNote: 'Salary payment for June'
+);
+$transferId = $disbursement->transfer($transfer);
+
+// Refund
+$refund = RefundRequest::make('1000', $originalTransactionId, 'REFUND-123');
+$refundId = $disbursement->refund($refund);
+
+// Check balance
+$balance = $disbursement->getBalance();
```
-#### Faire une requête requestToPay
+### Handling Callbacks
```php
collection()->requestToPay($request);
-```
+// Parse callback data
+$transaction = Transaction::parse($_GET);
-> Pour obtenir les numéros de téléphones de test, veuillez vous référer à [https://momodeveloper.mtn.com/api-documentation/testing/](https://momodeveloper.mtn.com/api-documentation/testing/)
+if ($transaction->isSuccessful()) {
+ // Update your database
+ $orderId = $transaction->getExternalId();
+ $amount = $transaction->getAmount();
-`$paymentId` est l'id du paiement qui vient d'être éffectuer, vous pouvez l'enregistrer dans votre base de données pour l'utiliser plus tard (vérifier le statut du paiement par exemple)
+ // Process order...
+} elseif ($transaction->isFailed()) {
+ $reason = $transaction->getReason();
+ echo "Failed: {$reason->getCode()} - {$reason->getMessage()}";
+}
+```
-#### Vérifier le status d'une transaction
+### Error Handling
```php
-collection()->checkRequestStatus($paymentId);
+use Lepresk\MomoApi\Exceptions\ResourceNotFoundException;
+use Lepresk\MomoApi\Exceptions\InternalServerErrorException;
+use Lepresk\MomoApi\Models\ErrorReason;
+
+try {
+ $paymentId = $collection->quickPay('1000', '242068511358', 'ORDER-999');
+} catch (ResourceNotFoundException $e) {
+ // Payment not found
+ echo "Error: " . $e->getMessage();
+} catch (InternalServerErrorException $e) {
+ // Server error
+ echo "Server error, please retry";
+}
+
+// Check error reason from transaction
+$transaction = $collection->getPaymentStatus($paymentId);
+if ($transaction->isFailed()) {
+ $reason = $transaction->getReason();
+
+ if ($reason->isNotEnoughFunds()) {
+ echo "Insufficient funds";
+ } elseif ($reason->isPayerLimitReached()) {
+ echo "Transaction limit exceeded";
+ }
+}
+```
-echo $transaction->getStatus(); // Pour obtenir le statut de la transaction
+## Available Environments
+
+| Constant | Value | Use Case |
+|----------|-------|----------|
+| `ENVIRONMENT_SANDBOX` | sandbox | Testing |
+| `ENVIRONMENT_MTN_CONGO` | mtncongo | Production - Congo |
+| `ENVIRONMENT_MTN_UGANDA` | mtnuganda | Production - Uganda |
+| `ENVIRONMENT_MTN_GHANA` | mtnghana | Production - Ghana |
+| `ENVIRONMENT_IVORY_COAST` | mtnivorycoast | Production - Ivory Coast |
+| `ENVIRONMENT_ZAMBIA` | mtnzambia | Production - Zambia |
+| `ENVIRONMENT_CAMEROON` | mtncameroon | Production - Cameroon |
+| `ENVIRONMENT_BENIN` | mtnbenin | Production - Benin |
+| `ENVIRONMENT_SWAZILAND` | mtnswaziland | Production - Swaziland |
+| `ENVIRONMENT_GUINEACONAKRY` | mtnguineaconakry | Production - Guinea Conakry |
+| `ENVIRONMENT_SOUTHAFRICA` | mtnsouthafrica | Production - South Africa |
+| `ENVIRONMENT_LIBERIA` | mtnliberia | Production - Liberia |
+
+## API Reference
+
+### Collection API
+
+| Method | Description |
+|--------|-------------|
+| `requestToPay(PaymentRequest $request)` | Request payment from customer |
+| `quickPay(string $amount, string $phone, string $ref)` | Quick payment helper |
+| `getPaymentStatus(string $paymentId)` | Check payment status |
+| `getBalance()` | Get account balance |
+| `getAccessToken()` | Get OAuth token (auto-managed) |
+
+### Disbursement API
+
+| Method | Description |
+|--------|-------------|
+| `transfer(TransferRequest $request)` | Transfer money to beneficiary |
+| `getTransferStatus(string $transferId)` | Check transfer status |
+| `deposit(PaymentRequest $request)` | Deposit funds |
+| `getDepositStatus(string $depositId)` | Check deposit status |
+| `refund(RefundRequest $request)` | Refund a transaction |
+| `getRefundStatus(string $refundId)` | Check refund status |
+| `getBalance()` | Get account balance |
+| `getAccessToken()` | Get OAuth token (auto-managed) |
+
+### Sandbox API
+
+| Method | Description |
+|--------|-------------|
+| `createApiUser(string $uuid, string $callback)` | Create sandbox API user |
+| `getApiUser(string $uuid)` | Get API user details |
+| `createApiKey(string $uuid)` | Generate API key |
+
+## Testing
+
+The package includes two types of tests:
+
+**Unit Tests** - Fast tests with mocked HTTP responses:
+```bash
+composer test
+# or run specific suite
+vendor/bin/phpunit --testsuite Unit
```
-#### Gérer le hook du callback
+**Fixture Tests** - Validate parsing of real MTN API responses:
+```bash
+vendor/bin/phpunit --testsuite Fixtures
+```
-```php
-getStatus(); // Pour obtenir le statut de la transaction
-echo $transaction->getAmount(); // Pour récuperer le montant de la transaction
-```
+- **Never hardcode credentials** - Use environment variables
+- **Validate callbacks** - Check transaction status via API, not just callback data
+- **Handle webhooks asynchronously** - Process in background queue
+- **Log all transactions** - Keep audit trail
+- **Test thoroughly in sandbox** before going live
-## Documentation supplémentaire
+## Contributing
-Pour plus d'informations sur l'utilisation de la librairie **lepresk/momo-api** et les fonctionnalités disponibles,
-veuillez consulter la documentation officielle dans le dossier "docs" du dépôt GitHub.
+Contributions are welcome! Please create an issue or pull request on [GitHub](https://github.com/lepresk/momo-api).
-## Contribution
+## License
-Les contributions sont les bienvenues ! Si vous souhaitez améliorer la librairie, signalez des problèmes ou soumettez
-des demandes de fonctionnalités, veuillez créer une issue sur le dépôt GitHub de la
-librairie : [lepresk/momo-api](https://github.com/lepresk/momo-api).
+MIT License - see [LICENSE](LICENSE) file for details.
-## Licence
+## Support
-Cette librairie est distribuée sous la licence [MIT](https://opensource.org/licenses/MIT). Vous êtes libre de l'utiliser
-et de la modifier selon vos besoins.
\ No newline at end of file
+- Documentation: [MTN MoMo Developer Portal](https://momodeveloper.mtn.com/)
+- Issues: [GitHub Issues](https://github.com/lepresk/momo-api/issues)
diff --git a/composer.json b/composer.json
index 223e9d5..945d414 100644
--- a/composer.json
+++ b/composer.json
@@ -17,7 +17,9 @@
},
"require-dev": {
"phpunit/phpunit": "^10.2",
- "symfony/var-dumper": "^6.3"
+ "symfony/var-dumper": "^6.3",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/extension-installer": "^1.4"
},
"license": "MIT",
"autoload": {
@@ -37,6 +39,13 @@
}
],
"scripts": {
- "test": "phpunit --colors=always --testdox"
+ "test": "phpunit --colors=always --testdox",
+ "phpstan": "phpstan analyse",
+ "phpstan-baseline": "phpstan analyse --generate-baseline"
+ },
+ "config": {
+ "allow-plugins": {
+ "phpstan/extension-installer": true
+ }
}
}
diff --git a/composer.lock b/composer.lock
index 61ed8d7..e053b83 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "a20778f1325a8775d0a2619e1e39efac",
+ "content-hash": "040499052c7e2304352b8eae600a9b8e",
"packages": [
{
"name": "psr/container",
@@ -656,6 +656,107 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
+ {
+ "name": "phpstan/extension-installer",
+ "version": "1.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/extension-installer.git",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.0",
+ "php": "^7.2 || ^8.0",
+ "phpstan/phpstan": "^1.9.0 || ^2.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2.0",
+ "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PHPStan\\ExtensionInstaller\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\ExtensionInstaller\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Composer plugin for automatic installation of PHPStan extensions",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpstan/extension-installer/issues",
+ "source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
+ },
+ "time": "2024-09-04T20:21:43+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.1.31",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96",
+ "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2025-10-10T14:14:11+00:00"
+ },
{
"name": "phpunit/php-code-coverage",
"version": "10.1.2",
@@ -2205,12 +2306,12 @@
],
"aliases": [],
"minimum-stability": "dev",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^7.4|^8.0"
},
- "platform-dev": [],
- "plugin-api-version": "2.3.0"
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
}
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..b2ef0c3
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,7 @@
+parameters:
+ level: 5
+ paths:
+ - src
+ excludePaths:
+ - vendor
+ reportUnmatchedIgnoredErrors: false
diff --git a/phpunit.xml b/phpunit.xml
index b81b98b..ce83ef4 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -5,8 +5,13 @@
colors="true"
>
-
- ./tests
+
+ ./tests/Unit
+ ./tests/MomoApiTest.php
+ ./tests/UuidTest.php
+
+
+ ./tests/FixtureTests
diff --git a/src/ApiProduct.php b/src/Abstracts/AbstractApiProduct.php
similarity index 74%
rename from src/ApiProduct.php
rename to src/Abstracts/AbstractApiProduct.php
index 1811e7f..6baa163 100644
--- a/src/ApiProduct.php
+++ b/src/Abstracts/AbstractApiProduct.php
@@ -1,20 +1,17 @@
client = $client;
@@ -26,4 +23,4 @@ public function getSubscriptionKey(): string
{
return $this->config->getSubscriptionKey();
}
-}
\ No newline at end of file
+}
diff --git a/src/Exceptions/BadRessourceExeption.php b/src/Exceptions/BadResourceException.php
similarity index 81%
rename from src/Exceptions/BadRessourceExeption.php
rename to src/Exceptions/BadResourceException.php
index d1b273d..8b2a461 100644
--- a/src/Exceptions/BadRessourceExeption.php
+++ b/src/Exceptions/BadResourceException.php
@@ -3,7 +3,7 @@
namespace Lepresk\MomoApi\Exceptions;
-class BadRessourceExeption extends MomoException
+class BadResourceException extends MomoException
{
public function __construct()
diff --git a/src/Exceptions/ExceptionFactory.php b/src/Exceptions/ExceptionFactory.php
index df6d1cb..bb4f79b 100644
--- a/src/Exceptions/ExceptionFactory.php
+++ b/src/Exceptions/ExceptionFactory.php
@@ -17,7 +17,7 @@ public static function create(ResponseInterface $response): MomoException
case 401:
return new InvalidSubscriptionKeyException($content['message'] ?? null);
case 404:
- return new RessourceNotFoundException($content['message'] ?? '', $response->getStatusCode());
+ return new ResourceNotFoundException($content['message'] ?? '', $response->getStatusCode());
case 409:
return new ConflictException($content['message'] ?? null);
case 500:
diff --git a/src/Exceptions/RessourceNotFoundException.php b/src/Exceptions/ResourceNotFoundException.php
similarity index 82%
rename from src/Exceptions/RessourceNotFoundException.php
rename to src/Exceptions/ResourceNotFoundException.php
index b28a323..c00e05d 100644
--- a/src/Exceptions/RessourceNotFoundException.php
+++ b/src/Exceptions/ResourceNotFoundException.php
@@ -3,7 +3,7 @@
namespace Lepresk\MomoApi\Exceptions;
-class RessourceNotFoundException extends MomoException
+class ResourceNotFoundException extends MomoException
{
public function __construct(?string $message, int $code = 404)
{
diff --git a/src/ApiToken.php b/src/Models/ApiToken.php
similarity index 96%
rename from src/ApiToken.php
rename to src/Models/ApiToken.php
index dbef823..1cf79ad 100644
--- a/src/ApiToken.php
+++ b/src/Models/ApiToken.php
@@ -1,7 +1,7 @@
code = $code;
+ $this->message = $message;
+ }
+
+ public static function fromArray(array $data): self
+ {
+ return new self($data['code'] ?? '', $data['message'] ?? '');
+ }
+
+ public function getCode(): string
+ {
+ return $this->code;
+ }
+
+ public function getMessage(): string
+ {
+ return $this->message;
+ }
+
+ public function is(string $code): bool
+ {
+ return $this->code === $code;
+ }
+
+ public function isPayeeNotFound(): bool
+ {
+ return $this->is(self::PAYEE_NOT_FOUND);
+ }
+
+ public function isNotEnoughFunds(): bool
+ {
+ return $this->is(self::NOT_ENOUGH_FUNDS);
+ }
+
+ public function isPayerLimitReached(): bool
+ {
+ return $this->is(self::PAYER_LIMIT_REACHED);
+ }
+
+ public function __toString(): string
+ {
+ return "[{$this->code}] {$this->message}";
+ }
+}
diff --git a/src/Models/PaymentRequest.php b/src/Models/PaymentRequest.php
index ecb37b2..eb535a7 100644
--- a/src/Models/PaymentRequest.php
+++ b/src/Models/PaymentRequest.php
@@ -6,14 +6,14 @@
class PaymentRequest
{
/**
- * @var float
+ * @var string
*/
- private $amount;
+ private string $amount;
/**
* @var string
*/
- private $currency;
+ private string $currency;
/**
* @var string
@@ -38,15 +38,21 @@ class PaymentRequest
private $payerNote;
/**
- * @param float $amount
+ * @param string $amount
* @param string $currency
* @param string $externalId
* @param string $payer
- * @param $payerMessage
- * @param $payerNote
+ * @param string $payerMessage
+ * @param string $payerNote
*/
- public function __construct(float $amount, string $currency, string $externalId, string $payer, $payerMessage, $payerNote)
- {
+ public function __construct(
+ string $amount,
+ string $currency,
+ string $externalId,
+ string $payer,
+ string $payerMessage = '',
+ string $payerNote = ''
+ ) {
$this->amount = $amount;
$this->currency = $currency;
$this->externalId = $externalId;
@@ -56,9 +62,31 @@ public function __construct(float $amount, string $currency, string $externalId,
}
/**
- * @return float
+ * Static factory with sensible defaults
+ *
+ * @param string $amount
+ * @param string $payer
+ * @param string $externalId
+ * @param string $currency
+ * @param string $payerMessage
+ * @param string $payeeNote
+ * @return self
+ */
+ public static function make(
+ string $amount,
+ string $payer,
+ string $externalId,
+ string $currency = 'XAF',
+ string $payerMessage = '',
+ string $payeeNote = ''
+ ): self {
+ return new self($amount, $currency, $externalId, $payer, $payerMessage, $payeeNote);
+ }
+
+ /**
+ * @return string
*/
- public function getAmount(): float
+ public function getAmount(): string
{
return $this->amount;
}
diff --git a/src/Models/RefundRequest.php b/src/Models/RefundRequest.php
new file mode 100644
index 0000000..87197b2
--- /dev/null
+++ b/src/Models/RefundRequest.php
@@ -0,0 +1,144 @@
+amount = $amount;
+ $this->currency = $currency;
+ $this->externalId = $externalId;
+ $this->referenceIdToRefund = $referenceIdToRefund;
+ $this->payerMessage = $payerMessage;
+ $this->payeeNote = $payeeNote;
+ }
+
+ /**
+ * Static factory with sensible defaults
+ *
+ * @param string $amount
+ * @param string $referenceIdToRefund
+ * @param string $externalId
+ * @param string $currency
+ * @param string $payerMessage
+ * @param string $payeeNote
+ * @return self
+ */
+ public static function make(
+ string $amount,
+ string $referenceIdToRefund,
+ string $externalId,
+ string $currency = 'XAF',
+ string $payerMessage = '',
+ string $payeeNote = ''
+ ): self {
+ return new self($amount, $currency, $externalId, $referenceIdToRefund, $payerMessage, $payeeNote);
+ }
+
+ /**
+ * @return string
+ */
+ public function getAmount(): string
+ {
+ return $this->amount;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCurrency(): string
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @return string
+ */
+ public function getExternalId(): string
+ {
+ return $this->externalId;
+ }
+
+ /**
+ * @return string
+ */
+ public function getReferenceIdToRefund(): string
+ {
+ return $this->referenceIdToRefund;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPayerMessage(): string
+ {
+ return $this->payerMessage;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPayeeNote(): string
+ {
+ return $this->payeeNote;
+ }
+
+ public function toArray(): array
+ {
+ return [
+ "amount" => $this->amount,
+ "currency" => $this->currency,
+ "externalId" => $this->externalId,
+ "payerMessage" => $this->payerMessage,
+ "payeeNote" => $this->payeeNote,
+ "referenceIdToRefund" => $this->referenceIdToRefund,
+ ];
+ }
+}
diff --git a/src/Models/Transaction.php b/src/Models/Transaction.php
index 688b646..9471401 100644
--- a/src/Models/Transaction.php
+++ b/src/Models/Transaction.php
@@ -18,7 +18,7 @@ class Transaction
private ?string $payeeNote;
private string $status;
- private ?string $reason;
+ private ?ErrorReason $reason;
/**
* @param string|null $financialTransactionId
@@ -29,9 +29,9 @@ class Transaction
* @param string|null $payerMessage
* @param string|null $payeeNote
* @param string $status
- * @param string|null $reason
+ * @param ErrorReason|null $reason
*/
- public function __construct(?string $financialTransactionId, ?string $externalId, ?string $amount, string $currency, array $payer, ?string $payerMessage, ?string $payeeNote, string $status, ?string $reason)
+ public function __construct(?string $financialTransactionId, ?string $externalId, ?string $amount, string $currency, array $payer, ?string $payerMessage, ?string $payeeNote, string $status, ?ErrorReason $reason)
{
$this->financialTransactionId = $financialTransactionId;
$this->externalId = $externalId;
@@ -53,16 +53,21 @@ public function __construct(?string $financialTransactionId, ?string $externalId
*/
public static function parse(array $array): Transaction
{
+ $reason = null;
+ if (isset($array['reason']) && is_array($array['reason'])) {
+ $reason = ErrorReason::fromArray($array['reason']);
+ }
+
return new self(
$array['financialTransactionId'] ?? null,
$array['externalId'],
$array['amount'],
$array['currency'],
- $array['payer'],
+ $array['payer'] ?? $array['payee'] ?? [],
$array['payerMessage'],
$array['payeeNote'],
$array['status'],
- $array['reason'] ?? null,
+ $reason,
);
}
@@ -100,9 +105,9 @@ public function getStatus(): string
}
/**
- * @return string|null
+ * @return ErrorReason|null
*/
- public function getReason(): ?string
+ public function getReason(): ?ErrorReason
{
return $this->reason;
}
@@ -147,6 +152,15 @@ public function getPayer(): ?string
return $this->payer['partyId'] ?? null;
}
+ /**
+ * Get payee (beneficiary) phone number
+ * @return string|null
+ */
+ public function getPayee(): ?string
+ {
+ return $this->getPayer();
+ }
+
/**
* @return string|null
*/
diff --git a/src/Models/TransferRequest.php b/src/Models/TransferRequest.php
new file mode 100644
index 0000000..9dd7c3b
--- /dev/null
+++ b/src/Models/TransferRequest.php
@@ -0,0 +1,148 @@
+amount = $amount;
+ $this->currency = $currency;
+ $this->externalId = $externalId;
+ $this->payee = $payee;
+ $this->payerMessage = $payerMessage;
+ $this->payeeNote = $payeeNote;
+ }
+
+ /**
+ * Static factory with sensible defaults
+ *
+ * @param string $amount
+ * @param string $payee
+ * @param string $externalId
+ * @param string $currency
+ * @param string $payerMessage
+ * @param string $payeeNote
+ * @return self
+ */
+ public static function make(
+ string $amount,
+ string $payee,
+ string $externalId,
+ string $currency = 'XAF',
+ string $payerMessage = '',
+ string $payeeNote = ''
+ ): self {
+ return new self($amount, $currency, $externalId, $payee, $payerMessage, $payeeNote);
+ }
+
+ /**
+ * @return string
+ */
+ public function getAmount(): string
+ {
+ return $this->amount;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCurrency(): string
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @return string
+ */
+ public function getExternalId(): string
+ {
+ return $this->externalId;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPayee(): string
+ {
+ return $this->payee;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPayerMessage(): string
+ {
+ return $this->payerMessage;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPayeeNote(): string
+ {
+ return $this->payeeNote;
+ }
+
+ public function toArray(): array
+ {
+ return [
+ "amount" => $this->amount,
+ "currency" => $this->currency,
+ "externalId" => $this->externalId,
+ "payee" => [
+ "partyIdType" => "MSISDN",
+ "partyId" => $this->payee,
+ ],
+ "payerMessage" => $this->payerMessage,
+ "payeeNote" => $this->payeeNote,
+ ];
+ }
+}
diff --git a/src/MomoApi.php b/src/MomoApi.php
index b817f49..e80f805 100644
--- a/src/MomoApi.php
+++ b/src/MomoApi.php
@@ -4,6 +4,7 @@
namespace Lepresk\MomoApi;
use InvalidArgumentException;
+use Lepresk\MomoApi\Models\Config;
use Lepresk\MomoApi\Products\CollectionApi;
use Lepresk\MomoApi\Products\DisbursementApi;
use Lepresk\MomoApi\Products\SandboxApi;
@@ -33,10 +34,6 @@ class MomoApi
private static ?HttpClientInterface $client = null;
- private ?Config $collectionConfig = null;
-
- private ?Config $disbursementConfig = null;
-
private function __construct(string $environment)
{
$this->environment = $environment;
@@ -67,58 +64,68 @@ public static function getClient(): ?HttpClientInterface
*/
public static function create(string $environment): MomoApi
{
- if (static::$client === null) {
- static::$client = HttpClient::create([
- 'base_uri' => static::getBaseUrl($environment),
+ if (self::$client === null) {
+ self::$client = HttpClient::create([
+ 'base_uri' => self::getBaseUrl($environment),
]);
}
return new self($environment);
}
- public static function getBaseUrl($environment): string
- {
- if ($environment === MomoApi::ENVIRONMENT_SANDBOX) {
- return self::SANDBOX_URL;
- }
- return self::PRODUCTION_URL;
- }
-
- public function setupCollection(Config $config): void
- {
- $this->collectionConfig = $config;
- }
-
- public function setupDisbursement(Config $config): void
- {
- $this->disbursementConfig = $config;
- }
-
/**
- * Momo API Collection factory
+ * Fluent factory for Collection API
*
+ * @param array $config Configuration array with keys: environment, subscription_key, api_user, api_key, callback_url
* @return CollectionApi
*/
- public function collection(): CollectionApi
+ public static function collection(array $config): CollectionApi
{
- if ($this->collectionConfig === null) {
- throw new InvalidArgumentException("Collection must be setup with `MomoApi::setupCollection` before call `MomoApi::collection`");
+ $environment = $config['environment'] ?? self::ENVIRONMENT_SANDBOX;
+ $subscriptionKey = $config['subscription_key'] ?? throw new InvalidArgumentException('subscription_key is required');
+ $apiUser = $config['api_user'] ?? throw new InvalidArgumentException('api_user is required');
+ $apiKey = $config['api_key'] ?? throw new InvalidArgumentException('api_key is required');
+ $callbackUrl = $config['callback_url'] ?? '';
+
+ if (self::$client === null) {
+ self::$client = HttpClient::create([
+ 'base_uri' => self::getBaseUrl($environment),
+ ]);
}
- return new CollectionApi(static::$client, $this->environment, $this->collectionConfig);
+ $configObject = Config::collection($subscriptionKey, $apiUser, $apiKey, $callbackUrl);
+ return new CollectionApi(self::$client, $environment, $configObject);
}
/**
- * Access to Disbursements product
+ * Fluent factory for Disbursement API
*
+ * @param array $config Configuration array with keys: environment, subscription_key, api_user, api_key, callback_url
* @return DisbursementApi
*/
- public function disbursement(): DisbursementApi
+ public static function disbursement(array $config): DisbursementApi
{
- if ($this->disbursementConfig === null) {
- throw new InvalidArgumentException("Disbursement must be setup with `MomoApi::setupDisbursement` before call `MomoApi::disbursement`");
+ $environment = $config['environment'] ?? self::ENVIRONMENT_SANDBOX;
+ $subscriptionKey = $config['subscription_key'] ?? throw new InvalidArgumentException('subscription_key is required');
+ $apiUser = $config['api_user'] ?? throw new InvalidArgumentException('api_user is required');
+ $apiKey = $config['api_key'] ?? throw new InvalidArgumentException('api_key is required');
+ $callbackUrl = $config['callback_url'] ?? '';
+
+ if (self::$client === null) {
+ self::$client = HttpClient::create([
+ 'base_uri' => self::getBaseUrl($environment),
+ ]);
}
- return new DisbursementApi(static::$client, $this->environment, $this->disbursementConfig);
+ $configObject = Config::disbursement($subscriptionKey, $apiUser, $apiKey, $callbackUrl);
+ return new DisbursementApi(self::$client, $environment, $configObject);
+ }
+
+ public static function getBaseUrl($environment): string
+ {
+ if ($environment === MomoApi::ENVIRONMENT_SANDBOX) {
+ return self::SANDBOX_URL;
+ }
+ return self::PRODUCTION_URL;
}
/**
@@ -133,6 +140,6 @@ public function sandbox(string $subscriptionKey): SandboxApi
throw new InvalidArgumentException("Environment must be " . self::ENVIRONMENT_SANDBOX);
}
- return new SandboxApi(static::$client, $this->environment, Config::sandbox($subscriptionKey));
+ return new SandboxApi(self::$client, $this->environment, Config::sandbox($subscriptionKey));
}
}
\ No newline at end of file
diff --git a/src/Products/CollectionApi.php b/src/Products/CollectionApi.php
index f6c415b..6d1daff 100644
--- a/src/Products/CollectionApi.php
+++ b/src/Products/CollectionApi.php
@@ -3,23 +3,22 @@
namespace Lepresk\MomoApi\Products;
-use Lepresk\MomoApi\ApiProduct;
-use Lepresk\MomoApi\ApiToken;
+use Lepresk\MomoApi\Abstracts\AbstractApiProduct;
use Lepresk\MomoApi\Exceptions\ExceptionFactory;
use Lepresk\MomoApi\Exceptions\MomoException;
use Lepresk\MomoApi\Models\AccountBalance;
+use Lepresk\MomoApi\Models\ApiToken;
use Lepresk\MomoApi\Models\PaymentRequest;
use Lepresk\MomoApi\Models\Transaction;
-use Lepresk\MomoApi\Utilities;
+use Lepresk\MomoApi\Support\Uuid;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
-class CollectionApi extends ApiProduct
+class CollectionApi extends AbstractApiProduct
{
-
/**
* Request a payment from a consumer (Payer). The payer will be asked to authorize the payment.
* The transaction will be executed once the payer has authorized the payment.
@@ -49,20 +48,27 @@ class CollectionApi extends ApiProduct
*/
public function requestToPay(PaymentRequest $paymentRequest): string
{
- $xReferenceId = Utilities::guidv4();
+ $xReferenceId = Uuid::v4();
$token = $this->getAccessToken();
+ $headers = [
+ 'Ocp-Apim-Subscription-Key' => $this->getSubscriptionKey(),
+ 'X-Reference-Id' => $xReferenceId,
+ 'X-Target-Environment' => $this->environment,
+ 'Authorization' => 'Bearer ' . $token->getAccessToken(),
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ];
+
+ // Add X-Callback-Url if configured
+ if (!empty($this->config->getCallbackUri())) {
+ $headers['X-Callback-Url'] = $this->config->getCallbackUri();
+ }
+
$response = $this->client->request('POST', '/collection/v1_0/requesttopay', [
'json' => $paymentRequest->toArray(),
- 'headers' => [
- 'Ocp-Apim-Subscription-Key' => $this->getSubscriptionKey(),
- 'X-Reference-Id' => $xReferenceId,
- 'X-Target-Environment' => $this->environment,
- 'Authorization' => 'Bearer ' . $token->getAccessToken(),
- 'Content-Type' => 'application/json',
- 'Accept' => 'application/json',
- ]
+ 'headers' => $headers
]);
$responseCode = $response->getStatusCode();
@@ -123,7 +129,7 @@ public function getAccessToken(): ApiToken
* $result->getAmount(); // 1500
* $result->getPayer(); // 46733123454
* }
- * } catch (BadRessourceExeption|InternalServerErrorException|RessourceNotFoundException $e) {
+ * } catch (BadResourceException|InternalServerErrorException|ResourceNotFoundException $e) {
* // Request failed, do something else
* }
* ```
@@ -137,7 +143,7 @@ public function getAccessToken(): ApiToken
* @throws TransportExceptionInterface
* @throws MomoException
*/
- public function checkRequestStatus(string $paymentId): Transaction
+ public function getPaymentStatus(string $paymentId): Transaction
{
$token = $this->getAccessToken();
$response = $this->client->request('GET', '/collection/v1_0/requesttopay/' . $paymentId, [
@@ -148,7 +154,8 @@ public function checkRequestStatus(string $paymentId): Transaction
]
]);
- if ($response->getStatusCode() === 200) {
+ $statusCode = $response->getStatusCode();
+ if ($statusCode === 200 || $statusCode === 202) {
return Transaction::parse($response->toArray());
}
@@ -158,6 +165,14 @@ public function checkRequestStatus(string $paymentId): Transaction
/**
* Get the balance of own account.
*
+ * ### Sample usage
+ *
+ * ```
+ * $balance = $collection->getBalance();
+ * echo $balance->getAvailableBalance(); // 50000
+ * echo $balance->getCurrency(); // EUR
+ * ```
+ *
* @return AccountBalance
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
@@ -166,7 +181,7 @@ public function checkRequestStatus(string $paymentId): Transaction
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
- public function getAccountBalance(): AccountBalance
+ public function getBalance(): AccountBalance
{
$token = $this->getAccessToken();
$response = $this->client->request('GET', '/collection/v1_0/account/balance', [
@@ -183,4 +198,46 @@ public function getAccountBalance(): AccountBalance
throw ExceptionFactory::create($response);
}
+
+ /**
+ * Quick payment helper with sensible defaults
+ *
+ * ### Sample usage
+ *
+ * ```
+ * $paymentId = $collection->quickPay('1000', '242068511358', 'ORDER-123');
+ * // Equivalent to:
+ * // $request = new PaymentRequest('1000', 'XAF', 'ORDER-123', '242068511358', '', '');
+ * // $collection->requestToPay($request);
+ * ```
+ *
+ * @param string $amount
+ * @param string $phone
+ * @param string $reference
+ * @param string $currency
+ * @return string payment ID
+ * @throws ClientExceptionInterface
+ * @throws DecodingExceptionInterface
+ * @throws MomoException
+ * @throws RedirectionExceptionInterface
+ * @throws ServerExceptionInterface
+ * @throws TransportExceptionInterface
+ */
+ public function quickPay(
+ string $amount,
+ string $phone,
+ string $reference,
+ string $currency = 'XAF'
+ ): string {
+ $request = new PaymentRequest(
+ $amount,
+ $currency,
+ $reference,
+ $phone,
+ '',
+ ''
+ );
+
+ return $this->requestToPay($request);
+ }
}
\ No newline at end of file
diff --git a/src/Products/DisbursementApi.php b/src/Products/DisbursementApi.php
index 7d9eaea..13cfa09 100644
--- a/src/Products/DisbursementApi.php
+++ b/src/Products/DisbursementApi.php
@@ -2,23 +2,24 @@
namespace Lepresk\MomoApi\Products;
-use Lepresk\MomoApi\ApiProduct;
-use Lepresk\MomoApi\ApiToken;
+use Lepresk\MomoApi\Abstracts\AbstractApiProduct;
use Lepresk\MomoApi\Exceptions\ExceptionFactory;
use Lepresk\MomoApi\Exceptions\MomoException;
use Lepresk\MomoApi\Models\AccountBalance;
+use Lepresk\MomoApi\Models\ApiToken;
use Lepresk\MomoApi\Models\PaymentRequest;
+use Lepresk\MomoApi\Models\RefundRequest;
+use Lepresk\MomoApi\Models\TransferRequest;
use Lepresk\MomoApi\Models\Transaction;
-use Lepresk\MomoApi\Utilities;
+use Lepresk\MomoApi\Support\Uuid;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
-class DisbursementApi extends ApiProduct
+class DisbursementApi extends AbstractApiProduct
{
-
/**
* Create an access token which can then be used to authorize and authenticate
* towards the other end-points of the API.
@@ -58,6 +59,14 @@ public function getAccessToken(): ApiToken
/**
* Get the balance of own account.
*
+ * ### Sample usage
+ *
+ * ```
+ * $balance = $disbursement->getBalance();
+ * echo $balance->getAvailableBalance(); // 1000000
+ * echo $balance->getCurrency(); // XAF
+ * ```
+ *
* @return AccountBalance
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
@@ -66,7 +75,7 @@ public function getAccessToken(): ApiToken
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
- public function getAccountBalance(): AccountBalance
+ public function getBalance(): AccountBalance
{
$token = $this->getAccessToken();
@@ -86,7 +95,7 @@ public function getAccountBalance(): AccountBalance
}
/**
- * Deposit an amount from the owner’s account to a payee account.
+ * Deposit an amount from the owner's account to a payee account.
*
* ### Sample usage
*
@@ -99,7 +108,7 @@ public function getAccountBalance(): AccountBalance
* 'Payment message',
* 'A note',
* );
- * $paymentId = $momo->disbursement()->requestToPay($request);
+ * $paymentId = $momo->disbursement()->deposit($request);
* ```
* @param PaymentRequest $paymentRequest
* @return string payment reference id
@@ -110,22 +119,29 @@ public function getAccountBalance(): AccountBalance
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
- public function getDepositV1(PaymentRequest $paymentRequest): string
+ public function deposit(PaymentRequest $paymentRequest): string
{
$token = $this->getAccessToken();
- $xReferenceId = Utilities::guidv4();
+ $xReferenceId = Uuid::v4();
+
+ $headers = [
+ 'Ocp-Apim-Subscription-Key' => $this->getSubscriptionKey(),
+ 'X-Reference-Id' => $xReferenceId,
+ 'X-Target-Environment' => $this->environment,
+ 'Authorization' => 'Bearer ' . $token->getAccessToken(),
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ];
- $response = $this->client->request('POST', '/disbursement/v1_0/account/deposit', [
+ // Add X-Callback-Url if configured
+ if (!empty($this->config->getCallbackUri())) {
+ $headers['X-Callback-Url'] = $this->config->getCallbackUri();
+ }
+
+ $response = $this->client->request('POST', '/disbursement/v1_0/deposit', [
'json' => $paymentRequest->toArray(),
- 'headers' => [
- 'Ocp-Apim-Subscription-Key' => $this->getSubscriptionKey(),
- 'X-Reference-Id' => $xReferenceId,
- 'X-Target-Environment' => $this->environment,
- 'Authorization' => 'Bearer ' . $token->getAccessToken(),
- 'Content-Type' => 'application/json',
- 'Accept' => 'application/json',
- ]
+ 'headers' => $headers
]);
$responseCode = $response->getStatusCode();
@@ -150,7 +166,7 @@ public function getDepositV1(PaymentRequest $paymentRequest): string
* $result->getAmount(); // 1500
* $result->getPayer(); // 46733123454
* }
- * } catch (BadRessourceExeption|InternalServerErrorException|RessourceNotFoundException $e) {
+ * } catch (BadResourceException|InternalServerErrorException|ResourceNotFoundException $e) {
* // Request failed, do something else
* }
* ```
@@ -175,7 +191,210 @@ public function getDepositStatus(string $depositId): Transaction
]
]);
- if ($response->getStatusCode() === 200) {
+ $statusCode = $response->getStatusCode();
+ if ($statusCode === 200 || $statusCode === 202) {
+ return Transaction::parse($response->toArray());
+ }
+
+ throw ExceptionFactory::create($response);
+ }
+
+ /**
+ * Transfer an amount from the owner's account to a payee account.
+ *
+ * ### Sample usage
+ *
+ * ```
+ * $request = \Lepresk\MomoApi\Models\TransferRequest::make(
+ * '1000',
+ * '242068511358',
+ * 'TRANSFER-001'
+ * );
+ * $transferId = $momo->disbursement()->transfer($request);
+ * ```
+ * @param TransferRequest $transferRequest
+ * @return string transfer reference id
+ * @throws ClientExceptionInterface
+ * @throws DecodingExceptionInterface
+ * @throws MomoException
+ * @throws RedirectionExceptionInterface
+ * @throws ServerExceptionInterface
+ * @throws TransportExceptionInterface
+ */
+ public function transfer(TransferRequest $transferRequest): string
+ {
+ $token = $this->getAccessToken();
+
+ $xReferenceId = Uuid::v4();
+
+ $headers = [
+ 'Ocp-Apim-Subscription-Key' => $this->getSubscriptionKey(),
+ 'X-Reference-Id' => $xReferenceId,
+ 'X-Target-Environment' => $this->environment,
+ 'Authorization' => 'Bearer ' . $token->getAccessToken(),
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ];
+
+ // Add X-Callback-Url if configured
+ if (!empty($this->config->getCallbackUri())) {
+ $headers['X-Callback-Url'] = $this->config->getCallbackUri();
+ }
+
+ $response = $this->client->request('POST', '/disbursement/v1_0/transfer', [
+ 'json' => $transferRequest->toArray(),
+ 'headers' => $headers
+ ]);
+
+ $responseCode = $response->getStatusCode();
+ if ($responseCode === 202) {
+ return $xReferenceId;
+ }
+
+ throw ExceptionFactory::create($response);
+ }
+
+ /**
+ * Get the status of a transfer. X-Reference-Id that was passed in the post is used as reference to the request.
+ *
+ * ### Sample usage
+ *
+ * ```
+ * $transferId = "07a461a4-e721-462b-81c6-b9aa2f8abf06";
+ * try {
+ * $result = $momo->disbursement()->getTransferStatus($transferId);
+ * if($result->isSuccessful()) {
+ * echo "Transfer successful";
+ * $result->getAmount(); // 1500
+ * }
+ * } catch (BadResourceException|InternalServerErrorException|ResourceNotFoundException $e) {
+ * // Request failed, do something else
+ * }
+ * ```
+ *
+ * @param string $transferId UUID of transaction to get result. Reference id used when creating the transfer.
+ * @return Transaction
+ * @throws ClientExceptionInterface
+ * @throws DecodingExceptionInterface
+ * @throws RedirectionExceptionInterface
+ * @throws ServerExceptionInterface
+ * @throws TransportExceptionInterface
+ * @throws MomoException
+ */
+ public function getTransferStatus(string $transferId): Transaction
+ {
+ $token = $this->getAccessToken();
+ $response = $this->client->request('GET', '/disbursement/v1_0/transfer/' . $transferId, [
+ 'headers' => [
+ 'Ocp-Apim-Subscription-Key' => $this->getSubscriptionKey(),
+ 'X-Target-Environment' => $this->environment,
+ 'Authorization' => 'Bearer ' . $token->getAccessToken(),
+ ]
+ ]);
+
+ $statusCode = $response->getStatusCode();
+ if ($statusCode === 200 || $statusCode === 202) {
+ return Transaction::parse($response->toArray());
+ }
+
+ throw ExceptionFactory::create($response);
+ }
+
+ /**
+ * Refund an amount to the payer.
+ *
+ * ### Sample usage
+ *
+ * ```
+ * $request = \Lepresk\MomoApi\Models\RefundRequest::make(
+ * '1000',
+ * '07a461a4-e721-462b-81c6-b9aa2f8abf06', // Original transaction ID
+ * 'REFUND-001'
+ * );
+ * $refundId = $momo->disbursement()->refund($request);
+ * ```
+ * @param RefundRequest $refundRequest
+ * @return string refund reference id
+ * @throws ClientExceptionInterface
+ * @throws DecodingExceptionInterface
+ * @throws MomoException
+ * @throws RedirectionExceptionInterface
+ * @throws ServerExceptionInterface
+ * @throws TransportExceptionInterface
+ */
+ public function refund(RefundRequest $refundRequest): string
+ {
+ $token = $this->getAccessToken();
+
+ $xReferenceId = Uuid::v4();
+
+ $headers = [
+ 'Ocp-Apim-Subscription-Key' => $this->getSubscriptionKey(),
+ 'X-Reference-Id' => $xReferenceId,
+ 'X-Target-Environment' => $this->environment,
+ 'Authorization' => 'Bearer ' . $token->getAccessToken(),
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ];
+
+ // Add X-Callback-Url if configured
+ if (!empty($this->config->getCallbackUri())) {
+ $headers['X-Callback-Url'] = $this->config->getCallbackUri();
+ }
+
+ $response = $this->client->request('POST', '/disbursement/v1_0/refund', [
+ 'json' => $refundRequest->toArray(),
+ 'headers' => $headers
+ ]);
+
+ $responseCode = $response->getStatusCode();
+ if ($responseCode === 202) {
+ return $xReferenceId;
+ }
+
+ throw ExceptionFactory::create($response);
+ }
+
+ /**
+ * Get the status of a refund. X-Reference-Id that was passed in the post is used as reference to the request.
+ *
+ * ### Sample usage
+ *
+ * ```
+ * $refundId = "07a461a4-e721-462b-81c6-b9aa2f8abf06";
+ * try {
+ * $result = $momo->disbursement()->getRefundStatus($refundId);
+ * if($result->isSuccessful()) {
+ * echo "Refund successful";
+ * $result->getAmount(); // 1500
+ * }
+ * } catch (BadResourceException|InternalServerErrorException|ResourceNotFoundException $e) {
+ * // Request failed, do something else
+ * }
+ * ```
+ *
+ * @param string $refundId UUID of transaction to get result. Reference id used when creating the refund.
+ * @return Transaction
+ * @throws ClientExceptionInterface
+ * @throws DecodingExceptionInterface
+ * @throws RedirectionExceptionInterface
+ * @throws ServerExceptionInterface
+ * @throws TransportExceptionInterface
+ * @throws MomoException
+ */
+ public function getRefundStatus(string $refundId): Transaction
+ {
+ $token = $this->getAccessToken();
+ $response = $this->client->request('GET', '/disbursement/v1_0/refund/' . $refundId, [
+ 'headers' => [
+ 'Ocp-Apim-Subscription-Key' => $this->getSubscriptionKey(),
+ 'X-Target-Environment' => $this->environment,
+ 'Authorization' => 'Bearer ' . $token->getAccessToken(),
+ ]
+ ]);
+
+ $statusCode = $response->getStatusCode();
+ if ($statusCode === 200 || $statusCode === 202) {
return Transaction::parse($response->toArray());
}
diff --git a/src/Products/SandboxApi.php b/src/Products/SandboxApi.php
index 98674f1..263e40f 100644
--- a/src/Products/SandboxApi.php
+++ b/src/Products/SandboxApi.php
@@ -3,7 +3,7 @@
namespace Lepresk\MomoApi\Products;
-use Lepresk\MomoApi\ApiProduct;
+use Lepresk\MomoApi\Abstracts\AbstractApiProduct;
use Lepresk\MomoApi\Exceptions\ExceptionFactory;
use Lepresk\MomoApi\Exceptions\MomoException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
@@ -12,7 +12,7 @@
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
-class SandboxApi extends ApiProduct
+class SandboxApi extends AbstractApiProduct
{
/**
diff --git a/src/Utilities.php b/src/Support/Uuid.php
similarity index 83%
rename from src/Utilities.php
rename to src/Support/Uuid.php
index 3f68b81..2f008b4 100644
--- a/src/Utilities.php
+++ b/src/Support/Uuid.php
@@ -1,11 +1,11 @@
assertInstanceOf(ApiToken::class, $token);
+ $this->assertStringStartsWith('eyJ', $token->getAccessToken());
+ $this->assertEquals('Bearer', $token->getTokenType());
+ $this->assertEquals(3600, $token->getExpiresIn());
+ }
+
+ public function testParsePaymentPending()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Collection/payment_pending.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('PENDING', $transaction->getStatus());
+ $this->assertEquals('1000', $transaction->getAmount());
+ $this->assertEquals('EUR', $transaction->getCurrency());
+ $this->assertEquals('46733123454', $transaction->getPayer());
+ $this->assertFalse($transaction->isSuccessful());
+ $this->assertFalse($transaction->isFailed());
+ $this->assertNull($transaction->getReason());
+ }
+
+ public function testParsePaymentSuccessful()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Collection/payment_successful.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('SUCCESSFUL', $transaction->getStatus());
+ $this->assertEquals('2500', $transaction->getAmount());
+ $this->assertEquals('EUR', $transaction->getCurrency());
+ $this->assertEquals('46733123453', $transaction->getPayer());
+ $this->assertEquals('987654321', $transaction->getFinancialTransactionId());
+ $this->assertTrue($transaction->isSuccessful());
+ $this->assertNull($transaction->getReason());
+ }
+
+ public function testParsePaymentFailedNotEnoughFunds()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Collection/payment_failed_not_enough_funds.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('FAILED', $transaction->getStatus());
+ $this->assertEquals('5000', $transaction->getAmount());
+ $this->assertEquals('XAF', $transaction->getCurrency());
+ $this->assertTrue($transaction->isFailed());
+ $this->assertFalse($transaction->isSuccessful());
+
+ $this->assertInstanceOf(ErrorReason::class, $transaction->getReason());
+ $this->assertEquals('NOT_ENOUGH_FUNDS', $transaction->getReason()->getCode());
+ $this->assertTrue($transaction->getReason()->isNotEnoughFunds());
+ }
+
+ public function testParsePaymentFailedPayerLimit()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Collection/payment_failed_payer_limit.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('FAILED', $transaction->getStatus());
+ $this->assertTrue($transaction->isFailed());
+
+ $this->assertInstanceOf(ErrorReason::class, $transaction->getReason());
+ $this->assertEquals('PAYER_LIMIT_REACHED', $transaction->getReason()->getCode());
+ $this->assertTrue($transaction->getReason()->isPayerLimitReached());
+ $this->assertFalse($transaction->getReason()->isNotEnoughFunds());
+ }
+
+ public function testParseBalance()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Collection/balance.json');
+ $data = json_decode($json, true);
+
+ $balance = AccountBalance::parse($data);
+
+ $this->assertEquals('50000', $balance->getAvailableBalance());
+ $this->assertEquals('EUR', $balance->getCurrency());
+ }
+
+ public function testParseErrorResourceNotFound()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Collection/error_resource_not_found.json');
+ $data = json_decode($json, true);
+
+ $error = ErrorReason::fromArray($data);
+
+ $this->assertEquals('RESOURCE_NOT_FOUND', $error->getCode());
+ $this->assertEquals('Requested resource was not found.', $error->getMessage());
+ }
+}
diff --git a/tests/FixtureTests/DisbursementFixtureTest.php b/tests/FixtureTests/DisbursementFixtureTest.php
new file mode 100644
index 0000000..be64e58
--- /dev/null
+++ b/tests/FixtureTests/DisbursementFixtureTest.php
@@ -0,0 +1,151 @@
+assertInstanceOf(ApiToken::class, $token);
+ $this->assertStringStartsWith('eyJ', $token->getAccessToken());
+ $this->assertEquals('Bearer', $token->getTokenType());
+ $this->assertEquals(3600, $token->getExpiresIn());
+ }
+
+ public function testParseTransferSuccessful()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Disbursement/transfer_successful.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('SUCCESSFUL', $transaction->getStatus());
+ $this->assertEquals('100', $transaction->getAmount());
+ $this->assertEquals('UGX', $transaction->getCurrency());
+ $this->assertEquals('4609274685', $transaction->getPayee());
+ $this->assertEquals('363440463', $transaction->getFinancialTransactionId());
+ $this->assertTrue($transaction->isSuccessful());
+ $this->assertNull($transaction->getReason());
+ }
+
+ public function testParseTransferFailedLimitReached()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Disbursement/transfer_failed_limit_reached.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('FAILED', $transaction->getStatus());
+ $this->assertTrue($transaction->isFailed());
+ $this->assertFalse($transaction->isSuccessful());
+
+ $this->assertInstanceOf(ErrorReason::class, $transaction->getReason());
+ $this->assertEquals('PAYER_LIMIT_REACHED', $transaction->getReason()->getCode());
+ $this->assertTrue($transaction->getReason()->isPayerLimitReached());
+ }
+
+ public function testParseTransferFailedNotEnoughFunds()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Disbursement/transfer_failed_not_enough_funds.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('FAILED', $transaction->getStatus());
+ $this->assertTrue($transaction->isFailed());
+
+ $this->assertInstanceOf(ErrorReason::class, $transaction->getReason());
+ $this->assertEquals('NOT_ENOUGH_FUNDS', $transaction->getReason()->getCode());
+ $this->assertTrue($transaction->getReason()->isNotEnoughFunds());
+ $this->assertFalse($transaction->getReason()->isPayerLimitReached());
+ }
+
+ public function testParseTransferPending()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Disbursement/transfer_pending.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('PENDING', $transaction->getStatus());
+ $this->assertEquals('250', $transaction->getAmount());
+ $this->assertEquals('XAF', $transaction->getCurrency());
+ $this->assertEquals('242068511358', $transaction->getPayee());
+ $this->assertFalse($transaction->isSuccessful());
+ $this->assertFalse($transaction->isFailed());
+ $this->assertNull($transaction->getReason());
+ }
+
+ public function testParseDepositSuccessful()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Disbursement/deposit_successful.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('SUCCESSFUL', $transaction->getStatus());
+ $this->assertEquals('500', $transaction->getAmount());
+ $this->assertEquals('EUR', $transaction->getCurrency());
+ $this->assertEquals('46733123454', $transaction->getPayee());
+ $this->assertTrue($transaction->isSuccessful());
+ }
+
+ public function testParseRefundSuccessful()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Disbursement/refund_successful.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('SUCCESSFUL', $transaction->getStatus());
+ $this->assertEquals('100', $transaction->getAmount());
+ $this->assertEquals('UGX', $transaction->getCurrency());
+ $this->assertTrue($transaction->isSuccessful());
+ }
+
+ public function testParseRefundPending()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Disbursement/refund_pending.json');
+ $data = json_decode($json, true);
+
+ $transaction = Transaction::parse($data);
+
+ $this->assertEquals('PENDING', $transaction->getStatus());
+ $this->assertEquals('150', $transaction->getAmount());
+ $this->assertFalse($transaction->isSuccessful());
+ }
+
+ public function testParseBalance()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Disbursement/balance.json');
+ $data = json_decode($json, true);
+
+ $balance = AccountBalance::parse($data);
+
+ $this->assertEquals('1000000', $balance->getAvailableBalance());
+ $this->assertEquals('XAF', $balance->getCurrency());
+ }
+
+ public function testParseErrorResourceNotFound()
+ {
+ $json = file_get_contents(__DIR__ . '/../Fixtures/Disbursement/error_resource_not_found.json');
+ $data = json_decode($json, true);
+
+ $error = ErrorReason::fromArray($data);
+
+ $this->assertEquals('RESOURCE_NOT_FOUND', $error->getCode());
+ $this->assertEquals('Requested resource was not found.', $error->getMessage());
+ }
+}
diff --git a/tests/FixtureTests/SandboxFixtureTest.php b/tests/FixtureTests/SandboxFixtureTest.php
new file mode 100644
index 0000000..a4005d4
--- /dev/null
+++ b/tests/FixtureTests/SandboxFixtureTest.php
@@ -0,0 +1,19 @@
+assertArrayHasKey('apiKey', $data);
+ $this->assertIsString($data['apiKey']);
+ $this->assertEquals(32, strlen($data['apiKey']));
+ }
+}
diff --git a/tests/Fixtures/Collection/balance.json b/tests/Fixtures/Collection/balance.json
new file mode 100644
index 0000000..8d806f6
--- /dev/null
+++ b/tests/Fixtures/Collection/balance.json
@@ -0,0 +1,4 @@
+{
+ "availableBalance": "50000",
+ "currency": "EUR"
+}
diff --git a/tests/Fixtures/Collection/error_resource_not_found.json b/tests/Fixtures/Collection/error_resource_not_found.json
new file mode 100644
index 0000000..99ab8e2
--- /dev/null
+++ b/tests/Fixtures/Collection/error_resource_not_found.json
@@ -0,0 +1,4 @@
+{
+ "code": "RESOURCE_NOT_FOUND",
+ "message": "Requested resource was not found."
+}
diff --git a/tests/Fixtures/Collection/payment_failed_not_enough_funds.json b/tests/Fixtures/Collection/payment_failed_not_enough_funds.json
new file mode 100644
index 0000000..18d6cd9
--- /dev/null
+++ b/tests/Fixtures/Collection/payment_failed_not_enough_funds.json
@@ -0,0 +1,16 @@
+{
+ "amount": "5000",
+ "currency": "XAF",
+ "externalId": "ORDER-789",
+ "payer": {
+ "partyIdType": "MSISDN",
+ "partyId": "242068511358"
+ },
+ "payerMessage": "Payment attempt",
+ "payeeNote": "",
+ "status": "FAILED",
+ "reason": {
+ "code": "NOT_ENOUGH_FUNDS",
+ "message": "The payer does not have enough funds."
+ }
+}
diff --git a/tests/Fixtures/Collection/payment_failed_payer_limit.json b/tests/Fixtures/Collection/payment_failed_payer_limit.json
new file mode 100644
index 0000000..51f0052
--- /dev/null
+++ b/tests/Fixtures/Collection/payment_failed_payer_limit.json
@@ -0,0 +1,16 @@
+{
+ "amount": "10000",
+ "currency": "XAF",
+ "externalId": "ORDER-999",
+ "payer": {
+ "partyIdType": "MSISDN",
+ "partyId": "242065599123"
+ },
+ "payerMessage": "",
+ "payeeNote": "",
+ "status": "FAILED",
+ "reason": {
+ "code": "PAYER_LIMIT_REACHED",
+ "message": "The payer's limit has been breached."
+ }
+}
diff --git a/tests/Fixtures/Collection/payment_pending.json b/tests/Fixtures/Collection/payment_pending.json
new file mode 100644
index 0000000..c5586d6
--- /dev/null
+++ b/tests/Fixtures/Collection/payment_pending.json
@@ -0,0 +1,13 @@
+{
+ "amount": "1000",
+ "currency": "EUR",
+ "financialTransactionId": "476321816",
+ "externalId": "ORDER-123",
+ "payer": {
+ "partyIdType": "MSISDN",
+ "partyId": "46733123454"
+ },
+ "payerMessage": "Payment for order",
+ "payeeNote": "Thank you",
+ "status": "PENDING"
+}
diff --git a/tests/Fixtures/Collection/payment_successful.json b/tests/Fixtures/Collection/payment_successful.json
new file mode 100644
index 0000000..f8787b2
--- /dev/null
+++ b/tests/Fixtures/Collection/payment_successful.json
@@ -0,0 +1,13 @@
+{
+ "amount": "2500",
+ "currency": "EUR",
+ "financialTransactionId": "987654321",
+ "externalId": "ORDER-456",
+ "payer": {
+ "partyIdType": "MSISDN",
+ "partyId": "46733123453"
+ },
+ "payerMessage": "Payment message",
+ "payeeNote": "A note",
+ "status": "SUCCESSFUL"
+}
diff --git a/tests/Fixtures/Collection/token_success.json b/tests/Fixtures/Collection/token_success.json
new file mode 100644
index 0000000..8368d3d
--- /dev/null
+++ b/tests/Fixtures/Collection/token_success.json
@@ -0,0 +1,5 @@
+{
+ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSMjU2In0.eyJjbGllbnRJZCI6IjJiZjA1OTJjLTQ3NWItNGY2YS05MTJlLWM1ZGY4NjFjMzAyNiIsImV4cGlyZXMiOiIyMDI0LTExLTIzVDE1OjMwOjAwLjAwMCIsInNlc3Npb25JZCI6IjI1NzBmZDIzLTUwZDUtNDQwMi05ODg3LWFkZWY2YzUzYTkxZiJ9.nKAIFzJx1fJsFdJ8YN6hMh0E4GdlJ_EXAMPLE",
+ "token_type": "Bearer",
+ "expires_in": 3600
+}
diff --git a/tests/Fixtures/Disbursement/balance.json b/tests/Fixtures/Disbursement/balance.json
new file mode 100644
index 0000000..abde1c7
--- /dev/null
+++ b/tests/Fixtures/Disbursement/balance.json
@@ -0,0 +1,4 @@
+{
+ "availableBalance": "1000000",
+ "currency": "XAF"
+}
diff --git a/tests/Fixtures/Disbursement/deposit_successful.json b/tests/Fixtures/Disbursement/deposit_successful.json
new file mode 100644
index 0000000..a1ac9df
--- /dev/null
+++ b/tests/Fixtures/Disbursement/deposit_successful.json
@@ -0,0 +1,13 @@
+{
+ "amount": "500",
+ "currency": "EUR",
+ "financialTransactionId": "789456123",
+ "externalId": "DEP-67890",
+ "payee": {
+ "partyIdType": "MSISDN",
+ "partyId": "46733123454"
+ },
+ "payerMessage": "Deposit",
+ "payeeNote": "Account credit",
+ "status": "SUCCESSFUL"
+}
diff --git a/tests/Fixtures/Disbursement/error_resource_not_found.json b/tests/Fixtures/Disbursement/error_resource_not_found.json
new file mode 100644
index 0000000..99ab8e2
--- /dev/null
+++ b/tests/Fixtures/Disbursement/error_resource_not_found.json
@@ -0,0 +1,4 @@
+{
+ "code": "RESOURCE_NOT_FOUND",
+ "message": "Requested resource was not found."
+}
diff --git a/tests/Fixtures/Disbursement/refund_pending.json b/tests/Fixtures/Disbursement/refund_pending.json
new file mode 100644
index 0000000..10f8917
--- /dev/null
+++ b/tests/Fixtures/Disbursement/refund_pending.json
@@ -0,0 +1,12 @@
+{
+ "amount": "150",
+ "currency": "XAF",
+ "externalId": "REF-98765",
+ "payee": {
+ "partyIdType": "MSISDN",
+ "partyId": "242068511358"
+ },
+ "payerMessage": "Refund request",
+ "payeeNote": "Order cancellation",
+ "status": "PENDING"
+}
diff --git a/tests/Fixtures/Disbursement/refund_successful.json b/tests/Fixtures/Disbursement/refund_successful.json
new file mode 100644
index 0000000..4c0a478
--- /dev/null
+++ b/tests/Fixtures/Disbursement/refund_successful.json
@@ -0,0 +1,11 @@
+{
+ "amount": "100",
+ "currency": "UGX",
+ "financialTransactionId": "363440463",
+ "externalId": "83453",
+ "payee": {
+ "partyIdType": "MSISDN",
+ "partyId": "4609274685"
+ },
+ "status": "SUCCESSFUL"
+}
diff --git a/tests/Fixtures/Disbursement/token_success.json b/tests/Fixtures/Disbursement/token_success.json
new file mode 100644
index 0000000..5459710
--- /dev/null
+++ b/tests/Fixtures/Disbursement/token_success.json
@@ -0,0 +1,5 @@
+{
+ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSMjU2In0.eyJjbGllbnRJZCI6ImExYjJjM2Q0LWU1ZjYtNGE1Yi05YzhkLTFlMmYzYTRiNWM2ZCIsImV4cGlyZXMiOiIyMDI0LTExLTI0VDEwOjQ1OjAwLjAwMCIsInNlc3Npb25JZCI6IjU0NzhlOWQyLTY3ZDctNDVhMi1hODg3LWJjOGU1ZjY0YTE4MyJ9.mDxYWvM2kQsJpE_EXAMPLE",
+ "token_type": "Bearer",
+ "expires_in": 3600
+}
diff --git a/tests/Fixtures/Disbursement/transfer_failed_limit_reached.json b/tests/Fixtures/Disbursement/transfer_failed_limit_reached.json
new file mode 100644
index 0000000..6aa1ac5
--- /dev/null
+++ b/tests/Fixtures/Disbursement/transfer_failed_limit_reached.json
@@ -0,0 +1,14 @@
+{
+ "amount": "100",
+ "currency": "UGX",
+ "externalId": "83453",
+ "payee": {
+ "partyIdType": "MSISDN",
+ "partyId": "4609274685"
+ },
+ "status": "FAILED",
+ "reason": {
+ "code": "PAYER_LIMIT_REACHED",
+ "message": "The payer's limit has been breached."
+ }
+}
diff --git a/tests/Fixtures/Disbursement/transfer_failed_not_enough_funds.json b/tests/Fixtures/Disbursement/transfer_failed_not_enough_funds.json
new file mode 100644
index 0000000..45b236e
--- /dev/null
+++ b/tests/Fixtures/Disbursement/transfer_failed_not_enough_funds.json
@@ -0,0 +1,14 @@
+{
+ "amount": "100",
+ "currency": "UGX",
+ "externalId": "83453",
+ "payee": {
+ "partyIdType": "MSISDN",
+ "partyId": "4609274685"
+ },
+ "status": "FAILED",
+ "reason": {
+ "code": "NOT_ENOUGH_FUNDS",
+ "message": "The payer does not have enough funds."
+ }
+}
diff --git a/tests/Fixtures/Disbursement/transfer_pending.json b/tests/Fixtures/Disbursement/transfer_pending.json
new file mode 100644
index 0000000..c3a59d2
--- /dev/null
+++ b/tests/Fixtures/Disbursement/transfer_pending.json
@@ -0,0 +1,13 @@
+{
+ "amount": "250",
+ "currency": "XAF",
+ "financialTransactionId": "123987456",
+ "externalId": "TRX-12345",
+ "payee": {
+ "partyIdType": "MSISDN",
+ "partyId": "242068511358"
+ },
+ "payerMessage": "Transfer payment",
+ "payeeNote": "Salary payment",
+ "status": "PENDING"
+}
diff --git a/tests/Fixtures/Disbursement/transfer_successful.json b/tests/Fixtures/Disbursement/transfer_successful.json
new file mode 100644
index 0000000..4c0a478
--- /dev/null
+++ b/tests/Fixtures/Disbursement/transfer_successful.json
@@ -0,0 +1,11 @@
+{
+ "amount": "100",
+ "currency": "UGX",
+ "financialTransactionId": "363440463",
+ "externalId": "83453",
+ "payee": {
+ "partyIdType": "MSISDN",
+ "partyId": "4609274685"
+ },
+ "status": "SUCCESSFUL"
+}
diff --git a/tests/Fixtures/Sandbox/apiuser_key.json b/tests/Fixtures/Sandbox/apiuser_key.json
new file mode 100644
index 0000000..5129d78
--- /dev/null
+++ b/tests/Fixtures/Sandbox/apiuser_key.json
@@ -0,0 +1,3 @@
+{
+ "apiKey": "b0f527d4f6f54b7a9b8c3d2e1f0a9b8c"
+}
diff --git a/tests/MomoApiTest.php b/tests/MomoApiTest.php
index a250951..4870a6b 100644
--- a/tests/MomoApiTest.php
+++ b/tests/MomoApiTest.php
@@ -19,18 +19,19 @@ public function testBaseUrlByEnvironnment()
$this->assertEquals(MomoApi::PRODUCTION_URL, $baseUrl);
}
- public function testFailGetCollectionWithoutConfig()
+ public function testFailGetCollectionWithoutRequiredConfig()
{
$this->expectException(InvalidArgumentException::class);
- $momo = MomoApi::create(MomoApi::ENVIRONMENT_SANDBOX);
- $momo->collection();
+ MomoApi::collection([
+ 'environment' => 'sandbox',
+ ]);
}
public function testFailUseSandboxInProduction()
{
$this->expectException(InvalidArgumentException::class);
$momo = MomoApi::create(MomoApi::ENVIRONMENT_MTN_CONGO);
- $momo->sandbox('subscriptionLey');
+ $momo->sandbox('subscriptionKey');
}
public function testUsingMockedClient()
diff --git a/tests/Unit/Models/ErrorReasonTest.php b/tests/Unit/Models/ErrorReasonTest.php
new file mode 100644
index 0000000..6a8d105
--- /dev/null
+++ b/tests/Unit/Models/ErrorReasonTest.php
@@ -0,0 +1,76 @@
+assertEquals('NOT_ENOUGH_FUNDS', $reason->getCode());
+ $this->assertEquals('Insufficient balance', $reason->getMessage());
+ }
+
+ public function testFromArray()
+ {
+ $data = [
+ 'code' => 'PAYER_LIMIT_REACHED',
+ 'message' => 'Transaction limit exceeded'
+ ];
+
+ $reason = ErrorReason::fromArray($data);
+
+ $this->assertEquals('PAYER_LIMIT_REACHED', $reason->getCode());
+ $this->assertEquals('Transaction limit exceeded', $reason->getMessage());
+ }
+
+ public function testIsMethod()
+ {
+ $reason = new ErrorReason('NOT_ENOUGH_FUNDS', 'Insufficient balance');
+
+ $this->assertTrue($reason->is('NOT_ENOUGH_FUNDS'));
+ $this->assertFalse($reason->is('PAYER_LIMIT_REACHED'));
+ }
+
+ public function testIsNotEnoughFunds()
+ {
+ $reason = new ErrorReason('NOT_ENOUGH_FUNDS', 'Insufficient balance');
+
+ $this->assertTrue($reason->isNotEnoughFunds());
+ $this->assertFalse($reason->isPayerLimitReached());
+ }
+
+ public function testIsPayerLimitReached()
+ {
+ $reason = new ErrorReason('PAYER_LIMIT_REACHED', 'Limit exceeded');
+
+ $this->assertTrue($reason->isPayerLimitReached());
+ $this->assertFalse($reason->isNotEnoughFunds());
+ }
+
+ public function testIsPayeeNotFound()
+ {
+ $reason = new ErrorReason('PAYEE_NOT_FOUND', 'Payee not found');
+
+ $this->assertTrue($reason->isPayeeNotFound());
+ }
+
+ public function testToString()
+ {
+ $reason = new ErrorReason('NOT_ENOUGH_FUNDS', 'Insufficient balance');
+
+ $this->assertEquals('[NOT_ENOUGH_FUNDS] Insufficient balance', (string)$reason);
+ }
+
+ public function testErrorCodeConstants()
+ {
+ $this->assertEquals('PAYEE_NOT_FOUND', ErrorReason::PAYEE_NOT_FOUND);
+ $this->assertEquals('NOT_ENOUGH_FUNDS', ErrorReason::NOT_ENOUGH_FUNDS);
+ $this->assertEquals('PAYER_LIMIT_REACHED', ErrorReason::PAYER_LIMIT_REACHED);
+ }
+}
diff --git a/tests/Unit/Models/RefundRequestTest.php b/tests/Unit/Models/RefundRequestTest.php
new file mode 100644
index 0000000..ae7a279
--- /dev/null
+++ b/tests/Unit/Models/RefundRequestTest.php
@@ -0,0 +1,70 @@
+assertEquals('1000', $request->getAmount());
+ $this->assertEquals('XAF', $request->getCurrency());
+ $this->assertEquals('REFUND-001', $request->getExternalId());
+ $this->assertEquals($originalTxId, $request->getReferenceIdToRefund());
+ $this->assertEquals('Refund message', $request->getPayerMessage());
+ $this->assertEquals('Refund note', $request->getPayeeNote());
+ }
+
+ public function testMakeFactory()
+ {
+ $originalTxId = '07a461a4-e721-462b-81c6-b9aa2f8abf06';
+
+ $request = RefundRequest::make(
+ '500',
+ $originalTxId,
+ 'REFUND-002'
+ );
+
+ $this->assertEquals('500', $request->getAmount());
+ $this->assertEquals('XAF', $request->getCurrency());
+ $this->assertEquals('REFUND-002', $request->getExternalId());
+ $this->assertEquals($originalTxId, $request->getReferenceIdToRefund());
+ }
+
+ public function testToArray()
+ {
+ $originalTxId = 'a1b2c3d4-e5f6-4a5b-9c8d-1e2f3a4b5c6d';
+
+ $request = new RefundRequest(
+ '2500',
+ 'EUR',
+ 'REF-123',
+ $originalTxId,
+ 'Refund message',
+ 'Refund note'
+ );
+
+ $array = $request->toArray();
+
+ $this->assertEquals('2500', $array['amount']);
+ $this->assertEquals('EUR', $array['currency']);
+ $this->assertEquals('REF-123', $array['externalId']);
+ $this->assertEquals($originalTxId, $array['referenceIdToRefund']);
+ $this->assertEquals('Refund message', $array['payerMessage']);
+ $this->assertEquals('Refund note', $array['payeeNote']);
+ }
+}
diff --git a/tests/Unit/Models/TransferRequestTest.php b/tests/Unit/Models/TransferRequestTest.php
new file mode 100644
index 0000000..d828348
--- /dev/null
+++ b/tests/Unit/Models/TransferRequestTest.php
@@ -0,0 +1,65 @@
+assertEquals('1000', $request->getAmount());
+ $this->assertEquals('XAF', $request->getCurrency());
+ $this->assertEquals('TRANSFER-001', $request->getExternalId());
+ $this->assertEquals('242068511358', $request->getPayee());
+ $this->assertEquals('Salary payment', $request->getPayerMessage());
+ $this->assertEquals('Monthly salary', $request->getPayeeNote());
+ }
+
+ public function testMakeFactory()
+ {
+ $request = TransferRequest::make(
+ '5000',
+ '242068511358',
+ 'TRANSFER-002'
+ );
+
+ $this->assertEquals('5000', $request->getAmount());
+ $this->assertEquals('XAF', $request->getCurrency());
+ $this->assertEquals('TRANSFER-002', $request->getExternalId());
+ $this->assertEquals('242068511358', $request->getPayee());
+ }
+
+ public function testToArray()
+ {
+ $request = new TransferRequest(
+ '2500',
+ 'EUR',
+ 'TRANS-123',
+ '33612345678',
+ 'Transfer message',
+ 'Transfer note'
+ );
+
+ $array = $request->toArray();
+
+ $this->assertEquals('2500', $array['amount']);
+ $this->assertEquals('EUR', $array['currency']);
+ $this->assertEquals('TRANS-123', $array['externalId']);
+ $this->assertEquals('MSISDN', $array['payee']['partyIdType']);
+ $this->assertEquals('33612345678', $array['payee']['partyId']);
+ $this->assertEquals('Transfer message', $array['payerMessage']);
+ $this->assertEquals('Transfer note', $array['payeeNote']);
+ }
+}
diff --git a/tests/Products/CollectionApiTest.php b/tests/Unit/Products/CollectionApiTest.php
similarity index 61%
rename from tests/Products/CollectionApiTest.php
rename to tests/Unit/Products/CollectionApiTest.php
index 39cf918..4fae228 100644
--- a/tests/Products/CollectionApiTest.php
+++ b/tests/Unit/Products/CollectionApiTest.php
@@ -1,12 +1,12 @@
provideClient($expectedRequests));
- $momo = MomoApi::create(MomoApi::ENVIRONMENT_SANDBOX);
- $momo->setupCollection(Config::collection($subscriptionKey, "apiUser", "apiKey", "aCalllback"));
- $momo->collection()->getAccessToken();
+ $collection = MomoApi::collection([
+ 'environment' => 'sandbox',
+ 'subscription_key' => $subscriptionKey,
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ 'callback_url' => 'aCalllback'
+ ]);
+ $collection->getAccessToken();
}
public function testGetAccessToken()
@@ -48,9 +53,14 @@ function ($method, $url, $options) use ($sampleToken): MockResponse {
];
MomoApi::useClient($this->provideClient($expectedRequests));
- $momo = MomoApi::create(MomoApi::ENVIRONMENT_SANDBOX);
- $momo->setupCollection(Config::collection('testSubKey', "apiUser", "apiKey", "aCalllback"));
- $token = $momo->collection()->getAccessToken();
+ $collection = MomoApi::collection([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ 'callback_url' => 'aCalllback'
+ ]);
+ $token = $collection->getAccessToken();
$this->assertEquals($sampleToken['access_token'], $token->getAccessToken());
}
@@ -68,9 +78,14 @@ function ($method, $url, $options): MockResponse {
$this->expectException(MomoException::class);
MomoApi::useClient($this->provideClient($expectedRequests));
- $momo = MomoApi::create(MomoApi::ENVIRONMENT_SANDBOX);
- $momo->setupCollection(Config::collection('testSubKey', "apiUser", "apiKey", "aCalllback"));
- $momo->collection()->getAccessToken();
+ $collection = MomoApi::collection([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ 'callback_url' => 'aCalllback'
+ ]);
+ $collection->getAccessToken();
}
public function testRequestToPay()
@@ -86,12 +101,17 @@ function ($method, $url, $options): MockResponse {
];
MomoApi::useClient($this->provideClient($expectedRequests));
- $momo = MomoApi::create(MomoApi::ENVIRONMENT_SANDBOX);
- $momo->setupCollection(Config::collection('testSubKey', "apiUser", "apiKey", "aCalllback"));
+ $collection = MomoApi::collection([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ 'callback_url' => 'aCalllback'
+ ]);
$request = new PaymentRequest(1000, 'EUR', 'ORDER-10', '46733123454', '', '');
- $paymentId = $momo->collection()->requestToPay($request);
+ $paymentId = $collection->requestToPay($request);
$this->assertValidGuidV4($paymentId);
}
@@ -118,18 +138,23 @@ function (): MockResponse {
];
MomoApi::useClient($this->provideClient($expectedRequests));
- $momo = MomoApi::create(MomoApi::ENVIRONMENT_SANDBOX);
- $momo->setupCollection(Config::collection('testSubKey', "apiUser", "apiKey", "aCalllback"));
+ $collection = MomoApi::collection([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ 'callback_url' => 'aCalllback'
+ ]);
$request = new PaymentRequest(1000, 'EUR', 'ORDER-10', '46733123454', '', '');
$this->expectException(MomoException::class);
- $momo->collection()->requestToPay($request);
+ $collection->requestToPay($request);
}
public function testCheckTransactionStats()
{
- $paymentId = Utilities::guidv4();
+ $paymentId = Uuid::v4();
$data = [
"financialTransactionId" => "476321816",
"externalId" => "ORDER-10",
@@ -154,12 +179,45 @@ function ($method, $url) use ($paymentId, $data): MockResponse {
];
MomoApi::useClient($this->provideClient($expectedRequests));
- $momo = MomoApi::create(MomoApi::ENVIRONMENT_SANDBOX);
- $momo->setupCollection(Config::collection('testSubKey', "apiUser", "apiKey", "aCalllback"));
- $transaction = $momo->collection()->checkRequestStatus($paymentId);
+ $collection = MomoApi::collection([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ 'callback_url' => 'aCalllback'
+ ]);
+ $transaction = $collection->getPaymentStatus($paymentId);
$this->assertEquals($data['status'], $transaction->getStatus());
$this->assertEquals($data['payer']['partyId'], $transaction->getPayer());
$this->assertTrue($transaction->isSuccessful());
}
+
+ public function testQuickPay()
+ {
+ $expectedRequests = [
+ $this->provideTokenResponse(),
+ function ($method, $url, $options): MockResponse {
+ $this->assertSame('POST', $method);
+ $this->assertSame($this->baseUrl() . '/collection/v1_0/requesttopay', $url);
+ $body = json_decode($options['body'], true);
+ $this->assertEquals('1000', $body['amount']);
+ $this->assertEquals('242068511358', $body['payer']['partyId']);
+ $this->assertEquals('ORDER-123', $body['externalId']);
+ return new MockResponse('{}', ['http_code' => 202]);
+ },
+ ];
+
+ MomoApi::useClient($this->provideClient($expectedRequests));
+ $collection = MomoApi::collection([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ ]);
+
+ $paymentId = $collection->quickPay('1000', '242068511358', 'ORDER-123');
+
+ $this->assertValidGuidV4($paymentId);
+ }
}
\ No newline at end of file
diff --git a/tests/Unit/Products/DisbursementApiTest.php b/tests/Unit/Products/DisbursementApiTest.php
new file mode 100644
index 0000000..3319246
--- /dev/null
+++ b/tests/Unit/Products/DisbursementApiTest.php
@@ -0,0 +1,189 @@
+assertSame($this->baseUrl() . '/disbursement/token/', $url);
+ return new MockResponse(json_encode([
+ 'access_token' => 'testToken',
+ 'expires_in' => 3600,
+ 'token_type' => 'Bearer'
+ ]), ['http_code' => 200]);
+ };
+ }
+
+ public function testTransfer()
+ {
+ $expectedRequests = [
+ $this->provideTokenResponse(),
+ function ($method, $url, $options): MockResponse {
+ $this->assertSame('POST', $method);
+ $this->assertSame($this->baseUrl() . '/disbursement/v1_0/transfer', $url);
+ $body = json_decode($options['body'], true);
+ $this->assertArrayHasKey('payee', $body);
+ $this->assertEquals('242068511358', $body['payee']['partyId']);
+ return new MockResponse('{}', ['http_code' => 202]);
+ },
+ ];
+
+ MomoApi::useClient($this->provideClient($expectedRequests));
+ $disbursement = MomoApi::disbursement([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ 'callback_url' => 'https://example.com/callback'
+ ]);
+
+ $request = TransferRequest::make('1000', '242068511358', 'TRANSFER-001');
+ $transferId = $disbursement->transfer($request);
+
+ $this->assertValidGuidV4($transferId);
+ }
+
+ public function testGetTransferStatus()
+ {
+ $transferId = '07a461a4-e721-462b-81c6-b9aa2f8abf06';
+ $data = [
+ "financialTransactionId" => "123456789",
+ "externalId" => "TRANSFER-001",
+ "amount" => "1000",
+ "currency" => "XAF",
+ "payee" => [
+ "partyIdType" => "MSISDN",
+ "partyId" => "242068511358"
+ ],
+ "payerMessage" => "Transfer message",
+ "payeeNote" => "Transfer note",
+ "status" => "SUCCESSFUL"
+ ];
+
+ $expectedRequests = [
+ $this->provideTokenResponse(),
+ function ($method, $url) use ($transferId, $data): MockResponse {
+ $this->assertSame('GET', $method);
+ $this->assertSame($this->baseUrl() . "/disbursement/v1_0/transfer/$transferId", $url);
+ return new MockResponse(json_encode($data), ['http_code' => 200]);
+ },
+ ];
+
+ MomoApi::useClient($this->provideClient($expectedRequests));
+ $disbursement = MomoApi::disbursement([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ 'callback_url' => ''
+ ]);
+
+ $transaction = $disbursement->getTransferStatus($transferId);
+
+ $this->assertEquals($data['status'], $transaction->getStatus());
+ $this->assertEquals($data['payee']['partyId'], $transaction->getPayee());
+ $this->assertTrue($transaction->isSuccessful());
+ }
+
+ public function testRefund()
+ {
+ $expectedRequests = [
+ $this->provideTokenResponse(),
+ function ($method, $url, $options): MockResponse {
+ $this->assertSame('POST', $method);
+ $this->assertSame($this->baseUrl() . '/disbursement/v1_0/refund', $url);
+ $body = json_decode($options['body'], true);
+ $this->assertArrayHasKey('referenceIdToRefund', $body);
+ $this->assertEquals('07a461a4-e721-462b-81c6-b9aa2f8abf06', $body['referenceIdToRefund']);
+ return new MockResponse('{}', ['http_code' => 202]);
+ },
+ ];
+
+ MomoApi::useClient($this->provideClient($expectedRequests));
+ $disbursement = MomoApi::disbursement([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ ]);
+
+ $request = RefundRequest::make('500', '07a461a4-e721-462b-81c6-b9aa2f8abf06', 'REFUND-001');
+ $refundId = $disbursement->refund($request);
+
+ $this->assertValidGuidV4($refundId);
+ }
+
+ public function testGetRefundStatus()
+ {
+ $refundId = '07a461a4-e721-462b-81c6-b9aa2f8abf06';
+ $data = [
+ "financialTransactionId" => "987654321",
+ "externalId" => "REFUND-001",
+ "amount" => "500",
+ "currency" => "XAF",
+ "payer" => [
+ "partyIdType" => "MSISDN",
+ "partyId" => "242068511358"
+ ],
+ "payerMessage" => "",
+ "payeeNote" => "",
+ "status" => "SUCCESSFUL"
+ ];
+
+ $expectedRequests = [
+ $this->provideTokenResponse(),
+ function ($method, $url) use ($refundId, $data): MockResponse {
+ $this->assertSame('GET', $method);
+ $this->assertSame($this->baseUrl() . "/disbursement/v1_0/refund/$refundId", $url);
+ return new MockResponse(json_encode($data), ['http_code' => 200]);
+ },
+ ];
+
+ MomoApi::useClient($this->provideClient($expectedRequests));
+ $disbursement = MomoApi::disbursement([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ ]);
+
+ $transaction = $disbursement->getRefundStatus($refundId);
+
+ $this->assertEquals($data['status'], $transaction->getStatus());
+ $this->assertTrue($transaction->isSuccessful());
+ }
+
+ public function testCallbackUrlHeader()
+ {
+ $callbackUrl = 'https://example.com/webhook';
+
+ $expectedRequests = [
+ $this->provideTokenResponse(),
+ function ($method, $url, $options) use ($callbackUrl): MockResponse {
+ $this->assertContains("X-Callback-Url: $callbackUrl", $options['headers']);
+ return new MockResponse('{}', ['http_code' => 202]);
+ },
+ ];
+
+ MomoApi::useClient($this->provideClient($expectedRequests));
+ $disbursement = MomoApi::disbursement([
+ 'environment' => 'sandbox',
+ 'subscription_key' => 'testSubKey',
+ 'api_user' => 'apiUser',
+ 'api_key' => 'apiKey',
+ 'callback_url' => $callbackUrl
+ ]);
+
+ $request = TransferRequest::make('1000', '242068511358', 'TRANSFER-001');
+ $disbursement->transfer($request);
+ }
+}
diff --git a/tests/Products/SandboxApiTest.php b/tests/Unit/Products/SandboxApiTest.php
similarity index 93%
rename from tests/Products/SandboxApiTest.php
rename to tests/Unit/Products/SandboxApiTest.php
index 281b405..c317e94 100644
--- a/tests/Products/SandboxApiTest.php
+++ b/tests/Unit/Products/SandboxApiTest.php
@@ -1,13 +1,13 @@
'https://my-domain.com/callback',
'targetEnvironment' => 'sandbox',
];
- $uuid = Utilities::guidv4();
+ $uuid = Uuid::v4();
$expectedRequests = [
function ($method, $url) use ($user, $uuid): MockResponse {
@@ -115,13 +115,13 @@ function ($method, $url) use ($user, $uuid): MockResponse {
public function test404IfApiUserNotFound()
{
- $this->expectException(RessourceNotFoundException::class);
+ $this->expectException(ResourceNotFoundException::class);
$user = [
'providerCallbackHost' => 'https://my-domain.com/callback',
'targetEnvironment' => 'sandbox',
];
- $uuid = Utilities::guidv4();
+ $uuid = Uuid::v4();
$expectedRequests = [
function () use ($user, $uuid): MockResponse {
@@ -136,7 +136,7 @@ function () use ($user, $uuid): MockResponse {
public function testCreateApiKey()
{
- $apiUser = Utilities::guidv4();
+ $apiUser = Uuid::v4();
$expectedRequests = [
function ($method, $url) use ($apiUser): MockResponse {
@@ -155,7 +155,7 @@ function ($method, $url) use ($apiUser): MockResponse {
public function testThrowIfUnableToCreateApiKey()
{
- $apiUser = Utilities::guidv4();
+ $apiUser = Uuid::v4();
$expectedRequests = [
function () use ($apiUser): MockResponse {
diff --git a/tests/UtilitiesTest.php b/tests/UuidTest.php
similarity index 57%
rename from tests/UtilitiesTest.php
rename to tests/UuidTest.php
index 2fc6b64..53e0376 100644
--- a/tests/UtilitiesTest.php
+++ b/tests/UuidTest.php
@@ -3,13 +3,13 @@
namespace Tests;
-use Lepresk\MomoApi\Utilities;
+use Lepresk\MomoApi\Support\Uuid;
-class UtilitiesTest extends TestCase
+class UuidTest extends TestCase
{
public function testValidGuidv4()
{
- $guidv4 = Utilities::guidv4();
+ $guidv4 = Uuid::v4();
$this->assertValidGuidV4($guidv4);
}
}
\ No newline at end of file