From b4256a890aa9b6770c909f960d3a93eac2e75a88 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Thu, 16 Oct 2014 18:10:22 +0300 Subject: [PATCH 01/18] Paybill parsing typo A small typo preventing the parsing of paybill receipt numbers caused them to not be registered properly. This has been fixed. --- .../PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php index 087064d..7e01f01 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php @@ -66,7 +66,7 @@ static public function scrubPayment(&$rawtext) { // Reciept $matches = array(); if (preg_match('/\s*(.+)<\/a\s*>\s*<\/td\s*>/iU', $rawtext, $matches) > 0) { - $result['RECIEPT'] = trim($matches[1]); + $result['RECEIPT'] = trim($matches[1]); } // Time From 795c498ca5ab67899921e9141b8d17eb6abc8107 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Thu, 16 Oct 2014 22:23:49 +0300 Subject: [PATCH 02/18] Redundant push notifications MpesaPaybill transactions had an issue where they would trigger a push notification several time for the same payment. This left the receiving application to keep track of repeated transactions and was generally not so cool. The behaviour has been changed it will not only trigger a push callback upon creation of a new transaction. This may have to be examined further to avoid situations where existing transactions gets cancled/rolled-back by Safcom as they might have the same receipt id. (hence a double trigger is actually needed in that case). --- .../PesaPi/MpesaPaybill/MpesaPaybill.php | 17 +++++++++-------- .../PesaPi/MpesaPaybill/Transaction.php | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php index 5ed45bb..61bd150 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php @@ -169,9 +169,9 @@ public function forceSyncronisation() { $rows = $scrubber->scrubRows($page); // save data to database foreach ($rows AS $row) { - $payment = Transaction::updateData($row, $this); - if (is_object($payment)) { - $this->handleCallback($payment); + $tuple = Transaction::updateData($row, $this); + if ($tuple[1] AND is_object($tuple[0])) { + $this->handleCallback($tuple[0]); } } } @@ -202,12 +202,13 @@ public function importIPN($get) { "COST" => 0); if ($temp['AMOUNT'] > 0 AND $temp['RECEIPT'] != "") { - $transaction = Transaction::updateData($temp, $this); + $tuple = Transaction::updateData($temp, $this); - // Callback if needed - $this->handleCallback($transaction); - - return $transaction; + if ($tuple[1]) { + // Callback if needed + $this->handleCallback($tuple[0]); + } + return $tuple[0]; } return null; } diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Transaction.php index 66ee6f9..05ccfd2 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Transaction.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Transaction.php @@ -79,10 +79,10 @@ public static function updateData($row, $account) { } $existing[0]->update(); - return $existing[0]; + return array($existing[0], false); } else { - return Transaction::import($account, $row); + return array(Transaction::import($account, $row), true); } } From 6f312d2e04e393076ca1028ef7d1e2f4b8c1852e Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Wed, 5 Nov 2014 10:49:42 +0100 Subject: [PATCH 03/18] Missing } A parse error in ChargeCalculator prevented pesapi from handling private incomming messages. Thanks to Barnabie Opiyo for noticing.. We should really start implementing some sort of structured/automated testing, too many of these little "typo" are getting out there. --- php/include/PLUSPEOPLE/PesaPi/MpesaPrivate/ChargeCalculator.php | 1 + 1 file changed, 1 insertion(+) diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPrivate/ChargeCalculator.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPrivate/ChargeCalculator.php index f58bc50..a9d6a04 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPrivate/ChargeCalculator.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPrivate/ChargeCalculator.php @@ -100,6 +100,7 @@ static protected function sendingCost($time, $amount) { } else { return 11000; } + } } static protected function withdrawCost($time, $amount) { From 5793d9a8be4bab84dbb0bf9ee193464846d2c866 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Sat, 8 Nov 2014 12:49:00 +0100 Subject: [PATCH 04/18] Paybill no results fix When a statement from the Mpesa paybill platform return no results it will instead return a semi result stating "No records to display". This would result in an "unknown" transaction being registered. This flaw has been fixed - the html scraber now proberly detects and ignores these no result transaction rows. In addition initial work on Airtel Paybill, and mobile money in Somalia has been included. --- .../PLUSPEOPLE/PesaPi/Base/Account.php | 6 +- .../PLUSPEOPLE/PesaPi/Base/AccountFactory.php | 12 + .../PesaPi/KenyaAirtelPaybill/Account.php | 90 +++++++ .../PesaPi/KenyaAirtelPaybill/Loader.php | 231 ++++++++++++++++++ .../PesaPi/KenyaAirtelPaybill/Transaction.php | 40 +++ .../MpesaPaybill/HTMLPaymentScrubber1.php | 6 + .../PesaPi/MpesaPaybill/Scrubber.php | 5 +- .../PesaPi/SomaliaGolisPrivate/Account.php | 90 +++++++ .../PesaPi/SomaliaHormuudPrivate/Account.php | 90 +++++++ .../PesaPi/SomaliaTelesomePrivate/Account.php | 90 +++++++ 10 files changed, 658 insertions(+), 2 deletions(-) create mode 100644 php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Account.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Transaction.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Account.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/SomaliaHormuudPrivate/Account.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/SomaliaTelesomePrivate/Account.php diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php index c92758d..d486503 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php @@ -34,12 +34,16 @@ class Account { //############### Properties #################### const MPESA_PAYBILL = 1; const MPESA_PRIVATE = 2; - const KENYA_YU_PRIVATE = 3; + const KENYA_YU_PRIVATE = 3; // Yu have folded - this will be removed. const GHANA_AIRTEL_PRIVATE = 4; const RWANDA_MTN_PRIVATE = 5; const TANZANIA_MPESA_PRIVATE = 6; const TANZANIA_TIGO_PRIVATE = 7; const KENYA_AIRTEL_PRIVATE = 8; + const KENYA_AIRTEL_PAYBILL = 9; + const SOMALIA_GOLIS_PRIVATE = 10; + const SOMALIA_TELESOME_PRIVATE = 11; + const SOMALIA_HORMUUD_PRIVATE = 12; protected $id = 0; protected $type = 0; diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php b/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php index a9816c1..07be334 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php @@ -119,6 +119,18 @@ public static function createEntry($type, $id, $initValues=NULL) { case Account::KENYA_AIRTEL_PRIVATE: $object = new \PLUSPEOPLE\PesaPi\KenyaAirtelPrivate\Account($id, $initValues); break; + case Account::KENYA_AIRTEL_PAYBILL: + $object = new \PLUSPEOPLE\PesaPi\KenyaAirtelPaybill\Account($id, $initValues); + break; + case Account::SOMALIA_GOLIS_PRIVATE: + $object = new \PLUSPEOPLE\PesaPi\SomaliaGolisPrivate\Account($id, $initValues); + break; + case Account::SOMALIA_TELESOME_PRIVATE: + $object = new \PLUSPEOPLE\PesaPi\SomaliaTelesomePrivate\Account($id, $initValues); + break; + case Account::SOMALIA_HORMUUD_PRIVATE: + $object = new \PLUSPEOPLE\PesaPi\SomaliaHormuudPrivate\Account($id, $initValues); + break; } return $object; } diff --git a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Account.php b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Account.php new file mode 100644 index 0000000..4f4fae7 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Account.php @@ -0,0 +1,90 @@ + + */ +namespace PLUSPEOPLE\PesaPi\KenyaAirtelPaybill; +use PLUSPEOPLE\PesaPi\Base\Database; +use PLUSPEOPLE\PesaPi\Base\TransactionFactory; + +class Account extends \PLUSPEOPLE\PesaPi\Base\Account { + public function getFormatedType() { + return "Kenya - Airtel paybill"; + } + + public function availableBalance($time = null) { + $time = (int)$time; + if (0 == $time) { + $time = time(); + } + + $balance = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $time); + if (is_object($balance)) { + return $balance->getPostBalance(); + } + return $amount; + } + + public function locateByReceipt($receipt) { + return TransactionFactory::factoryByReceipt($this, $receipt); + } + + public function initTransaction($id, $initValues = null) { + return new Transaction($id, $initValues); + } + + public function importTransaction($message) { + if ($message != "") { + $parser = new Parser(); + $temp = $parser->parse($message); + + $transaction = Transaction::createNew($this->getId(), $temp['SUPER_TYPE'], $temp['TYPE']); + $transaction->setReceipt($temp['RECEIPT']); + $transaction->setTime($temp["TIME"]); + $transaction->setPhonenumber($temp['PHONE']); + $transaction->setName($temp['NAME']); + $transaction->setAccount($temp['ACCOUNT']); + $transaction->setStatus($temp['STATUS']); + $transaction->setAmount($temp['AMOUNT']); + $transaction->setPostBalance($temp['BALANCE']); + $transaction->setNote($temp['NOTE']); + $transaction->setTransactionCost($temp['COST']); + + $transaction->update(); + + // Callback if needed + $this->handleCallback($transaction); + + return $transaction; + } + return null; + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php new file mode 100644 index 0000000..65f7e66 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php @@ -0,0 +1,231 @@ + + */ +namespace PLUSPEOPLE\PesaPi\KenyaAirtelPaybill; + +class Loader { + protected $baseUrl = "https://41.223.56.58:7556"; + protected $config = null; + protected $curl = null; + protected $cookieFile = null; + protected $account = null; + protected $settings = array(); + + public function __construct($account) { + $this->account = $account; + $this->settings = $account->getSettings(); + + $this->cookieFile = tmpfile(); + + $this->curl = curl_init($this->baseUrl); + curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($this->curl, CURLOPT_COOKIESESSION, true); + curl_setopt($this->curl, CURLOPT_COOKIEJAR, $this->cookieFile); + curl_setopt($this->curl, CURLOPT_HEADER, true); + curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, 0); + } + + public function retrieveData($fromTime) { + $fromTime = (int)$fromTime; + $pages = array(); + if ($fromTime > 0) { + $login = $this->login(); + if (false) { + // change password + $this->changePassword($login); + } + + $pages = $this->loadResults($search, $fromTime); + } + // return the reverse array - we want the oldest data first. + return array_reverse($pages); + } + + //////////////////////////////////////////////////////////////// + // private functions + //////////////////////////////////////////////////////////////// + private function login() { + curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . "/Login.aspx"); + curl_setopt($this->curl, CURLOPT_POST, false); + $first = curl_exec($this->curl); + + + $viewState = $this->getViewState($first); + $postData = + '__EVENTTARGET=' . + '&__EVENTARGUMENT=' . + '&__VIEWSTATE=' . urlencode($viewState) . + '&__PREVIOUSPAGE=VmAx5lrT-DxfU3L5JTkDo6-MsSLxDbUDPbv3Jf2rLJYNK6S-D0Layu8v0-Q4iqdhjdj-1r_OJ1bI3G7gZuFwoxJpbxd11LwAHWC9dsytjZk1' . + '&__EVENTVALIDATION=%2FwEWDwK4vrG1BgKz5rnaCgKNodiVBgKxhoC7DAKg7duGAQLV4%2BWwBwLvuoeDDQKUrv2cBgLao5eqDQK%2BnYa6CgLxhcC%2FCwLvuo%2FqAgLJ4frZBwL90KKTCAKO9e%2BRAcr3OVzyza%2FzZgW90K8j%2BWLzSSl53QHO1SCkXe%2BPR9ND' . + '&ctl00%24ContentPlaceHolder1%24txtCompany=' . urlencode($this->settings["COMPANY"]) . + '&ctl00%24ContentPlaceHolder1%24txtNickname=' . urlencode($this->settings["NICKNAME"]) . + '&ctl00%24ContentPlaceHolder1%24txtUsername=' . urlencode($this->settings["USERNAME"]) . + '&ctl00%24ContentPlaceHolder1%24txtPassword=' . urlencode($this->settings["PASSWORD"]) . + '&ctl00%24ContentPlaceHolder1%24btnLogin.x=' . (String)rand(1,69) . + '&ctl00%24ContentPlaceHolder1%24btnLogin.y=' . (String)rand(1,19); + + curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . "/Login.aspx"); + curl_setopt($this->curl, CURLOPT_POST, true); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $postData); + curl_setopt($this->curl, CURLOPT_COOKIEFILE, $this->cookieFile); + + $login = curl_exec($this->curl); + file_put_contents("STEP0_LOGIN.txt", $login); + // TODO: missing error detection + return $login; + } + + + + private function loadResults($searchPage, $fromTime) { + return ""; + $fromTime = (int)$fromTime; + $pages = array(); + if ($fromTime > 0) { + $viewState = $this->getViewState($searchPage); + $accounts = $this->findAccounts($searchPage); + $account = $accounts[2]; + $now = time(); + + $postData = + '__VIEWSTATE=' . urlencode($viewState) . + '&ctl00$Main$ctl00$ctlDatePicker$dlpagesize_Input=' . '500' . + '&ctl00$Main$ctl00$ctlDatePicker$dlpagesize_text=' . '500' . + '&ctl00$Main$ctl00$ctlDatePicker$dlpagesize_value=' . '500' . + '&ctl00$Main$ctl00$ctlDatePicker$dlpagesize_index=' . '3' . + '&ctl00_Main_ctl00_ctlDatePicker_datePickerStartDate=' . date("Y-m-d", $fromTime) . + '&ctl00$Main$ctl00$ctlDatePicker$datePickerStartDate$dateInput=' . urlencode(date("Y-m-d H:i:s", $fromTime)) . + '&ctl00$Main$ctl00$ctlDatePicker$datePickerStartDate$dateInput_TextBox=' . date("Y-m-d", $fromTime) . + '&ctl00_Main_ctl00_ctlDatePicker_datePickerStartDate_calendar_SD=' . urlencode('[]') . + '&ctl00_Main_ctl00_ctlDatePicker_datePickerEndDate=' . date("Y-m-d", $now) . + '&ctl00$Main$ctl00$ctlDatePicker$datePickerEndDate$dateInput=' . urlencode(date("Y-m-d H:i:s", $now)) . + '&ctl00$Main$ctl00$ctlDatePicker$datePickerEndDate$dateInput_TextBox=' . date("Y-m-d", $now) . + '&ctl00_Main_ctl00_ctlDatePicker_datePickerEndDate_calendar_SD=' . urlencode('[]') . + '&ctl00$Main$ctl00$cbAccountType_Input=' . urlencode($account[1]) . + '&ctl00$Main$ctl00$cbAccountType_text=' . urlencode($account[1]) . + '&ctl00$Main$ctl00$cbAccountType_value=' . urlencode($account[2]) . + '&ctl00$Main$ctl00$cbAccountType_index=' . $account[0] . + '&ctl00$Main$ctl00$rblTransType=' . 'All' . + '&ctl00$Main$ctl00$btnSearch=' . 'Search' . + '&ctl00$Main$ctl00$cpeExpandedFilter_ClientState=' . '' . // unkown + '&ctl00_Main_ctl00_AccountStatementGrid1_dgStatementPostDataValue=' . '' // unkown + ; + + curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . "/ke/Main/home2.aspx?MenuID=1826"); + curl_setopt($this->curl, CURLOPT_POST, true); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $postData); + curl_setopt($this->curl, CURLOPT_COOKIEFILE, $this->cookieFile); + + // TODO: needs to retrieve the following pages, in case there is more than 500 entries + $result = curl_exec($this->curl); + // file_put_contents("STEP1_STATEMENT.txt", $result); + // TODO: missing error detection + $pages[] = $result; + } + return $pages; + } + + private function getViewState($input) { + $temp = array(); + preg_match("/(?<=__VIEWSTATE\" value=\")(?.*?)(?=\")/", $input, $temp); + return isset($temp[1]) ? $temp[1] : ""; + } + + /* + Finds all the accounts available. + Returns a 2-dimensional array with the following format: + [[offset, name, account-no]] + */ + private function findAccounts($input) { + $results = array(); + $temp = array(); + preg_match('/ctl00\$Main\$ctl00\$cbAccountType.+ScrollDownDisabled\.gif"\},(\[.+])\);<\/script>/U', $input, $temp); + + if (isset($temp[1])) { + $accounts = json_decode($temp[1]); + $count = 0; + foreach ($accounts AS $account) { + $results[] = array($count++, $account->Text, $account->Value); + } + } + return $results; + } + + private function changePassword($page) { + $viewState = $this->getViewState($page); + $oldPassword = $this->getPassword(); + + $toEmail = $this->config->getConfig("AdminEmail"); + if ($toEmail != "") { + $message = "PesaPi is changing your password"; + mail($toEmail, "PesaPi information", $message); + } + + // generate new pw - NOT very secure! + $temp = array(); + preg_match_all("/(.*)(\d+)$/U", $oldPassword, $temp); + if (isset($temp[1][0]) AND isset($temp[2][0])) { + $newPassword = $temp[1][0] . (string)($temp[2][0] + 1); + } else { + $newPassword = $oldPassword . "2"; + } + $this->setPassword($newPassword); + + $postData = + '__VIEWSTATE=' . urlencode($viewState) . + '&OperatorPasswordChangeControl1$txtPassword=' . urlencode($newPassword) . + '&OperatorPasswordChangeControl1$txtConfirm=' . urlencode($newPassword) . + '&OperatorPasswordChangeControl1$txtSecurityQuestion=' . 'favorite country' . + '&OperatorPasswordChangeControl1$txtSecurityAnswer=' . 'kenya' . // HARDCODED + '&btnUpdatePassword=' . urlencode('Update Password'); + + curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . "/ke/default.aspx?ReturnUrl=%2fke%2fMain%2fhome2.aspx%3fMenuID%3d1826&MenuID=1826"); + curl_setopt($this->curl, CURLOPT_POST, true); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $postData); + curl_setopt($this->curl, CURLOPT_COOKIEFILE, $this->cookieFile); + + $result = curl_exec($this->curl); // could potentially detect if the new pw is refused + return $result; + } + + private function getPassword() { + return $this->settings["PASSWORD"]; + } + + private function setPassword($input) { + $this->settings["PASSWORD"] = $input; + $this->account->setSettings($this->settings); + return $this->account->update(); + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Transaction.php new file mode 100644 index 0000000..a9e8f69 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Transaction.php @@ -0,0 +1,40 @@ + + */ +namespace PLUSPEOPLE\PesaPi\KenyaAirtelPaybill; + +class Transaction extends \PLUSPEOPLE\PesaPi\Base\Transaction { + // Extended attributes + const KE_AIRTEL_PAYBILL_PAYMENT_RECEIVED = 901; + + const KE_AIRTEL_PAYBILL_UNKOWN = 999; +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php index 7e01f01..f73a830 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php @@ -61,6 +61,12 @@ static public function scrubPayment(&$rawtext) { "NOTE" => "", "COST" => 0); + /////////////////////// + // DETECT if it is a Null result row + if (strpos ($rawtext, 'No records to display.') !== FALSE) { + return null; + } + ///////////////////////// // First identify those properties that are the same for all types // Reciept diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Scrubber.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Scrubber.php index aa33d56..74c46a2 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Scrubber.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Scrubber.php @@ -62,7 +62,10 @@ public static function ScrubRows(&$data) { $rows = HTMLPaymentScrubber1::scrubPaymentRows($data); foreach($rows AS $row) { - $result[] = HTMLPaymentScrubber1::scrubPayment($row); + $transaction = HTMLPaymentScrubber1::scrubPayment($row); + if ($transaction != null) { + $result[] = $transaction; + } } // return the reverse array - we want the oldest first diff --git a/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Account.php b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Account.php new file mode 100644 index 0000000..4426585 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Account.php @@ -0,0 +1,90 @@ + + */ +namespace PLUSPEOPLE\PesaPi\SomaliaGolisPrivate; +use PLUSPEOPLE\PesaPi\Base\Database; +use PLUSPEOPLE\PesaPi\Base\TransactionFactory; + +class Account extends \PLUSPEOPLE\PesaPi\Base\Account { + public function getFormatedType() { + return "Somalia - Golis Sahal Private"; + } + + public function availableBalance($time = null) { + $time = (int)$time; + if (0 == $time) { + $time = time(); + } + + $balance = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $time); + if (is_object($balance)) { + return $balance->getPostBalance(); + } + return $amount; + } + + public function locateByReceipt($receipt) { + return TransactionFactory::factoryByReceipt($this, $receipt); + } + + public function initTransaction($id, $initValues = null) { + return new Transaction($id, $initValues); + } + + public function importTransaction($message) { + if ($message != "") { + $parser = new Parser(); + $temp = $parser->parse($message); + + $transaction = Transaction::createNew($this->getId(), $temp['SUPER_TYPE'], $temp['TYPE']); + $transaction->setReceipt($temp['RECEIPT']); + $transaction->setTime($temp["TIME"]); + $transaction->setPhonenumber($temp['PHONE']); + $transaction->setName($temp['NAME']); + $transaction->setAccount($temp['ACCOUNT']); + $transaction->setStatus($temp['STATUS']); + $transaction->setAmount($temp['AMOUNT']); + $transaction->setPostBalance($temp['BALANCE']); + $transaction->setNote($temp['NOTE']); + $transaction->setTransactionCost($temp['COST']); + + $transaction->update(); + + // Callback if needed + $this->handleCallback($transaction); + + return $transaction; + } + return null; + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/SomaliaHormuudPrivate/Account.php b/php/include/PLUSPEOPLE/PesaPi/SomaliaHormuudPrivate/Account.php new file mode 100644 index 0000000..06f22c4 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/SomaliaHormuudPrivate/Account.php @@ -0,0 +1,90 @@ + + */ +namespace PLUSPEOPLE\PesaPi\SomaliaHormuudPrivate; +use PLUSPEOPLE\PesaPi\Base\Database; +use PLUSPEOPLE\PesaPi\Base\TransactionFactory; + +class Account extends \PLUSPEOPLE\PesaPi\Base\Account { + public function getFormatedType() { + return "Somalia - Hormuud EVC Private"; + } + + public function availableBalance($time = null) { + $time = (int)$time; + if (0 == $time) { + $time = time(); + } + + $balance = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $time); + if (is_object($balance)) { + return $balance->getPostBalance(); + } + return $amount; + } + + public function locateByReceipt($receipt) { + return TransactionFactory::factoryByReceipt($this, $receipt); + } + + public function initTransaction($id, $initValues = null) { + return new Transaction($id, $initValues); + } + + public function importTransaction($message) { + if ($message != "") { + $parser = new Parser(); + $temp = $parser->parse($message); + + $transaction = Transaction::createNew($this->getId(), $temp['SUPER_TYPE'], $temp['TYPE']); + $transaction->setReceipt($temp['RECEIPT']); + $transaction->setTime($temp["TIME"]); + $transaction->setPhonenumber($temp['PHONE']); + $transaction->setName($temp['NAME']); + $transaction->setAccount($temp['ACCOUNT']); + $transaction->setStatus($temp['STATUS']); + $transaction->setAmount($temp['AMOUNT']); + $transaction->setPostBalance($temp['BALANCE']); + $transaction->setNote($temp['NOTE']); + $transaction->setTransactionCost($temp['COST']); + + $transaction->update(); + + // Callback if needed + $this->handleCallback($transaction); + + return $transaction; + } + return null; + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/SomaliaTelesomePrivate/Account.php b/php/include/PLUSPEOPLE/PesaPi/SomaliaTelesomePrivate/Account.php new file mode 100644 index 0000000..1328090 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/SomaliaTelesomePrivate/Account.php @@ -0,0 +1,90 @@ + + */ +namespace PLUSPEOPLE\PesaPi\SomaliaTelesomePrivate; +use PLUSPEOPLE\PesaPi\Base\Database; +use PLUSPEOPLE\PesaPi\Base\TransactionFactory; + +class Account extends \PLUSPEOPLE\PesaPi\Base\Account { + public function getFormatedType() { + return "Somalia - Telesome ZAAD Private"; + } + + public function availableBalance($time = null) { + $time = (int)$time; + if (0 == $time) { + $time = time(); + } + + $balance = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $time); + if (is_object($balance)) { + return $balance->getPostBalance(); + } + return $amount; + } + + public function locateByReceipt($receipt) { + return TransactionFactory::factoryByReceipt($this, $receipt); + } + + public function initTransaction($id, $initValues = null) { + return new Transaction($id, $initValues); + } + + public function importTransaction($message) { + if ($message != "") { + $parser = new Parser(); + $temp = $parser->parse($message); + + $transaction = Transaction::createNew($this->getId(), $temp['SUPER_TYPE'], $temp['TYPE']); + $transaction->setReceipt($temp['RECEIPT']); + $transaction->setTime($temp["TIME"]); + $transaction->setPhonenumber($temp['PHONE']); + $transaction->setName($temp['NAME']); + $transaction->setAccount($temp['ACCOUNT']); + $transaction->setStatus($temp['STATUS']); + $transaction->setAmount($temp['AMOUNT']); + $transaction->setPostBalance($temp['BALANCE']); + $transaction->setNote($temp['NOTE']); + $transaction->setTransactionCost($temp['COST']); + + $transaction->update(); + + // Callback if needed + $this->handleCallback($transaction); + + return $transaction; + } + return null; + } + +} + +?> \ No newline at end of file From 6401dda240501d3474daa72d62491c075d5356d7 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Mon, 10 Nov 2014 00:03:39 +0100 Subject: [PATCH 05/18] Airtel Paybill improvements I have been working on the Airtel Paybill support. The main http requests flow is now sorted - Airtel Paybill is now almost working. --- .../PesaPi/KenyaAirtelPaybill/Loader.php | 146 +++++++++--------- .../PesaPi/KenyaAirtelPaybill/Parser.php | 64 ++++++++ 2 files changed, 138 insertions(+), 72 deletions(-) create mode 100644 php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Parser.php diff --git a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php index 65f7e66..f1f1664 100644 --- a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php +++ b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php @@ -50,7 +50,12 @@ public function __construct($account) { curl_setopt($this->curl, CURLOPT_COOKIEJAR, $this->cookieFile); curl_setopt($this->curl, CURLOPT_HEADER, true); curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($this->curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:33.0) Gecko/20100101 Firefox/33.0"); + curl_setopt($this->curl, CURLOPT_HTTPHEADER, array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language: en-US,en;q=0.5')); + } public function retrieveData($fromTime) { @@ -62,8 +67,8 @@ public function retrieveData($fromTime) { // change password $this->changePassword($login); } - - $pages = $this->loadResults($search, $fromTime); + $searchForm = $this->loadSearchForm($login); + $pages = $this->loadResults($searchForm, $fromTime); } // return the reverse array - we want the oldest data first. return array_reverse($pages); @@ -76,9 +81,9 @@ private function login() { curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . "/Login.aspx"); curl_setopt($this->curl, CURLOPT_POST, false); $first = curl_exec($this->curl); + file_put_contents("STEP0_LOGIN.txt", $first); - - $viewState = $this->getViewState($first); + $viewState = $this->getHidden($first); $postData = '__EVENTTARGET=' . '&__EVENTARGUMENT=' . @@ -98,89 +103,86 @@ private function login() { curl_setopt($this->curl, CURLOPT_COOKIEFILE, $this->cookieFile); $login = curl_exec($this->curl); - file_put_contents("STEP0_LOGIN.txt", $login); + file_put_contents("STEP1_POST_LOGIN.txt", $login); // TODO: missing error detection return $login; } + private function loadSearchForm($searchPage) { + $viewState = $this->getHidden($searchPage); + $prevPage = $this->getHidden($searchPage, '__PREVIOUSPAGE'); + $eventValidation = $this->getHidden($searchPage, '__EVENTVALIDATION'); + $postData = + '__EVENTTARGET=ctl00%24LnkReports' . + '&__EVENTARGUMENT=' . + '&__VIEWSTATE=' . urlencode($viewState) . + '&__PREVIOUSPAGE=' . urlencode($prevPage) . + '&__EVENTVALIDATION=' . urlencode($eventValidation); + + curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . "/NewTransctionsReport.aspx"); + curl_setopt($this->curl, CURLOPT_POST, true); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $postData); + curl_setopt($this->curl, CURLOPT_COOKIEFILE, $this->cookieFile); + curl_setopt($this->curl, CURLOPT_REFERER, 'https://41.223.56.58:7556/NewZapMerchant.aspx'); + + // TODO: needs to retrieve the following pages, in case there is more than 500 entries + $searchForm = curl_exec($this->curl); + file_put_contents("STEP2_STATEMENT.txt", $searchForm); + + return $searchForm; + } private function loadResults($searchPage, $fromTime) { - return ""; - $fromTime = (int)$fromTime; $pages = array(); - if ($fromTime > 0) { - $viewState = $this->getViewState($searchPage); - $accounts = $this->findAccounts($searchPage); - $account = $accounts[2]; - $now = time(); - - $postData = - '__VIEWSTATE=' . urlencode($viewState) . - '&ctl00$Main$ctl00$ctlDatePicker$dlpagesize_Input=' . '500' . - '&ctl00$Main$ctl00$ctlDatePicker$dlpagesize_text=' . '500' . - '&ctl00$Main$ctl00$ctlDatePicker$dlpagesize_value=' . '500' . - '&ctl00$Main$ctl00$ctlDatePicker$dlpagesize_index=' . '3' . - '&ctl00_Main_ctl00_ctlDatePicker_datePickerStartDate=' . date("Y-m-d", $fromTime) . - '&ctl00$Main$ctl00$ctlDatePicker$datePickerStartDate$dateInput=' . urlencode(date("Y-m-d H:i:s", $fromTime)) . - '&ctl00$Main$ctl00$ctlDatePicker$datePickerStartDate$dateInput_TextBox=' . date("Y-m-d", $fromTime) . - '&ctl00_Main_ctl00_ctlDatePicker_datePickerStartDate_calendar_SD=' . urlencode('[]') . - '&ctl00_Main_ctl00_ctlDatePicker_datePickerEndDate=' . date("Y-m-d", $now) . - '&ctl00$Main$ctl00$ctlDatePicker$datePickerEndDate$dateInput=' . urlencode(date("Y-m-d H:i:s", $now)) . - '&ctl00$Main$ctl00$ctlDatePicker$datePickerEndDate$dateInput_TextBox=' . date("Y-m-d", $now) . - '&ctl00_Main_ctl00_ctlDatePicker_datePickerEndDate_calendar_SD=' . urlencode('[]') . - '&ctl00$Main$ctl00$cbAccountType_Input=' . urlencode($account[1]) . - '&ctl00$Main$ctl00$cbAccountType_text=' . urlencode($account[1]) . - '&ctl00$Main$ctl00$cbAccountType_value=' . urlencode($account[2]) . - '&ctl00$Main$ctl00$cbAccountType_index=' . $account[0] . - '&ctl00$Main$ctl00$rblTransType=' . 'All' . - '&ctl00$Main$ctl00$btnSearch=' . 'Search' . - '&ctl00$Main$ctl00$cpeExpandedFilter_ClientState=' . '' . // unkown - '&ctl00_Main_ctl00_AccountStatementGrid1_dgStatementPostDataValue=' . '' // unkown - ; - - curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . "/ke/Main/home2.aspx?MenuID=1826"); - curl_setopt($this->curl, CURLOPT_POST, true); - curl_setopt($this->curl, CURLOPT_POSTFIELDS, $postData); - curl_setopt($this->curl, CURLOPT_COOKIEFILE, $this->cookieFile); - - // TODO: needs to retrieve the following pages, in case there is more than 500 entries - $result = curl_exec($this->curl); - // file_put_contents("STEP1_STATEMENT.txt", $result); - // TODO: missing error detection - $pages[] = $result; - } + $viewState = $this->getHidden($searchPage); + $prevPage = $this->getHidden($searchPage, '__PREVIOUSPAGE'); + $eventValidation = $this->getHidden($searchPage, '__EVENTVALIDATION'); + + $postData = + '__EVENTTARGET=' . + '&__EVENTARGUMENT=' . + '&__LASTFOCUS=' . + '&__VIEWSTATE=' . urlencode($viewState) . + '&__PREVIOUSPAGE=' . urlencode($prevPage) . + '&__EVENTVALIDATION=' . urlencode($eventValidation) . + '&ctl00%24ContentPlaceHolder1%24rdReportType=My+Reports' . + '&ctl00%24ContentPlaceHolder1%24rdbTrans=FUNDS'. + '&ctl00_ContentPlaceHolder1_dtFromDate_Raw=1414713600000' . // not done + '&ctl00%24ContentPlaceHolder1%24dtFromDate=31-Oct-2014' . // not done + '&ctl00_ContentPlaceHolder1_dtFromDate_DDDWS=0%3A0%3A12000%3A787%3A270%3A0%3A-10000%3A-10000' . // not done + '&ctl00_ContentPlaceHolder1_dtFromDate_DDD_C_FNPWS=0%3A0%3A-1%3A-10000%3A-10000%3A0%3A0px%3A-10000' . // not done + '&ctl00%24ContentPlaceHolder1%24dtFromDate%24DDD%24C=11%2F01%2F2014%3A10%2F31%2F2014' . // not done + '&ctl00_ContentPlaceHolder1_dtTODate_Raw=1415491200000' . // not done + '&ctl00%24ContentPlaceHolder1%24dtTODate=09-Nov-2014' . // not done + '&ctl00_ContentPlaceHolder1_dtTODate_DDDWS=0%3A0%3A12000%3A1005%3A270%3A0%3A-10000%3A-10000' . // not done + '&ctl00_ContentPlaceHolder1_dtTODate_DDD_C_FNPWS=0%3A0%3A-1%3A-10000%3A-10000%3A0%3A0px%3A-10000' . // not done + '&ctl00%24ContentPlaceHolder1%24dtTODate%24DDD%24C=11%2F09%2F2014%3A11%2F09%2F2014' . // not done + '&ctl00%24ContentPlaceHolder1%24btnViewAudit.x=' . (String)rand(1,79) . + '&ctl00%24ContentPlaceHolder1%24btnViewAudit.y=' . (String)rand(1,24) . + '&DXScript=1_23%2C2_21%2C2_28%2C2_20%2C1_27%2C1_44%2C1_41%2C2_15'; // not done + + curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . "/NewTransctionsReport.aspx"); + curl_setopt($this->curl, CURLOPT_POST, true); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $postData); + curl_setopt($this->curl, CURLOPT_COOKIEFILE, $this->cookieFile); + curl_setopt($this->curl, CURLOPT_REFERER, 'https://41.223.56.58:7556/NewTransctionsReport.aspx'); + + // TODO: needs to retrieve the following pages, in case there is more than 500 entries + $pages[] = curl_exec($this->curl); + file_put_contents("STEP3_RESULT.txt", $pages[0]); + return $pages; } - private function getViewState($input) { + private function getHidden($input, $tag="__VIEWSTATE") { $temp = array(); - preg_match("/(?<=__VIEWSTATE\" value=\")(?.*?)(?=\")/", $input, $temp); + preg_match("/(?<=" . $tag . "\" value=\")(?.*?)(?=\")/", $input, $temp); return isset($temp[1]) ? $temp[1] : ""; } - /* - Finds all the accounts available. - Returns a 2-dimensional array with the following format: - [[offset, name, account-no]] - */ - private function findAccounts($input) { - $results = array(); - $temp = array(); - preg_match('/ctl00\$Main\$ctl00\$cbAccountType.+ScrollDownDisabled\.gif"\},(\[.+])\);<\/script>/U', $input, $temp); - - if (isset($temp[1])) { - $accounts = json_decode($temp[1]); - $count = 0; - foreach ($accounts AS $account) { - $results[] = array($count++, $account->Text, $account->Value); - } - } - return $results; - } - private function changePassword($page) { - $viewState = $this->getViewState($page); + $viewState = $this->getHidden($page); $oldPassword = $this->getPassword(); $toEmail = $this->config->getConfig("AdminEmail"); diff --git a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Parser.php b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Parser.php new file mode 100644 index 0000000..aaed027 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Parser.php @@ -0,0 +1,64 @@ + + */ +namespace PLUSPEOPLE\PesaPi\KenyaAirtelPaybill; + +class Parser { + static public function scrubPaymentRows(&$rawtext) { + $temp = array(); + + preg_match_all('/Source Info<\/td>[\s\n\r]*<\/tr>(.+)[\s\n\r]*<\/td>/msi', $rawtext, $temp); + if (isset($temp[1][0])) { + return array_reverse(preg_split('/<\/tr>[\s\n\r]*/', $temp[1][0])); + } + + return array(); + } + + static public function scrubPayment(&$rawtext) { + $result = array("SUPER_TYPE" => 0, + "TYPE" => 0, + "RECEIPT" => "", + "TIME" => 0, + "PHONE" => "", + "NAME" => "", + "ACCOUNT" => "", + "STATUS" => "", + "AMOUNT" => 0, + "BALANCE" => 0, + "NOTE" => "", + "COST" => 0); + + return $result; + } + +} + +?> \ No newline at end of file From 08210d55ff5919a6928be9c0d061da03e48d7940 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Tue, 11 Nov 2014 14:39:58 +0100 Subject: [PATCH 06/18] Airtel paybill success The Airtel paybill code just managed to scrape it's first transactions and record them into PesaPi. There is still a bit of details to be sorted before it can be considered "fully operational" but all the pieces needed are now in place. --- .../PesaPi/KenyaAirtelPaybill/Account.php | 33 +++++++++- .../PesaPi/KenyaAirtelPaybill/Loader.php | 4 ++ .../PesaPi/KenyaAirtelPaybill/Parser.php | 41 ++++++++++-- .../PesaPi/KenyaAirtelPaybill/Transaction.php | 66 +++++++++++++++++++ .../PesaPi/MpesaPaybill/MpesaPaybill.php | 1 - 5 files changed, 137 insertions(+), 8 deletions(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Account.php b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Account.php index 4f4fae7..240304c 100644 --- a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Account.php +++ b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Account.php @@ -29,7 +29,6 @@ File originally by Michael Pedersen */ namespace PLUSPEOPLE\PesaPi\KenyaAirtelPaybill; -use PLUSPEOPLE\PesaPi\Base\Database; use PLUSPEOPLE\PesaPi\Base\TransactionFactory; class Account extends \PLUSPEOPLE\PesaPi\Base\Account { @@ -43,7 +42,7 @@ public function availableBalance($time = null) { $time = time(); } - $balance = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $time); + $balance = TransactionFactory::factoryOneByTime($this, $time); if (is_object($balance)) { return $balance->getPostBalance(); } @@ -85,6 +84,36 @@ public function importTransaction($message) { return null; } + public function forceSyncronisation() { + // determine the start time + $settings = $this->getSettings(); + $lastSync = $settings["LAST_SYNC"]; + + // We keep the timestamp from just _BEFORE_ we start connecting - this way we ensure that incomming payment while + // the process is in operation will be discovered at the NEXT request. + $now = time(); + + // perform file fetch + $loader = new Loader($this); + $pages = $loader->retrieveData($lastSync); + // perform analysis/scrubbing + $parser = new Parser(); + foreach ($pages AS $page) { + $rows = $parser->scrubTransactions($page); + // save data to database + foreach ($rows AS $row) { + $tuple = Transaction::updateData($row, $this); + if ($tuple[1] AND is_object($tuple[0])) { + $this->handleCallback($tuple[0]); + } + } + } + + // TODO save last entry time as last sync - not "now" sometimes the statement updates are delayed + $settings["LAST_SYNC"] = $now; + $this->setSettings($settings); + $this->update(); + } } ?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php index f1f1664..c225ecf 100644 --- a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php +++ b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Loader.php @@ -59,6 +59,10 @@ public function __construct($account) { } public function retrieveData($fromTime) { + //// DEBUG + // $rawtext = file_get_contents('STEP3_RESULT.txt'); + // return array($rawtext); + $fromTime = (int)$fromTime; $pages = array(); if ($fromTime > 0) { diff --git a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Parser.php b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Parser.php index aaed027..308bee9 100644 --- a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Parser.php +++ b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Parser.php @@ -31,18 +31,25 @@ namespace PLUSPEOPLE\PesaPi\KenyaAirtelPaybill; class Parser { - static public function scrubPaymentRows(&$rawtext) { + public function scrubTransactions($rawtext) { + $result = array(); $temp = array(); preg_match_all('/Source Info<\/td>[\s\n\r]*<\/tr>(.+)[\s\n\r]*<\/td>/msi', $rawtext, $temp); if (isset($temp[1][0])) { - return array_reverse(preg_split('/<\/tr>[\s\n\r]*/', $temp[1][0])); + $rows = array_reverse(preg_split('/<\/tr>[\s\n\r]*/', $temp[1][0])); + foreach($rows AS $row) { + $transaction = $this->scrubRow($row); + if ($transaction != null) { + $result[] = $transaction; + } + } } - return array(); + return $result; } - static public function scrubPayment(&$rawtext) { + public function scrubRow($rawtext) { $result = array("SUPER_TYPE" => 0, "TYPE" => 0, "RECEIPT" => "", @@ -56,7 +63,31 @@ static public function scrubPayment(&$rawtext) { "NOTE" => "", "COST" => 0); - return $result; + $temp = array(); + preg_match_all('/]*>(.*)<\/td>/Umsi', $rawtext, $temp); + + if (isset($temp[1]) AND count($temp[1]) >= 11) { + if ($temp[1][9] == " - CASH RECEIVE") { + $result["SUPER_TYPE"] = Transaction::MONEY_IN; + $result["TYPE"] = Transaction::KE_AIRTEL_PAYBILL_PAYMENT_RECEIVED; + $result["RECEIPT"] = trim(strip_tags($temp[1][3])); + $result["TIME"] = strtotime(str_replace(' ', ' ', $temp[1][2])); + $result["PHONE"] = trim(strip_tags($temp[1][4])); + $result["NAME"] = trim(str_replace(' ', ' ', $temp[1][10])); + // $result["ACCOUNT"] = "NOT DONE"; // NOT DONE + $result["STATUS"] = Transaction::STATUS_COMPLETED; + $result["AMOUNT"] = (int)(((double)$temp[1][8])*100); + $result["BALANCE"] = (int)(((double)$temp[1][7])*100); + + } else { + $result["SUPER_TYPE"] = Transaction::MONEY_NEUTRAL; + $result["TYPE"] = Transaction::KE_AIRTEL_PAYBILL_UNKOWN; + $result["NOTE"] = $rawtext; + } + + return $result; + } + return null; } } diff --git a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Transaction.php index a9e8f69..7dc69a2 100644 --- a/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Transaction.php +++ b/php/include/PLUSPEOPLE/PesaPi/KenyaAirtelPaybill/Transaction.php @@ -35,6 +35,72 @@ class Transaction extends \PLUSPEOPLE\PesaPi\Base\Transaction { const KE_AIRTEL_PAYBILL_PAYMENT_RECEIVED = 901; const KE_AIRTEL_PAYBILL_UNKOWN = 999; + + + public static function updateData($row, $account) { + $existing = $account->locateByReceipt($row['RECEIPT'], false); + + if (count($existing) > 0 AND is_object($existing[0])) { + if ($existing[0]->getSuperType() != $row['SUPER_TYPE']) { + // NOT DONE - MAJOR ISSUE - ALERT OPERATOR.. + } + if ($existing[0]->getType() != $row['TYPE']) { + // NOT DONE - MAJOR ISSUE - ALERT OPERATOR.. + } + + // Merge the information assuming we can piece together a full picture by two half successfull notifications + if ($existing[0]->getTime() == 0 AND $row['TIME'] > 0) { + $existing[0]->setTime($row["TIME"]); + } + if (trim($existing[0]->getPhonenumber()) == "" AND !empty($row['PHONE'])) { + $existing[0]->setPhonenumber($row['PHONE']); + } + if (trim($existing[0]->getName()) == "" AND !empty($row['NAME'])) { + $existing[0]->setName($row['NAME']); + } + if (trim($existing[0]->getAccount()) == "" AND !empty($row['ACCOUNT'])) { + $existing[0]->setAccount($row['ACCOUNT']); + } + if ($row['STATUS'] == "" AND $existing[0]->getStatus() != $row['STATUS']) { + $existing[0]->setStatus($row['STATUS']); + } + if ($existing[0]->getAmount() < $row['AMOUNT']) { + $existing[0]->setAmount($row['AMOUNT']); + } + if ($existing[0]->getPostBalance() < $row['BALANCE']) { + $existing[0]->setPostBalance($row['BALANCE']); + } + if (trim($existing[0]->getNote()) == "" AND !empty($row['NOTE'])) { + $existing[0]->setNote($row['NOTE']); + } + + $existing[0]->update(); + return array($existing[0], false); + + } else { + return array(Transaction::import($account, $row), true); + } + } + + public static function import($account, $row) { + $payment = Transaction::createNew($account->getId(), $row['SUPER_TYPE'], $row['TYPE']); + if (is_object($payment)) { + $payment->setReceipt($row['RECEIPT']); + $payment->setTime($row["TIME"]); + $payment->setPhonenumber($row['PHONE']); + $payment->setName($row['NAME']); + $payment->setAccount($row['ACCOUNT']); + $payment->setStatus($row['STATUS']); + $payment->setAmount($row['AMOUNT']); + $payment->setPostBalance($row['BALANCE']); + $payment->setNote($row['NOTE']); + + $payment->update(); + return $payment; + } + return null; + } + } ?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php index 61bd150..f88b75b 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php @@ -29,7 +29,6 @@ File originally by Michael Pedersen */ namespace PLUSPEOPLE\PesaPi\MpesaPaybill; -use PLUSPEOPLE\PesaPi\Base\Database; use PLUSPEOPLE\PesaPi\Base\TransactionFactory; class MpesaPaybill extends \PLUSPEOPLE\PesaPi\Base\Account { From caf38b568ab762c2755d0e219a51333757d3d185 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Tue, 9 Dec 2014 01:00:05 +0300 Subject: [PATCH 07/18] Golis's Sahal support Support for Golis's Sahal mobile money system in Somalia has been added. Take note that as usual all amounts are listed in the lowest currency denomications As Sahal uses fractions of cents (aka uses 3 decimals) you have to divide the amounts by 1000 to get a "normal" representation. Sahal support has been marked as experimental, until we have a confirmed real life test by someone "on the ground". Thanks goes out to Saiid Cali for providing information about the system. --- README.md | 56 +-------- .../PesaPi/MpesaPaybill/MpesaPaybill.php | 110 +++++++++++----- .../PesaPi/MpesaPaybill/Transaction.php | 1 + .../PesaPi/SomaliaGolisPrivate/Parser.php | 118 ++++++++++++++++++ .../SomaliaGolisPrivate/Transaction.php | 40 ++++++ php/webroot/index.tpl | 79 ++++++++++++ 6 files changed, 324 insertions(+), 80 deletions(-) create mode 100644 php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Transaction.php diff --git a/README.md b/README.md index 6cf6e0f..c8b4ca2 100644 --- a/README.md +++ b/README.md @@ -3,65 +3,17 @@ PesaPI PesaPI is an unofficial open source API for mobile money systems, released under the BSD(lite) license. The system currently support: * Kenya: Mpesa paybill accounts +* Kenya: Mpesa buygoods (lipa na mpesa) accounts * Kenya: Mpesa private accounts * Kenya: Airtel private accounts -* Kenya: Yu private accounts (experimental) +* Kenya: Airtel paybill accounts (experimental) * Ghana: Airtel private accounts (experimental) * Rwanda: MNT private accounts (experimental) * Tanzania: Mpesa private accounts * Tanzania: Tigo private accounts +* Somalia: Golis Sahal private accounts (experimental) The API supports both PUSH and PULL versions. The PHP version of the API is generally the most mature and recomended at this point - the system is build using Mysql and Curl. -Please direct all questions to the public mailing-list "pesaPi" on Google-groups: http://groups.google.com/group/pesapi - - -Short example -------------- -For a short usage example please see: -http://www.youtube.com/gifair - - -Current status --------------- -The current system should be considered as beta version - at least one commercial solution is using it as its payment gateway. -However it is not recomended that you deploy the system without having a developer available to support the setup. - -Version 0.1.0 was recently released and is contains a major rework of the internal structures of PesaPi - be alert that things that worked previously may not be fully working yet - as detailed testing is ongoing. - -Currently we are looking for people to provide copies of the SMS messages they are getting from various payment systems. - - -System design overview ----------------------- -* Supports both push and pull notifications. -* Does synchroization between local and server database. -* Transaction data are available even when main server is down. -* Super easy to utilize for integrators. -* Fast response on historical data. -* Keep the load on servers as low as possible. -* Hopefully more reliable than other APIs. - - -API Overview ------------- -The PesaPi class contains several static methods, these methods are the main interface. - -* availableBalance(time) -- returns the balance at a given point in time -* locateByReceipt(receipt) -- returns a payment or null for the given receipt number -* locateByPhone(phone, from, until) -- returns an array of payments from a particular phone -* locateByName(name, from, until) -- returns an array of payments from a particular client name -* locateByAccount(account, from, until) -- returns an array of payments from a particular account-no -* locateByTimeInterval(from, until) -- returns an array of all payments within a given time interval - -As an alternative you can ask PesaPi to call a your on your site when a new transaction is received (push mechanics). - - -Way forward ------------ -The following is a highlevel "todo" list for the project - -* Getting the code to release/production quality. -* Getting more developers onboard. -* Add support for more payment systems. +For further information please visit http://www.pesapi.com or the the public mailing-list "pesaPi" on Google-groups: http://groups.google.com/group/pesapi diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php index f88b75b..b164816 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php @@ -154,9 +154,6 @@ public function forceSyncronisation() { // determine the start time $settings = $this->getSettings(); $lastSync = $settings["LAST_SYNC"]; - - // We keep the timestamp from just _BEFORE_ we start connecting - this way we ensure that incomming payment while - // the process is in operation will be discovered at the NEXT request. $now = time(); // perform file fetch @@ -175,10 +172,14 @@ public function forceSyncronisation() { } } - // save last entry time as last sync - $settings["LAST_SYNC"] = $now; - $this->setSettings($settings); - $this->update(); + // save last entry time as last sync - but only if any is found. + // this way we are safeguarded against MPESA fallouts like the one of 3-5 November 2014 + $lastFound = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $now); + if (is_object($lastFound)) { + $settings["LAST_SYNC"] = $lastFound->getTime(); + $this->setSettings($settings); + $this->update(); + } } @@ -187,29 +188,82 @@ public function initTransaction($id, $initValues = null) { } public function importIPN($get) { - $temp = array("SUPER_TYPE" => Transaction::MONEY_IN, - "TYPE" => Transaction::MPESA_PAYBILL_PAYMENT_RECIEVED, - "RECEIPT" => $get['mpesa_code'], - "TIME" => Scrubber::dateInput($get['tstamp']), - "PHONE" => '0' . substr($get['mpesa_msisdn'], -9), - "NAME" => $get['mpesa_sender'], - "ACCOUNT" => $get['mpesa_acc'], - "STATUS" => Transaction::STATUS_COMPLETED, - "AMOUNT" => Scrubber::numberInput($get['mpesa_amt']), - "BALANCE" => 0, - "NOTE" => $get['text'], - "COST" => 0); - - if ($temp['AMOUNT'] > 0 AND $temp['RECEIPT'] != "") { - $tuple = Transaction::updateData($temp, $this); - - if ($tuple[1]) { - // Callback if needed - $this->handleCallback($tuple[0]); + if (strpos($get['text'], ' received from ') !== FALSE) { + $temp = array("SUPER_TYPE" => Transaction::MONEY_IN, + "TYPE" => Transaction::MPESA_PAYBILL_PAYMENT_RECIEVED, + "RECEIPT" => $get['mpesa_code'], + "TIME" => Scrubber::dateInput($get['tstamp']), + "PHONE" => '0' . substr($get['mpesa_msisdn'], -9), + "NAME" => $get['mpesa_sender'], + "ACCOUNT" => $get['mpesa_acc'], + "STATUS" => Transaction::STATUS_COMPLETED, + "AMOUNT" => Scrubber::numberInput($get['mpesa_amt']), + "BALANCE" => 0, + "NOTE" => $get['text'], + "COST" => 0); + + if ($temp['AMOUNT'] > 0 AND $temp['RECEIPT'] != "") { + $tuple = Transaction::updateData($temp, $this); + + if ($tuple[1]) { + // Callback if needed + $this->handleCallback($tuple[0]); + } + return $tuple[0]; + } + return null; + + } elseif (strpos($get['text'], ' transferred from Utility Account to Working Account.') !== FALSE) { + $temp = array("SUPER_TYPE" => Transaction::MONEY_IN, + "TYPE" => Transaction::MPESA_PAYBILL_TRANSFER_FROM_UTILITY, + "RECEIPT" => $get['mpesa_code'], + "TIME" => Scrubber::dateInput($get['tstamp']), + "PHONE" => '', + "NAME" => '', + "ACCOUNT" => '', + "STATUS" => Transaction::STATUS_COMPLETED, + "AMOUNT" => Scrubber::numberInput($get['mpesa_amt']), // NOT DONE + "BALANCE" => 0, // NOT DONE + "NOTE" => $get['text'], + "COST" => 0); + + if ($temp['AMOUNT'] > 0 AND $temp['RECEIPT'] != "") { + $tuple = Transaction::updateData($temp, $this); + + if ($tuple[1]) { + // Callback if needed + $this->handleCallback($tuple[0]); + } + return $tuple[0]; + } + return null; + + } else { + // Unknown transaction. + $temp = array("SUPER_TYPE" => Transaction::MONEY_NEUTRAL, + "TYPE" => Transaction::MPESA_PAYBILL_UNKOWN, + "RECEIPT" => $get['mpesa_code'], + "TIME" => Scrubber::dateInput($get['tstamp']), + "PHONE" => '', + "NAME" => '', + "ACCOUNT" => '', + "STATUS" => Transaction::STATUS_COMPLETED, + "AMOUNT" => 0, + "BALANCE" => 0, + "NOTE" => serialize($get), + "COST" => 0); + + if ($temp['AMOUNT'] > 0 AND $temp['RECEIPT'] != "") { + $tuple = Transaction::updateData($temp, $this); + + if ($tuple[1]) { + // Callback if needed + $this->handleCallback($tuple[0]); + } + return $tuple[0]; } - return $tuple[0]; + return null; } - return null; } } diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Transaction.php index 05ccfd2..5a6f5c1 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Transaction.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/Transaction.php @@ -39,6 +39,7 @@ class Transaction extends \PLUSPEOPLE\PesaPi\Base\Transaction { const MPESA_PAYBILL_FUNDS_CANCELLATION = 4; const MPESA_PAYBILL_BUSINESS_CHARGES = 5; const MPESA_PAYBILL_BUSINESS_CHARGES_CANCELLATION = 6; + const MPESA_PAYBILL_TRANSFER_FROM_UTILITY = 107; const MPESA_PAYBILL_UNKOWN = 199; public static function updateData($row, $account) { diff --git a/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php new file mode 100644 index 0000000..cfed9a6 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php @@ -0,0 +1,118 @@ + + Based on examples provided by Humphrey William + */ +namespace PLUSPEOPLE\PesaPi\SomaliaGolisPrivate; +use \PLUSPEOPLE\PesaPi\Base\Utility; + +class Parser { + public function dateInput($time) { + $dt = \DateTime::createFromFormat("n/j/Y h:i:s A", $time); + return $dt->getTimestamp(); + } + + public function numberInput($input) { + $input = trim($input); + $amount = 0; + + if (preg_match("/^[0-9,]+\.?$/", $input)) { + $amount = 1000 * (int)str_replace(',', '', $input); + } elseif (preg_match("/^[0-9,]+\.[0-9]$/", $input)) { + $amount = 100 * (int)str_replace(array('.', ','), '', $input); + } elseif (preg_match("/^[0-9,]*\.[0-9][0-9]$/", $input)) { + $amount = 10 * (int)str_replace(array('.', ','), '', $input); + } elseif (preg_match("/^[0-9,]*\.[0-9][0-9][0-9]$/", $input)) { + $amount = (int)str_replace(array('.', ','), '', $input); + } else { + $amount = (int)$input; + } + return $amount; + } + + public function parse($input) { + $result = array("SUPER_TYPE" => 0, + "TYPE" => 0, + "RECEIPT" => "", + "TIME" => 0, + "PHONE" => "", + "NAME" => "", + "ACCOUNT" => "", + "STATUS" => "", + "AMOUNT" => 0, + "BALANCE" => 0, + "NOTE" => "", + "COST" => 0); + + + // REFACTOR: should be split into subclasses + // [SAHAL] Ref:302228123 confirmed. $100 Received from c/risaaq axmed(7763277) on 10/23/2014 12:07:57 PM. New A/c balance is $101.780. + if (strpos($input, " Received from ") !== FALSE) { + $result["SUPER_TYPE"] = Transaction::MONEY_IN; + $result["TYPE"] = Transaction::SO_GOLIS_PRIVATE_PAYMENT_RECEIVED; + + $temp = array(); + preg_match_all("/.*Ref:(\d+) confirmed\. \\$([0-9\.\,]+) Received from ([^\(]+)\((\d+)\) on (\d\d?\/\d\d?\/\d{4}) (\d\d?:\d\d:\d\d [AP]M)\. New A\/c balance is \\$([0-9\.\,]+)\./mi", $input, $temp); + if (isset($temp[1][0])) { + $result["RECEIPT"] = $temp[1][0]; + $result["AMOUNT"] = $this->numberInput($temp[2][0]); + $result["NAME"] = $temp[3][0]; + $result["PHONE"] = $temp[4][0]; + $result["TIME"] = $this->dateInput($temp[5][0] . " " . $temp[6][0]); + $result["BALANCE"] = $this->numberInput($temp[7][0]); + } + + // [SAHAL] Tix:307013277 waxaad $1 ka heshay CABDILAAHI MIRRE AXMED MUUSE(252633659717) tar:11/6/2014 10:32:40 AM. Haraagaagu waa $55.980. + } elseif (strpos($input, " ka heshay ") !== FALSE) { + $result["SUPER_TYPE"] = Transaction::MONEY_IN; + $result["TYPE"] = Transaction::SO_GOLIS_PRIVATE_PAYMENT_RECEIVED; + + $temp = array(); + preg_match_all("/.*Tix:(\d+) waxaad \\$([0-9\.\,]+) ka heshay ([^\(]+)\((\d+)\) tar:(\d\d?\/\d\d?\/\d{4}) (\d\d?:\d\d:\d\d [AP]M)\. Haraagaagu waa \\$([0-9\.\,]+)\./mi", $input, $temp); + if (isset($temp[1][0])) { + $result["RECEIPT"] = $temp[1][0]; + $result["AMOUNT"] = $this->numberInput($temp[2][0]); + $result["NAME"] = $temp[3][0]; + $result["PHONE"] = $temp[4][0]; + $result["TIME"] = $this->dateInput($temp[5][0] . " " . $temp[6][0]); + $result["BALANCE"] = $this->numberInput($temp[7][0]); + } + + + } else { + $result["SUPER_TYPE"] = Transaction::MONEY_NEUTRAL; + $result["TYPE"] = Transaction::SO_GOLIS_PRIVATE_UNKOWN; + } + + return $result; + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Transaction.php new file mode 100644 index 0000000..8a36e76 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Transaction.php @@ -0,0 +1,40 @@ + + */ +namespace PLUSPEOPLE\PesaPi\SomaliaGolisPrivate; + +class Transaction extends \PLUSPEOPLE\PesaPi\Base\Transaction { + // Extended attributes + const SO_GOLIS_PRIVATE_PAYMENT_RECEIVED = 1001; + + const SO_GOLIS_PRIVATE_UNKOWN = 1099; +} + +?> \ No newline at end of file diff --git a/php/webroot/index.tpl b/php/webroot/index.tpl index 67fff0f..a2cdb4d 100644 --- a/php/webroot/index.tpl +++ b/php/webroot/index.tpl @@ -626,6 +626,85 @@ + +
+
+

Somalia: Golis Sahal private

+
+ +
+ + +

Commercial account types

From be726c0ee62075fa658cbd89d36cc0624c766481 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Tue, 9 Dec 2014 16:59:34 +0300 Subject: [PATCH 08/18] Ghana MTN support I have added support for MTN mobile money in Ghana. Only receiving money is supported at this particular time, and in general the support is experimental (untill properly tested). It's based on examples and information provided by Baba Musah --- .../PLUSPEOPLE/PesaPi/Base/Account.php | 1 + .../PLUSPEOPLE/PesaPi/Base/AccountFactory.php | 3 + .../PesaPi/GhanaMTNPrivate/Account.php | 90 +++++++++++++++++ .../PesaPi/GhanaMTNPrivate/Parser.php | 98 +++++++++++++++++++ .../PesaPi/GhanaMTNPrivate/Transaction.php | 40 ++++++++ .../PesaPi/SomaliaGolisPrivate/Parser.php | 2 +- php/webroot/index.tpl | 79 +++++++++++++++ 7 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Account.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Parser.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Transaction.php diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php index d486503..f0c0cac 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php @@ -44,6 +44,7 @@ class Account { const SOMALIA_GOLIS_PRIVATE = 10; const SOMALIA_TELESOME_PRIVATE = 11; const SOMALIA_HORMUUD_PRIVATE = 12; + const GHANA_MTN_PRIVATE = 13; protected $id = 0; protected $type = 0; diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php b/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php index 07be334..5120162 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php @@ -131,6 +131,9 @@ public static function createEntry($type, $id, $initValues=NULL) { case Account::SOMALIA_HORMUUD_PRIVATE: $object = new \PLUSPEOPLE\PesaPi\SomaliaHormuudPrivate\Account($id, $initValues); break; + case Account::GHANA_MTN_PRIVATE: + $object = new \PLUSPEOPLE\PesaPi\GhanaMTNPrivate\Account($id, $initValues); + break; } return $object; } diff --git a/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Account.php b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Account.php new file mode 100644 index 0000000..5738324 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Account.php @@ -0,0 +1,90 @@ + + */ +namespace PLUSPEOPLE\PesaPi\GhanaMTNPrivate; +use PLUSPEOPLE\PesaPi\Base\Database; +use PLUSPEOPLE\PesaPi\Base\TransactionFactory; + +class Account extends \PLUSPEOPLE\PesaPi\Base\Account { + public function getFormatedType() { + return "Ghana - MTN Private"; + } + + public function availableBalance($time = null) { + $time = (int)$time; + if (0 == $time) { + $time = time(); + } + + $balance = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $time); + if (is_object($balance)) { + return $balance->getPostBalance(); + } + return $amount; + } + + public function locateByReceipt($receipt) { + return TransactionFactory::factoryByReceipt($this, $receipt); + } + + public function initTransaction($id, $initValues = null) { + return new Transaction($id, $initValues); + } + + public function importTransaction($message) { + if ($message != "") { + $parser = new Parser(); + $temp = $parser->parse($message); + + $transaction = Transaction::createNew($this->getId(), $temp['SUPER_TYPE'], $temp['TYPE']); + $transaction->setReceipt($temp['RECEIPT']); + $transaction->setTime($temp["TIME"]); + $transaction->setPhonenumber($temp['PHONE']); + $transaction->setName($temp['NAME']); + $transaction->setAccount($temp['ACCOUNT']); + $transaction->setStatus($temp['STATUS']); + $transaction->setAmount($temp['AMOUNT']); + $transaction->setPostBalance($temp['BALANCE']); + $transaction->setNote($temp['NOTE']); + $transaction->setTransactionCost($temp['COST']); + + $transaction->update(); + + // Callback if needed + $this->handleCallback($transaction); + + return $transaction; + } + return null; + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Parser.php b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Parser.php new file mode 100644 index 0000000..9194f7a --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Parser.php @@ -0,0 +1,98 @@ + + Based on examples provided by Baba Musah + */ +namespace PLUSPEOPLE\PesaPi\GhanaMTNPrivate; +use \PLUSPEOPLE\PesaPi\Base\Utility; + +class Parser { + public function dateInput($time) { + $dt = \DateTime::createFromFormat("n/j/Y h:i:s A", $time); + return $dt->getTimestamp(); + } + + public function numberInput($input) { + $input = trim($input); + $amount = 0; + + if (preg_match("/^[0-9,]+\.?$/", $input)) { + $amount = 100 * (int)str_replace(',', '', $input); + } elseif (preg_match("/^[0-9,]+\.[0-9]$/", $input)) { + $amount = 10 * (int)str_replace(array('.', ','), '', $input); + } elseif (preg_match("/^[0-9,]*\.[0-9][0-9]$/", $input)) { + $amount = (int)str_replace(array('.', ','), '', $input); + } else { + $amount = (int)$input; + } + return $amount; + } + + public function parse($input) { + $result = array("SUPER_TYPE" => 0, + "TYPE" => 0, + "RECEIPT" => "", + "TIME" => 0, + "PHONE" => "", + "NAME" => "", + "ACCOUNT" => "", + "STATUS" => "", + "AMOUNT" => 0, + "BALANCE" => 0, + "NOTE" => "", + "COST" => 0); + + + // REFACTOR: should be split into subclasses + if (strpos($input, "Payment received for GHC") !== FALSE) { + $result["SUPER_TYPE"] = Transaction::MONEY_IN; + $result["TYPE"] = Transaction::GH_MTN_PRIVATE_PAYMENT_RECEIVED; + + $temp = array(); + preg_match_all("/MobileMoney Advice[\s\n\r]+Payment received for GHC([0-9\.\,]+) from ([0-9A-Za-z '\.]+)[\s\n\r]+Current Balance: GHC([0-9\.\,]+)[\s\n\r]+Available Balance: GHC([0-9\.\,]+)[\s\n\r]+Reference: (.*)[\s\n\r]+QJV/mi", $input, $temp); + if (isset($temp[1][0])) { + $result["RECEIPT"] = time(); // MTN does not publish reference numbers - stupid. + $result["AMOUNT"] = $this->numberInput($temp[1][0]); + $result["NAME"] = $temp[2][0]; + $result["TIME"] = time(); + $result["BALANCE"] = $this->numberInput($temp[3][0]); + $result["ACCOUNT"] = trim($temp[5][0]); + } + + } else { + $result["SUPER_TYPE"] = Transaction::MONEY_NEUTRAL; + $result["TYPE"] = Transaction::GH_MTN_PRIVATE_UNKOWN; + } + + return $result; + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Transaction.php new file mode 100644 index 0000000..7ef0377 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Transaction.php @@ -0,0 +1,40 @@ + + */ +namespace PLUSPEOPLE\PesaPi\GhanaMTNPrivate; + +class Transaction extends \PLUSPEOPLE\PesaPi\Base\Transaction { + // Extended attributes + const GH_MTN_PRIVATE_PAYMENT_RECEIVED = 1301; + + const GH_MTN_PRIVATE_UNKOWN = 1399; +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php index cfed9a6..d973941 100644 --- a/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php +++ b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php @@ -27,7 +27,7 @@ SUCH DAMAGE. File originally by Michael Pedersen - Based on examples provided by Humphrey William + Based on examples provided by Ali Saiid */ namespace PLUSPEOPLE\PesaPi\SomaliaGolisPrivate; use \PLUSPEOPLE\PesaPi\Base\Utility; diff --git a/php/webroot/index.tpl b/php/webroot/index.tpl index a2cdb4d..ec3eb02 100644 --- a/php/webroot/index.tpl +++ b/php/webroot/index.tpl @@ -549,6 +549,84 @@ + +
+
+

Ghana: MTN private

+
+ +
+ +
@@ -705,6 +783,7 @@ +

Commercial account types

From f14327cc515c93a267bde608a9490b3f6318b182 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Tue, 20 Jan 2015 22:52:39 +0300 Subject: [PATCH 09/18] DR Congo MPESA support I have added initial support for Mpesa in DR Congo, at this point the support is considered experimental. --- .../congo_mpesa_private_examples.txt | 6 ++ .../PLUSPEOPLE/PesaPi/Base/Account.php | 5 +- .../PLUSPEOPLE/PesaPi/Base/AccountFactory.php | 5 +- php/include/PLUSPEOPLE/PesaPi/Base/Parser.php | 72 ++++++++++++++++ .../PLUSPEOPLE/PesaPi/Base/PrivateAccount.php | 86 +++++++++++++++++++ .../PesaPi/CongoMpesaPrivate/Account.php | 48 +++++++++++ .../PesaPi/CongoMpesaPrivate/Parser.php | 67 +++++++++++++++ .../PesaPi/CongoMpesaPrivate/Transaction.php | 40 +++++++++ .../PesaPi/GhanaMTNPrivate/Account.php | 53 ++---------- .../PesaPi/GhanaMTNPrivate/Parser.php | 38 +------- .../PesaPi/SomaliaGolisPrivate/Account.php | 54 ++---------- .../PesaPi/SomaliaGolisPrivate/Parser.php | 27 ++---- php/webroot/index.tpl | 76 ++++++++++++++++ 13 files changed, 422 insertions(+), 155 deletions(-) create mode 100644 php/documentation/congo_mpesa_private_examples.txt create mode 100644 php/include/PLUSPEOPLE/PesaPi/Base/Parser.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/Base/PrivateAccount.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Account.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Parser.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Transaction.php diff --git a/php/documentation/congo_mpesa_private_examples.txt b/php/documentation/congo_mpesa_private_examples.txt new file mode 100644 index 0000000..db7183b --- /dev/null +++ b/php/documentation/congo_mpesa_private_examples.txt @@ -0,0 +1,6 @@ +ARGENT RECU du 243881260264 le 13/01/2015 12:51:37 +Du compte: 1000624832 +Montant: 0.20 USD +Frais: 0.00 USD +Ref: 181346285 +Solde Disponible: 0.20 USD diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php index f0c0cac..60a7c54 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php @@ -1,5 +1,5 @@ + */ +namespace PLUSPEOPLE\PesaPi\Base; + +class Parser { + public function getBlankStructure() { + return array("SUPER_TYPE" => 0, + "TYPE" => 0, + "RECEIPT" => "", + "TIME" => 0, + "PHONE" => "", + "NAME" => "", + "ACCOUNT" => "", + "STATUS" => "", + "AMOUNT" => 0, + "BALANCE" => 0, + "NOTE" => "", + "COST" => 0); + } + + public function dateInput($time, $format) { + $dt = \DateTime::createFromFormat($format, $time); + return $dt->getTimestamp(); + } + + public function numberInput($input) { + $input = trim($input); + $amount = 0; + + if (preg_match("/^[0-9,]+\.?$/", $input)) { + $amount = 100 * (int)str_replace(',', '', $input); + } elseif (preg_match("/^[0-9,]+\.[0-9]$/", $input)) { + $amount = 10 * (int)str_replace(array('.', ','), '', $input); + } elseif (preg_match("/^[0-9,]*\.[0-9][0-9]$/", $input)) { + $amount = (int)str_replace(array('.', ','), '', $input); + } else { + $amount = (int)$input; + } + return $amount; + } + + +} +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/PrivateAccount.php b/php/include/PLUSPEOPLE/PesaPi/Base/PrivateAccount.php new file mode 100644 index 0000000..4c49ac8 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/Base/PrivateAccount.php @@ -0,0 +1,86 @@ + + */ +namespace PLUSPEOPLE\PesaPi\Base; +use PLUSPEOPLE\PesaPi\Base\TransactionFactory; + +class PrivateAccount extends Account { + + public function availableBalance($time = null) { + $time = (int)$time; + if (0 == $time) { + $time = time(); + } + + $balance = TransactionFactory::factoryOneByTime($this, $time); + if (is_object($balance)) { + return $balance->getPostBalance(); + } + return $amount; + } + + public function locateByReceipt($receipt) { + return TransactionFactory::factoryByReceipt($this, $receipt); + } + + public function initTransaction($id, $initValues = null) { + return new Transaction($id, $initValues); + } + + public function importTransaction($message) { + if ($message != "") { + $parser = $this->parserFactory(); + $temp = $parser->parse($message); + + $transaction = Transaction::createNew($this->getId(), $temp['SUPER_TYPE'], $temp['TYPE']); + $transaction->setReceipt($temp['RECEIPT']); + $transaction->setTime($temp["TIME"]); + $transaction->setPhonenumber($temp['PHONE']); + $transaction->setName($temp['NAME']); + $transaction->setAccount($temp['ACCOUNT']); + $transaction->setStatus($temp['STATUS']); + $transaction->setAmount($temp['AMOUNT']); + $transaction->setPostBalance($temp['BALANCE']); + $transaction->setNote($temp['NOTE']); + $transaction->setTransactionCost($temp['COST']); + + $transaction->update(); + + // Callback if needed + $this->handleCallback($transaction); + + return $transaction; + } + return null; + } + + +} +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Account.php b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Account.php new file mode 100644 index 0000000..8368714 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Account.php @@ -0,0 +1,48 @@ + + */ +namespace PLUSPEOPLE\PesaPi\CongoMpesaPrivate; + +class Account extends \PLUSPEOPLE\PesaPi\Base\PrivateAccount { + public function getFormatedType() { + return "Congo - Vodaphone MPESA Private"; + } + + public function getSender() { + return ""; // unknown at this point - security risk + } + + // Namespace mismatch workaround + public function parserFactory() { + return new Parser(); + } +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Parser.php b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Parser.php new file mode 100644 index 0000000..728fa93 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Parser.php @@ -0,0 +1,67 @@ + + Based on examples provided by Hassan Al Jirani + */ +namespace PLUSPEOPLE\PesaPi\CongoMpesaPrivate; + +class Parser extends \PLUSPEOPLE\PesaPi\Base\Parser{ + const DATE_FORMAT = "j/n/Y h:i:s"; + + public function parse($input) { + $result = $this->getBlankStructure(); + + // REFACTOR: should be split into subclasses + // ARGENT RECU du 243881260264 le 13/01/2015 12:51:37 Du compte: 1000624832 Montant: 0.20 USD Frais: 0.00 USD Ref: 181346285 Solde Disponible: 0.20 USD + if (strpos($input, " ARGENT RECU du ") !== FALSE) { + $result["SUPER_TYPE"] = Transaction::MONEY_IN; + $result["TYPE"] = Transaction::CO_MPESA_PRIVATE_PAYMENT_RECEIVED; + + $temp = array(); + preg_match_all("/ARGENT RECU du (\d+) le (\d\d?\/\d\d\/\d{4} \d\d?:\d\d:\d\d)[\s\n]+Du compte: (\d+)[\s\n]+Montant: ([0-9\.\,]+) USD[\s\n]+Frais: ([0-9\.\,]+) USD[\s\n]+Ref: (\d+)[\s\n]+Solde Disponible: ([0-9\.\,]+) USD/mi", $input, $temp); + if (isset($temp[1][0])) { + $result["RECEIPT"] = $temp[6][0]; + $result["AMOUNT"] = $this->numberInput($temp[4][0]); + $result["PHONE"] = $temp[0][0]; + $result["TIME"] = $this->dateInput(Parser::DATE_FORMAT, $temp[2][0]); + $result["BALANCE"] = $this->numberInput($temp[7][0]); + $result["COST"] = $this->numberInput($temp[5][0]); + } + + } else { + $result["SUPER_TYPE"] = Transaction::MONEY_NEUTRAL; + $result["TYPE"] = Transaction::CO_MPESA_PRIVATE_UNKOWN; + } + + return $result; + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Transaction.php new file mode 100644 index 0000000..62f7ff7 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Transaction.php @@ -0,0 +1,40 @@ + + */ +namespace PLUSPEOPLE\PesaPi\CongoMpesaPrivate; + +class Transaction extends \PLUSPEOPLE\PesaPi\Base\Transaction { + // Extended attributes + const CO_MPESA_PRIVATE_PAYMENT_RECEIVED = 1401; + + const CO_MPESA_PRIVATE_UNKOWN = 1499; +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Account.php b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Account.php index 5738324..14c5b44 100644 --- a/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Account.php +++ b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Account.php @@ -29,60 +29,19 @@ File originally by Michael Pedersen */ namespace PLUSPEOPLE\PesaPi\GhanaMTNPrivate; -use PLUSPEOPLE\PesaPi\Base\Database; -use PLUSPEOPLE\PesaPi\Base\TransactionFactory; -class Account extends \PLUSPEOPLE\PesaPi\Base\Account { +class Account extends \PLUSPEOPLE\PesaPi\Base\PrivateAccount { public function getFormatedType() { return "Ghana - MTN Private"; } - public function availableBalance($time = null) { - $time = (int)$time; - if (0 == $time) { - $time = time(); - } - - $balance = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $time); - if (is_object($balance)) { - return $balance->getPostBalance(); - } - return $amount; - } - - public function locateByReceipt($receipt) { - return TransactionFactory::factoryByReceipt($this, $receipt); - } - - public function initTransaction($id, $initValues = null) { - return new Transaction($id, $initValues); + public function getSender() { + return ""; // Unknown at this point - security risk } - public function importTransaction($message) { - if ($message != "") { - $parser = new Parser(); - $temp = $parser->parse($message); - - $transaction = Transaction::createNew($this->getId(), $temp['SUPER_TYPE'], $temp['TYPE']); - $transaction->setReceipt($temp['RECEIPT']); - $transaction->setTime($temp["TIME"]); - $transaction->setPhonenumber($temp['PHONE']); - $transaction->setName($temp['NAME']); - $transaction->setAccount($temp['ACCOUNT']); - $transaction->setStatus($temp['STATUS']); - $transaction->setAmount($temp['AMOUNT']); - $transaction->setPostBalance($temp['BALANCE']); - $transaction->setNote($temp['NOTE']); - $transaction->setTransactionCost($temp['COST']); - - $transaction->update(); - - // Callback if needed - $this->handleCallback($transaction); - - return $transaction; - } - return null; + // Namespace mismatch workaround + public function parserFactory() { + return new Parser(); } } diff --git a/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Parser.php b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Parser.php index 9194f7a..468eb11 100644 --- a/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Parser.php +++ b/php/include/PLUSPEOPLE/PesaPi/GhanaMTNPrivate/Parser.php @@ -30,44 +30,10 @@ Based on examples provided by Baba Musah */ namespace PLUSPEOPLE\PesaPi\GhanaMTNPrivate; -use \PLUSPEOPLE\PesaPi\Base\Utility; - -class Parser { - public function dateInput($time) { - $dt = \DateTime::createFromFormat("n/j/Y h:i:s A", $time); - return $dt->getTimestamp(); - } - - public function numberInput($input) { - $input = trim($input); - $amount = 0; - - if (preg_match("/^[0-9,]+\.?$/", $input)) { - $amount = 100 * (int)str_replace(',', '', $input); - } elseif (preg_match("/^[0-9,]+\.[0-9]$/", $input)) { - $amount = 10 * (int)str_replace(array('.', ','), '', $input); - } elseif (preg_match("/^[0-9,]*\.[0-9][0-9]$/", $input)) { - $amount = (int)str_replace(array('.', ','), '', $input); - } else { - $amount = (int)$input; - } - return $amount; - } +class Parser extends \PLUSPEOPLE\PesaPi\Base\Parser { public function parse($input) { - $result = array("SUPER_TYPE" => 0, - "TYPE" => 0, - "RECEIPT" => "", - "TIME" => 0, - "PHONE" => "", - "NAME" => "", - "ACCOUNT" => "", - "STATUS" => "", - "AMOUNT" => 0, - "BALANCE" => 0, - "NOTE" => "", - "COST" => 0); - + $result = $this->getBlankStructure(); // REFACTOR: should be split into subclasses if (strpos($input, "Payment received for GHC") !== FALSE) { diff --git a/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Account.php b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Account.php index 4426585..23b41c2 100644 --- a/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Account.php +++ b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Account.php @@ -29,62 +29,20 @@ File originally by Michael Pedersen */ namespace PLUSPEOPLE\PesaPi\SomaliaGolisPrivate; -use PLUSPEOPLE\PesaPi\Base\Database; -use PLUSPEOPLE\PesaPi\Base\TransactionFactory; -class Account extends \PLUSPEOPLE\PesaPi\Base\Account { +class Account extends \PLUSPEOPLE\PesaPi\Base\PrivateAccount { public function getFormatedType() { return "Somalia - Golis Sahal Private"; } - public function availableBalance($time = null) { - $time = (int)$time; - if (0 == $time) { - $time = time(); - } - - $balance = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $time); - if (is_object($balance)) { - return $balance->getPostBalance(); - } - return $amount; - } - - public function locateByReceipt($receipt) { - return TransactionFactory::factoryByReceipt($this, $receipt); + public function getSender() { + return ""; // unknown at this point - security risk } - public function initTransaction($id, $initValues = null) { - return new Transaction($id, $initValues); + // Namespace mismatch workaround + public function parserFactory() { + return new Parser(); } - - public function importTransaction($message) { - if ($message != "") { - $parser = new Parser(); - $temp = $parser->parse($message); - - $transaction = Transaction::createNew($this->getId(), $temp['SUPER_TYPE'], $temp['TYPE']); - $transaction->setReceipt($temp['RECEIPT']); - $transaction->setTime($temp["TIME"]); - $transaction->setPhonenumber($temp['PHONE']); - $transaction->setName($temp['NAME']); - $transaction->setAccount($temp['ACCOUNT']); - $transaction->setStatus($temp['STATUS']); - $transaction->setAmount($temp['AMOUNT']); - $transaction->setPostBalance($temp['BALANCE']); - $transaction->setNote($temp['NOTE']); - $transaction->setTransactionCost($temp['COST']); - - $transaction->update(); - - // Callback if needed - $this->handleCallback($transaction); - - return $transaction; - } - return null; - } - } ?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php index d973941..a5a94f0 100644 --- a/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php +++ b/php/include/PLUSPEOPLE/PesaPi/SomaliaGolisPrivate/Parser.php @@ -30,14 +30,11 @@ Based on examples provided by Ali Saiid */ namespace PLUSPEOPLE\PesaPi\SomaliaGolisPrivate; -use \PLUSPEOPLE\PesaPi\Base\Utility; -class Parser { - public function dateInput($time) { - $dt = \DateTime::createFromFormat("n/j/Y h:i:s A", $time); - return $dt->getTimestamp(); - } +class Parser extends \PLUSPEOPLE\PesaPi\Base\Parser{ + const DATE_FORMAT = "n/j/Y h:i:s A"; + // Custom numberInput function needed as Somalia uses 3 decimal digits. public function numberInput($input) { $input = trim($input); $amount = 0; @@ -57,19 +54,7 @@ public function numberInput($input) { } public function parse($input) { - $result = array("SUPER_TYPE" => 0, - "TYPE" => 0, - "RECEIPT" => "", - "TIME" => 0, - "PHONE" => "", - "NAME" => "", - "ACCOUNT" => "", - "STATUS" => "", - "AMOUNT" => 0, - "BALANCE" => 0, - "NOTE" => "", - "COST" => 0); - + $result = $this->getBlankStructure(); // REFACTOR: should be split into subclasses // [SAHAL] Ref:302228123 confirmed. $100 Received from c/risaaq axmed(7763277) on 10/23/2014 12:07:57 PM. New A/c balance is $101.780. @@ -84,7 +69,7 @@ public function parse($input) { $result["AMOUNT"] = $this->numberInput($temp[2][0]); $result["NAME"] = $temp[3][0]; $result["PHONE"] = $temp[4][0]; - $result["TIME"] = $this->dateInput($temp[5][0] . " " . $temp[6][0]); + $result["TIME"] = $this->dateInput(Parser::DATE_FORMAT, $temp[5][0] . " " . $temp[6][0]); $result["BALANCE"] = $this->numberInput($temp[7][0]); } @@ -100,7 +85,7 @@ public function parse($input) { $result["AMOUNT"] = $this->numberInput($temp[2][0]); $result["NAME"] = $temp[3][0]; $result["PHONE"] = $temp[4][0]; - $result["TIME"] = $this->dateInput($temp[5][0] . " " . $temp[6][0]); + $result["TIME"] = $this->dateInput(Parser::DATE_FORMAT, $temp[5][0] . " " . $temp[6][0]); $result["BALANCE"] = $this->numberInput($temp[7][0]); } diff --git a/php/webroot/index.tpl b/php/webroot/index.tpl index ec3eb02..10ad22e 100644 --- a/php/webroot/index.tpl +++ b/php/webroot/index.tpl @@ -782,6 +782,82 @@
+ +
+
+

Congo: MPESA private

+
+ +

From 9b8707bf28e8f9b69f7fc52fb12a9c0e3dfc3a59 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Wed, 11 Feb 2015 11:33:37 +0300 Subject: [PATCH 10/18] switch to mysqli and small congo fix --- .../PLUSPEOPLE/PesaPi/Base/Account.php | 2 +- .../PLUSPEOPLE/PesaPi/Base/AccountFactory.php | 2 +- .../PLUSPEOPLE/PesaPi/Base/Database.php | 39 ++++++++++--------- .../PesaPi/CongoMpesaPrivate/Parser.php | 4 +- .../PesaPi/CongoMpesaPrivate/Transaction.php | 4 +- .../MpesaPaybill/HTMLPaymentScrubber1.php | 3 +- php/webroot/index.php | 2 +- 7 files changed, 29 insertions(+), 27 deletions(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php index 60a7c54..bb59970 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php @@ -45,7 +45,7 @@ class Account { const SOMALIA_TELESOME_PRIVATE = 11; const SOMALIA_HORMUUD_PRIVATE = 12; const GHANA_MTN_PRIVATE = 13; - const CONGO_MPESA_PRIVATE = 14; + const DR_CONGO_MPESA_PRIVATE = 14; protected $id = 0; protected $type = 0; diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php b/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php index 7963b70..3b23a04 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php @@ -134,7 +134,7 @@ public static function createEntry($type, $id, $initValues=NULL) { case Account::GHANA_MTN_PRIVATE: $object = new \PLUSPEOPLE\PesaPi\GhanaMTNPrivate\Account($id, $initValues); break; - case Account::CONGO_MPESA_PRIVATE: + case Account::DR_CONGO_MPESA_PRIVATE: $object = new \PLUSPEOPLE\PesaPi\CongoMpesaPrivate\Account($id, $initValues); break; } diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/Database.php b/php/include/PLUSPEOPLE/PesaPi/Base/Database.php index dae5128..8ec6159 100755 --- a/php/include/PLUSPEOPLE/PesaPi/Base/Database.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/Database.php @@ -1,5 +1,5 @@ config = \PLUSPEOPLE\PesaPi\Configuration::instantiate(); - // if we need to use same credentials to the database then we need to use mysql_connect instead of mysql_pconnect. - $this->dbId = mysql_pconnect($this->config->getConfig("DatabaseHost" . $type),$this->config->getConfig("DatabaseUser" . $type),$this->config->getConfig("DatabasePassword" . $type), true); - if ($this->dbId > 0) { - if (!mysql_select_db($this->config->getConfig("DatabaseDatabase" . $type), $this->dbId)) { - exit; - } + + $this->db = new mysqli($this->config->getConfig("DatabaseHost" . $type), + $this->config->getConfig("DatabaseUser" . $type), + $this->config->getConfig("DatabasePassword" . $type), + $this->config->getConfig("DatabaseDatabase" . $type)); + + if ($this->db->connect_errno) { + print "DB connection error"; + exit(); } } @@ -68,42 +71,42 @@ public static function instantiate($type = Database::TYPE_READ) { } public function dbIn($input) { - return addslashes($input); + return $this->db->real_escape_string($input); } public function dbOut($input) { - return stripslashes($input); + return $input; } public function query($input) { ++$this->queryAmount; - return mysql_query($input, $this->dbId); + return $this->db->query($input); } public function fetchObject($input) { - return mysql_fetch_object($input); + return $input->fetch_object(); } public function freeResult($input) { - return mysql_free_result($input); + return $input->close(); } public function insertId() { - return mysql_insert_id($this->dbId); + return $this->db->insert_id; } public function affectedRows() { - return mysql_affected_rows($this->dbId); + return $this->db->affected_rows; } public function numRows($input) { - return mysql_num_rows($input); + return $input->num_rows; } public function beginTransaction() { $this->transactionCount++; if ($this->transactionCount == 1) { - return (bool)mysql_query("START TRANSACTION"); + return (bool)$this->query("START TRANSACTION"); } return true; } @@ -113,7 +116,7 @@ public function commitTransaction() { $this->transactionCount--; } if ($this->transactionCount == 0) { - return (bool)mysql_query("COMMIT"); + return (bool)$this->query("COMMIT"); } return true; } @@ -121,7 +124,7 @@ public function commitTransaction() { public function rollbackTransaction() { if ($this->transactionCount != 0) { $this->transactionCount = 0; - return (bool)mysql_query("ROLLBACK"); + return (bool)$this->query("ROLLBACK"); } return true; } diff --git a/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Parser.php b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Parser.php index 728fa93..c95977b 100644 --- a/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Parser.php +++ b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Parser.php @@ -41,7 +41,7 @@ public function parse($input) { // ARGENT RECU du 243881260264 le 13/01/2015 12:51:37 Du compte: 1000624832 Montant: 0.20 USD Frais: 0.00 USD Ref: 181346285 Solde Disponible: 0.20 USD if (strpos($input, " ARGENT RECU du ") !== FALSE) { $result["SUPER_TYPE"] = Transaction::MONEY_IN; - $result["TYPE"] = Transaction::CO_MPESA_PRIVATE_PAYMENT_RECEIVED; + $result["TYPE"] = Transaction::CD_MPESA_PRIVATE_PAYMENT_RECEIVED; $temp = array(); preg_match_all("/ARGENT RECU du (\d+) le (\d\d?\/\d\d\/\d{4} \d\d?:\d\d:\d\d)[\s\n]+Du compte: (\d+)[\s\n]+Montant: ([0-9\.\,]+) USD[\s\n]+Frais: ([0-9\.\,]+) USD[\s\n]+Ref: (\d+)[\s\n]+Solde Disponible: ([0-9\.\,]+) USD/mi", $input, $temp); @@ -56,7 +56,7 @@ public function parse($input) { } else { $result["SUPER_TYPE"] = Transaction::MONEY_NEUTRAL; - $result["TYPE"] = Transaction::CO_MPESA_PRIVATE_UNKOWN; + $result["TYPE"] = Transaction::CD_MPESA_PRIVATE_UNKOWN; } return $result; diff --git a/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Transaction.php index 62f7ff7..66cb732 100644 --- a/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Transaction.php +++ b/php/include/PLUSPEOPLE/PesaPi/CongoMpesaPrivate/Transaction.php @@ -32,9 +32,9 @@ class Transaction extends \PLUSPEOPLE\PesaPi\Base\Transaction { // Extended attributes - const CO_MPESA_PRIVATE_PAYMENT_RECEIVED = 1401; + const CD_MPESA_PRIVATE_PAYMENT_RECEIVED = 1401; - const CO_MPESA_PRIVATE_UNKOWN = 1499; + const CD_MPESA_PRIVATE_UNKOWN = 1499; } ?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php index f73a830..756d76d 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/HTMLPaymentScrubber1.php @@ -1,5 +1,5 @@ query("SHOW TABLES;")) { - while ($row = mysql_fetch_row($result)) { + while ($row = $result->fetch_row()) { $tables[] = $row[0]; } } From c09839dabf26942822c30119165a380d3cec1975 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Wed, 11 Feb 2015 11:56:06 +0300 Subject: [PATCH 11/18] Fixed reference to old SettingFactory in the MpesaPaybill code --- .../PesaPi/MpesaPaybill/MpesaPaybill.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php index b164816..dca55ce 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPaybill/MpesaPaybill.php @@ -1,5 +1,5 @@ getValue(); + $settings = $this->getSettings(); + $lastSync = $settings["LAST_SYNC"]; if ($lastSync < $time) { // we must have data all the way up to the specified time. @@ -64,8 +64,8 @@ public function locateByReceipt($receipt, $autoSync = true) { public function locateByPhone($phone, $from=0, $until=0) { - $lastSyncSetting = \PLUSPEOPLE\PesaPi\Base\SettingFactory::factoryByName("LastSync"); - $lastSync = $lastSyncSetting->getValue(); + $settings = $this->getSettings(); + $lastSync = $settings["LAST_SYNC"]; $config = \PLUSPEOPLE\PesaPi\Configuration::instantiate(); $initSyncDate = strtotime($config->getConfig('MpesaInitialSyncDate')); @@ -86,8 +86,8 @@ public function locateByPhone($phone, $from=0, $until=0) { } public function locateByName($name, $from = 0, $until = 0) { - $lastSyncSetting = \PLUSPEOPLE\PesaPi\Base\SettingFactory::factoryByName("LastSync"); - $lastSync = $lastSyncSetting->getValue(); + $settings = $this->getSettings(); + $lastSync = $settings["LAST_SYNC"]; $config = \PLUSPEOPLE\PesaPi\Configuration::instantiate(); $initSyncDate = strtotime($config->getConfig('MpesaInitialSyncDate')); @@ -107,8 +107,8 @@ public function locateByName($name, $from = 0, $until = 0) { } public function locateByAccount($account, $from=0, $until=0) { - $lastSyncSetting = \PLUSPEOPLE\PesaPi\Base\SettingFactory::factoryByName("LastSync"); - $lastSync = $lastSyncSetting->getValue(); + $settings = $this->getSettings(); + $lastSync = $settings["LAST_SYNC"]; $config = \PLUSPEOPLE\PesaPi\Configuration::instantiate(); $initSyncDate = strtotime($config->getConfig('MpesaInitialSyncDate')); @@ -129,8 +129,8 @@ public function locateByAccount($account, $from=0, $until=0) { public function locateByTimeInterval($from, $until, $type) { $type = (int)$type; - $lastSyncSetting = \PLUSPEOPLE\PesaPi\Base\SettingFactory::factoryByName("LastSync"); - $lastSync = $lastSyncSetting->getValue(); + $settings = $this->getSettings(); + $lastSync = $settings["LAST_SYNC"]; $config = \PLUSPEOPLE\PesaPi\Configuration::instantiate(); $initSyncDate = strtotime($config->getConfig('MpesaInitialSyncDate')); From b8390cfc257f2a3da81e8b2501a203c78080fa3d Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Wed, 18 Feb 2015 17:47:50 +0300 Subject: [PATCH 12/18] Namespace issue with mysqli sorted --- php/include/PLUSPEOPLE/PesaPi/Base/Database.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/Database.php b/php/include/PLUSPEOPLE/PesaPi/Base/Database.php index 8ec6159..834d4c5 100755 --- a/php/include/PLUSPEOPLE/PesaPi/Base/Database.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/Database.php @@ -45,10 +45,10 @@ class Database { protected function __construct($type) { $this->config = \PLUSPEOPLE\PesaPi\Configuration::instantiate(); - $this->db = new mysqli($this->config->getConfig("DatabaseHost" . $type), - $this->config->getConfig("DatabaseUser" . $type), - $this->config->getConfig("DatabasePassword" . $type), - $this->config->getConfig("DatabaseDatabase" . $type)); + $this->db = new \mysqli($this->config->getConfig("DatabaseHost" . $type), + $this->config->getConfig("DatabaseUser" . $type), + $this->config->getConfig("DatabasePassword" . $type), + $this->config->getConfig("DatabaseDatabase" . $type)); if ($this->db->connect_errno) { print "DB connection error"; From a6d0877c0860da1bca42e91ca75fe2da6757b9bb Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Thu, 26 Feb 2015 19:39:29 +0300 Subject: [PATCH 13/18] Simple composer.json file added --- composer.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 composer.json diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..990a338 --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "pluspeople/pesapi", + "description": "Mobile money middleware", + "version": "1.0.0", + "authors": [ + { + "name": "Michael Pedersen", + "email": "michael@pluspeople.dk" + } + ], + "minimum-stability": "dev", + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-0": { + "PLUSPEOPLE": "php/include/" + } + } +} From fad2c42b8c4694f95e8e90902aae6ae52e16030b Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Thu, 23 Apr 2015 03:19:30 +0300 Subject: [PATCH 14/18] Mpesa Private/lipa fix The recent "Move MPESA back to Kenya" aka Mpesa Generation 2 (G2) introduced some changes to the mpesa texts. This is the first fix to make pesapi understand the "updated" sms's - this needs to be tested in more detail but it was important to get any fixes out asap. --- .../PesaPi/MpesaPrivate/PersonalParser.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/MpesaPrivate/PersonalParser.php b/php/include/PLUSPEOPLE/PesaPi/MpesaPrivate/PersonalParser.php index 83ba475..497fcaa 100644 --- a/php/include/PLUSPEOPLE/PesaPi/MpesaPrivate/PersonalParser.php +++ b/php/include/PLUSPEOPLE/PesaPi/MpesaPrivate/PersonalParser.php @@ -57,7 +57,7 @@ public function parse($input) { $result["SUPER_TYPE"] = Transaction::MONEY_IN; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+You have received Ksh([0-9\.\,]+00) from[\s\n]+([0-9A-Z '\.]+) ([0-9]+)[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*You have received Ksh([0-9\.\,]+00) from[\s\n]+([0-9A-Z '\.]+) ([0-9]+)[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["TYPE"] = Transaction::MPESA_PRIVATE_PAYMENT_RECEIVED; $result["RECEIPT"] = $temp[1][0]; @@ -68,7 +68,7 @@ public function parse($input) { $result["BALANCE"] = Utility::numberInput($temp[7][0]); } else { - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+You have received Ksh([0-9\.\,]+00) from[\s\n]+([0-9]+) - ([A-Z '\.]+) [\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*You have received Ksh([0-9\.\,]+00) from[\s\n]+([0-9]+) - ([A-Z '\.]+) [\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["TYPE"] = Transaction::MPESA_PRIVATE_B2C_RECEIVED; $result["RECEIPT"] = $temp[1][0]; @@ -85,7 +85,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_BUYGOODS_RECEIVED; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+Ksh([0-9\.\,]+00) received from[\s\n]+([0-9]+) ([0-9A-Z '\.]+)[\s\n]+New Account balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*Ksh([0-9\.\,]+00) received from[\s\n]+([0-9]+) ([0-9A-Z '\.]+)[\s\n]*New Account balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[4][0]); @@ -100,7 +100,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_PAYBILL_PAID; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+Ksh([0-9\.\,]+) sent to[\s\n]+(.+)[\s\n]+for account (.+)[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*Ksh([0-9\.\,]+) sent to[\s\n]*(.+)[\s\n]*for account (.+)[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[2][0]); @@ -115,7 +115,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_BUY_GOODS; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+Ksh([0-9\.\,]+) paid to[\s\n]+([.]+)[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*Ksh([0-9\.\,]+) paid to[\s\n]*([.]+)[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[2][0]); @@ -129,7 +129,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_PAYMENT_SENT; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+Ksh([0-9\.\,]+00) sent to ([0-9A-Z '\.]+) ([0-9]+) on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*Ksh([0-9\.\,]+00) sent to ([0-9A-Z '\.]+) ([0-9]+) on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[2][0]); @@ -144,7 +144,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_DEPOSIT; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+Give Ksh([0-9\.\,]+00) cash to (.+)[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*Give Ksh([0-9\.\,]+00) cash to (.+)[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[4][0]); @@ -158,7 +158,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_WITHDRAW; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+Withdraw Ksh([0-9\.\,]+) from[\s\n]+(.+)[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*Withdraw Ksh([0-9\.\,]+) from[\s\n]*(.+)[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[4][0]); @@ -172,7 +172,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_WITHDRAW_ATM; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M).[\s\n]+Ksh([0-9\.\,]+) withdrawn from (\d+) - AGENT ATM\.[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M).[\s\n]*Ksh([0-9\.\,]+) withdrawn from (\d+) - AGENT ATM\.[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[4][0]); @@ -186,7 +186,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_AIRTIME_YOU; $temp = array(); - preg_match_all("/([A-Z0-9]+) confirmed\.[\s\n]+You bought Ksh([0-9\.\,]+00) of airtime on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) confirmed\.[\s\n]*You bought Ksh([0-9\.\,]+00) of airtime on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[2][0]); @@ -200,7 +200,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_AIRTIME_OTHER; $temp = array(); - preg_match_all("/([A-Z0-9]+) confirmed\.[\s\n]+You bought Ksh([0-9\.\,]+) of airtime for (\d+) on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]+New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) confirmed\.[\s\n]*You bought Ksh([0-9\.\,]+) of airtime for (\d+) on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)[\s\n]*New M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[2][0]); @@ -214,7 +214,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_FROM_MSHWARI; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+You have transferred Ksh([0-9\.\,]+00)[\s\n]+from your M-Shwari account[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)\.[\s\n]+M-Shwari balance is Ksh[0-9\.\,]+\.[\s\n]+M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*You have transferred Ksh([0-9\.\,]+00)[\s\n]*from your M-Shwari account[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)\.[\s\n]*M-Shwari balance is Ksh[0-9\.\,]+\.[\s\n]*M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[2][0]); $result["NAME"] = "M-Shwari"; @@ -226,7 +226,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_TO_MSHWARI; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+Ksh([0-9\.\,]+00) transferred to M-Shwari account[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)\.[\s\n]+M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*Ksh([0-9\.\,]+00) transferred to M-Shwari account[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)\.[\s\n]*M-PESA balance is Ksh([0-9\.\,]+00)/mi", $input, $temp); $result["RECEIPT"] = $temp[1][0]; $result["AMOUNT"] = Utility::numberInput($temp[2][0]); $result["NAME"] = "M-Shwari"; @@ -238,7 +238,7 @@ public function parse($input) { $result["TYPE"] = Transaction::MPESA_PRIVATE_BALANCE_REQUEST; $temp = array(); - preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]+Your M-PESA balance was Ksh([0-9\.\,]+00)[\s\n]+on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)/mi", $input, $temp); + preg_match_all("/([A-Z0-9]+) Confirmed\.[\s\n]*Your M-PESA balance was Ksh([0-9\.\,]+00)[\s\n]*on (\d\d?\/\d\d?\/\d\d) at (\d\d?:\d\d [AP]M)/mi", $input, $temp); if (isset($temp[1][0])) { $result["RECEIPT"] = $temp[1][0]; $result["TIME"] = $this->dateInput($temp[3][0] . " " . $temp[4][0]); From a75bb1a81ae3beeca4d402b853714939aad5c3f4 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Sun, 9 Aug 2015 16:06:12 +0200 Subject: [PATCH 15/18] MTN UGANDA SUPPORT I have added initial support for MTN private accounts in Uganda. Currently I only know about the format of receiving money in a direct transfer, so the system is limited to this type of transaction. If anyone got more examples of MTN mobile money SMS's in Uganda then feel free to post them on the pesapi mailinglist. --- README.md | 1 + .../PLUSPEOPLE/PesaPi/Base/Account.php | 1 + .../PLUSPEOPLE/PesaPi/Base/AccountFactory.php | 3 + .../PesaPi/UgandaMTNPrivate/Account.php | 90 +++++++++++++++++ .../PesaPi/UgandaMTNPrivate/Parser.php | 99 +++++++++++++++++++ .../PesaPi/UgandaMTNPrivate/Transaction.php | 41 ++++++++ php/webroot/index.tpl | 76 ++++++++++++++ python/readme.txt | 1 - ruby/readme.txt | 1 - 9 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Account.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Parser.php create mode 100644 php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Transaction.php delete mode 100644 python/readme.txt delete mode 100644 ruby/readme.txt diff --git a/README.md b/README.md index c8b4ca2..1fc6490 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The system currently support: * Tanzania: Mpesa private accounts * Tanzania: Tigo private accounts * Somalia: Golis Sahal private accounts (experimental) +* Uganda: MTN private accounts (experimental) The API supports both PUSH and PULL versions. The PHP version of the API is generally the most mature and recomended at this point - the system is build using Mysql and Curl. diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php index bb59970..fd7a39a 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/Account.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/Account.php @@ -46,6 +46,7 @@ class Account { const SOMALIA_HORMUUD_PRIVATE = 12; const GHANA_MTN_PRIVATE = 13; const DR_CONGO_MPESA_PRIVATE = 14; + const UGANDA_MTN_PRIVATE = 15; protected $id = 0; protected $type = 0; diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php b/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php index 3b23a04..3ba3e13 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/AccountFactory.php @@ -137,6 +137,9 @@ public static function createEntry($type, $id, $initValues=NULL) { case Account::DR_CONGO_MPESA_PRIVATE: $object = new \PLUSPEOPLE\PesaPi\CongoMpesaPrivate\Account($id, $initValues); break; + case Account::UGANDA_MTN_PRIVATE: + $object = new \PLUSPEOPLE\PesaPi\UgandaMTNPrivate\Account($id, $initValues); + break; } return $object; } diff --git a/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Account.php b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Account.php new file mode 100644 index 0000000..2aef012 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Account.php @@ -0,0 +1,90 @@ + + */ +namespace PLUSPEOPLE\PesaPi\UgandaMTNPrivate; +use PLUSPEOPLE\PesaPi\Base\Database; +use PLUSPEOPLE\PesaPi\Base\TransactionFactory; + +class Account extends \PLUSPEOPLE\PesaPi\Base\Account { + public function getFormatedType() { + return "Uganda - MTN"; + } + + public function availableBalance($time = null) { + $time = (int)$time; + if (0 == $time) { + $time = time(); + } + + $balance = \PLUSPEOPLE\PesaPi\Base\TransactionFactory::factoryOneByTime($this, $time); + if (is_object($balance)) { + return $balance->getPostBalance(); + } + return 0; + } + + public function locateByReceipt($receipt) { + return TransactionFactory::factoryByReceipt($this, $receipt); + } + + public function initTransaction($id, $initValues = null) { + return new Transaction($id, $initValues); + } + + public function importTransaction($message) { + if ($message != "") { + $parser = new Parser(); + $temp = $parser->parse($message); + + $transaction = Transaction::createNew($this->getId(), $temp['SUPER_TYPE'], $temp['TYPE']); + $transaction->setReceipt($temp['RECEIPT']); + $transaction->setTime($temp["TIME"]); + $transaction->setPhonenumber($temp['PHONE']); + $transaction->setName($temp['NAME']); + $transaction->setAccount($temp['ACCOUNT']); + $transaction->setStatus($temp['STATUS']); + $transaction->setAmount($temp['AMOUNT']); + $transaction->setPostBalance($temp['BALANCE']); + $transaction->setNote($temp['NOTE']); + $transaction->setTransactionCost($temp['COST']); + + $transaction->update(); + + // Callback if needed + $this->handleCallback($transaction); + + return $transaction; + } + return null; + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Parser.php b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Parser.php new file mode 100644 index 0000000..fbbc8af --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Parser.php @@ -0,0 +1,99 @@ + + Based on examples provided by Humphrey William + */ +namespace PLUSPEOPLE\PesaPi\UgandaMTNPrivate; +use \PLUSPEOPLE\PesaPi\Base\Utility; + +class Parser { + public function dateInput($time) + $dt = \DateTime::createFromFormat("Y-m-d H:i:s", $time); + return $dt->getTimestamp(); + } + + public function parse($input) { + $result = array("SUPER_TYPE" => 0, + "TYPE" => 0, + "RECEIPT" => "", + "TIME" => 0, + "PHONE" => "", + "NAME" => "", + "ACCOUNT" => "", + "STATUS" => "", + "AMOUNT" => 0, + "BALANCE" => 0, + "NOTE" => "", + "COST" => 0); + + + /* +$input = "Y'ello. You have received +UGX 1000000.00 from +ABACUS +INVESTMENTS LIMITED +COMMISSION +(256774656827) on your +mobile money account at +2015-05-10 +13:35:12.Your new +balance:UGX +1068816.00. +"; + */ + + // REFACTOR: should be split into subclasses + if (strpos($input, "You have received") !== FALSE) { + $result["SUPER_TYPE"] = Transaction::MONEY_IN; + + $temp = array(); + preg_match_all("/You have received[\s\n]+UGX ([0-9\.\,]+) from[\s\n]+([^\(]+)[\s\n]+\(([0-9]+)\) on your[\s\n]+mobile money account at[\s\n]+(\d{4}-\d\d-\d\d?)[\s\n]+(\d\d?:\d\d:\d\d)\.Your new[\s\n]+balance:UGX[\s\n]+([0-9\.\,]+)\./mi", $input, $temp); + if (isset($temp[1][0])) { + $result["TYPE"] = Transaction::UG_MTN_PRIVATE_PAYMENT_RECEIVED; + + $result["AMOUNT"] = Utility::numberInput($temp[1][0]); + $result["NAME"] = $temp[2][0]; + $result["PHONE"] = $temp[3][0]; + $result["TIME"] = $this->dateInput($temp[4][0] . " " . $temp[5][0]); + $result["BALANCE"] = Utility::numberInput($temp[6][0]); + $result["RECEIPT"] = $result["TIME"]; // since no unique code is given we will use the timestamp + } + + + } else { + $result["SUPER_TYPE"] = Transaction::MONEY_NEUTRAL; + $result["TYPE"] = Transaction::UG_MTN_PRIVATE_UNKOWN; + } + + return $result; + } + +} + +?> \ No newline at end of file diff --git a/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Transaction.php new file mode 100644 index 0000000..e2a0449 --- /dev/null +++ b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Transaction.php @@ -0,0 +1,41 @@ + + */ +namespace PLUSPEOPLE\PesaPi\UgandaMTNPrivate; + +class Transaction extends \PLUSPEOPLE\PesaPi\Base\Transaction { + // Extended attributes + const UG_MTN_PRIVATE_PAYMENT_RECEIVED = 1501; + const UG_MTN_PRIVATE_PAYMENT_SENT = 1502; + + const UT_MTN_PRIVATE_UNKOWN = 1599; +} + +?> \ No newline at end of file diff --git a/php/webroot/index.tpl b/php/webroot/index.tpl index 10ad22e..34188a5 100644 --- a/php/webroot/index.tpl +++ b/php/webroot/index.tpl @@ -470,6 +470,82 @@
+ +
+
+

Uganda: MTN private

+
+ +
diff --git a/python/readme.txt b/python/readme.txt deleted file mode 100644 index 67eeff0..0000000 --- a/python/readme.txt +++ /dev/null @@ -1 +0,0 @@ -You could be the one to build this port \ No newline at end of file diff --git a/ruby/readme.txt b/ruby/readme.txt deleted file mode 100644 index 67eeff0..0000000 --- a/ruby/readme.txt +++ /dev/null @@ -1 +0,0 @@ -You could be the one to build this port \ No newline at end of file From 28315feed77441f622d4ab2204b6def6be98d9b4 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Tue, 15 Sep 2015 13:47:53 +0300 Subject: [PATCH 16/18] Resiliant date-parsing I have made the data parsing more resiliant so that it does not fail as hard when the dateformat of a system changes. Obviously this is a two-edged tool - it will make installations more robust, but also make it easier for changes in dateformats to go by unnoticed._ --- php/include/PLUSPEOPLE/PesaPi/Base/Parser.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/Base/Parser.php b/php/include/PLUSPEOPLE/PesaPi/Base/Parser.php index c366f0e..ecf3667 100644 --- a/php/include/PLUSPEOPLE/PesaPi/Base/Parser.php +++ b/php/include/PLUSPEOPLE/PesaPi/Base/Parser.php @@ -48,7 +48,10 @@ public function getBlankStructure() { public function dateInput($time, $format) { $dt = \DateTime::createFromFormat($format, $time); - return $dt->getTimestamp(); + if ($dt !== FALSE) { + return $dt->getTimestamp(); + } + return 0; } public function numberInput($input) { From 1c94fce76dab57450b806dde7e2b4f7da1863b4c Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Mon, 21 Sep 2015 21:24:22 +0300 Subject: [PATCH 17/18] Missing { in Uganda parser Fixed the missing { at the beginning of the Parser for MTN uganda. --- php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Parser.php b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Parser.php index fbbc8af..2f2401e 100644 --- a/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Parser.php +++ b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Parser.php @@ -33,7 +33,7 @@ use \PLUSPEOPLE\PesaPi\Base\Utility; class Parser { - public function dateInput($time) + public function dateInput($time) { $dt = \DateTime::createFromFormat("Y-m-d H:i:s", $time); return $dt->getTimestamp(); } From 413a22497db46afc0deabe46b6d282265b64bdd9 Mon Sep 17 00:00:00 2001 From: Michael Pedersen Date: Mon, 21 Sep 2015 22:20:51 +0300 Subject: [PATCH 18/18] Fixed typo in UG MTN types --- php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Transaction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Transaction.php b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Transaction.php index e2a0449..5294de8 100644 --- a/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Transaction.php +++ b/php/include/PLUSPEOPLE/PesaPi/UgandaMTNPrivate/Transaction.php @@ -35,7 +35,7 @@ class Transaction extends \PLUSPEOPLE\PesaPi\Base\Transaction { const UG_MTN_PRIVATE_PAYMENT_RECEIVED = 1501; const UG_MTN_PRIVATE_PAYMENT_SENT = 1502; - const UT_MTN_PRIVATE_UNKOWN = 1599; + const UG_MTN_PRIVATE_UNKOWN = 1599; } ?> \ No newline at end of file