From 74bd56f33e9f586f63bbf4b7e02a7a4be636fcde Mon Sep 17 00:00:00 2001 From: Ghostscypher Date: Fri, 25 Feb 2022 23:25:47 +0300 Subject: [PATCH] Breaking changes: Unify the whole mpesa class, TODO: write better documentation --- README.md | 24 +-- composer.json | 2 +- composer.lock | 73 ++++--- specifications.md | 259 ++++++++--------------- src/Main/MpesaAuth.php | 12 +- src/Main/MpesaB2B.php | 121 ----------- src/Main/MpesaB2C.php | 103 --------- src/Main/MpesaC2B.php | 222 -------------------- src/Main/MpesaConfig.php | 18 +- src/Main/MpesaHttp.php | 2 +- src/Mpesa.php | 443 ++++++++++++++++++++++++++++++++++----- 11 files changed, 553 insertions(+), 726 deletions(-) delete mode 100644 src/Main/MpesaB2B.php delete mode 100644 src/Main/MpesaB2C.php delete mode 100644 src/Main/MpesaC2B.php diff --git a/README.md b/README.md index 3f4602b..d469a48 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ use Hackdelta\Mpesa\Exceptions\MpesaInternalException; use Hackdelta\Mpesa\Exceptions\MpesaClientExceptions; use Hackdelta\Mpesa\Exceptions\MpesaServerException; -$config = new MpesaConfig([ +$config = [ // API Credentials 'consumer_key' => 'YOUR_CONSUMER_KEY', // <--- Change this 'consumer_secret' => 'YOUR_CONSUMER_SECRET', // <--- Change this @@ -233,36 +233,36 @@ $config = new MpesaConfig([ // number that receives confirmation messages 'pull_callback_url' => 'https://domain.example.com/callback.php', -]); +]; $mpesa = new Mpesa($config); // Check internal function test_getting_auth_token() { - echo $mpesa->getConfig()->getAuth()->getToken(); + echo $mpesa->getConfig()->getToken(); } // C2B function test_registering_c2b_callbacks() { - echo $mpesa->C2B()->registerURL(); + echo $mpesa->registerURL(); } function test_simulating_transaction() { - echo $mpesa->C2B()->simulate('254708374149', 100, 'test account reference'); + echo $mpesa->simulate('254708374149', 100, 'test account reference'); } function test_initiating_stk_push() { $mpesa->getConfig() - ->setConfig("short_code", '174379') - ->setConfig("business_short_code", '174379'); + ->setShortcode("short_code", '174379') + ->setBusinessShortCode("business_short_code", '174379'); // Change "254700000000" to your phone number - echo $mpesa->C2B()->initiateSTKPush("254700000000", 1, "Test", "Test"); + echo $mpesa->STKPush("254700000000", 1, "Test", "Test"); // Reset config back $mpesa->getConfig() - ->setConfig("short_code", '601497') - ->setConfig("business_short_code", '601497'); + ->setShortcode("short_code", '601497') + ->setBusinessShortCode("business_short_code", '601497'); } @@ -272,7 +272,7 @@ function test_getting_Stk_push_status() { ->setConfig("business_short_code", '174379'); // Change the checkout id to the one you git from running the above - echo $mpesa->C2B()->STKPushQuery("ws_CO_030520210342393444"); + echo $mpesa->STKPushQuery("ws_CO_030520210342393444"); // Reset config back $mpesa->getConfig() @@ -314,7 +314,7 @@ function test_B2C() { // B2B function test_B2B() { - echo $mpesa->B2B()->send( + echo $mpesa->sendB2B( 10, '600000', MpesaConstants::MPESA_COMMAND_ID_BUSINESS_PAY_BILL, diff --git a/composer.json b/composer.json index cd932b5..05fbf98 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "hackdelta" ], "require": { - "php": "^7.2|^8.0", + "php": "^7.4|^8.0", "guzzlehttp/guzzle": "^6.0|^7.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 629a90f..ebd73a0 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": "0fd99b38c037c32134dd7fc3878cb782", + "content-hash": "3f8affbed6491d19e1f79ccc9d94f037", "packages": [ { "name": "guzzlehttp/guzzle", @@ -50,12 +50,12 @@ } }, "autoload": { - "psr-4": { - "GuzzleHttp\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -157,12 +157,12 @@ } }, "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -535,25 +535,25 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/c726b64c1ccfe2896cb7df2e1331c357ad1c8ced", + "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.0.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.0-dev" }, "thanks": { "name": "symfony/contracts", @@ -582,7 +582,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.0" }, "funding": [ { @@ -598,7 +598,7 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2021-11-01T23:48:49+00:00" } ], "packages-dev": [ @@ -688,9 +688,6 @@ "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" - }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", @@ -791,16 +788,16 @@ }, { "name": "phar-io/version", - "version": "3.1.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "bae7c545bef187884426f042434e561ab1ddb182" + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", - "reference": "bae7c545bef187884426f042434e561ab1ddb182", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { @@ -836,9 +833,9 @@ "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.1.0" + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2021-02-23T14:00:09+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -1798,16 +1795,16 @@ }, { "name": "sebastian/global-state", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b" + "reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b", - "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/de036ec91d55d2a9e0db2ba975b512cdb1c23921", + "reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921", "shasum": "" }, "require": { @@ -1850,7 +1847,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1" + "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.2" }, "funding": [ { @@ -1858,7 +1855,7 @@ "type": "github" } ], - "time": "2020-11-30T07:43:24+00:00" + "time": "2022-02-10T06:55:38+00:00" }, { "name": "sebastian/object-enumerator", @@ -2224,12 +2221,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2387,8 +2384,8 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.2.0" } diff --git a/specifications.md b/specifications.md index 6985cc8..d9676e0 100644 --- a/specifications.md +++ b/specifications.md @@ -27,9 +27,6 @@ implementation of the specifications below. - [MPesa configuration class](#mpesa-configuration-class) - [Mpesa HTTP client class](#mpesa-http-client-class) - [Mpesa response class](#mpesa-response-class) - - [Mpesa client to business (C2B) class](#mpesa-client-to-business-c2b-class) - - [Mpesa business to business (B2B) client class](#mpesa-business-to-business-b2b-client-class) - - [Mpesa business to client (B2C) class](#mpesa-business-to-client-b2c-class) ## The folder structure @@ -66,12 +63,6 @@ You can copy the certificates from the [Certificates folder]('./../certificates' `Main/MpesaResponse.ext` - The MpesaResponse class -`Main/MpesaC2B.ext` - The MpesaC2B class - -`Main/MpesaB2B.ext` - The MpesaB2B class - -`Main/MpesaB2C.ext` - The MpesaB2C class - #### Exceptions folder `src/Exceptions/` - Where to store exceptions @@ -94,7 +85,7 @@ You can copy the certificates from the [Certificates folder]('./../certificates' ### Base directory -`LICENCE` - Copy of the licence, preferably MIT licence +`LICENSE` - Copy of the LICENSE, preferably MIT LICENSE `README.md` - An introduction to our library and how to use it for that particular language. @@ -258,38 +249,107 @@ requests such as 1. Account balance query 2. Transaction status query 3. Reversal request +4. Register and confirmationURL/ValidationURL endpoints +5. Simulate transaction - Only available in sandbox environment +6. Initiate an STK push request +7. Check the status of the STK push i.e. STK push query +8. Register pull transaction URL +9. Initiate pull request +10. B2B send money (Deprecated) +11. B2C send money The code below shows what is expected to be in the mpesa classes ```c# class Mpesa { - // Create singleton instance of the following classes - protected static MpesaB2C B2C; - protected static MpesaC2B C2B; - protected static MpesaB2B B2B; - // Initialize the class here, also initialize the singleton instance // only once - Mpesa(config: MpesaConfig); + Mpesa(config: MpesaConfig | dictionary: key: string, value: string); // Allows a user to override the current config // options - public Mpesa setConfig(config: MpesaConfig); + public Mpesa setConfig(config: MpesaConfig | dictionary: key: string, value: string); // Gets the Mpesa config singleton instance and allows a // user to change the configurations before the next request public MpesaConfig getConfig(); - // Returns an instance of the MpesaC2B class - public MpesaC2B C2B(); + // Performs the register + // All data is in config + public MpesaResponse registerURL(); + + // Simulate a transaction, this is only available in sandbox + // amount is an unsigned int always check for negative or 0 + // Account reference is used as the account number for paybill, will be ignored, + // when using till + public MpesaResponse simulate(MSISDN: string, amount :int, optional account_reference = '': string); + + // Initiate STK push query + // amount: is an unsigned int always check for negative or 0 + // to: the MSISDN sending the funds + // account_reference: + // timestamp: must be in the format 'yyyymmddhhiiss' + // Alias to initiateSTKPush, just call initiateSTKPush from + // here + public MpesaResponse STKPush( + to: string, + amount: int, + optional account_reference = '': string, + optional description = 'Description': string + optional timestamp = '': string); + + // Initiate STK push query + // amount: is an unsigned int always check for negative or 0 + // to: the MSISDN sending the funds + // account_reference: + // timestamp: must be in the format 'yyyymmddhhiiss' + public MpesaResponse initiateSTKPush( + to: string, + amount: int, + optional account_reference = '': string, + optional description = 'Description': string + optional timestamp = '': string); + + // Query the mpesa client gateway to check for the status of an STK push + // We generate our timestamp if it is not filled, timestamp must be in the format 'yyyymmddhhiiss' + public MpesaResponse STKPushQuery( + checkout_request_id: string, + optional timestamp = '': string + ); // Returns an instance of the MpesaB2B class. // @deprecated - public MpesaB2B B2B(); + // Perform a B2B transaction + // amount: amount to transact + // to: Organization’s short code receiving funds being transacted. + // receiver_identifier_type: specifies the identifier type of the receiver + // supported types include: TODO: add supported constants here + // account_reference: optional account sent if we are using paybill + // remarks: comments sent along with the transaction + public MpesaResponse sendB2B( + amount: int, + to: string, + command_id: CONSTANT, + receiver_identifier_type: CONSTANT, + optional account_reference = '': string, + optional remarks = 'remarks': string + ); - // Returns an instance of the MpesaB2C class - public MpesaB2C B2C(); + // Perform a B2C transaction + // amount: amount to transact + // to: MSISDN receiving funds being transacted. + // command IDs + // account_reference: optional account sent if we are using paybill + // remarks: comments sent along with the transaction + // occasion: optional description sent along with the transaction + public MpesaResponse sendB2C( + amount: int, + to: string, + command_id: CONSTANT, + optional remarks = 'remarks': string, + optional occasion = '': string + ); // The following specifies the general requests found in this class // these are: @@ -566,6 +626,12 @@ class MpesaConfig { // token public MpesaConfig setToken(token: string, optional $expires_at = 0: int); + //Returns the auth token + public function getToken(); + + //Returns timestamp when the current token will expire + public function getExpiresAtTime(); + // Used to set the shortcode, this is usually the // partyA or partyB depending on the transaction // identifier_type, indicates the kind of shortcode this is @@ -768,152 +834,3 @@ class MpesaResponse { } ``` - -## Mpesa client to business (C2B) class - -This class will contain logic for handling all the client to business requests/logic this includes - -1. Register and confirmationURL/ValidationURL endpoints -2. Simulate transaction - Only available in sandbox environment -3. Initiate an STK push request -4. Check the status of the STK push i.e. STK push query -5. Register pull transaction URL - -``` java -class MpesaC2B { - // Constructor - // We need to pass in the config by ref - // The command id, of the shortcode this is will be determined by the shortcode identifier type - MpesaC2B(config: MpesaConfig | array); - - // Used to overwrite the default config set - public MpesaC2B setConfig(config: MpesaConfig | array); - - // Return the current config - public MpesaConfig getConfig(); - - // Performs the register - // All data is in config - public MpesaResponse registerURL(); - - // Simulate a transaction, this is only available in sandbox - // amount is an unsigned int always check for negative or 0 - // Account reference is used as the account number for paybill, will be ignored, - // when using till - public MpesaResponse simulate(MSISDN: string, amount :int, optional account_reference = '': string); - - // Initiate STK push query - // amount: is an unsigned int always check for negative or 0 - // to: the MSISDN sending the funds - // account_reference: - // timestamp: must be in the format 'yyyymmddhhiiss' - // Alias to initiateSTKPush, just call initiateSTKPush from - // here - public MpesaResponse STKPush( - to: string, - amount: int, - optional account_reference = '': string, - optional description = 'Description': string - optional timestamp = '': string); - - // Initiate STK push query - // amount: is an unsigned int always check for negative or 0 - // to: the MSISDN sending the funds - // account_reference: - // timestamp: must be in the format 'yyyymmddhhiiss' - public MpesaResponse initiateSTKPush( - to: string, - amount: int, - optional account_reference = '': string, - optional description = 'Description': string - optional timestamp = '': string); - - // Query the mpesa client gateway to check for the status of an STK push - // We generate our timestamp if it is not filled, timestamp must be in the format 'yyyymmddhhiiss' - public MpesaResponse STKPushQuery( - checkout_request_id: string, - optional timestamp = '': string - ); - -} - -``` - -## Mpesa business to business (B2B) client class - -This class will handle the logic of performing a B2B related request, it has one method only - -1. B2Bpayment - -``` java -class MpesaB2B { - - // Constructor - // Pass in the mpesa config by ref, this configuration will be immutable - // command_id will be a constant specifying the commandID - MpesaB2B(config: MpesaConfig); - - // Used to overwrite the default config set - public MpesaB2B setConfig(config: MpesaConfig); - - // Return the current config - public MpesaConfig getConfig(); - - // Perform a B2B transaction - // amount: amount to transact - // to: Organization’s short code receiving funds being transacted. - // receiver_identifier_type: specifies the identifier type of the receiver - // supported types include: TODO: add supported constants here - // account_reference: optional account sent if we are using paybill - // remarks: comments sent along with the transaction - public MpesaResponse send( - amount: int, - to: string, - command_id: CONSTANT, - receiver_identifier_type: CONSTANT, - optional account_reference = '': string, - optional remarks = 'remarks': string - ); - -} - -``` - -## Mpesa business to client (B2C) class - -This class will handle logic of performing B2C related request, it has one method only - -1. B2Cpayment - -``` java -class MpesaB2C { - // Constructor - // Pass in the mpesa config by ref, this configuration will be immutable - // command_id will be a constant specifying the commandID - // TODO: Add the supported command ids here - MpesaB2C(config: MpesaConfig); - - // Used to overwrite the default config set - public MpesaB2C setConfig(config: MpesaConfig); - - // Return the current config - public MpesaConfig getConfig(); - - // Perform a B2C transaction - // amount: amount to transact - // to: MSISDN receiving funds being transacted. - // command IDs - // account_reference: optional account sent if we are using paybill - // remarks: comments sent along with the transaction - // occasion: optional description sent along with the transaction - public MpesaResponse send( - amount: int, - to: string, - command_id: CONSTANT, - optional remarks = 'remarks': string, - optional occasion = '': string - ); - -} - -``` diff --git a/src/Main/MpesaAuth.php b/src/Main/MpesaAuth.php index a07ced1..a0baa7b 100644 --- a/src/Main/MpesaAuth.php +++ b/src/Main/MpesaAuth.php @@ -22,10 +22,7 @@ class MpesaAuth public function __construct(MpesaConfig $config) { - $this->validateString('Consumer key', $config->getConsumerKey()); - $this->validateString('Consumer secret', $config->getConsumerSecret()); - - $this->config = $config; + $this->setConfig($config); if (self::$http_client === null) { self::$http_client = new MpesaHttp($this->config); @@ -34,6 +31,8 @@ public function __construct(MpesaConfig $config) public function setAuthToken(string $token, int $expires_at_timestamp = 0): self { + $this->validateString('token', $token); + $this->token = $token; $this->expires_at = $expires_at_timestamp === 0 ? time() + 3600 : $expires_at_timestamp; @@ -55,7 +54,7 @@ public function getConfig(): MpesaConfig return $this->config; } - private function refreshToken(): void + protected function refreshToken(): void { $auth_url = sprintf( '%s%s', @@ -106,6 +105,9 @@ public function hasExpired(): bool public function getExpiresAtTime(): int { + // Will refresh token if it has expired + $this->getAuthToken(); + return $this->expires_at; } diff --git a/src/Main/MpesaB2B.php b/src/Main/MpesaB2B.php deleted file mode 100644 index b4d775c..0000000 --- a/src/Main/MpesaB2B.php +++ /dev/null @@ -1,121 +0,0 @@ -config = $config; - - if (self::$http_client === null) { - self::$http_client = new MpesaHttp($this->config); - } - } - - public function setConfig(MpesaConfig | array $config): self - { - if (is_array($config)) { - $config = new MpesaConfig($config); - } - - $this->config = $config; - - return $this; - } - - public function getConfig(): MpesaConfig - { - return $this->config; - } - - public function send( - int $amount, - string $to, - string $command_id, - string $receiver_identifier_type, - string $account_reference = '', - string $remarks = 'remarks' - ): MpesaResponse { - $url = sprintf( - '%s%s', - $this->config->getBaseURL(), - MpesaConstants::MPESA_URIS['payment_request_b2b'] - ); - - // Validate that data is correct - $this->validateString('initiator_name', $this->config->getInitiator()); - $this->validateString('security_credential', $this->config->getSecurityCredential()); - - // $this->validateArray('command_id', $command_id, [ - // MpesaConstants::MPESA_COMMAND_ID_BUSINESS_PAY_BILL, - // MpesaConstants::MPESA_COMMAND_ID_MERCHANT_TO_MERCHANT_TRANSFER, - // MpesaConstants::MPESA_COMMAND_ID_MERCHANT_FROM_MERCHANT_TO_WORKING, - // MpesaConstants::MPESA_COMMAND_ID_MERCHANT_TO_MMF, - // MpesaConstants::MPESA_COMMAND_ID_AGENCY_FLOAT_ADVANCE, - // ]); - - $this->validateArray('reciever_identifier_type', $receiver_identifier_type, [ - MpesaConstants::MPESA_IDENTIFIER_TYPE_PAYBILL, - MpesaConstants::MPESA_IDENTIFIER_TYPE_TILL, - MpesaConstants::MPESA_IDENTIFIER_TYPE_SHORTCODE, - ]); - - $this->validateInt('amount', $amount, 1); - $this->validateString('short_code', $this->config->getShortCode()); - $this->validateString('to', $to); - $this->validateString('queue_timeout_url', $this->config->getQueueTimeoutURL()); - $this->validateString('result_url', $this->config->getResultURL()); - - $this->validateString('remarks', $remarks); - - $temp = [ - 'Initiator' => $this->config->getInitiator(), - 'SecurityCredential' => $this->config->getSecurityCredential(), - 'CommandID' => $command_id, - 'SenderIdentifierType' => $this->config->getIdentifierType(), - 'RecieverIdentifierType' => $receiver_identifier_type, - 'Amount' => $amount, - 'PartyA' => $this->config->getShortCode(), - 'PartyB' => $to, - 'Remarks' => $remarks, - 'QueueTimeOutURL' => $this->config->getQueueTimeoutURL(), - 'ResultURL' => $this->config->getResultURL(), - ]; - - if ($command_id === MpesaConstants::MPESA_COMMAND_ID_BUSINESS_PAY_BILL) { - $this->validateString('account_reference', $account_reference); - - $temp['AccountReference'] = $account_reference; - } - - $response = self::$http_client->request( - 'POST', - $url, - $temp, - [ - 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), - ] - ); - - return $response; - } -} diff --git a/src/Main/MpesaB2C.php b/src/Main/MpesaB2C.php deleted file mode 100644 index 1af262b..0000000 --- a/src/Main/MpesaB2C.php +++ /dev/null @@ -1,103 +0,0 @@ -config = $config; - - if (self::$http_client === null) { - self::$http_client = new MpesaHttp($this->config); - } - } - - public function setConfig(MpesaConfig | array $config): self - { - if (is_array($config)) { - $config = new MpesaConfig($config); - } - - $this->config = $config; - - return $this; - } - - public function getConfig(): MpesaConfig - { - return $this->config; - } - - public function send( - int $amount, - string $to, - string $command_id, - string $remarks = 'remarks', - string $occasion = '' - ): MpesaResponse { - $url = sprintf( - '%s%s', - $this->config->getBaseURL(), - MpesaConstants::MPESA_URIS['payment_request_b2c'] - ); - - // Validate that data is correct - $this->validateString('initiator_name', $this->config->getInitiator()); - $this->validateString('security_credential', $this->config->getSecurityCredential()); - - // $this->validateArray( 'command_id', $command_id, [ - // MpesaConstants::MPESA_COMMAND_ID_BUSINESS_PAYMENT, - // MpesaConstants::MPESA_COMMAND_ID_SALARY_PAYMENT, - // MpesaConstants::MPESA_COMMAND_ID_PROMOTION_PAYMENT, - // ]); - - $this->validateInt('amount', $amount, 1); - $this->validateString('short_code', $this->config->getShortCode()); - $this->validateString('to', $to); - $this->validateString('queue_timeout_url', $this->config->getQueueTimeoutURL()); - $this->validateString('result_url', $this->config->getResultURL()); - - $this->validateString('remarks', $remarks); - - $temp = [ - 'InitiatorName' => $this->config->getInitiator(), - 'SecurityCredential' => $this->config->getSecurityCredential(), - 'CommandID' => $command_id, - 'PartyA' => $this->config->getShortCode(), - 'PartyB' => $to, - 'Amount' => $amount, - 'Remarks' => $remarks, - 'QueueTimeOutURL' => $this->config->getQueueTimeoutURL(), - 'ResultURL' => $this->config->getResultURL(), - 'Occasion' => $occasion, - ]; - - $response = self::$http_client->request( - 'POST', - $url, - $temp, - [ - 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), - ] - ); - - return $response; - } -} diff --git a/src/Main/MpesaC2B.php b/src/Main/MpesaC2B.php deleted file mode 100644 index 6d95768..0000000 --- a/src/Main/MpesaC2B.php +++ /dev/null @@ -1,222 +0,0 @@ -config = $config; - - if (self::$http_client === null) { - self::$http_client = new MpesaHttp($this->config); - } - } - - public function setConfig(MpesaConfig | array $config): self - { - if (is_array($config)) { - $config = new MpesaConfig($config); - } - - $this->config = $config; - - return $this; - } - - public function getConfig(): MpesaConfig - { - return $this->config; - } - - public function registerURL(): MpesaResponse - { - $url = sprintf( - '%s%s', - $this->config->getBaseURL(), - MpesaConstants::MPESA_URIS['register_c2b'] - ); - - // Validate that data is correct - $this->validateString('short_code', $this->config->getShortCode()); - $this->validateString('confirmation_url', $this->config->getConfirmationURL()); - $this->validateString('validation_url', $this->config->getValidationURL()); - - $response = self::$http_client->request( - 'POST', - $url, - [ - 'ShortCode' => $this->config->getShortCode(), - 'ResponseType' => ' ', - 'ConfirmationURL' => $this->config->getConfirmationURL(), - 'ValidationURL' => $this->config->getValidationURL(), - ], - [ - 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), - ] - ); - - return $response; - } - - public function simulate(string $MSISDN, int $amount, $account_reference = ''): MpesaResponse - { - if ($this->config->isProductionEnvironment()) { - throw new MpesaInternalException('This can only work in sandbox'); - } - - $url = sprintf( - '%s%s', - $this->config->getBaseURL(), - MpesaConstants::MPESA_URIS['simulate_c2b'] - ); - - // Validate that data is correct - $this->validateString('short_code', $this->config->getShortCode()); - $this->validateString('MSISDN', $MSISDN); - $this->validateInt('amount', $amount, 1); - - $temp = [ - 'ShortCode' => $this->config->getShortCode(), - 'CommandID' => $this->config->getIdentifierType() === MpesaConstants::MPESA_IDENTIFIER_TYPE_TILL ? - MpesaConstants::MPESA_COMMAND_ID_CUSTOMER_BUY_GOODS_ONLINE : MpesaConstants::MPESA_COMMAND_ID_CUSTOMER_PAYBILL_ONLINE, - 'Amount' => "{$amount}", - 'Msisdn' => $MSISDN, - 'BillRefNumber' => '', - ]; - - // Append account number if we are using paybill - if ($this->config->getIdentifierType() !== MpesaConstants::MPESA_IDENTIFIER_TYPE_TILL) { - $temp['BillRefNumber'] = $account_reference; - } - - $response = self::$http_client->request( - 'POST', - $url, - $temp, - [ - 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), - ] - ); - - return $response; - } - - /** - * Alias to initiateSTKPush - */ - public function STKPush( - string $to, - int $amount, - string $account_reference = '', - string $description = 'Description', - string $timestamp = '' - ): MpesaResponse { - return $this->initiateSTKPush($to, $amount, $account_reference, $description, $timestamp); - } - - public function initiateSTKPush( - string $to, - int $amount, - string $account_reference = '', - string $description = 'Description', - string $timestamp = '' - ): MpesaResponse { - $url = sprintf( - '%s%s', - $this->config->getBaseURL(), - MpesaConstants::MPESA_URIS['stk_push'] - ); - - $my_timestamp = trim($timestamp); - - if ($my_timestamp === '') { - $my_timestamp = date('Ymdhis', time()); - } - - // Validate that data is correct - $this->validateString('business_short_code', $this->config->getBusinessShortCode()); - $this->validateString('timestamp', $my_timestamp); - $this->validateString('passkey', $this->config->getPasskey()); - $this->validateInt('amount', $amount, 1); - $this->validateString('to', $to); - $this->validateString('short_code', $this->config->getShortCode()); - $this->validateString('stk_callback_url', $this->config->getSTKCallbackURL()); - - $response = self::$http_client->request( - 'POST', - $url, - [ - 'BusinessShortCode' => $this->config->getBusinessShortCode(), - 'Password' => $this->config->getPassword($my_timestamp), - 'Timestamp' => $my_timestamp, - 'TransactionType' => MpesaConstants::MPESA_COMMAND_ID_CUSTOMER_PAYBILL_ONLINE, - 'Amount' => $amount, - 'PartyA' => $to, - 'PartyB' => $this->config->getShortCode(), - 'PhoneNumber' => $to, - 'CallBackURL' => $this->config->getSTKCallbackURL(), - 'AccountReference' => $account_reference, - 'TransactionDesc' => $description, - ], - [ - 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), - ] - ); - - return $response; - } - - public function STKPushQuery(string $checkout_request_id, string $timestamp = ''): MpesaResponse - { - $url = sprintf( - '%s%s', - $this->config->getBaseURL(), - MpesaConstants::MPESA_URIS['stk_push_query'] - ); - - // Validate that data is correct - $this->validateString('business_short_code', $this->config->getBusinessShortCode()); - $this->validateString('passkey', $this->config->getPasskey()); - $this->validateString('checkout_request_id', $checkout_request_id); - - $my_timestamp = trim($timestamp); - - if ($my_timestamp === '') { - $my_timestamp = date('Ymdhis', time()); - } - - $response = self::$http_client->request( - 'POST', - $url, - [ - 'BusinessShortCode' => $this->config->getBusinessShortCode(), - 'Password' => $this->config->getPassword($my_timestamp), - 'Timestamp' => $my_timestamp, - 'CheckoutRequestID' => $checkout_request_id, - ], - [ - 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), - ] - ); - - return $response; - } -} diff --git a/src/Main/MpesaConfig.php b/src/Main/MpesaConfig.php index e06f30a..6585634 100644 --- a/src/Main/MpesaConfig.php +++ b/src/Main/MpesaConfig.php @@ -668,7 +668,7 @@ public function getPasskey(): string } /** - * Gets the Mpesa password. + * Gets the Mpesa STK password. * * The password for encrypting the request. This is generated by * base64 encoding BusinessShortcode, Passkey and Timestamp. @@ -815,6 +815,22 @@ public function setToken(string $token, int $expires_at = 0): self return $this; } + /** + * Returns the auth token + */ + public function getToken() + { + return $this->getAuth()->getToken(); + } + + /** + * Get timestamp when the current token will expire + */ + public function getExpiresAtTime(): int + { + return $this->getAuth()->getExpiresAtTime(); + } + /** * Checks to see if a certain config key is set. * diff --git a/src/Main/MpesaHttp.php b/src/Main/MpesaHttp.php index a62cf69..a53e04e 100644 --- a/src/Main/MpesaHttp.php +++ b/src/Main/MpesaHttp.php @@ -58,7 +58,7 @@ public function request( } } - private function client( + protected function client( string $method, string $uri, ?array $body = null, diff --git a/src/Mpesa.php b/src/Mpesa.php index 0864ef9..a01ee22 100644 --- a/src/Mpesa.php +++ b/src/Mpesa.php @@ -2,11 +2,9 @@ namespace Hackdelta\Mpesa; +use Hackdelta\Mpesa\Exceptions\MpesaInternalException; use Hackdelta\Mpesa\Extras\MpesaConstants; use Hackdelta\Mpesa\Extras\Validatable; -use Hackdelta\Mpesa\Main\MpesaB2B; -use Hackdelta\Mpesa\Main\MpesaB2C; -use Hackdelta\Mpesa\Main\MpesaC2B; use Hackdelta\Mpesa\Main\MpesaConfig; use Hackdelta\Mpesa\Main\MpesaHttp; use Hackdelta\Mpesa\Main\MpesaResponse; @@ -19,73 +17,54 @@ class Mpesa use Validatable; protected static ?MpesaHttp $http_client = null; - - protected static ?MpesaB2C $B2C = null; - protected static ?MpesaC2B $C2B = null; - protected static ?MpesaB2B $B2B = null; - protected MpesaConfig $config; - public function __construct(MpesaConfig | array $config) + /** + * Constructor + * @param array|MpesaConfig $config The config to pass to this class instance + */ + public function __construct($config) { - if (is_array($config)) { - $config = new MpesaConfig($config); - } - - $this->config = $config; + $this->setConfig($config); if (self::$http_client === null) { self::$http_client = new MpesaHttp($this->config); } } - public function setConfig(MpesaConfig | array $config): self + /** + * Change the config of this instance + * @param array|MpesaConfig $config - The config to pass to this class instance + * + * @return self + */ + public function setConfig($config): self { if (is_array($config)) { $config = new MpesaConfig($config); } - $this->config = $config; + if (! $config instanceof MpesaConfig) { + throw new MpesaInternalException("Config must be either array or an instance of MpesaConfig class"); + } - // Change the other classes config to - $this->B2B()->setConfig($config); - $this->B2C()->setConfig($config); - $this->C2B()->setConfig($config); + $this->config = $config; return $this; } + /** + * @return MpesaConfig the config class for this instance + */ public function getConfig(): MpesaConfig { return $this->config; } - public function C2B(): MpesaC2B - { - return self::$C2B === null ? - self::$C2B = new MpesaC2B($this->config) : - self::$C2B; - } - /** - * @deprecated - This set of API is no longer supported + * Account balance query - Check for mpesa balance + * @param string $remarks Optional remarks sent with the request */ - public function B2B(): MpesaB2B - { - return self::$B2B === null ? - self::$B2B = new MpesaB2B($this->config) : - self::$B2B; - } - - public function B2C(): MpesaB2C - { - return self::$B2C === null ? - self::$B2C = new MpesaB2C($this->config) : - self::$B2C; - } - - // Account balance query - // Optional remarks sent with the request public function checkBalance(string $remarks = 'remarks'): MpesaResponse { $url = sprintf( @@ -124,11 +103,15 @@ public function checkBalance(string $remarks = 'remarks'): MpesaResponse return $response; } - // Transaction status, check the transaction status of an mpesa code - // transaction_id: This is the mpesa code - // Remarks: comments sent along with the transaction - // Occasion: additional data sent with the transaction - // original_conversation_id You can use this instead of transaction id + /** + * Check the status of a transaction using either transaction ID or conversation ID + * + * Transaction status, check the transaction status of an mpesa code + * @param string $transaction_id This is the mpesa code + * @param string $remarks comments sent along with the transaction + * @param string $occasion additional data sent with the transaction + * @param string $original_conversation_id You can use this instead of transaction id + */ public function checkTransactionStatus( string $transaction_id, string $remarks = 'remarks', @@ -187,8 +170,11 @@ public function checkTransactionStatus( /** * Initiate reversal request - * transaction_id: The Mpesa code. - * amount: The amount that is being reversed + * + * @param string $transaction_id The Mpesa code. + * @param int $amount The amount that is being reversed + * @param string $remarks optional remarks sent with this request + * @param string $occasion optional value sent with this request */ public function reverseTransaction( string $transaction_id, @@ -313,4 +299,359 @@ public function pullRequestQuery(string $start_date, string $end_date, int $offs return $response; } + + /** + * Send money from business to business + * + * @deprecated This API is no longer supported, kept here for completeness of the library + * + * @param int $amount The amount to send + * @param string $to The shortcode to send the money to + * @param string $command_id command ID for this request check daraja documentation for supported commands + * @param string $receiver_identifier_type type of the reciever + * @param string $account_reference optional value for account number + * @param string $remarks optional remarks sent with the request + */ + public function sendB2B( + int $amount, + string $to, + string $command_id, + string $receiver_identifier_type, + string $account_reference = '', + string $remarks = 'remarks' + ): MpesaResponse { + $url = sprintf( + '%s%s', + $this->config->getBaseURL(), + MpesaConstants::MPESA_URIS['payment_request_b2b'] + ); + + // Validate that data is correct + $this->validateString('initiator_name', $this->config->getInitiator()); + $this->validateString('security_credential', $this->config->getSecurityCredential()); + + // $this->validateArray('command_id', $command_id, [ + // MpesaConstants::MPESA_COMMAND_ID_BUSINESS_PAY_BILL, + // MpesaConstants::MPESA_COMMAND_ID_MERCHANT_TO_MERCHANT_TRANSFER, + // MpesaConstants::MPESA_COMMAND_ID_MERCHANT_FROM_MERCHANT_TO_WORKING, + // MpesaConstants::MPESA_COMMAND_ID_MERCHANT_TO_MMF, + // MpesaConstants::MPESA_COMMAND_ID_AGENCY_FLOAT_ADVANCE, + // ]); + + $this->validateArray('reciever_identifier_type', $receiver_identifier_type, [ + MpesaConstants::MPESA_IDENTIFIER_TYPE_PAYBILL, + MpesaConstants::MPESA_IDENTIFIER_TYPE_TILL, + MpesaConstants::MPESA_IDENTIFIER_TYPE_SHORTCODE, + ]); + + $this->validateInt('amount', $amount, 1); + $this->validateString('short_code', $this->config->getShortCode()); + $this->validateString('to', $to); + $this->validateString('queue_timeout_url', $this->config->getQueueTimeoutURL()); + $this->validateString('result_url', $this->config->getResultURL()); + + $this->validateString('remarks', $remarks); + + $temp = [ + 'Initiator' => $this->config->getInitiator(), + 'SecurityCredential' => $this->config->getSecurityCredential(), + 'CommandID' => $command_id, + 'SenderIdentifierType' => $this->config->getIdentifierType(), + 'RecieverIdentifierType' => $receiver_identifier_type, + 'Amount' => $amount, + 'PartyA' => $this->config->getShortCode(), + 'PartyB' => $to, + 'Remarks' => $remarks, + 'QueueTimeOutURL' => $this->config->getQueueTimeoutURL(), + 'ResultURL' => $this->config->getResultURL(), + ]; + + if ($command_id === MpesaConstants::MPESA_COMMAND_ID_BUSINESS_PAY_BILL) { + $this->validateString('account_reference', $account_reference); + + $temp['AccountReference'] = $account_reference; + } + + $response = self::$http_client->request( + 'POST', + $url, + $temp, + [ + 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), + ] + ); + + return $response; + } + + /** + * Send money from B2C shortcode to client + * + * @param int $amount The amount to send + * @param string $to The shortcode to send the money to + * @param string $command_id command ID for this request check daraja documentation for supported commands + * @param string $remarks optional remarks sent with the request + * @param string $occasion optional value sent with the request + */ + public function sendB2C( + int $amount, + string $to, + string $command_id, + string $remarks = 'remarks', + string $occasion = '' + ): MpesaResponse { + $url = sprintf( + '%s%s', + $this->config->getBaseURL(), + MpesaConstants::MPESA_URIS['payment_request_b2c'] + ); + + // Validate that data is correct + $this->validateString('initiator_name', $this->config->getInitiator()); + $this->validateString('security_credential', $this->config->getSecurityCredential()); + + // $this->validateArray( 'command_id', $command_id, [ + // MpesaConstants::MPESA_COMMAND_ID_BUSINESS_PAYMENT, + // MpesaConstants::MPESA_COMMAND_ID_SALARY_PAYMENT, + // MpesaConstants::MPESA_COMMAND_ID_PROMOTION_PAYMENT, + // ]); + + $this->validateInt('amount', $amount, 1); + $this->validateString('short_code', $this->config->getShortCode()); + $this->validateString('to', $to); + $this->validateString('queue_timeout_url', $this->config->getQueueTimeoutURL()); + $this->validateString('result_url', $this->config->getResultURL()); + + $this->validateString('remarks', $remarks); + + $temp = [ + 'InitiatorName' => $this->config->getInitiator(), + 'SecurityCredential' => $this->config->getSecurityCredential(), + 'CommandID' => $command_id, + 'PartyA' => $this->config->getShortCode(), + 'PartyB' => $to, + 'Amount' => $amount, + 'Remarks' => $remarks, + 'QueueTimeOutURL' => $this->config->getQueueTimeoutURL(), + 'ResultURL' => $this->config->getResultURL(), + 'Occasion' => $occasion, + ]; + + $response = self::$http_client->request( + 'POST', + $url, + $temp, + [ + 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), + ] + ); + + return $response; + } + + /** + * Register C2B validation and confirmation callbacks + */ + public function registerURL(): MpesaResponse + { + $url = sprintf( + '%s%s', + $this->config->getBaseURL(), + MpesaConstants::MPESA_URIS['register_c2b'] + ); + + // Validate that data is correct + $this->validateString('short_code', $this->config->getShortCode()); + $this->validateString('confirmation_url', $this->config->getConfirmationURL()); + $this->validateString('validation_url', $this->config->getValidationURL()); + + $response = self::$http_client->request( + 'POST', + $url, + [ + 'ShortCode' => $this->config->getShortCode(), + 'ResponseType' => ' ', + 'ConfirmationURL' => $this->config->getConfirmationURL(), + 'ValidationURL' => $this->config->getValidationURL(), + ], + [ + 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), + ] + ); + + return $response; + } + + /** + * Simulate a transaction on sandbox environment + * + * @param string $MSISDN phone number + * @param int $amount amount to simulate + * @param string $account_reference the account number for this simulated transaction + */ + public function simulate(string $MSISDN, int $amount, $account_reference = ''): MpesaResponse + { + if ($this->config->isProductionEnvironment()) { + throw new MpesaInternalException('This can only work in sandbox'); + } + + $url = sprintf( + '%s%s', + $this->config->getBaseURL(), + MpesaConstants::MPESA_URIS['simulate_c2b'] + ); + + // Validate that data is correct + $this->validateString('short_code', $this->config->getShortCode()); + $this->validateString('MSISDN', $MSISDN); + $this->validateInt('amount', $amount, 1); + + $temp = [ + 'ShortCode' => $this->config->getShortCode(), + 'CommandID' => $this->config->getIdentifierType() === MpesaConstants::MPESA_IDENTIFIER_TYPE_TILL ? + MpesaConstants::MPESA_COMMAND_ID_CUSTOMER_BUY_GOODS_ONLINE : MpesaConstants::MPESA_COMMAND_ID_CUSTOMER_PAYBILL_ONLINE, + 'Amount' => "{$amount}", + 'Msisdn' => $MSISDN, + 'BillRefNumber' => '', + ]; + + // Append account number if we are using paybill + if ($this->config->getIdentifierType() !== MpesaConstants::MPESA_IDENTIFIER_TYPE_TILL) { + $temp['BillRefNumber'] = $account_reference; + } + + $response = self::$http_client->request( + 'POST', + $url, + $temp, + [ + 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), + ] + ); + + return $response; + } + + /** + * Alias to initiateSTKPush + * + * @param string $to phone number to send STK request to + * @param int $amount amount to request + * @param string $account_reference account number + * @param string $description description to accompany the request + * @param string $timestamp custom timestam in the format 'yyyymmddhhiiss' e.g.20220110232005 + * + * @link STKPushQuery + */ + public function STKPush( + string $to, + int $amount, + string $account_reference = '', + string $description = 'Description', + string $timestamp = '' + ): MpesaResponse { + return $this->initiateSTKPush($to, $amount, $account_reference, $description, $timestamp); + } + /** + * Send STK push query + * + * @param string $to phone number to send STK request to + * @param int $amount amount to request + * @param string $account_reference account number + * @param string $description description to accompany the request + * @param string $timestamp custom timestam in the format 'yyyymmddhhiiss' e.g. 20220110232005 + */ + public function initiateSTKPush( + string $to, + int $amount, + string $account_reference = '', + string $description = 'Description', + string $timestamp = '' + ): MpesaResponse { + $url = sprintf( + '%s%s', + $this->config->getBaseURL(), + MpesaConstants::MPESA_URIS['stk_push'] + ); + + $my_timestamp = trim($timestamp); + + if ($my_timestamp === '') { + $my_timestamp = date('Ymdhis', time()); + } + + // Validate that data is correct + $this->validateString('business_short_code', $this->config->getBusinessShortCode()); + $this->validateString('timestamp', $my_timestamp); + $this->validateString('passkey', $this->config->getPasskey()); + $this->validateInt('amount', $amount, 1); + $this->validateString('to', $to); + $this->validateString('short_code', $this->config->getShortCode()); + $this->validateString('stk_callback_url', $this->config->getSTKCallbackURL()); + + $response = self::$http_client->request( + 'POST', + $url, + [ + 'BusinessShortCode' => $this->config->getBusinessShortCode(), + 'Password' => $this->config->getPassword($my_timestamp), + 'Timestamp' => $my_timestamp, + 'TransactionType' => MpesaConstants::MPESA_COMMAND_ID_CUSTOMER_PAYBILL_ONLINE, + 'Amount' => $amount, + 'PartyA' => $to, + 'PartyB' => $this->config->getShortCode(), + 'PhoneNumber' => $to, + 'CallBackURL' => $this->config->getSTKCallbackURL(), + 'AccountReference' => $account_reference, + 'TransactionDesc' => $description, + ], + [ + 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), + ] + ); + + return $response; + } + + /** + * Check on the status of an STK push query + * + * @param string $checkout_request_id The checkout request id received after initiating an STK push query + * @param string $timestamp Timestamp associated with the transaction in the format 'yyyymmddhhiiss' + */ + public function STKPushQuery(string $checkout_request_id, string $timestamp = ''): MpesaResponse + { + $url = sprintf( + '%s%s', + $this->config->getBaseURL(), + MpesaConstants::MPESA_URIS['stk_push_query'] + ); + + // Validate that data is correct + $this->validateString('business_short_code', $this->config->getBusinessShortCode()); + $this->validateString('passkey', $this->config->getPasskey()); + $this->validateString('checkout_request_id', $checkout_request_id); + + $my_timestamp = trim($timestamp); + + if ($my_timestamp === '') { + $my_timestamp = date('Ymdhis', time()); + } + + $response = self::$http_client->request( + 'POST', + $url, + [ + 'BusinessShortCode' => $this->config->getBusinessShortCode(), + 'Password' => $this->config->getPassword($my_timestamp), + 'Timestamp' => $my_timestamp, + 'CheckoutRequestID' => $checkout_request_id, + ], + [ + 'Authorization' => sprintf('Bearer %s', $this->config->getAuth()->getToken()), + ] + ); + + return $response; + } }