You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

563 lines
21 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
class PaymentLib extends TikiDb_Bridge
{
private $gateways = [];
public $fieldmap = [
'paymentRequestId' => [
'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(['</td>','</td></tr>'], [' </td>','<br></td></tr>'], $parserlib->parse_data($payment['request_detail'])), '<a><br>');
}
}
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;
}
}