diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..34e52b0 --- /dev/null +++ b/_config.yml @@ -0,0 +1,39 @@ +# _config.yml + +# Site settings +title: M-Pesa SDK for PHP +description: A lightweight SDK for seamless M-Pesa integrations in PHP applications +baseurl: "/mpesa-sdk-php" +url: "https://levizwannah.github.io" + +# Author details +author: + name: Levi Kamara Zwannah + email: your-email@example.com + +# Theme setup +theme: jekyll-theme-cayman + +# Plugins +plugins: + - jekyll-feed + - jekyll-seo-tag + - jekyll-sitemap + - jemoji + +# Features +markdown: kramdown +highlighter: rouge + +# SEO settings +seo: + title: "M-Pesa SDK PHP" + description: "Simple, lightweight SDK for integrating M-Pesa in PHP applications." + +# Footer +footer: + message: "Developed by Levi Kamara Zwannah" + powered_by: "Powered by GitHub Pages" + links: + - title: "GitHub" + url: "https://github.com/levizwannah/mpesa-sdk-php" diff --git a/index.md b/index.md new file mode 120000 index 0000000..5926f8d --- /dev/null +++ b/index.md @@ -0,0 +1 @@ +readme.md \ No newline at end of file diff --git a/readme.md b/readme.md index 930367c..4e955f8 100644 --- a/readme.md +++ b/readme.md @@ -641,6 +641,56 @@ $conversationId = $response->ConversationID; //... save to db, etc ``` + +## Mpesa Ratiba (Subscription) +This API enables you to create Mpesa Standing Orders. Take it as a subscription API for Mpesa + +### Requirements +Ensure these values were set as shown in the setup section: +- Consumer Key (`key`) +- Consumer Secret(`secret`) +- Business Short Code (`code`); + +### Usage +```php +// ...setup... +$subscription = $mpesa->subscription(); // same as $mpesa->ratiba(); + +$subscription->amount(100) + ->plan("Gold Plan") // Standing order Name (same as $sub->name('Gold Plan')) + ->phone('0740958756') + ->startDate(10, 10, 2024) // month, day, year + ->endDate(10, 11, 2024) // month, day, year + ->frequency(Constant::FREQ_DAILY) + ->callback('https://my.url/path/to/result'); + +# if your code is paybill number +$subscription->paybill() // if paybill number + ->account('account-number'); + +# if using a till till number +$subscription->buygoods() + ->till(1234567); // if till number + +# optional +$subscription->description('optional description'); + +# create subscription +$subscription->create(); + +if(!$subscription->accepted()) { + $error = $subscription->error(); + echo "$error->code $error->message"; + // exit; +} + +$response = $subscription->response(); +$refId = $response->responseRefID; +//... + +//... save to db, etc +``` + # Quick Note If you are confused on how to handle the results in the callback, please read the earlier sections of this README file. diff --git a/src/Helpers/AccountBalance.php b/src/Helpers/AccountBalance.php index a2a403c..ab64445 100644 --- a/src/Helpers/AccountBalance.php +++ b/src/Helpers/AccountBalance.php @@ -28,7 +28,7 @@ class AccountBalance extends MpesaWithInitiator { protected string $type = Constant::ACCOUNT_BALANCE; /** - * You should not call this directly. Use $mpesa->b2c() + * You should not call this directly. Use $mpesa->balance() * @param array $config */ public function __construct(array $config) diff --git a/src/Helpers/BusinessToBulk.php b/src/Helpers/BusinessToBulk.php index a6a4790..2d8a1fe 100644 --- a/src/Helpers/BusinessToBulk.php +++ b/src/Helpers/BusinessToBulk.php @@ -54,7 +54,7 @@ class BusinessToBulk extends MpesaWithInitiator protected $receiverIdentifierType = 4; /** - * You should not call this directly. Use $mpesa->b2c() + * You should not call this directly. Use $mpesa->btb() * @param array $config */ public function __construct(array $config) diff --git a/src/Helpers/BusinessToBusiness.php b/src/Helpers/BusinessToBusiness.php index 927ff8c..fa95c2d 100644 --- a/src/Helpers/BusinessToBusiness.php +++ b/src/Helpers/BusinessToBusiness.php @@ -64,7 +64,7 @@ class BusinessToBusiness extends MpesaWithInitiator { protected $identifierType = 4; /** - * You should not call this directly. Use $mpesa->b2c() + * You should not call this directly. Use $mpesa->b2b() * @param array $config */ public function __construct(array $config) diff --git a/src/Helpers/Constant.php b/src/Helpers/Constant.php index bc9a3f7..055ae01 100644 --- a/src/Helpers/Constant.php +++ b/src/Helpers/Constant.php @@ -42,6 +42,20 @@ private function __construct(){} // Business To Bulk const BUSINESS_TO_BULK = "BusinessPayToBulk"; const BUSINESS_MMF_UTILITY = "BusinessTransferFromMMFToUtility"; + + // Standing Orders + const STANDING_ORDERS_PAYBILL = "Standing Order Customer Pay Bill"; + const STANDING_ORDERS_TILL = "Standing Order Customer Pay Merchant"; + + // Standing Orders + const FREQ_ONE_OFF = 1; + const FREQ_DAILY = 2; + const FREQ_WEEKLY = 3; + const FREQ_MONTHLY = 4; + const FREQ_BI_MONTHLY = 5; + const FREQ_QUARTERLY = 6; + const FREQ_HALF_YEARLY = 7; + const FREQ_YEARLY = 8; } ?> diff --git a/src/Helpers/Stk.php b/src/Helpers/Stk.php index f8484ca..0d064a5 100644 --- a/src/Helpers/Stk.php +++ b/src/Helpers/Stk.php @@ -1,225 +1,235 @@ stk(); - * @param array $config initial configurations for the stk push. - * This is gotten from the parent Mpesa. - */ - public function __construct(array $config) - { - $this->configure($config); - } +class Stk extends Mpesa +{ + + /** + * Phone number making the payment + * + * @var string + */ + public string $phone; + + /** + * The amount to pay + * @var int + */ + public int $amount; + + /** + * The account Reference + * @var string + */ + public string $reference = "default"; + + /** + * Callback Url + * + * @var string + */ + public string $callback; + + /** + * Transaction description + * @var string + */ + public string $description = "payment"; + + /** + * The transaction type. + * @var string + */ + public string $type = Constant::PAY_BILL_ONLINE; + + /** + * !!!Do not call this directly. It needs config from the main mpesa class. + * Use $mpesa->stk(); + * @param array $config initial configurations for the stk push. + * This is gotten from the parent Mpesa. + */ + public function __construct(array $config) + { + $this->configure($config); + } - /** - * Gets the configured StkQuery Object - * @return StkQuery - */ - public function query(){ - return new StkQuery([ - "key" => $this->key, - "secret" => $this->secret, - "passkey" => $this->passkey, - "code" => $this->code, - "till" => $this->till, - "baseUrl" => $this->baseUrl - ]); - } + /** + * Gets the configured StkQuery Object + * @return StkQuery + */ + public function query() + { + return new StkQuery([ + "key" => $this->key, + "secret" => $this->secret, + "passkey" => $this->passkey, + "code" => $this->code, + "till" => $this->till, + "baseUrl" => $this->baseUrl + ]); + } - public function configure(array $config) - { - parent::configure($config); - if(isset($config["phone"])) $this->phone($config["phone"]); - return $this; - } + public function configure(array $config) + { + parent::configure($config); + if (isset($config["phone"])) $this->phone($config["phone"]); + return $this; + } - /** - * Sets the phone to pay to. - * Accepts 07xxxxxxxx, +2547xxxxxxxxx, or 2547xxxxxxxxx - * @param string $phone - * - */ - public function phone(string $phone){ - $phone = "254" . substr(preg_replace("/\s+/", "", $phone), -9); - $this->phone = $phone; - return $this; - } + /** + * Sets the phone to pay to. + * Accepts 07xxxxxxxx, +2547xxxxxxxxx, or 2547xxxxxxxxx + * @param string $phone + * + */ + public function phone(string $phone) + { + $phone = "254" . substr(preg_replace("/\s+/", "", $phone), -9); + $this->phone = $phone; + return $this; + } - /** - * Sets the Amount. Kindly note, amount will be converted to int. - * @param mixed $amount - * - */ - public function amount($amount){ - $this->amount = (int)$amount; - return $this; - } + /** + * Sets the Amount. Kindly note, amount will be converted to int. + * @param mixed $amount + * + */ + public function amount($amount) + { + $this->amount = (int)$amount; + return $this; + } - /** - * Sets the account number - * @param string $number - * - */ - public function reference(string $number){ - $this->reference = $number; - return $this; - } + /** + * Sets the account number + * @param string $number + * + */ + public function reference(string $number) + { + $this->reference = $number; + return $this; + } - /** - * Sets the account number for paybill - * @param string $number - * - */ - public function account(string $number){ - $this->reference = $number; - return $this; - } + /** + * Sets the account number for paybill + * @param string $number + * + */ + public function account(string $number) + { + $this->reference = $number; + return $this; + } - /** - * Sets the transaction description - * @param string $description - * - */ - public function description(string $description){ - $this->description = $description; - return $this; - } + /** + * Sets the transaction description + * @param string $description + * + */ + public function description(string $description) + { + $this->description = $description; + return $this; + } - /** - * Sets the command ID to buy goods. - */ - public function buygoods(){ - $this->type = Constant::BUY_GOODS_ONLINE; - return $this; - } + /** + * Sets the command ID to buy goods. + */ + public function buygoods() + { + $this->type = Constant::BUY_GOODS_ONLINE; + return $this; + } - /** - * Sets the command ID to paybill - */ - public function paybill(){ - $this->type = Constant::PAY_BILL_ONLINE; - return $this; - } + /** + * Sets the command ID to paybill + */ + public function paybill() + { + $this->type = Constant::PAY_BILL_ONLINE; + return $this; + } - /** - * Sets the callback URL - * @param string $callback - * - */ - public function callback(string $callback){ - $this->callback = $callback; - return $this; - } + /** + * Sets the callback URL + * @param string $callback + * + */ + public function callback(string $callback) + { + $this->callback = $callback; + return $this; + } - /** - * Makes the STK push - */ - public function push(){ - $this->okay(); - - $timestamp = date("YmdHis"); - $password = base64_encode("$this->code$this->passkey$timestamp"); - - $data = [ - "BusinessShortCode" => $this->code, - "Password" => $password, - "Timestamp" => $timestamp, - "TransactionType" => $this->type, - "Amount" => $this->amount, - "PartyA" => $this->phone, - "PartyB" => ($this->type == Constant::BUY_GOODS_ONLINE)? $this->till : $this->code, - "PhoneNumber" => $this->phone, - "CallBackURL" => $this->callback, - "AccountReference" => $this->reference, - "TransactionDesc" => $this->description - ]; - - $this->response = $this->request($data, "/mpesa/stkpush/v1/processrequest"); - - $this->response->Timestamp = $timestamp; - - return $this; - } + /** + * Makes the STK push + */ + public function push() + { + $this->okay(); + + $timestamp = date("YmdHis"); + $password = base64_encode("$this->code$this->passkey$timestamp"); + + $data = [ + "BusinessShortCode" => $this->code, + "Password" => $password, + "Timestamp" => $timestamp, + "TransactionType" => $this->type, + "Amount" => $this->amount, + "PartyA" => $this->phone, + "PartyB" => ($this->type == Constant::BUY_GOODS_ONLINE) ? $this->till : $this->code, + "PhoneNumber" => $this->phone, + "CallBackURL" => $this->callback, + "AccountReference" => $this->reference, + "TransactionDesc" => $this->description + ]; + + $this->response = $this->request($data, "/mpesa/stkpush/v1/processrequest"); + + $this->response->Timestamp = $timestamp; + + return $this; + } - /** - * Makes the STK push - * @return object response - */ - public function __invoke() - { - $this->push(); + /** + * Makes the STK push + * @return object response + */ + public function __invoke() + { + $this->push(); - return $this->response(); - } + return $this->response(); + } - /** - * Checks if all the required data to make an stk push exist and is not empty. - * @return bool - */ - public function okay(){ - parent::okay(); - - if($this->type == Constant::BUY_GOODS_ONLINE - && empty($this->till)) { - throw new Exception("Till Number is empty while using buy goods transaction type"); - } - - $this->assertExists("phone"); - $this->assertExists("amount"); - $this->assertExists("passkey"); - $this->assertExists("callback", "Callback URL"); - return true; + /** + * Checks if all the required data to make an stk push exist and is not empty. + * @return bool + */ + public function okay() + { + parent::okay(); + + if ( + $this->type == Constant::BUY_GOODS_ONLINE + && empty($this->till) + ) { + throw new Exception("Till Number is empty while using buy goods transaction type"); } - + $this->assertExists("phone"); + $this->assertExists("amount"); + $this->assertExists("passkey"); + $this->assertExists("callback", "Callback URL"); + return true; } - -?> +} diff --git a/src/Helpers/StkQuery.php b/src/Helpers/StkQuery.php index 9c9b498..ecb7561 100644 --- a/src/Helpers/StkQuery.php +++ b/src/Helpers/StkQuery.php @@ -15,7 +15,7 @@ class StkQuery extends Mpesa { /** * !!!Do not call this directly. It needs config from the parent STK class. - * User $mpesa->stk()->query(); + * Use $mpesa->stk()->query(); * @param array $config initial configurations for the stk push. * This is gotten from the parent Mpesa. */ diff --git a/src/Helpers/Subscription.php b/src/Helpers/Subscription.php new file mode 100644 index 0000000..6759614 --- /dev/null +++ b/src/Helpers/Subscription.php @@ -0,0 +1,310 @@ +subscription(); + * @param array $config initial configurations for the stk push. + * This is gotten from the parent Mpesa. + */ + public function __construct(array $config) + { + $this->configure($config); + } + + public function configure(array $config) + { + parent::configure($config); + if (isset($config["phone"])) $this->phone($config["phone"]); + return $this; + } + + /** + * Set the start date of the subscription. + * The SDK will handle the formatting. + * @param int $month + * @param int $day + * @param int $year + * + */ + public function startDate(int $month, int $day, int $year) + { + + $year = str_pad($year, 4, "0", STR_PAD_LEFT); + $month = str_pad($month, 2, "0", STR_PAD_LEFT); + $day = str_pad($day, 2, "0", STR_PAD_LEFT); + + $this->startDate = "{$year}{$month}{$day}"; + return $this; + } + + /** + * Set the end date of the subscription. + * The SDK will handle the formatting. + * @param int $month + * @param int $day + * @param int $year + * + */ + public function endDate(int $month, int $day, int $year) + { + + $year = str_pad($year, 4, "0", STR_PAD_LEFT); + $month = str_pad($month, 2, "0", STR_PAD_LEFT); + $day = str_pad($day, 2, "0", STR_PAD_LEFT); + + $this->endDate = "{$year}{$month}{$day}"; + + return $this; + } + + /** + * Sets the standing order Name + * @param string $planName + * + */ + public function plan(string $planName) + { + return $this->name($planName); + } + + /** + * @param string $planName sets the plan name same as the + * plan method. + * + */ + public function name(string $planName) + { + $this->name = $planName; + return $this; + } + + /** + * @param Constant::FREQ_ONE_OFF|Constant::FREQ_DAILY|Constant::FREQ_WEEKLY|Constant::FREQ_MONTHLY|Constant::FREQ_BI_MONTHLY|Constant::FREQ_QUARTERLY|Constant::FREQ_HALF_YEARLY|Constant::FREQ_YEARLY $frequency + */ + public function frequency(int $frequency = Constant::FREQ_ONE_OFF) + { + $this->frequency = $frequency; + return $this; + } + + /** + * Sets the phone to pay to. + * Accepts 07xxxxxxxx, +2547xxxxxxxxx, or 2547xxxxxxxxx + * @param string $phone + * + */ + public function phone(string $phone) + { + $phone = "254" . substr(preg_replace("/\s+/", "", $phone), -9); + $this->phone = $phone; + return $this; + } + + /** + * Sets the Amount. Kindly note, amount will be converted to int. + * @param mixed $amount + * + */ + public function amount($amount) + { + $this->amount = (int)$amount; + return $this; + } + + /** + * Sets the account number + * @param string $number + * + */ + public function reference(string $number) + { + $this->reference = $number; + return $this; + } + + /** + * Sets the account number for paybill + * @param string $number + * + */ + public function account(string $number) + { + $this->reference = $number; + return $this; + } + + /** + * Sets the transaction description + * @param string $description + * + */ + public function description(string $description) + { + $this->description = $description; + return $this; + } + + /** + * Sets the command ID to Standing order for Merchant (Till numbers). + */ + public function buygoods() + { + $this->type = Constant::STANDING_ORDERS_TILL; + return $this; + } + + /** + * Sets the command ID to Standing order for Paybill + */ + public function paybill() + { + $this->type = Constant::STANDING_ORDERS_PAYBILL; + return $this; + } + + /** + * Sets the callback URL + * @param string $callback + * + */ + public function callback(string $callback) + { + $this->callback = $callback; + return $this; + } + + /** + * Makes the STK push + */ + public function create() + { + $this->okay(); + + $data = [ + "StandingOrderName" => $this->name, + "ReceiverPartyIdentifierType" => ($this->type == Constant::STANDING_ORDERS_TILL) + ? 2 : 4, + "TransactionType" => $this->type, + "BusinessShortCode" => ($this->type == Constant::STANDING_ORDERS_TILL) + ? $this->till : $this->code, + "PartyA" => $this->phone, + "Amount" => $this->amount, + "StartDate" => $this->startDate, + "EndDate" => $this->endDate, + "Frequency" => $this->frequency, + "AccountReference" => $this->reference, + "TransactionDesc" => $this->description, + "CallBackURL" => $this->callback, + ]; + + $this->response = $this->request($data, "/standingorder/v1/createStandingOrderExternal"); + return $this; + } + + /** + * Makes the STK push + * @return object response + */ + public function __invoke() + { + $this->create(); + + return $this->response(); + } + + /** + * Checks if all the required data to make an stk push exist and is not empty. + * @return bool + */ + public function okay() + { + parent::okay(); + + if ( + $this->type == Constant::STANDING_ORDERS_TILL + && empty($this->till) + ) { + throw new Exception("Till cannot be empty when using standing orders for merchant"); + } + + $this->assertExists("phone"); + $this->assertExists("amount"); + $this->assertExists("startDate", "Start Date"); + $this->assertExists("endDate", "End Date"); + $this->assertExists("frequency", "Frequency of Payment"); + $this->assertExists("callback", "Callback URL"); + return true; + } +} diff --git a/src/Mpesa.php b/src/Mpesa.php index fe5353f..c23ac43 100644 --- a/src/Mpesa.php +++ b/src/Mpesa.php @@ -14,6 +14,7 @@ use LeviZwannah\MpesaSdk\Helpers\RequestError; use LeviZwannah\MpesaSdk\Helpers\Reversal; use LeviZwannah\MpesaSdk\Helpers\Stk; +use LeviZwannah\MpesaSdk\Helpers\Subscription; use LeviZwannah\MpesaSdk\Helpers\Traits\FieldToPropertyTrait; use LeviZwannah\MpesaSdk\Helpers\TransactionQuery; use LeviZwannah\MpesaSdk\Helpers\UrlManager; @@ -310,7 +311,7 @@ public function token() $obj = json_decode($response); - if(is_null($obj)) { + if (is_null($obj)) { throw new Exception("Unable to access Mpesa Gateway"); } @@ -456,6 +457,31 @@ public function stk() ]); } + /** + * Gets the configured Subscription (Ratiba) object + * @return Subscription + */ + public function subscription() + { + return new Subscription([ + "key" => $this->key, + "secret" => $this->secret, + "code" => $this->code, + "till" => $this->till, + "baseUrl" => $this->baseUrl + ]); + } + + /** + * Gets the configured Ratiba (Subscription) object + * @see subscription() + * @return Subscription + */ + public function ratiba() + { + return $this->subscription(); + } + /** * Gets the configured QrCode Object * @return QrCode @@ -474,7 +500,8 @@ public function qr() * Gets the configured BusinessToBulk Object * @return BusinessToBulk */ - public function btb(){ + public function btb() + { return new BusinessToBulk([ "key" => $this->key, "secret" => $this->secret,