[IMPROVED] Payout logics

* [ADDED] More methods to our transaction class
 * `createDebitAPRecord` and `createDebitMPRecord`, will handle the
 * entire debit process
  * Adds Debit transaction
  * Adds TXFee transaction
  * mark transactions as archived
  * validate user is fully paid out
  * send notification to user
 * `getMPQueue` was added to unify the process of getting payout queues
* [MOVED] Only one mail template for both payout methods
* [ADDED] Some minor calls to user class
* [ADDED] Full address validation to bitcoin class
* [SQL] New SQL upgrade and Version Increment
 * Adding UNIQUE index to coin_address in accounts table
 * preperation for `sendmany` implementation
This commit is contained in:
Sebastian Grewe 2014-01-31 14:51:00 +01:00
parent a9b56433cb
commit c00b6d6757
10 changed files with 195 additions and 182 deletions

View File

@ -34,169 +34,77 @@ if ($bitcoin->can_connect() !== true) {
$log->logFatal(" unable to connect to RPC server, exiting");
$monitoring->endCronjob($cron_name, 'E0006', 1, true);
}
if ($setting->getValue('disable_manual_payouts') != 1) {
// Fetch outstanding payout requests
if ($aPayouts = $oPayout->getUnprocessedPayouts()) {
if (count($aPayouts) > 0) {
$log->logInfo("\tStarting Manual Payments...");
$log->logInfo("\tAccount ID\tUsername\tBalance\t\tCoin Address");
foreach ($aPayouts as $aData) {
$transaction_id = NULL;
$rpc_txid = NULL;
$aBalance = $transaction->getBalance($aData['account_id']);
$dBalance = $aBalance['confirmed'];
$aData['coin_address'] = $user->getCoinAddress($aData['account_id']);
$aData['username'] = $user->getUserName($aData['account_id']);
// Validate address against RPC
try {
$aStatus = $bitcoin->validateaddress($aData['coin_address']);
if (!$aStatus['isvalid']) {
$log->logError('User: ' . $aData['username'] . ' - Failed to verify this users coin address, skipping payout');
continue;
}
} catch (Exception $e) {
$log->logError('User: ' . $aData['username'] . ' - Failed to verify this users coin address, skipping payout');
continue;
}
if ($dBalance > $config['txfee_manual']) {
// To ensure we don't run this transaction again, lets mark it completed
if (!$oPayout->setProcessed($aData['id'])) {
$log->logFatal('unable to mark transactions ' . $aData['id'] . ' as processed. ERROR: ' . $oPayout->getCronError());
$monitoring->endCronjob($cron_name, 'E0010', 1, true);
}
$log->logInfo("\t" . $aData['account_id'] . "\t\t" . $aData['username'] . "\t" . $dBalance . "\t\t" . $aData['coin_address']);
if ($transaction->addTransaction($aData['account_id'], $dBalance - $config['txfee_manual'], 'Debit_MP', NULL, $aData['coin_address'], NULL)) {
// Store debit transaction ID for later update
$transaction_id = $transaction->insert_id;
if (!$transaction->addTransaction($aData['account_id'], $config['txfee_manual'], 'TXFee', NULL, $aData['coin_address']))
$log->logError('Failed to add TXFee record: ' . $transaction->getCronError());
// Mark all older transactions as archived
if (!$transaction->setArchived($aData['account_id'], $transaction->insert_id))
$log->logError('Failed to mark transactions for #' . $aData['account_id'] . ' prior to #' . $transaction->insert_id . ' as archived. ERROR: ' . $transaction->getCronError());
// Run the payouts from RPC now that the user is fully debited
try {
$rpc_txid = $bitcoin->sendtoaddress($aData['coin_address'], $dBalance - $config['txfee_manual']);
} catch (Exception $e) {
$log->logError('E0078: RPC method did not return 200 OK: Address: ' . $aData['coin_address'] . ' ERROR: ' . $e->getMessage());
// Remove this line below if RPC calls are failing but transactions are still added to it
// Don't blame MPOS if you run into issues after commenting this out!
$monitoring->endCronjob($cron_name, 'E0078', 1, true);
}
// Update our transaction and add the RPC Transaction ID
if (empty($rpc_txid) || !$transaction->setRPCTxId($transaction_id, $rpc_txid))
$log->logError('Unable to add RPC transaction ID ' . $rpc_txid . ' to transaction record ' . $transaction_id . ': ' . $transaction->getCronError());
// Notify user via mail
$aMailData['email'] = $user->getUserEmail($user->getUserName($aData['account_id']));
$aMailData['subject'] = 'Manual Payout Completed';
$aMailData['amount'] = $dBalance - $config['txfee_manual'];
$aMailData['payout_id'] = $aData['id'];
if (!$notification->sendNotification($aData['account_id'], 'manual_payout', $aMailData))
$log->logError('Failed to send notification email to users address: ' . $aMailData['email'] . 'ERROR: ' . $notification->getCronError());
// Recheck the users balance to make sure it is now 0
if (!$aBalance = $transaction->getBalance($aData['account_id'])) {
$log->logFatal('Failed to fetch balance for account ' . $aData['account_id'] . '. ERROR: ' . $transaction->getCronError());
$monitoring->endCronjob($cron_name, 'E0065', 1, true);
}
if ($aBalance['confirmed'] > 0) {
$log->logFatal('User has a remaining balance of ' . $aBalance['confirmed'] . ' after a successful payout!');
$monitoring->endCronjob($cron_name, 'E0065', 1, true);
}
} else {
$log->logFatal('Failed to add new Debit_MP transaction in database for user ' . $user->getUserName($aData['account_id']) . ' ERROR: ' . $transaction->getCronError());
$monitoring->endCronjob($cron_name, 'E0064', 1, true);
}
}
}
// Fetch our manual payouts, process them
if ($setting->getValue('disable_manual_payouts') != 1 && $aManualPayouts = $transaction->getMPQueue()) {
$log->logInfo(' found ' . count($aManualPayouts) . ' queued manual payouts');
$mask = ' | %-10.10s | %-25.25s | %-20.20s | %-40.40s | %-20.20s |';
$log->logInfo(sprintf($mask, 'UserID', 'Username', 'Balance', 'Address', 'Payout ID'));
foreach ($aManualPayouts as $aUserData) {
$transaction_id = NULL;
$rpc_txid = NULL;
$log->logInfo(sprintf($mask, $aUserData['id'], $aUserData['username'], $aUserData['confirmed'], $aUserData['coin_address'], $aUserData['payout_id']));
if (!$oPayout->setProcessed($aUserData['payout_id'])) {
$log->logFatal(' unable to mark transactions ' . $aData['id'] . ' as processed. ERROR: ' . $oPayout->getCronError());
$monitoring->endCronjob($cron_name, 'E0010', 1, true);
}
} else if (empty($aPayouts)) {
$log->logInfo("\tStopping Payments. No Payout Requests Found.");
} else {
$log->logFatal("\tFailed Processing Manual Payment Queue...Aborting...");
$monitoring->endCronjob($cron_name, 'E0050', 1, true);
}
if (count($aPayouts > 0)) $log->logDebug(" found " . count($aPayouts) . " queued manual payout requests");
} else {
$log->logDebug("Manual payouts are disabled via admin panel");
}
if ($setting->getValue('disable_auto_payouts') != 1) {
// Fetch all users balances
if ($users = $transaction->getAPQueue()) {
if (!empty($users)) {
if (count($users) > 0) $log->logDebug(" found " . count($users) . " queued payout(s)");
// Go through users and run transactions
$log->logInfo("Starting Payments...");
$log->logInfo("\tUserID\tUsername\tBalance\tThreshold\tAddress");
foreach ($users as $aUserData) {
$transaction_id = NULL;
$rpc_txid = NULL;
$dBalance = $aUserData['confirmed'];
// Validate address against RPC
if ($bitcoin->validateaddress($aUserData['coin_address'])) {
if (!$transaction_id = $transaction->createDebitAPRecord($aUserData['id'], $aUserData['coin_address'], $aUserData['confirmed'] - $config['txfee_manual'])) {
$log->logFatal(' failed to fullt debit user ' . $aUserData['username'] . ': ' . $transaction->getCronError());
$monitoring->endCronjob($cron_name, 'E0064', 1, true);
} else {
// Run the payouts from RPC now that the user is fully debited
try {
$aStatus = $bitcoin->validateaddress($aUserData['coin_address']);
if (!$aStatus['isvalid']) {
$log->logError('User: ' . $aUserData['username'] . ' - Failed to verify this users coin address, skipping payout');
continue;
}
$rpc_txid = $bitcoin->sendtoaddress($aUserData['coin_address'], $aUserData['confirmed'] - $config['txfee_manual']);
} catch (Exception $e) {
$log->logError('User: ' . $aUserData['username'] . ' - Failed to verify this users coin address, skipping payout');
continue;
}
$log->logInfo("\t" . $aUserData['id'] . "\t" . $aUserData['username'] . "\t" . $dBalance . "\t" . $aUserData['ap_threshold'] . "\t\t" . $aUserData['coin_address']);
// Only run if balance meets threshold and can pay the potential transaction fee
if ($dBalance > $aUserData['ap_threshold'] && $dBalance > $config['txfee_auto']) {
// Create transaction record
if ($transaction->addTransaction($aUserData['id'], $dBalance - $config['txfee_auto'], 'Debit_AP', NULL, $aUserData['coin_address'], NULL)) {
// Store debit ID for later update
$transaction_id = $transaction->insert_id;
if (!$transaction->addTransaction($aUserData['id'], $config['txfee_auto'], 'TXFee', NULL, $aUserData['coin_address']))
$log->logError('Failed to add TXFee record: ' . $transaction->getCronError());
// Mark all older transactions as archived
if (!$transaction->setArchived($aUserData['id'], $transaction->insert_id))
$log->logError('Failed to mark transactions for user #' . $aUserData['id'] . ' prior to #' . $transaction->insert_id . ' as archived. ERROR: ' . $transaction->getCronError());
// Run the payouts from RPC now that the user is fully debited
try {
$rpc_txid = $bitcoin->sendtoaddress($aUserData['coin_address'], $dBalance - $config['txfee_auto']);
} catch (Exception $e) {
$log->logError('E0078: RPC method did not return 200 OK: Address: ' . $aUserData['coin_address'] . ' ERROR: ' . $e->getMessage());
// Remove this line below if RPC calls are failing but transactions are still added to it
// Don't blame MPOS if you run into issues after commenting this out!
$monitoring->endCronjob($cron_name, 'E0078', 1, true);
}
// Update our transaction and add the RPC Transaction ID
if (empty($rpc_txid) || !$transaction->setRPCTxId($transaction_id, $rpc_txid))
$log->logError('Unable to add RPC transaction ID ' . $rpc_txid . ' to transaction record ' . $transaction_id . ': ' . $transaction->getCronError());
// Notify user via mail
$aMailData['email'] = $user->getUserEmail($user->getUserName($aUserData['id']));
$aMailData['subject'] = 'Auto Payout Completed';
$aMailData['amount'] = $dBalance - $config['txfee_auto'];
if (!$notification->sendNotification($aUserData['id'], 'auto_payout', $aMailData))
$log->logError('Failed to send notification email to users address: ' . $aMailData['email'] . ' ERROR: ' . $notification->getCronError());
// Recheck the users balance to make sure it is now 0
$aBalance = $transaction->getBalance($aUserData['id']);
if ($aBalance['confirmed'] > 0) {
$log->logFatal('User has a remaining balance of ' . $aBalance['confirmed'] . ' after a successful payout!');
$monitoring->endCronjob($cron_name, 'E0065', 1, true);
}
} else {
$log->logFatal('Failed to add new Debit_AP transaction in database for user ' . $user->getUserName($aUserData['id']) . ' ERROR: ' . $transaction->getCronError());
$monitoring->endCronjob($cron_name, 'E0064', 1, true);
}
$log->logError('E0078: RPC method did not return 200 OK: Address: ' . $aUserData['coin_address'] . ' ERROR: ' . $e->getMessage());
// Remove this line below if RPC calls are failing but transactions are still added to it
// Don't blame MPOS if you run into issues after commenting this out!
$monitoring->endCronjob($cron_name, 'E0078', 1, true);
}
// Update our transaction and add the RPC Transaction ID
if (empty($rpc_txid) || !$transaction->setRPCTxId($transaction_id, $rpc_txid))
$log->logError('Unable to add RPC transaction ID ' . $rpc_txid . ' to transaction record ' . $transaction_id . ': ' . $transaction->getCronError());
}
} else {
$log->logInfo(' failed to validate address for user: ' . $aUserData['username']);
continue;
}
}
}
// Fetch our auto payouts, process them
if ($setting->getValue('disable_auto_payouts') != 1 && $aAutoPayouts = $transaction->getAPQueue()) {
$log->logInfo(' found ' . count($aAutoPayouts) . ' queued auto payouts');
$mask = ' | %-10.10s | %-25.25s | %-20.20s | %-40.40s | %-20.20s |';
$log->logInfo(sprintf($mask, 'UserID', 'Username', 'Balance', 'Address', 'Threshold'));
foreach ($aAutoPayouts as $aUserData) {
$transaction_id = NULL;
$rpc_txid = NULL;
$log->logInfo(sprintf($mask, $aUserData['id'], $aUserData['username'], $aUserData['confirmed'], $aUserData['coin_address'], $aUserData['ap_threshold']));
if ($bitcoin->validateaddress($aUserData['coin_address'])) {
if (!$transaction_id = $transaction->createDebitAPRecord($aUserData['id'], $aUserData['coin_address'], $aUserData['confirmed'] - $config['txfee_manual'])) {
$log->logFatal(' failed to fully debit user ' . $aUserData['username'] . ': ' . $transaction->getCronError());
$monitoring->endCronjob($cron_name, 'E0064', 1, true);
} else {
// Run the payouts from RPC now that the user is fully debited
try {
$rpc_txid = $bitcoin->sendtoaddress($aUserData['coin_address'], $aUserData['confirmed'] - $config['txfee_manual']);
} catch (Exception $e) {
$log->logError('E0078: RPC method did not return 200 OK: Address: ' . $aUserData['coin_address'] . ' ERROR: ' . $e->getMessage());
// Remove this line below if RPC calls are failing but transactions are still added to it
// Don't blame MPOS if you run into issues after commenting this out!
$monitoring->endCronjob($cron_name, 'E0078', 1, true);
}
// Update our transaction and add the RPC Transaction ID
if (empty($rpc_txid) || !$transaction->setRPCTxId($transaction_id, $rpc_txid))
$log->logError('Unable to add RPC transaction ID ' . $rpc_txid . ' to transaction record ' . $transaction_id . ': ' . $transaction->getCronError());
}
} else {
$log->logInfo(' failed to validate address for user: ' . $aUserData['username']);
continue;
}
} else if(empty($users)) {
$log->logInfo("\tSkipping payments. No Auto Payments Eligible.");
$log->logDebug("Users have not configured their AP > 0");
} else{
$log->logFatal("\tFailed Processing Auto Payment Payment Queue. ERROR: " . $transaction->getCronError());
$monitoring->endCronjob($cron_name, 'E0050', 1, true);
}
} else {
$log->logDebug("Auto payouts disabled via admin panel");
}
$log->logInfo("Completed Payouts");
// Cron cleanup and monitoring
require_once('cron_end.inc.php');
?>

View File

@ -52,6 +52,12 @@ class Base {
public function setBlock($block) {
$this->block = $block;
}
public function setPayout($payout) {
$this->payout = $payout;
}
public function setNotification($notification) {
$this->notification = $notification;
}
public function setTransaction($transaction) {
$this->transaction = $transaction;
}

View File

@ -296,4 +296,16 @@ class BitcoinClient extends jsonRPCClient {
}
return true;
}
public function validateaddress($coin_address) {
try {
$aStatus = parent::validateaddress($coin_address);
if (!$aStatus['isvalid']) {
return false;
}
} catch (Exception $e) {
return false;
}
return true;
}
}

View File

@ -16,18 +16,6 @@ class Payout Extends Base {
return $this->sqlError('E0048');
}
/**
* Get all new, unprocessed payout requests
* @param none
* @return data Associative array with DB Fields
**/
public function getUnprocessedPayouts() {
$stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE completed = 0");
if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result())
return $result->fetch_all(MYSQLI_ASSOC);
return $this->sqlError('E0050');
}
/**
* Insert a new payout request
* @param account_id int Account ID

View File

@ -312,21 +312,110 @@ class Transaction extends Base {
ON t.account_id = a.id
WHERE t.archived = 0 AND a.ap_threshold > 0 AND a.coin_address IS NOT NULL AND a.coin_address != ''
GROUP BY t.account_id
HAVING confirmed > a.ap_threshold
");
HAVING confirmed > a.ap_threshold AND confirmed > " . $this->config['txfee_auto']);
if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result())
return $result->fetch_all(MYSQLI_ASSOC);
return $this->sqlError();
}
/**
* Debit a user account
* @param account_id int Account ID
* @param coin_address string Coin Address
* @param amount float Balance to record
* @return int Debit transaction ID or false
**/
public function createDebitMPRecord($account_id, $coin_address, $amount) {
return $this->createDebitRecord($account_id, $coin_address, $amount, 'Debit_MP');
}
public function createDebitAPRecord($account_id, $coin_address, $amount) {
return $this->createDebitRecord($account_id, $coin_address, $amount, 'Debit_AP');
}
private function createDebitRecord($account_id, $coin_address, $amount, $type) {
$type == 'Debit_MP' ? $txfee = $this->config['txfee_manual'] : $txfee = $this->config['txfee_auto'];
// Add Debit record
if (!$this->addTransaction($account_id, $amount, $type, NULL, $coin_address, NULL)) {
$this->setErrorMessage('Failed to create ' . $type . ' transaction record in database');
return false;
}
// Fetch the inserted record ID so we can return this at the end
$transaction_id = $this->insert_id;
// Add TXFee record
if (!$this->addTransaction($account_id, $txfee, 'TXFee', NULL, $coin_address)) {
$this->setErrorMessage('Failed to create TXFee transaction record in database: ' . $this->getError());
return false;
}
// Mark transactions archived
if (!$this->setArchived($account_id, $this->insert_id)) {
$this->setErrorMessage('Failed to mark transactions <= #' . $this->insert_id . ' as archived. ERROR: ' . $this->getError());
return false;
}
// Recheck the users balance to make sure it is now 0
if (!$aBalance = $this->getBalance($account_id)) {
$this->setErrorMessage('Failed to fetch balance for account ' . $account_id . '. ERROR: ' . $this->getCronError());
return false;
}
if ($aBalance['confirmed'] > 0) {
$this->setErrorMessage('User has a remaining balance of ' . $aBalance['confirmed'] . ' after a successful payout!');
return false;
}
// Notify user via mail
$aMailData['email'] = $this->user->getUserEmailById($account_id);
$aMailData['subject'] = $type . ' Completed';
$aMailData['amount'] = $amount - $txfee;
if (!$this->notification->sendNotification($account_id, 'payout', $aMailData)) {
$this->setErrorMessage('Failed to send notification email to users address: ' . $aMailData['email'] . 'ERROR: ' . $this->notification->getCronError());
}
return $transaction_id;
}
/**
* Get all new, unprocessed manual payout requests
* @param none
* @return data Associative array with DB Fields
**/
public function getMPQueue() {
$stmt = $this->mysqli->prepare("
SELECT
a.id,
a.username,
a.ap_threshold,
a.coin_address,
p.id AS payout_id,
IFNULL(
ROUND(
(
SUM( IF( ( t.type IN ('Credit','Bonus') AND b.confirmations >= " . $this->config['confirmations'] . ") OR t.type = 'Credit_PPS', t.amount, 0 ) ) -
SUM( IF( t.type IN ('Debit_MP', 'Debit_AP'), t.amount, 0 ) ) -
SUM( IF( ( t.type IN ('Donation','Fee') AND b.confirmations >= " . $this->config['confirmations'] . ") OR ( t.type IN ('Donation_PPS', 'Fee_PPS', 'TXFee') ), t.amount, 0 ) )
), 8
), 0
) AS confirmed
FROM " . $this->payout->getTableName() . " AS p
JOIN " . $this->user->getTableName() . " AS a
ON p.account_id = a.id
JOIN " . $this->getTableName() . " AS t
ON t.account_id = p.account_id
JOIN " . $this->block->getTableName() . " AS b
ON t.block_id = b.id
WHERE p.completed = 0 AND t.archived = 0 AND a.coin_address IS NOT NULL AND a.coin_address != ''
GROUP BY t.account_id
HAVING confirmed > " . $this->config['txfee_manual']);
if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result())
return $result->fetch_all(MYSQLI_ASSOC);
return $this->sqlError('E0050');
}
}
$transaction = new Transaction();
$transaction->setMemcache($memcache);
$transaction->setNotification($notification);
$transaction->setDebug($debug);
$transaction->setMysql($mysqli);
$transaction->setConfig($config);
$transaction->setBlock($block);
$transaction->setUser($user);
$transaction->setPayout($oPayout);
$transaction->setErrorCodes($aErrorCodes);
?>

View File

@ -25,8 +25,8 @@ class User extends Base {
public function getUserEmail($username, $lower=false) {
return $this->getSingle($username, 'email', 'username', 's', $lower);
}
public function getUserNotifyEmail($username, $lower=false) {
return $this->getSingle($username, 'notify_email', 'username', 's', $lower);
public function getUserEmailById($id) {
return $this->getSingle($id, 'email', 'id', 'i');
}
public function getUserNoFee($id) {
return $this->getSingle($id, 'no_fees', 'id');
@ -280,6 +280,16 @@ class User extends Base {
return $this->getSingle($userID, 'coin_address', 'id');
}
/**
* Check if a coin address exists already
* @param address string Coin Address
* @return bool True of false
**/
public function existsCoinAddress($address) {
$this->debug->append("STA " . __METHOD__, 4);
return $this->getSingle($address, 'coin_address', 'coin_address') === $address;
}
/**
* Fetch users donation value
* @param userID int UserID
@ -413,6 +423,10 @@ class User extends Base {
return false;
}
if (!empty($address)) {
if ($this->existsCoinAddress($address)) {
$this->setErrorMessage('Address is already in use');
return false;
}
if ($this->bitcoin->can_connect() === true) {
try {
$aStatus = $this->bitcoin->validateaddress($address);
@ -428,6 +442,8 @@ class User extends Base {
$this->setErrorMessage('Unable to connect to RPC server for coin address validation');
return false;
}
} else {
$address = NULL;
}
// Number sanitizer, just in case we fall through above

View File

@ -2,7 +2,7 @@
$defflip = (!cfip()) ? exit(header('HTTP/1.1 401 Unauthorized')) : 1;
define('MPOS_VERSION', '0.0.3');
define('DB_VERSION', '0.0.4');
define('DB_VERSION', '0.0.5');
define('CONFIG_VERSION', '0.0.7');
// Fetch installed database version

View File

@ -1,8 +0,0 @@
<html>
<body>
<p>An manual payout request completed.</p>
<p>Amount: {nocache}{$DATA.amount}{/nocache}</p>
<br/>
<br/>
</body>
</html>

View File

@ -1,6 +1,6 @@
<html>
<body>
<p>An automated payout completed.</p>
<p>You account has been debited and the coins have been sent to your wallet.</p>
<p>Amount: {nocache}{$DATA.amount}{/nocache}</p>
<br/>
<br/>

View File

@ -0,0 +1,2 @@
ALTER TABLE `accounts` ADD UNIQUE INDEX ( `coin_address` ) ;
INSERT INTO `settings` (`name`, `value`) VALUES ('DB_VERSION', '0.0.5') ON DUPLICATE KEY UPDATE `value` = '0.0.5';