Repository: SmoDav/mpesa Branch: master Commit: 8191b77fdab3 Files: 46 Total size: 82.0 KB Directory structure: gitextract_nvdhfbhw/ ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── .php_cs ├── .phpunit.result.cache ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── composer.json ├── config/ │ └── mpesa.php ├── phpunit.xml ├── sonar-project.properties ├── src/ │ └── Mpesa/ │ ├── Auth/ │ │ └── Authenticator.php │ ├── C2B/ │ │ ├── Identity.php │ │ ├── Registrar.php │ │ ├── STK.php │ │ └── Simulate.php │ ├── Contracts/ │ │ ├── CacheStore.php │ │ └── ConfigurationStore.php │ ├── Engine/ │ │ └── Core.php │ ├── Exceptions/ │ │ ├── ConfigurationException.php │ │ └── ErrorException.php │ ├── Laravel/ │ │ ├── Facades/ │ │ │ ├── Identity.php │ │ │ ├── Registrar.php │ │ │ ├── STK.php │ │ │ └── Simulate.php │ │ ├── ServiceProvider.php │ │ └── Stores/ │ │ ├── LaravelCache.php │ │ └── LaravelConfig.php │ ├── Native/ │ │ ├── NativeCache.php │ │ └── NativeConfig.php │ ├── Repositories/ │ │ ├── ConfigurationRepository.php │ │ └── Endpoint.php │ ├── Support/ │ │ ├── Installer.php │ │ └── helpers.php │ └── Traits/ │ ├── UsesCore.php │ ├── UsesSTKMethods.php │ └── Validates.php └── tests/ ├── TestCase.php ├── Unit/ │ ├── AuthenticatorTest.php │ ├── ConfigurationRepositoryTest.php │ ├── NativeImplementationsTest.php │ ├── RegistrarTest.php │ └── STKTest.php └── files/ ├── .gitignore └── mpesa.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Build concurrency: group: production cancel-in-progress: true on: push: branches: - master pull_request: types: [opened, synchronize, reopened] jobs: tests: name: Tests and SonarCloud runs-on: ubuntu-latest steps: - uses: shivammathur/setup-php@v2 with: php-version: "8.4" coverage: "xdebug" - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Install composer dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - name: Static Checks & Test run: ./vendor/bin/phpunit - name: SonarCloud Scan uses: SonarSource/sonarqube-scan-action@v7.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} ================================================ FILE: .gitignore ================================================ /.idea /vendor composer.lock .php_cs.cache index.php /cache /.phpintel .php-cs-fixer.cache clover.xml junit.xml ================================================ FILE: .php_cs ================================================ setRiskyAllowed(true) ->setRules( [ 'array_syntax' => ['syntax' => 'short'], 'binary_operator_spaces' => [ 'align_double_arrow' => true, 'align_equals' => true ], 'blank_line_after_namespace' => true, 'blank_line_before_return' => true, 'braces' => true, 'cast_spaces' => true, 'concat_space' => ['spacing' => 'one'], 'elseif' => true, 'encoding' => true, 'full_opening_tag' => true, 'function_declaration' => true, 'indentation_type' => true, 'line_ending' => true, 'lowercase_constants' => true, 'lowercase_keywords' => true, 'method_argument_space' => true, 'native_function_invocation' => true, 'no_alias_functions' => true, 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, 'no_closing_tag' => true, 'no_empty_phpdoc' => true, 'no_empty_statement' => true, 'no_extra_consecutive_blank_lines' => true, 'no_leading_namespace_whitespace' => true, 'no_singleline_whitespace_before_semicolons' => true, 'no_spaces_after_function_name' => true, 'no_spaces_inside_parenthesis' => true, 'no_trailing_comma_in_list_call' => true, 'no_trailing_whitespace' => true, 'no_unused_imports' => true, 'no_whitespace_in_blank_line' => true, 'phpdoc_align' => true, 'phpdoc_indent' => true, 'phpdoc_no_access' => true, 'phpdoc_no_empty_return' => true, 'phpdoc_no_package' => true, 'phpdoc_scalar' => true, 'phpdoc_separation' => true, 'phpdoc_to_comment' => true, 'phpdoc_trim' => true, 'phpdoc_types' => true, 'phpdoc_var_without_name' => true, 'self_accessor' => true, 'simplified_null_return' => true, 'single_blank_line_at_eof' => true, 'single_import_per_statement' => true, 'single_line_after_imports' => true, 'single_quote' => true, 'ternary_operator_spaces' => true, 'trim_array_spaces' => true, 'visibility_required' => true, ] ) ->setFinder( PhpCsFixer\Finder::create() ->files() ->in(__DIR__ . '/src') ->in(__DIR__ . '/tests') ->name('*.php') ); ================================================ FILE: .phpunit.result.cache ================================================ {"version":1,"defects":{"SmoDav\\Mpesa\\Tests\\Unit\\AuthenticatorTest::testCanAuthenticateUsingRequestAndCached":8,"SmoDav\\Mpesa\\Tests\\Unit\\ConfigurationRepositoryTest::testCanConstruct":8,"SmoDav\\Mpesa\\Tests\\Unit\\ConfigurationRepositoryTest::testCanExtractConfigAndAccountValues":8,"SmoDav\\Mpesa\\Tests\\Unit\\ConfigurationRepositoryTest::testCanResolveUrl":8,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testCanUseNativeConfig":8,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testCanGetWholeConfiguration":7,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testCanConstructWithoutConfig":8,"SmoDav\\Mpesa\\Tests\\Unit\\RegistrarTest::testRegisterUrls":8,"SmoDav\\Mpesa\\Tests\\Unit\\STKTest::testPushRequest":8,"SmoDav\\Mpesa\\Tests\\Unit\\STKTest::testValidateTransaction":8},"times":{"SmoDav\\Mpesa\\Tests\\Unit\\AuthenticatorTest::testCanAuthenticateUsingRequestAndCached":0.06,"SmoDav\\Mpesa\\Tests\\Unit\\ConfigurationRepositoryTest::testCanConstruct":0,"SmoDav\\Mpesa\\Tests\\Unit\\ConfigurationRepositoryTest::testCanExtractConfigAndAccountValues":0,"SmoDav\\Mpesa\\Tests\\Unit\\ConfigurationRepositoryTest::testCanResolveUrl":0,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testCanUseNativeConfig":0,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testCanGetWholeConfiguration":0,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testCanUseNativeCache":0,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testCanOverwriteCacheItem":0.001,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testCacheExpires":1.001,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testCanConstructWithoutConfig":0.03,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testTTLImplementation":0,"SmoDav\\Mpesa\\Tests\\Unit\\NativeImplementationsTest::testShouldPullCacheItem":0.001,"SmoDav\\Mpesa\\Tests\\Unit\\RegistrarTest::testRegisterUrls":0.011,"SmoDav\\Mpesa\\Tests\\Unit\\STKTest::testPushRequest":0.02,"SmoDav\\Mpesa\\Tests\\Unit\\STKTest::testValidateTransaction":0.002}} ================================================ FILE: .travis.yml ================================================ dist: trusty language: php php: - '8.2' - '8.3' - '8.4' - '8.5' before_script: - composer self-update - composer install --prefer-source --no-interaction --dev script: vendor/bin/phpunit ================================================ FILE: CHANGELOG.md ================================================ ## CHANGELOG ### 2019-09-26 :: v5.0.0 #### NativeCache - Takes a custom storage path as first constructor argument. If none is provided it uses the default configuration cache location. >v5 takes the `ConfigurationStore` as the first argument. - Uses seconds instead of minutes as ttl to conform to PSR-16 spec. - Added a new method `pull` to extract the key and delete it from the cache if present. #### NativeConfig - Takes the path of the custom configuration file location as the first argument. If none is provided it uses the default configuration file location. - Moved the extracting of configuration values to the `ConfigurationRepository`. #### Core - The core can now be initialized without passing the Configuration and Cache store argument. Only the Client is required. If either of the Configuration and Cache stores are not provided, the default will be used in vanilla, and the Laravel defaults will be used in Laravel. - The account to be used is now set via the `Core` method `useAccount`. #### Authenticator - Removed all static methods. - Added a `flushTokens` method to remove all authentication tokens from the store. `$core->auth()->flushTokens();` or `app(Core::class)->auth()->flushTokens();` - #### Authenticator - `Identity`, `Registrar`, `Simulate` and `STK` all receive the `Core` instance as the first constructor argument. ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2016 SmoDav Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # M-PESA API Package [![Build Status](https://travis-ci.org/SmoDav/mpesa.svg?branch=master)](https://travis-ci.org/SmoDav/mpesa) [![Total Downloads](https://poser.pugx.org/smodav/mpesa/d/total.svg)](https://packagist.org/packages/smodav/mpesa) [![Latest Stable Version](https://poser.pugx.org/smodav/mpesa/v/stable.svg)](https://packagist.org/packages/smodav/mpesa) [![Latest Unstable Version](https://poser.pugx.org/smodav/mpesa/v/unstable.svg)](https://packagist.org/packages/smodav/mpesa) [![License](https://poser.pugx.org/smodav/mpesa/license.svg)](https://packagist.org/packages/smodav/mpesa) This is a PHP package for the Safaricom's M-Pesa API. The API allows a merchant to initiate C2B online checkout (paybill via web) transactions. The merchant submits authentication details, transaction details, callback url and callback method. After request submission, the merchant receives instant feedback with validity status of their requests. The C2B API handles customer validation and authentication via USSD push. The customer then confirms the transaction. If the validation of the customer fails or the customer declines the transaction, the API makes a callback to merchant. Otherwise the transaction is processed and its status is made through a callback. If you enjoy using this package, please take a moment and [buy me some coffee.](https://rave.flutterwave.com/donate/fiqyumudlt6t) ## Installation Pull in the package through Composer. ### Native Addon When using vanilla PHP, modify your `composer.json` file to include: ```json "scripts": { "post-update-cmd": [ "SmoDav\\Mpesa\\Support\\Installer::install" ] }, ``` This script will copy the default configuration file to a config folder in the root directory of your project. Now proceed to require the package. ### General Install Run `composer require smodav/mpesa` to get the latest stable version of the package. ## Migration from previous versions v5 of the package changes the implementation and introduces some breaking changes. Please have a look at the [CHANGELOG](https://github.com/SmoDav/mpesa/blob/master/CHANGELOG.md). v4 of this package uses a new configuration setup. You will need to update your config file in order to upgrade v3 to v4. v2 is still incompatible since it uses the older API version. ### Laravel When using Laravel 5.5+, the package will automatically register. For laravel 5.4 and below, include the service provider and its alias within your `config/app.php`. ```php 'providers' => [ SmoDav\Mpesa\Laravel\ServiceProvider::class, ], 'aliases' => [ 'STK' => SmoDav\Mpesa\Laravel\Facades\STK::class, 'Simulate' => SmoDav\Mpesa\Laravel\Facades\Simulate::class, 'Registrar' => SmoDav\Mpesa\Laravel\Facades\Registrar::class, 'Identity' => SmoDav\Mpesa\Laravel\Facades\Identity::class, ], ``` Publish the package specific config using: ```bash php artisan vendor:publish ``` ### Other Frameworks To implement this package, a configuration repository is needed, thus any other framework will need to create its own implementation of the `ConfigurationStore` and `CacheStore` interfaces. ### Configuration The package allows you to have multiple accounts. Each account will have its specific credentials and endpoints that are independent of the rest. You will be required to set the default account to be used for all transactions, which you can override on each request you make. The package comes with two default accounts that you can modify. ``` /* |-------------------------------------------------------------------------- | Default Account |-------------------------------------------------------------------------- | | This is the default account to be used when none is specified. */ 'default' => 'staging', /* |-------------------------------------------------------------------------- | File Cache Location |-------------------------------------------------------------------------- | | When using the Native Cache driver, this will be the relative directory | where the cache information will be stored. */ 'cache_location' => '../cache', /* |-------------------------------------------------------------------------- | Accounts |-------------------------------------------------------------------------- | | These are the accounts that can be used with the package. You can configure | as many as needed. Two have been setup for you. | | Sandbox: Determines whether to use the sandbox, Possible values: sandbox | production | Initiator: This is the username used to authenticate the transaction request | LNMO: | shortcode: The till number | passkey: The passkey for the till number | callback: Endpoint that will be be queried on completion or failure of the transaction. | */ 'accounts' => [ 'staging' => [ 'sandbox' => true, 'key' => 'your development consumer key', 'secret' => 'your development consumer secret', 'initiator' => 'your development username', 'id_validation_callback' => 'http://example.com/callback?secret=some_secret_hash_key', 'lnmo' => [ 'paybill' => 'your development paybill number', 'shortcode' => 'your development business code', 'passkey' => 'your development passkey', 'callback' => 'http://example.com/callback?secret=some_secret_hash_key', ] ], 'paybill_1' => [ 'sandbox' => false, 'key' => 'your production consumer key', 'secret' => 'your production consumer secret', 'initiator' => 'your production username', 'id_validation_callback' => 'http://example.com/callback?secret=some_secret_hash_key', 'lnmo' => [ 'paybill' => 'your production paybill number', 'shortcode' => 'your production business code', 'passkey' => 'your production passkey', 'callback' => 'http://example.com/callback?secret=some_secret_hash_key', ] ], 'paybill_2' => [ 'sandbox' => false, 'key' => 'your production consumer key', 'secret' => 'your production consumer secret', 'initiator' => 'your production username', 'id_validation_callback' => 'http://example.com/callback?secret=some_secret_hash_key', 'lnmo' => [ 'paybill' => 'your production paybill number', 'shortcode' => 'your production business code', 'passkey' => 'your production passkey', 'callback' => 'http://example.com/callback?secret=some_secret_hash_key', ] ], ], ``` You can add as many accounts as required and switch the connection using the method `usingAccount` on `STK`, `Register` and `Simulate` as shown below. Also, note the difference between the `business shortcode` and your `paybill number` in the configuration as getting them wrong will cost you a lot of time debugging. ## Usage For Vanilla PHP you will need to initialize the core engine before any requests as shown below. The package comes with a vanilla php implementation of the cache and configuration store, `NativeCache` and `NativeConfig`. The `NativeConfig` receives the custom location for the configuration file to be used as the first constructor argument. If no value is passed when creating the instance, it will use the default configuration and look for a configuration file on the root of the project under `configs` directory. The `NativeCache` receives a custom directory path as the first constructor argument. The path denotes where the cache should store its files. If no path is provided, the default cache location in the config will be used. ```php use GuzzleHttp\Client; use SmoDav\Mpesa\Engine\Core; use SmoDav\Mpesa\Native\NativeCache; use SmoDav\Mpesa\Native\NativeConfig; require "vendor/autoload.php"; $config = new NativeConfig(); $cache = new NativeCache($config->get('cache_location')); // or $cache = new NativeCache(__DIR__ . '/../some/awesome/directory'); $core = new Core(new Client, $config, $cache); ``` ### URL Registration #### submit(shortCode = null, confirmationURL = null, validationURL = null, onTimeout = 'Completed|Cancelled', account = null) Register callback URLs ##### Vanilla ```php use SmoDav\Mpesa\C2B\Registrar; $conf = 'http://example.com/mpesa/confirm?secret=some_secret_hash_key'; $val = 'http://example.com/mpesa/validate?secret=some_secret_hash_key'; $response = (new Registrar($core))->register(600000) ->onConfirmation($conf) ->onValidation($val) ->submit(); /****** OR ********/ $response = (new Registrar($core))->submit(600000, $conf, $val); ``` When having multiple accounts, switch using the `usingAccount` method. We currently have `staging`, `paybill_1` and `paybill_2` with `staging` as the default: ```php $response = (new Registrar($core)) ->register(600000) ->usingAccount('paybill_1') ->onConfirmation($conf) ->onValidation($val) ->submit(); /****** OR ********/ $response = (new Registrar($core))->submit(600000, $conf, $val, null, 'paybill_1'); ``` ##### Laravel ```php use SmoDav\Mpesa\Laravel\Facades\Registrar; $conf = 'http://example.com/mpesa/confirm?secret=some_secret_hash_key'; $val = 'http://example.com/mpesa/validate?secret=some_secret_hash_key'; $response = Registrar::register(600000) ->onConfirmation($conf) ->onValidation($val) ->submit(); /****** OR ********/ $response = Registrar::submit(600000, $conf, $val); ``` Using the `paybill_1` account: ```php use SmoDav\Mpesa\Laravel\Facades\Registrar; $response = Registrar::register(600000) ->usingAccount('paybill_1') ->onConfirmation($conf) ->onValidation($val) ->submit(); /****** OR ********/ $response = Registrar::submit(600000, $conf, $val, null, 'paybill_1'); ``` ### Simulate Transaction #### push(amount = null, number = null, reference = null, account = null, command = null) Initiate a C2B simulation transaction request. Note that when initiating a C2B simulation, setting the command type is optional and by default `CustomerPaybillOnline` will be used. ##### Vanilla ```php use SmoDav\Mpesa\C2B\Simulate; $simulate = new Simulate($core) $response = $simulate->request(10) ->from(254722000000) ->usingReference('Some Reference') ->push(); /****** OR ********/ $response = $simulate->push(10, 254722000000, 'Some Reference'); ``` Using the `paybill_1` account: ```php $response = $simulate->request(10) ->from(254722000000) ->usingReference('Some Reference') ->usingAccount('paybill_1') ->push(); /****** OR ********/ $response = $simulate->push(10, 254722000000, 'Some Reference', 'paybill_1'); ``` Using the `CustomerBuyGoodsOnline` command: ```php $response = $simulate->request(10) ->from(254722000000) ->usingReference('Some Reference') ->setCommand(CUSTOMER_BUYGOODS_ONLINE) ->push(); /****** OR ********/ $response = $simulate->push(10, 254722000000, 'Some Reference', null, CUSTOMER_BUYGOODS_ONLINE); ``` ##### Laravel ```php use SmoDav\Mpesa\Laravel\Facades\Simulate; $response = Simulate::request(10) ->from(254722000000) ->usingReference('Some Reference') ->push(); /****** OR ********/ $response = Simulate::push(10, 254722000000, 'Some Reference'); ``` Using the `paybill_1` account: ```php use SmoDav\Mpesa\Laravel\Facades\Simulate; $response = Simulate::request(10) ->from(254722000000) ->usingReference('Some Reference') ->usingAccount('paybill_1') ->push(); /****** OR ********/ $response = Simulate::push(10, 254722000000, 'Some Reference', 'paybill_1'); ``` Using the `CustomerBuyGoodsOnline` command: ```php use SmoDav\Mpesa\Laravel\Facades\Simulate; $response = Simulate::request(10) ->from(254722000000) ->usingReference('Some Reference') ->setCommand(CUSTOMER_BUYGOODS_ONLINE) ->push(); /****** OR ********/ $response = Simulate::push(10, 254722000000, 'Some Reference', null, CUSTOMER_BUYGOODS_ONLINE); ``` ### STK PUSH #### push(amount = null, number = null, reference = null, description = null, account = null, command = null) Initiate a C2B STK Push request. Note that when initiating an STK Push, setting the command type is optional and by default `CustomerPaybillOnline` will be used. ##### Vanilla ```php use SmoDav\Mpesa\C2B\STK; $stk = new STK($core); $response = $stk->request(10) ->from(254722000000) ->usingReference('Some Reference', 'Test Payment') ->push(); /****** OR ********/ $response = $stk->push(10, 254722000000, 'Some Reference', 'Test Payment'); ``` Using the `paybill_2` account: ```php $response = $stk->request(10) ->from(254722000000) ->usingAccount('paybill_2') ->usingReference('Some Reference', 'Test Payment') ->push(); /****** OR ********/ $response = $stk->push(10, 254722000000, 'Some Reference', 'Test Payment', 'paybill_2'); ``` Using `CustomerBuyGoodsOnline` command: ```php $response = $stk->request(10) ->from(254722000000) ->usingReference('Some Reference', 'Test Payment') ->setCommand(CUSTOMER_BUYGOODS_ONLINE) ->push(); /****** OR ********/ $response = $stk->push(10, 254722000000, 'Some Reference', 'Test Payment', null, CUSTOMER_BUYGOODS_ONLINE); ``` ##### Laravel ```php use SmoDav\Mpesa\Laravel\Facades\STK; $response = STK::request(10) ->from(254722000000) ->usingReference('Some Reference', 'Test Payment') ->push(); /****** OR ********/ $response = STK::push(10, 254722000000, 'Some Reference', 'Test Payment'); ``` Using the `paybill_2` account: ```php use SmoDav\Mpesa\Laravel\Facades\STK; $response = STK::request(10) ->from(254722000000) ->usingAccount('paybill_2') ->usingReference('Some Reference', 'Test Payment') ->push(); $response = STK::push(10, 254722000000, 'Some Reference', 'Test Payment', 'paybill_2'); ``` Using the `CustomerGoodsOnline` command: ```php use SmoDav\Mpesa\Laravel\Facades\STK; $response = STK::request(10) ->from(254722000000) ->usingReference('Some Reference', 'Test Payment') ->setCommand(CUSTOMER_BUYGOODS_ONLINE) ->push(); $response = STK::push(10, 254722000000, 'Some Reference', 'Test Payment', null, CUSTOMER_BUYGOODS_ONLINE); ``` ### STK PUSH Transaction Validation #### validate(merchantReferenceId, account = null) Validate a C2B STK Push transaction. ##### Vanilla ```php use SmoDav\Mpesa\C2B\STK; $stk = new STK($core); $response = $stk->validate('ws_CO_16022018125'); ``` Using the `paybill_2` account: ```php $response = $stk->validate('ws_CO_16022018125', 'paybill_2'); ``` ##### Laravel ```php use SmoDav\Mpesa\Laravel\Facades\STK; $response = STK::validate('ws_CO_16022018125'); ``` Using the `paybill_1` account: ```php use SmoDav\Mpesa\Laravel\Facades\STK; $response = STK::validate('ws_CO_16022018125', 'paybill_2'); ``` ##### When going live, you should change the `default` value of the config file to the production account. ## License The M-Pesa Package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). ================================================ FILE: composer.json ================================================ { "name": "smodav/mpesa", "description": "M-Pesa API implementation", "type": "library", "keywords": [ "mpesa", "safaricom", "laravel", "transactions", "api" ], "license": "MIT", "authors": [ { "name": "SmoDav", "email": "smodavprivate@gmail.com" } ], "autoload": { "files": [ "src/Mpesa/Support/helpers.php" ], "psr-4": { "SmoDav\\Mpesa\\": "src/Mpesa/" } }, "autoload-dev": { "psr-4": { "SmoDav\\Mpesa\\Tests\\": "tests/" } }, "extra": { "laravel": { "providers": [ "SmoDav\\Mpesa\\Laravel\\ServiceProvider" ], "aliases": { "STK": "SmoDav\\Mpesa\\Laravel\\Facades\\STK", "Simulate": "SmoDav\\Mpesa\\Laravel\\Facades\\Simulate", "Registrar": "SmoDav\\Mpesa\\Laravel\\Facades\\Registrar", "Identity": "SmoDav\\Mpesa\\Laravel\\Facades\\Identity" } } }, "require": { "php": ">=8.2", "guzzlehttp/guzzle": "^6.2|^7.4.5", "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "nesbot/carbon": "^2.0|^3.0", "ext-json": "*" }, "require-dev": { "mockery/mockery": "dev-master|^1.3.1", "phpunit/phpunit": "~8.5|^9.3|^10.0" }, "minimum-stability": "stable" } ================================================ FILE: config/mpesa.php ================================================ 'staging', /* |-------------------------------------------------------------------------- | Native File Cache Location |-------------------------------------------------------------------------- | | When using the Native Cache driver, this will be the relative directory | where the cache information will be stored. */ 'cache_location' => '../cache', /* |-------------------------------------------------------------------------- | Accounts |-------------------------------------------------------------------------- | | These are the accounts that can be used with the package. You can configure | as many as needed. Two have been setup for you. | | Sandbox: Determines whether to use the sandbox, Possible values: sandbox | production | Initiator: This is the username used to authenticate the transaction request | LNMO: | paybill: Your paybill number | shortcode: Your business shortcode | passkey: The passkey for the paybill number | callback: Endpoint that will be be queried on completion or failure of the transaction. | */ 'accounts' => [ 'staging' => [ 'sandbox' => true, 'key' => '', 'secret' => '', 'initiator' => 'apitest363', 'id_validation_callback' => 'http://example.com/callback?secret=some_secret_hash_key', 'lnmo' => [ 'paybill' => 174379, 'shortcode' => 174379, 'passkey' => 'bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919', 'callback' => 'http://example.com/callback?secret=some_secret_hash_key', ] ], 'production' => [ 'sandbox' => false, 'key' => '', 'secret' => '', 'initiator' => 'apitest363', 'id_validation_callback' => 'http://example.com/callback?secret=some_secret_hash_key', 'lnmo' => [ 'paybill' => 174379, 'shortcode' => 174379, 'passkey' => 'bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919', 'callback' => 'http://example.com/callback?secret=some_secret_hash_key', ] ], ], ]; ================================================ FILE: phpunit.xml ================================================ tests ./src ================================================ FILE: sonar-project.properties ================================================ sonar.projectKey=smodav_m-pesa sonar.organization=smodav # This is the name and version displayed in the SonarCloud UI. #sonar.projectName=SmoDav #sonar.projectVersion=1.0 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. sonar.sources=./src sonar.php.coverage.reportPaths=clover.xml sonar.php.tests.reportPath=junit.xml sonar.php.exclusions=**/vendor/** sonar.sourceEncoding=UTF-8 ================================================ FILE: src/Mpesa/Auth/Authenticator.php ================================================ */ class Authenticator { /** * Cache key. */ const AC_TOKEN = 'MP:'; /** * @var Core */ private $core; public function __construct(Core $core) { $this->core = $core; } /** * Remove all the access tokens * * @return void */ public function flushTokens() { collect($this->core->configRepository()->config('accounts')) ->each(function ($account) { $this->core->cache()->pull($this->getCacheKey($account['key'], $account['secret'])); }); } /** * Get the cache key for the given key and secret * * @param string $key * @param string $secret * * @return void */ protected function getCacheKey($key, $secret) { return self::AC_TOKEN . "{$key}{$secret}"; } /** * Get the access token required to transact. * * @return mixed * * @throws ConfigurationException */ public function authenticate() { $key = $this->core->configRepository()->getAccountKey('key'); $secret = $this->core->configRepository()->getAccountKey('secret'); $cacheKey = $this->getCacheKey($key, $secret); if ($token = $this->core->cache()->get($cacheKey)) { return $token; } try { $response = $this->makeRequest($key, $secret); $body = json_decode($response->getBody()); $this->saveCredentials($cacheKey, $body); return $body->access_token; } catch (RequestException $exception) { $message = $exception->getResponse() ? $exception->getResponse()->getReasonPhrase() : $exception->getMessage(); throw $this->generateException($message); } } /** * Initiate the authentication request. * * @return mixed|\Psr\Http\Message\ResponseInterface */ private function makeRequest($key, $secret) { $credentials = base64_encode($key . ':' . $secret); $endpoint = $this->core->configRepository()->url(Endpoint::MPESA_AUTH); return $this->core->client()->request('GET', $endpoint, [ 'headers' => [ 'Authorization' => 'Basic ' . $credentials, 'Content-Type' => 'application/json', ], ]); } /** * Store the credentials in the cache. * * @param $credentials */ private function saveCredentials($key, $credentials) { $ttlSeconds = (int) $credentials->expires_in; $ttl = Carbon::now()->addSeconds($ttlSeconds)->subMinute(); $this->core->cache()->put($key, $credentials->access_token, $ttl); } /** * Throw a contextual exception. * * @param $reason * * @return ErrorException|ConfigurationException */ private function generateException($reason) { switch (strtolower($reason)) { case 'bad request: invalid credentials': return new ConfigurationException('Invalid consumer key and secret combination'); default: return new ErrorException($reason); } } } ================================================ FILE: src/Mpesa/C2B/Identity.php ================================================ validateNumber($number); $time = Carbon::now()->format('YmdHis'); $shortCode = $this->core->configRepository()->getAccountKey('lnmo.shortcode'); $passkey = $this->core->configRepository()->getAccountKey('lnmo.passkey'); $lmnoCallback = $callback ?: $this->core->configRepository()->getAccountKey('lnmo.callback'); $defaultCallback = $this->core->configRepository()->getAccountKey('id_validation_callback'); $initiator = $this->core->configRepository()->getAccountKey('initiator'); $body = [ 'Initiator' => $initiator, 'BusinessShortCode' => $shortCode, 'Password' => $this->password($shortCode, $passkey, $time), 'Timestamp' => $time, 'TransactionType' => 'CheckIdentity', 'PhoneNumber' => $number, 'CallBackURL' => $lmnoCallback ?: $defaultCallback, 'TransactionDesc' => ' ' ]; try { $response = $this->clientRequest( $body, $this->core->configRepository()->url(Endpoint::MPESA_ID_CHECK) ); return json_decode($response->getBody()); } catch (RequestException $exception) { return json_decode($exception->getResponse()->getBody()); } } } ================================================ FILE: src/Mpesa/C2B/Registrar.php ================================================ shortCode = $shortCode; return $this; } /** * Submit the callback to be used for validation. * * @param $validationURL * * @return self */ public function onValidation($validationURL) { $this->validationURL = $validationURL; return $this; } /** * Submit the callback to be used for confirmation. * * @param $confirmationURL * * @return self */ public function onConfirmation($confirmationURL) { $this->confirmationURL = $confirmationURL; return $this; } /** * Set the transaction status on timeout. * * @param string $onTimeout * * @return self */ public function onTimeout($onTimeout = 'Completed') { if ($onTimeout != 'Completed' && $onTimeout != 'Cancelled') { throw new InvalidArgumentException('Invalid timeout argument. Use Completed or Cancelled'); } $this->onTimeout = $onTimeout; return $this; } /** * Set the account to be used. * * @param string $account * * @return self */ public function usingAccount($account) { $this->account = $account; return $this; } /** * Initiate the registration process. * * @param string|null $shortCode * @param string|null $confirmationURL * @param string|null $validationURL * @param string|null $onTimeout * @param string|null $account * * @return mixed * * @throws \Exception */ public function submit($shortCode = null, $confirmationURL = null, $validationURL = null, $onTimeout = null, $account = null) { if ($onTimeout) { $this->onTimeout($onTimeout); } $this->core->useAccount($account ?: $this->account); $body = [ 'ShortCode' => $shortCode ?: $this->shortCode, 'ResponseType' => $this->onTimeout, 'ConfirmationURL' => $confirmationURL ?: $this->confirmationURL, 'ValidationURL' => $validationURL ?: $this->validationURL ]; try { $response = $this->clientRequest( $body, $this->core->configRepository()->url(Endpoint::MPESA_REGISTER) ); return json_decode($response->getBody()); } catch (RequestException $exception) { $message = $exception->getResponse() ? $exception->getResponse()->getReasonPhrase() : $exception->getMessage(); throw new Exception($message); } } } ================================================ FILE: src/Mpesa/C2B/STK.php ================================================ callback = $callback; return $this; } /** * Prepare the STK Push request. * * @param int|null $amount * @param int|null $number * @param string|null $reference * @param string|null $description * @param string|null $account * @param string|null $command * * @return mixed */ public function push( $amount = null, $number = null, $reference = null, $description = null, $account = null, $command = null ) { $this->set($amount, $number, $command); $this->core->useAccount($account ?: $this->account); $time = Carbon::now()->format('YmdHis'); $paybill = $this->core->configRepository()->getAccountKey('lnmo.paybill'); $shortCode = $this->core->configRepository()->getAccountKey('lnmo.shortcode'); $passkey = $this->core->configRepository()->getAccountKey('lnmo.passkey'); $callback = $this->callback ?: $this->core->configRepository()->getAccountKey('lnmo.callback'); $partyB = $this->command == self::CUSTOMER_PAYBILL_ONLINE ? $shortCode : $paybill; $body = [ 'BusinessShortCode' => $shortCode, 'Password' => $this->password($shortCode, $passkey, $time), 'Timestamp' => $time, 'TransactionType' => $this->command, 'Amount' => $this->amount, 'PartyA' => $this->number, 'PartyB' => $partyB, 'PhoneNumber' => $number ?: $this->number, 'CallBackURL' => $callback, 'AccountReference' => $reference ?: $this->reference, 'TransactionDesc' => $description ?: $this->description, ]; try { $response = $this->clientRequest( $body, $this->core->configRepository()->url(Endpoint::MPESA_LNMO) ); return json_decode($response->getBody()); } catch (RequestException $exception) { return json_decode($exception->getResponse()->getBody()); } } /** * Validate an initialized transaction. * * @param string $checkoutRequestID * @param mixed|null $account * * @return stdClass */ public function validate($checkoutRequestID, $account = null) { $this->core->useAccount($account ?: $this->account); $time = Carbon::now()->format('YmdHis'); $shortCode = $this->core->configRepository()->getAccountKey('lnmo.shortcode'); $passkey = $this->core->configRepository()->getAccountKey('lnmo.passkey'); $body = [ 'BusinessShortCode' => $shortCode, 'Password' => $this->password($shortCode, $passkey, $time), 'Timestamp' => $time, 'CheckoutRequestID' => $checkoutRequestID, ]; try { $response = $this->clientRequest( $body, $this->core->configRepository()->url(Endpoint::MPESA_LNMO_VALIDATE) ); return json_decode($response->getBody()); } catch (RequestException $exception) { return json_decode($exception->getResponse()->getBody()); } } } ================================================ FILE: src/Mpesa/C2B/Simulate.php ================================================ set($amount, $number, $command); $this->core->useAccount($account ?: $this->account); if (!$this->core->configRepository()->getAccountKey('sandbox')) { throw new ErrorException('Cannot simulate a transaction in the live environment.'); } $shortCode = $this->core->configRepository()->getAccountKey('lnmo.shortcode'); $body = [ 'CommandID' => $this->command, 'Amount' => $this->amount, 'Msisdn' => $this->number, 'ShortCode' => $shortCode, 'BillRefNumber' => $reference ?: $this->reference, ]; try { $response = $this->clientRequest( $body, $this->core->configRepository()->url(Endpoint::MPESA_SIMULATE) ); return json_decode($response->getBody()); } catch (RequestException $exception) { return json_decode($exception->getResponse()->getBody()); } } } ================================================ FILE: src/Mpesa/Contracts/CacheStore.php ================================================ */ interface CacheStore { /** * Get the cache value from the store or a default value to be supplied. * * @param $key * @param $default * * @return mixed */ public function get($key, $default = null); /** * Store an item in the cache. * * @param string $key * @param mixed $value * @param \DateTimeInterface|\DateInterval|float|int $seconds */ public function put($key, $value, $seconds = null); /** * Get the cache or default value from the store and delete it. * * @param $key * @param $default * * @return mixed */ public function pull($key, $default = null); } ================================================ FILE: src/Mpesa/Contracts/ConfigurationStore.php ================================================ */ interface ConfigurationStore { /** * Get the configuration value from the store or a default value to be supplied. * * @param $key * @param $default * * @return mixed */ public function get($key, $default = null); } ================================================ FILE: src/Mpesa/Engine/Core.php ================================================ client = $client; $this->setupStores($configStore, $cacheStore); $this->initialise(); } /** * Use the native implementation of the stores. * * @return void */ protected function setupStores(ConfigurationStore $configStore = null, CacheStore $cacheStore = null) { $this->configRepository = new ConfigurationRepository($configStore ?: new NativeConfig); $this->cache = $cacheStore ?: new NativeCache($this->configRepository->config('cache_location')); } /** * Initialise the Core process. */ private function initialise() { $this->auth = new Authenticator($this); } /** * Get the configuration repository. * * @return ConfigurationRepository */ public function configRepository() { return $this->configRepository; } /** * Get the cache store. * * @return CacheStore */ public function cache() { return $this->cache; } /** * Get the client. * * @return ClientInterface */ public function client() { return $this->client; } /** * Get the client. * * @return Authenticator */ public function auth() { return $this->auth; } /** * Switch the current account * * @param string|null $account * * @throws Exception * @return self */ public function useAccount($account = null) { $this->configRepository->useAccount($account); return $this; } /** * Switch the client instance. * * @param string|null $account * * @return self */ public function useClient(ClientInterface $client) { $this->client = $client; return $this; } } ================================================ FILE: src/Mpesa/Exceptions/ConfigurationException.php ================================================ * * @method static stdClass validate(string $number, callable $callback, string $account = null) * * @see \SmoDav\Mpesa\C2B\Registrar */ class Identity extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'mp_identity'; } } ================================================ FILE: src/Mpesa/Laravel/Facades/Registrar.php ================================================ * * @method static Registrar onConfirmation(string $confirmationURL) * @method static Registrar onTimeout(string $onTimeout) * @method static Registrar onValidation(string $validationURL) * @method static Registrar register(string $shortCode) * @method static stdClass submit(string $shortCode = null, string $confirmationURL = null, string $validationURL = null, string $onTimeout = null, string $account = null) * @method static Registrar usingAccount(string $account) * * @see \SmoDav\Mpesa\C2B\Registrar */ class Registrar extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'mp_registrar'; } } ================================================ FILE: src/Mpesa/Laravel/Facades/STK.php ================================================ * * @method static STK from(string $number) * @method static STK request(string $amount) * @method static stdClass push(string $amount = null, string $number = null, string $reference = null, string $description = null, string $account = null) * @method static STK usingAccount(string $account) * @method static STK usingReference(string $reference, string $description) * @method static stdClass validate(string $checkoutRequestID, string $account = null) * * @see \SmoDav\Mpesa\C2B\STK */ class STK extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'mp_stk'; } } ================================================ FILE: src/Mpesa/Laravel/Facades/Simulate.php ================================================ * * @method static Simulate from(string $number) * @method static Simulate request(string $amount) * @method static stdClass push(string $amount = null, string $number = null, string $reference = null, string $command = null, string $account = null) * @method static Simulate setCommand(string $command) * @method static Simulate usingAccount(string $account) * @method static Simulate usingReference(string $reference) * @method static stdClass validate(string $checkoutRequestID, string $account = null) * * @see \SmoDav\Mpesa\C2B\Simulate */ class Simulate extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'mp_simulate'; } } ================================================ FILE: src/Mpesa/Laravel/ServiceProvider.php ================================================ publishes([ __DIR__ . '/../../../config/mpesa.php' => config_path('mpesa.php') ]); } /** * Registrar the application services. */ public function register() { $this->bindInstances(); $this->registerFacades(); } /** * Bind the MPesa Instances. * * @return void */ private function bindInstances() { $this->app->bind(ConfigurationStore::class, LaravelConfig::class); $this->app->bind(CacheStore::class, LaravelCache::class); $this->app->singleton(Core::class, function ($app) { $config = $app->make(ConfigurationStore::class); $cache = $app->make(CacheStore::class); return new Core(new Client, $config, $cache); }); } private function registerFacades() { $this->app->bind('mp_stk', function () { return $this->app->make(STK::class); }); $this->app->bind('mp_registrar', function () { return $this->app->make(Registrar::class); }); $this->app->bind('mp_identity', function () { return $this->app->make(Identity::class); }); $this->app->bind('mp_simulate', function () { return $this->app->make(Simulate::class); }); } } ================================================ FILE: src/Mpesa/Laravel/Stores/LaravelCache.php ================================================ repository = $repository; } /** * Get given config value from the configuration store. * * @param string $key * @param null $default * * @return mixed */ public function get($key, $default = null) { return $this->repository->get($key, $default); } /** * Store an item in the cache. * * @param string $key * @param mixed $value * @param \DateTimeInterface|\DateInterval|float|int $seconds */ public function put($key, $value, $seconds = null) { $this->repository->put($key, $value, $seconds); } /** * Get the cache or default value from the store and delete it. * * @param $key * @param $default * * @return mixed */ public function pull($key, $default = null) { return $this->repository->pull($key, $default); } } ================================================ FILE: src/Mpesa/Laravel/Stores/LaravelConfig.php ================================================ repository = $repository; } /** * Get given config value from the configuration store. * * @param string $key * @param null $default * * @return mixed */ public function get($key, $default = null) { return $this->repository->get($key, $default); } } ================================================ FILE: src/Mpesa/Native/NativeCache.php ================================================ */ class NativeCache implements CacheStore { /** * @var string */ private $cacheFile; /** * NativeCache constructor. * * @param string|null $cacheDirectory */ public function __construct($cacheDirectory = null) { $this->setUp($cacheDirectory); } /** * Setup the cache file location. * * @param string $cacheDirectory * * @return void */ private function setUp($cacheDirectory = null) { $cacheDirectory = $cacheDirectory ?: (new ConfigurationRepository(new NativeConfig))->config('cache_location'); $cacheDirectory = rtrim($cacheDirectory, '/'); if (! is_dir($cacheDirectory)) { mkdir($cacheDirectory, 0755, true); } $this->cacheFile = $cacheDirectory . '/.mpc'; if (!is_file($this->cacheFile)) { file_put_contents($this->cacheFile, serialize([])); } } /** * Get the cache value. * * @param $key * @param null $default * * @return mixed|null */ public function get($key, $default = null) { $cache = unserialize(file_get_contents($this->cacheFile)); $cache = $this->cleanCache($cache, $this->cacheFile); if (! isset($cache[$key])) { return $default; } return $cache[$key]['v']; } /** * Store an item in the cache. * * @param string $key * @param mixed $value * @param \DateTimeInterface|\DateInterval|float|int $seconds * * @return bool */ public function put($key, $value, $seconds = null) { $initial = unserialize(file_get_contents($this->cacheFile)); $initial = $this->cleanCache($initial, $this->cacheFile, false); $payload = [$key => ['v' => $value, 't' => $this->formatTimeFromSeconds($seconds)]]; $payload = serialize(array_merge($initial, $payload)); return file_put_contents($this->cacheFile, $payload) !== false; } /** * Get the seconds and format it. * * @param int|null $seconds * * @return string|null */ private function formatTimeFromSeconds($seconds = null) { if (!$seconds) { return null; } if ($seconds instanceof DateTimeInterface || $seconds instanceof DateInterval) { return Carbon::parse($seconds)->toDateTimeString(); } if (!is_numeric($seconds)) { throw new InvalidArgumentException('The seconds argument should be numeric'); } return Carbon::now()->addSeconds($seconds)->toDateTimeString(); } /** * Clean out the expired items * * @param array $initial * @param string $location * @param bool $save * * @return array */ private function cleanCache($initial, $location, $save = true) { $initial = array_filter($initial, function ($value) { if (! $value['t']) { return true; } if (Carbon::now()->gt(Carbon::parse($value['t']))) { return false; } return true; }); if ($save) { file_put_contents($location, serialize($initial)); } return $initial; } /** * Get the cache or default value from the store and delete it. * * @param $key * @param $default * * @return mixed */ public function pull($key, $default = null) { $cache = unserialize(file_get_contents($this->cacheFile)); $cache = $this->cleanCache($cache, $this->cacheFile, false); if (! isset($cache[$key])) { file_put_contents($this->cacheFile, serialize($cache)); return $default; } $value = $cache[$key]['v']; unset($cache[$key]); file_put_contents($this->cacheFile, serialize($cache)); return $value; } } ================================================ FILE: src/Mpesa/Native/NativeConfig.php ================================================ */ class NativeConfig implements ConfigurationStore { /** * Mpesa configuration file. * * @var array */ protected $config; /** * NativeConfig constructor. * * @param string|null $configPath */ public function __construct($configPath = null) { $defaultConfig = require __DIR__ . '/../../../config/mpesa.php'; $configPath = $configPath ?: __DIR__ . '/../../../../../../config/mpesa.php'; $custom = []; if (is_file($configPath)) { $custom = require $configPath; } $this->config = ['mpesa' => array_merge($defaultConfig, $custom)]; } /** * Get the configuration value. * * @param $key * @param null $default * * @return mixed|null */ public function get($key, $default = null) { $pieces = explode('.', $key); $config = $this->config; foreach ($pieces as $piece) { if (!isset($config[$piece])) { return $default; } $config = $config[$piece]; } return $config; } } ================================================ FILE: src/Mpesa/Repositories/ConfigurationRepository.php ================================================ */ class ConfigurationRepository { const SANDBOX_URL = 'https://sandbox.safaricom.co.ke/'; const PRODUCTION_URL = 'https://api.safaricom.co.ke/'; /** * @var string */ private $account; /** * @var ConfigurationStore */ protected $store; /** * @var array */ protected $config; /** * Build up a new instance. * * @param ConfigurationStore $store */ public function __construct(ConfigurationStore $store) { $this->store = $store; $this->config = $this->store->get('mpesa'); $this->useAccount(); } /** * Get the configuration instance. */ public function store() { return $this->store; } /** * Get the configuration value. * * @param string $key * @param mixed $default * * @return mixed */ public function config($key = null, $default = null) { if (!$key) { return $this->config; } $key = explode('.', $key); $value = $this->config; foreach ($key as $prop) { if (!isset($value[$prop])) { return $default; } $value = $value[$prop]; } return $value; } /** * Set the account to be used when resoving configs. * * @param string $account * * @return self */ public function useAccount($account = null) { $account = $account ?: $this->config('default'); if (!$this->config("accounts.{$account}")) { throw new Exception('Invalid account selected'); } $this->account = $account; return $this; } /** * Get a configuration value from the store. * * @param string $key * @param mixed $default * @param string $account * * @return mixed */ public function getAccountKey($key, $default = null) { return $this->config("accounts.{$this->account}.{$key}", $default); } /** * Get the endpoint relative to the current * * @param string $endpoint * @param string $account * * @return string */ public function url($endpoint) { return $this->resolveUrl( $this->getAccountKey('sandbox', true) ? self::SANDBOX_URL : self::PRODUCTION_URL, $endpoint ); } /** * Resolve the provided URL * * @param string $base * @param string $key * * @return string */ private function resolveUrl($base, $key) { return $base . trim($key, '/'); } } ================================================ FILE: src/Mpesa/Repositories/Endpoint.php ================================================ getComposer()->getConfig()->get('vendor-dir'); $configDir = $vendorDir . '/../config'; if (! is_dir($configDir)) { mkdir($configDir, 0755, true); } return $configDir; } } ================================================ FILE: src/Mpesa/Support/helpers.php ================================================ core = $core; } /** * Initiate the request. * * @param array $body * @param string $endpoint * * @return mixed|\Psr\Http\Message\ResponseInterface */ private function clientRequest($body, $endpoint) { return $this->core->client()->request('POST', $endpoint, [ 'headers' => [ 'Authorization' => 'Bearer ' . $this->bearer(), 'Content-Type' => 'application/json', ], 'json' => $body, ]); } /** * Get the bearer token. * * @return string */ protected function bearer() { return $this->core->auth()->authenticate(); } /** * Get the password for the * * @param string $shortCode * @param string $passkey * @param string $time * * @return string */ private function password($shortCode, $passkey, $time) { return base64_encode($shortCode . $passkey . $time); } } ================================================ FILE: src/Mpesa/Traits/UsesSTKMethods.php ================================================ account = $account; return $this; } /** * Set the product reference number to bill the account. * * @param int $reference * @param string $description * * @return self */ public function usingReference($reference, $description) { $this->reference = $reference; $this->description = $description; return $this; } /** * Set the request amount to be deducted. * * @param int $amount * * @throws InvalidArgumentException * * @return self */ public function request($amount) { $this->validateAmount($amount); $this->amount = $amount; return $this; } /** * Set the Mobile Subscriber Number to deduct the amount from. * Must be in format 2547XXXXXXXX. * * @param int $number * * @throws InvalidArgumentException * * @return self */ public function from($number) { $this->validateNumber($number); $this->number = $number; return $this; } /** * Set the unique command for this transaction type. * * @param string $command * * @throws InvalidArgumentException * * @return self */ public function setCommand($command) { if (!in_array($command, STK::VALID_COMMANDS)) { throw new InvalidArgumentException('Invalid command sent'); } $this->command = $command; return $this; } /** * Set the properties that require validation. * * @param string|null $amount * @param string|null $number * @param string|null $command * * @return void */ private function set($amount, $number, $command) { if ($amount) { $this->request($amount); } if ($number) { $this->from($number); } if ($command) { $this->setCommand($command); } } } ================================================ FILE: src/Mpesa/Traits/Validates.php ================================================ 'access', 'expires_in' => 3599])), ]); $core = $this->core(new Client(['handler' => HandlerStack::create($mock)])); $this->assertEquals('access', $core->auth()->authenticate()); $mock = new MockHandler([ new Response(403, [], json_encode(['access_token' => 'access', 'expires_in' => 3599])), ]); $core = $this->core(new Client(['handler' => HandlerStack::create($mock)])); $this->assertEquals('access', $core->auth()->authenticate()); $mock = new MockHandler([ new Response(403, [], json_encode(['access_token' => 'access', 'expires_in' => 600000])), ]); $core = $this->core(new Client(['handler' => HandlerStack::create($mock)])); $core->auth()->flushTokens(); $this->expectException(ErrorException::class); $core->auth()->authenticate(); } } ================================================ FILE: tests/Unit/ConfigurationRepositoryTest.php ================================================ nativeConfig()); $this->assertInstanceOf(ConfigurationRepository::class, $repository); } /** * @return void */ public function testCanExtractConfigAndAccountValues() { $repository = new ConfigurationRepository($this->nativeConfig()); $this->assertEquals('test', $repository->config('default')); $this->assertTrue($repository->useAccount('test')->getAccountKey('sandbox')); $this->assertFalse($repository->useAccount('production')->getAccountKey('sandbox')); $this->assertEquals('default', $repository->config('fake_config', 'default')); $this->assertEquals( 'default_account', $repository->useAccount('production')->getAccountKey('fake_account_config', 'default_account') ); } /** * @return void */ public function testCanResolveUrl() { $repository = new ConfigurationRepository($this->nativeConfig()); $this->assertEquals(ConfigurationRepository::SANDBOX_URL . 'test-url', $repository->url('test-url')); $this->assertEquals( ConfigurationRepository::PRODUCTION_URL . 'test-url', $repository->useAccount('production')->url('test-url') ); } } ================================================ FILE: tests/Unit/NativeImplementationsTest.php ================================================ assertEquals(true, $config->get('mpesa.accounts.staging.sandbox')); $config = new NativeConfig(self::CONFIG_FILE); $this->assertEquals(true, $config->get('mpesa.accounts.test.sandbox')); } /** * @return void */ public function testCanGetWholeConfiguration() { $config = new NativeConfig(self::CONFIG_FILE); $this->assertEquals(require(self::CONFIG_FILE), $config->get('mpesa')); } /** * @return void */ public function testCanUseNativeCache() { $cache = new NativeCache(self::CACHE_LOCATION); $cache->put('test', 123, 10); $this->assertEquals(123, $cache->get('test')); } /** * @return void */ public function testCanOverwriteCacheItem() { $cache = new NativeCache(self::CACHE_LOCATION); $cache->put('overwrite', 123, 10); $this->assertEquals(123, $cache->get('overwrite')); $cache->put('overwrite', 456, 10); $this->assertEquals(456, $cache->get('overwrite')); } /** * @return void */ public function testCacheExpires() { $cache = new NativeCache(self::CACHE_LOCATION); $cache->put('expire', 123, 1); $this->assertEquals(123, $cache->get('expire')); sleep(1); $this->assertEquals(null, $cache->get('expire')); } /** * @return void */ public function testCanConstructWithoutConfig() { $cache = new NativeCache; $cache->put('without_config', 'YES', 5); $this->assertEquals('YES', $cache->get('without_config')); } /** * @return void */ public function testTTLImplementation() { $cache = new NativeCache(self::CACHE_LOCATION); $this->expectException(InvalidArgumentException::class); $cache->put('expire', 123, 'fake'); } /** * @return void */ public function testShouldPullCacheItem() { $cache = new NativeCache(self::CACHE_LOCATION); $cache->put('to_pull', 'YES', 5); $this->assertEquals('YES', $cache->get('to_pull')); $this->assertEquals('YES', $cache->pull('to_pull')); $this->assertEquals('pulled', $cache->pull('to_pull', 'pulled')); } } ================================================ FILE: tests/Unit/RegistrarTest.php ================================================ 'access', 'expires_in' => 3599])), new Response(200, [], json_encode([ 'OriginatorConverstionID' => '123', 'ConversationID' => '500', 'ResponseDescription' => 'Success', ])), ]); $core = $this->core(new Client(['handler' => HandlerStack::create($mock)])); $core->auth()->flushTokens(); $registrar = new Registrar($core); $response = $registrar->submit(123456, 'http://example.com', 'http://example.com'); $this->assertEquals('123', $response->OriginatorConverstionID); $this->assertEquals('500', $response->ConversationID); $this->assertEquals('Success', $response->ResponseDescription); } } ================================================ FILE: tests/Unit/STKTest.php ================================================ '19465-780693-1', 'CheckoutRequestID' => 'ws_CO_27072017154747416', 'ResponseCode' => 0, 'ResponseDescription' => 'Success. Request accepted for processing', 'CustomerMessage' => 'Success. Request accepted for processing', ]; $mock = new MockHandler([ new Response(200, [], json_encode(['access_token' => 'access', 'expires_in' => 3599])), new Response(200, [], json_encode($response)), ]); $core = $this->core(new Client(['handler' => HandlerStack::create($mock)])); $core->auth()->flushTokens(); $stk = new STK($core); $response = $stk->push(100, 254722000000, 'Test', 'Awesome'); $this->assertEquals('19465-780693-1', $response->MerchantRequestID); $this->assertEquals('ws_CO_27072017154747416', $response->CheckoutRequestID); $this->assertEquals(0, $response->ResponseCode); $this->assertEquals('Success. Request accepted for processing', $response->ResponseDescription); $this->assertEquals('Success. Request accepted for processing', $response->CustomerMessage); } public function testValidateTransaction() { $response = [ 'MerchantRequestID' => '19465-780693-1', 'CheckoutRequestID' => 'ws_CO_27072017154747416', 'ResponseCode' => 0, 'ResponseDescription' => 'Success. Request accepted for processing', 'ResultCode' => 0, 'ResultDesc' => 'The service request is processed successfully.' ]; $mock = new MockHandler([ new Response(200, [], json_encode(['access_token' => 'access', 'expires_in' => 3599])), new Response(200, [], json_encode($response)), ]); $core = $this->core(new Client(['handler' => HandlerStack::create($mock)])); $core->auth()->flushTokens(); $stk = new STK($core); $response = $stk->validate('ws_CO_27072017154747416'); $this->assertEquals('19465-780693-1', $response->MerchantRequestID); $this->assertEquals('ws_CO_27072017154747416', $response->CheckoutRequestID); $this->assertEquals(0, $response->ResponseCode); $this->assertEquals('Success. Request accepted for processing', $response->ResponseDescription); $this->assertEquals(0, $response->ResultCode); $this->assertEquals('The service request is processed successfully.', $response->ResultDesc); } } ================================================ FILE: tests/files/.gitignore ================================================ /cache ================================================ FILE: tests/files/mpesa.php ================================================ 'test', /* |-------------------------------------------------------------------------- | Native File Cache Location |-------------------------------------------------------------------------- | | When using the Native Cache driver, this will be the relative directory | where the cache information will be stored. */ 'cache_location' => __DIR__ . '/cache', /* |-------------------------------------------------------------------------- | Accounts |-------------------------------------------------------------------------- | | These are the accounts that can be used with the package. You can configure | as many as needed. Two have been setup for you. | | Sandbox: Determines whether to use the sandbox, Possible values: sandbox | production | Initiator: This is the username used to authenticate the transaction request | LNMO: | paybill: Your paybill number | shortcode: Your business shortcode | passkey: The passkey for the paybill number | callback: Endpoint that will be be queried on completion or failure of the transaction. | */ 'accounts' => [ 'test' => [ 'sandbox' => true, 'key' => 'key1', 'secret' => 'secret1', 'initiator' => 'apitest363', 'id_validation_callback' => 'http://example.com/callback?secret=some_secret_hash_key', 'lnmo' => [ 'paybill' => 174379, 'shortcode' => 174379, 'passkey' => 'bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919', 'callback' => 'http://example.com/callback?secret=some_secret_hash_key', ] ], 'production' => [ 'sandbox' => false, 'key' => 'key2', 'secret' => 'secret2', 'initiator' => 'apitest363', 'id_validation_callback' => 'http://example.com/callback?secret=some_secret_hash_key', 'lnmo' => [ 'paybill' => 174379, 'shortcode' => 174379, 'passkey' => 'bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919', 'callback' => 'http://example.com/callback?secret=some_secret_hash_key', ] ], ], ];