[ 'table' => 'tpr', ], 'description' => [ 'table' => 'tpr', ], 'detail' => [ 'table' => 'tpr', ], 'details' => [ 'table' => 'tp', ], 'amount' => [ 'table' => 'tpr', ], 'request_date' => [ 'table' => 'tpr', ], 'payment_date' => [ 'table' => 'tp', ], 'type' => [ 'table' => 'tp', ], 'login' => [ 'table' => 'uu', ], 'payer' => [ 'table' => 'uup', 'field' => 'login', ], ]; private function setTable($field) { return isset($this->fieldmap[$field]['table']) ? $this->fieldmap[$field]['table'] : ''; } private function setField($field) { return isset($this->fieldmap[$field]['field']) ? $this->fieldmap[$field]['field'] : $field; } private function fieldTableArray() { $ret = []; foreach ($this->fieldmap as $field => $info) { $table = $this->setTable($field); $rfield = $this->setField($field); $ret[] = $table . '.' . $rfield; } return $ret; } public function request_payment($description, $amount, $paymentWithin, $detail = null, $currency = null) { global $prefs, $user; $userlib = TikiLib::lib('user'); $description = substr($description, 0, 100); if (empty($currency)) { $currency = $prefs['payment_currency']; } else { $currency = substr($currency, 0, 3); } $query = 'INSERT INTO `tiki_payment_requests`' . ' ( `amount`, `amount_paid`, `currency`, `request_date`, `due_date`, `description`, `detail`, `userId` )' . ' VALUES( ?, 0, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY), ?, ?, ? )'; $bindvars = [ $amount, $currency, (int) $paymentWithin, $description, $detail, $userlib->get_user_id($user) ]; $this->query($query, $bindvars); return $this->lastInsertId(); } private function get_payments($conditions, $offset, $max, array $bindvars, $what = '') { $mid = '`tiki_payment_requests` tpr LEFT JOIN `users_users` uu ON (uu.`userId` = tpr.`userId`)'; $count = 'SELECT COUNT(*) FROM ' . $mid . ' WHERE ' . $conditions; $data = 'SELECT tpr.*, uu.`login` as `user` ' . $what . ' FROM ' . $mid . ' WHERE ' . $conditions; $all = $this->fetchAll($data, $bindvars, $max, $offset); return [ 'cant' => $this->getOne($count, $bindvars), 'data' => Perms::filter( [ 'type' => 'payment' ], 'object', $all, [ 'object' => 'paymentRequestId' ], 'payment_view' ), ]; } public function get_outstanding($offset, $max, $ofUser = '', $filter = [], $sort = null) { $conditions = '`amount_paid` < `amount` AND NOW() <= `due_date` AND `cancel_date` IS NULL AND (`authorized_until` IS NULL OR `authorized_until` <= NOW())'; if ($ofUser) { $conditions .= " AND uu.`login` = " . $this->qstr($ofUser); } $bindvars = []; $conditions .= $this->addFilterSort($filter, $sort, $bindvars); return $this->get_payments($conditions, $offset, $max, $bindvars); } public function get_past($offset, $max, $ofUser = '', $filter = [], $sort = null) { global $prefs; $parserlib = TikiLib::lib('parser'); $conditions = 'tpr.`amount` <= tpr.`amount_paid` AND tpr.`cancel_date` IS NULL'; if ($ofUser) { $conditions .= " AND uu.`login` = " . $this->qstr($ofUser); } $bindvars = []; $conditions .= $this->addFilterSort($filter, $sort, $bindvars); $count = 'SELECT COUNT(*)' . ' FROM `tiki_payment_requests` tpr' . ' LEFT JOIN `users_users` uu ON (uu.`userId` = tpr.`userId`)' . ' LEFT JOIN `tiki_payment_received` tp ON (tp.`paymentRequestId`=tpr.`paymentRequestId` AND tp.`status` = "paid")' . ' LEFT JOIN `users_users` uup ON (uup.`userId` = tp.`userId`) WHERE ' . $conditions; $data = 'SELECT tpr.*, uu.`login` as `user`, tp.`type`, tp.`payment_date`,' . ' tp.`details` as `payment_detail`, tpr.`detail` as `request_detail`, uup.`login` as `payer`' . ' FROM `tiki_payment_requests` tpr' . ' LEFT JOIN `users_users` uu ON (uu.`userId` = tpr.`userId`)' . ' LEFT JOIN `tiki_payment_received` tp ON (tp.`paymentRequestId`=tpr.`paymentRequestId` AND tp.`status` = "paid")' . ' LEFT JOIN `users_users` uup ON (uup.`userId` = tp.`userId`) WHERE ' . $conditions; $all = $this->fetchAll($data, $bindvars, $max, $offset); foreach ($all as & $payment) { if (empty($payment['payer'])) { // anonymous $details = json_decode($payment['payment_detail'], true); if ($details && ! empty($details['payer_email'])) { $payment['payer_email'] = $details['payer_email']; } } if (! empty($payment['request_detail']) && $prefs['feature_jquery_tablesorter']) { $payment['request_detail'] = strip_tags(str_replace(['',''], [' ','
'], $parserlib->parse_data($payment['request_detail'])), '
'); } } return [ 'cant' => $this->getOne($count, $bindvars), 'data' => Perms::filter( [ 'type' => 'payment' ], 'object', $all, [ 'object' => 'paymentRequestId' ], 'payment_view' ), ]; } public function get_overdue($offset, $max, $ofUser = '', $filter = [], $sort = null) { $conditions = '`amount_paid` < `amount` AND NOW() > `due_date` AND `cancel_date` IS NULL AND (`authorized_until` IS NULL OR `authorized_until` <= NOW())'; if ($ofUser) { $conditions .= " AND uu.`login` = " . $this->qstr($ofUser); } $bindvars = []; $conditions .= $this->addFilterSort($filter, $sort, $bindvars); return $this->get_payments($conditions, $offset, $max, $bindvars); } public function get_authorized($offset, $max, $ofUser = '', $filter = [], $sort = null) { $conditions = '`amount_paid` < `amount` AND `cancel_date` IS NULL AND `authorized_until` IS NOT NULL AND `authorized_until` >= NOW()'; if ($ofUser) { $conditions .= " AND uu.`login` = " . $this->qstr($ofUser); } $bindvars = []; $conditions .= $this->addFilterSort($filter, $sort, $bindvars); return $this->get_payments($conditions, $offset, $max, $bindvars); } public function get_canceled($offset, $max, $ofUser = '', $filter = [], $sort = null) { $conditions = '`cancel_date` IS NOT NULL'; if ($ofUser) { $conditions .= " AND uu.`login` = " . $this->qstr($ofUser); } $bindvars = []; $conditions .= $this->addFilterSort($filter, $sort, $bindvars); return $this->get_payments($conditions, $offset, $max, $bindvars); } public function uncancel_payment($id) { $this->query('UPDATE `tiki_payment_requests` SET `cancel_date` = NULL WHERE `paymentRequestId` = ?', [ $id ]); } public function cancel_payment($id) { if ($info = $this->get_payment($id)) { if ($info['state'] != 'canceled') { $this->run_behaviors($info, 'cancel'); } } $this->query('UPDATE `tiki_payment_requests` SET `cancel_date` = NOW() WHERE `paymentRequestId` = ?', [ $id ]); } public function get_payment($id) { global $tikilib, $prefs; $info = $this->fetchAll( 'SELECT tpr.*, uu.`login` as `user` FROM `tiki_payment_requests` tpr' . ' LEFT JOIN `users_users` uu ON (uu.`userId` = tpr.`userId`)' . ' WHERE `paymentRequestId` = ?', [$id] ); $info = reset($info); if ($info) { $info['state'] = $this->find_state($info); $info['amount_original'] = number_format($info['amount'], 2, '.', ','); $info['amount_remaining_raw'] = $info['amount'] - $info['amount_paid']; $info['amount_remaining'] = number_format($info['amount_remaining_raw'], 2, '.', ','); $info['url'] = $tikilib->tikiUrl( 'tiki-payment.php', ['invoice' => $info['paymentRequestId'],] ); $info['returnurl'] = $tikilib->tikiUrl( 'tiki-payment.php', ['invoice' => $info['paymentRequestId'],] ); // Add token if feature is activated (need prefs global $user; if ( $prefs['auth_token_access'] == 'y' && (! $user || isset($_SESSION['forceanon']) && $_SESSION['forceanon'] == 'y' && ! Perms::get('payment', $info['paymentRequestId'])->manual_payment) ) { require_once('lib/wiki-plugins/wikiplugin_getaccesstoken.php'); $info['returnurl'] = $tikilib->tikiUrl( 'tiki-payment.php', [ 'invoice' => $info['paymentRequestId'], 'TOKEN' => wikiplugin_getaccesstoken( '', [ 'entry' => 'tiki-payment.php', 'keys' => ['invoice'], 'values' => [$info['paymentRequestId']] ] ), ] ); } $info['paypal_ipn'] = $tikilib->tikiUrl( 'tiki-payment.php', ['ipn' => 1,'invoice' => $info['paymentRequestId'],] ); $info['payments'] = []; $payments = $this->fetchAll( 'SELECT * FROM `tiki_payment_received` WHERE `paymentRequestId` = ? ORDER BY `payment_date` DESC', [$id] ); foreach ($payments as $payment) { $payment['details'] = json_decode($payment['details'], true); $payment['amount_paid'] = number_format($payment['amount'], 2, '.', ','); $info['payments'][] = $payment; } $info['actions'] = $this->extract_actions($info['actions']); return $info; } } private function find_state($info) { if (! empty($info['cancel_date'])) { return 'canceled'; } if ($info['amount_paid'] >= $info['amount']) { return 'past'; } $current = date('Y-m-d H:i:s'); if ($info['authorized_until'] && $info['authorized_until'] > $current) { return 'authorized'; } if ($info['due_date'] < $current) { return 'overdue'; } return 'outstanding'; } private function extract_actions($actions) { $out = [ 'authorize' => [], 'complete' => [], 'cancel' => [], ]; if (! empty($actions)) { $out = array_merge( $out, json_decode($actions, true) ); } return $out; } public function enter_payment($invoice, $amount, $type, array $data) { $tx = TikiDb::get()->begin(); global $user; $userlib = TikiLib::lib('user'); if ($info = $this->get_payment($invoice)) { if ($info['state'] != 'past' && $info['amount_remaining_raw'] - $amount <= 0) { $results = $this->run_behaviors($info, 'complete'); if ($info['state'] == 'canceled') { // in the case of canceled payments being paid (e.g. user was delayed at Paypal when cancellation happened) $this->uncancel_payment($invoice); } } if (! empty($results)) { $data = array_merge($results, $data); } $data = json_encode($data); $this->query( 'INSERT INTO `tiki_payment_received` ( `paymentRequestId`, `payment_date`, `amount`, `type`, `details`, `userId` )' . ' VALUES( ?, NOW(), ?, ?, ?, ? )', [ $invoice, $amount, $type, $data, empty($user) ? $info['userId'] : $userlib->get_user_id($user) ] ); $this->query( 'UPDATE `tiki_payment_requests` SET `amount_paid` = `amount_paid` + ? WHERE `paymentRequestId` = ?', [ $amount, $invoice ] ); } $tx->commit(); } public function enter_authorization($invoice, $type, $validForDays, array $data) { global $user; $userlib = TikiLib::lib('user'); if ($info = $this->get_payment($invoice)) { if ($info['state'] != 'past') { $results = $this->run_behaviors($info, 'authorize'); if ($info['state'] == 'canceled') { // in the case of canceled payments being paid (e.g. user was delayed at Paypal when cancellation happened) $this->uncancel_payment($invoice); } } if (! empty($results)) { $data = array_merge($results, $data); } $data = json_encode($data); $this->query( 'INSERT INTO `tiki_payment_received` ( `paymentRequestId`, `payment_date`, `amount`, `type`, `details`, `userId`, `status` )' . ' VALUES( ?, NOW(), ?, ?, ?, ?, "auth_pending" )', [ $invoice, 0, $type, $data, empty($user) ? $info['userId'] : $userlib->get_user_id($user) ] ); $this->query( 'UPDATE `tiki_payment_requests` SET `authorized_until` = DATE_ADD(NOW(), INTERVAL ? DAY) WHERE `paymentRequestId` = ?', [ (int) $validForDays, $invoice ] ); } } public function capture_payment($paymentId) { if ($info = $this->get_payment($paymentId)) { foreach ($info['payments'] as $received) { if ($received['status'] != 'auth_pending') { continue; } if ($received['amount']) { // When electing to capture a specific amount, assume that amount is the total to be paid. $table = $this->table('tiki_payment_requests'); $table->update(['amount' => (float) $received['amount']], ['paymentRequestId' => $paymentId]); } if ($gateway = $this->gateway($received['type'])) { if (is_callable([$gateway, 'capture_payment'])) { // Result is about request reception success, not actual capture success $result = $gateway->capture_payment($info, $received); if ($result) { $this->query('UPDATE `tiki_payment_received` SET `status` = "auth_captured" WHERE `paymentReceivedId` = ?', [$received['paymentReceivedId']]); } } } } } } public function register_behavior($invoice, $event, $behavior, array $arguments) { if (! in_array($event, [ 'complete', 'cancel', 'authorize' ])) { return false; } if (! $callback = $this->get_behavior($behavior)) { return false; } if ($info = $this->get_payment($invoice)) { $actions = $info['actions']; $actions[$event][] = [ 'behavior' => $behavior, 'arguments' => $arguments ]; $this->query( 'UPDATE `tiki_payment_requests` SET `actions` = ? WHERE `paymentRequestId` = ?', [json_encode($actions), $invoice] ); } else { return false; } } private function run_behaviors($info, $event) { $behaviors = $info['actions'][$event]; $results = []; foreach ($behaviors as $b) { if ($callback = $this->get_behavior($b['behavior'])) { $results[str_replace('payment_behavior_', '', $callback)] = call_user_func_array($callback, $b['arguments']); } } return $results; } private function get_behavior($name) { $file = __DIR__ . "/behavior/$name.php"; $function = 'payment_behavior_' . $name; if (is_readable($file)) { require_once $file; if (is_callable($function)) { return $function; } } } public function gateway($name) { if (isset($this->gateways[$name])) { return $this->gateways[$name]; } switch ($name) { case 'israelpost': require_once 'lib/payment/israelpostlib.php'; return $this->gateways[$name] = new IsraelPostLib($this); } } private function addFilterSort(array $filter, $sort, array &$bindvars) { $ret = ''; if (! empty($filter)) { foreach ($filter as $field => $value) { if (isset($this->fieldmap[$field])) { if ($field === 'payer') { $field = 'details'; } $table = $this->setTable($field); $col = $this->setField($field); $ret .= " AND " . $table . '.`' . $col . '`'; if ($field == 'description' || $field == 'detail' || $field == 'details') { $ret .= ' LIKE ?'; $bindvars[] = "%$value%"; } elseif (in_array($field, ['payment_date', 'request_date'])) { if (preg_match('/([<>=]*) \'?([^\']*)\' AND ([^ ]*) ([<>=]*) \'?([^\']*)/i', $value, $matches)) { // quoted date range? if ($matches[3] === $table . '.`' . $col . '`') { $ret .= " {$matches[1]} ? AND {$matches[3]} {$matches[4]} ?"; $bindvars[] = $matches[2]; $bindvars[] = $matches[5]; } } elseif (preg_match('/([<>=]*) \'([^\']*)/', $value, $matches)) { // single quoted date $ret .= " {$matches[1]} ? "; $bindvars[] = $matches[2]; } elseif (preg_match('/BETWEEN FROM_UNIXTIME\(([^\)]*?)\) AND FROM_UNIXTIME\(([^\)]*)\)/i', $value, $matches)) { // single quoted date // between date range $ret .= " BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)"; $bindvars[] = $matches[1]; $bindvars[] = $matches[2]; } elseif (preg_match('/([<>=]*) FROM_UNIXTIME\(([^\)]*)\)/i', $value, $matches)) { // single quoted date $ret .= " {$matches[1]} FROM_UNIXTIME(?) "; $bindvars[] = $matches[2]; } } else { $ret .= ' LIKE ?'; $bindvars[] = "$value%"; } } } } if (! empty($sort)) { if (! is_array($sort)) { $sort = explode(',', $sort); } foreach ($sort as $s) { if (strpos($s, '.') === false) { $dir = strrchr($s, '_'); $sfield = substr($s, 0, strlen($s) - strlen($dir)); $stable = $this->setTable($sfield); $scol = $this->setField($sfield); $newsort[] = $stable . '.' . $scol . $dir; } } if (! empty($newsort)) { $fields = $this->fieldTableArray(); $newsort = implode(',', $newsort); $ret .= ' ORDER BY ' . $this->convertSortMode($newsort, $fields); } } return $ret; } }