From edb24475facd6a91c4dab8ad4ba974e3f3b4c374 Mon Sep 17 00:00:00 2001
From: Attila Santo <attisan@2041338.no-reply.drupal.org>
Date: Mon, 20 Apr 2015 14:10:12 +0200
Subject: [PATCH] Issue #885174 by dhmalex: Expiring unspent points only in
 order to avoid negative balance.

---
 userpoints.install |  18 +++++++++
 userpoints.module  | 116 +++++++++++++++++++++++++++++++++++++++--------------
 2 files changed, 104 insertions(+), 30 deletions(-)

diff --git a/userpoints.install b/userpoints.install
index 990c381..aa37586 100644
--- a/userpoints.install
+++ b/userpoints.install
@@ -119,6 +119,12 @@ function userpoints_schema() {
         'not null' => TRUE,
         'default' => 0,
       ),
+      'remainder' => array(
+        'description' => 'Calculated remainder for expiry operations.',
+        'type' => 'int',
+        'not null' => FALSE,
+        'default' => NULL,
+      ),
       'time_stamp' => array(
         'description' => 'Timestamp',
         'type' => 'int',
@@ -331,4 +337,16 @@ function userpoints_update_7004(&$sandbox) {
   $sandbox['current_uid'] = $last_uid;
   // Set #finished based on sandbox.
   $sandbox['#finished'] = (empty($sandbox['max']) || $last_uid == 0) ? 1 : ($sandbox['current_uid'] / $sandbox['max']);
+}
+
+/**
+ * Add field for remainder calculations.
+ **/
+function userpoints_update_7005() {
+  db_add_field('userpoints_txn', 'remainder', array(
+    'description' => 'Calculated remainder for expiry operations.',
+    'type' => 'int',
+    'not null' => FALSE,
+    'default' => NULL,
+  ));
 }
\ No newline at end of file
diff --git a/userpoints.module b/userpoints.module
index 0f51b96..d0ce906 100644
--- a/userpoints.module
+++ b/userpoints.module
@@ -1549,44 +1549,100 @@ function userpoints_date_to_timestamp($date) {
 /**
  * Finds and expires expired points.
  *
- * Finds all transactions with a expirydate < REQUEST_TIME and posts
- * opposite transactions (sum of 0).
+ * Finds all transactions with a expirydate < REQUEST_TIME
+ * calculats pending points and posts opposite transaction.
  */
 function userpoints_expire_transactions() {
-  $sql = "SELECT txn_id, uid, points, time_stamp, operation, description, tid
-          FROM {userpoints_txn}
-          WHERE status = 0 AND expired = 0
-          AND (expirydate < :expiry_date AND expirydate != 0)";
-  $result = db_query($sql, array(':expiry_date' => REQUEST_TIME));
-  foreach ($result as $line) {
-    $time_stamp_formatted = format_date($line->time_stamp, 'custom', 'Y-m-d H:i');
-    $arguments = array_merge(userpoints_translation(), array(
-      '!operation' => $line->operation,
-      '!description' => $line->description,
-      '!txn_id' => $line->txn_id,
-      '!date' => $time_stamp_formatted,
-    ));
-    $description = strtr(variable_get(USERPOINTS_EXPIRY_DESCRIPTION, NULL), $arguments);
+  // Select all unresolved transactions
+  $perishables = db_select('userpoints_txn', 'up_txn')
+    ->fields('up_txn', array('txn_id', 'uid', 'points', 'remainder', 'time_stamp', 'tid', 'operation', 'description'))
+    ->condition('status', 0, '=')
+    ->condition('expired', 0, '=')
+    ->condition('expirydate', 0, '!=')
+    ->condition('expirydate', REQUEST_TIME, '<')
+    ->execute();
+
+  // Loop through all perishable transactions.
+  foreach ($perishables as $perishable) {
+    // Gather all possible consumptions for
+    // this transaction.
+    $consumptions = db_select('userpoints_txn', 'up_txn')
+      ->fields('up_txn', array('txn_id', 'points', 'remainder'))
+      ->condition('status', 0, '=')
+      ->condition('expired', 0, '=')
+      ->condition(db_or()
+        ->isNull('remainder')
+        ->condition('remainder', 0, '!=')
+      )
+      ->condition('uid', $perishable->uid, '=')
+      ->condition('tid', $perishable->tid, '=')
+      ->condition('time_stamp', $perishable->time_stamp, '>')
+      ->condition('points', 0, ($perishable->points > 0) ? '<' : '>')
+      ->execute();
 
-    $params = array(
-      'points' => -$line->points,
-      'uid' => $line->uid,
+    // Prepare description.
+    $description = strtr(variable_get(USERPOINTS_EXPIRY_DESCRIPTION, NULL), array_merge(userpoints_translation(), array(
+      '!operation' => $perishable->operation,
+      '!description' => $perishable->description,
+      '!txn_id' => $perishable->txn_id,
+      '!date' => format_date($perishable->time_stamp, 'custom', 'Y-m-d H:i'),
+    )));
+
+    // Prepare remaining perishable points
+    // in this transaction.
+    $perishable_rmd = is_null($perishable->remainder) ? $perishable->points : $perishable->remainder;
+    $perishable_sign = min(1, max(-1, $perishable->points));
+
+    // Start calculations.
+    foreach ($consumptions as $consumption) {
+
+      error_log(print_r($consumption,true),3,'/var/tmp/dump.log');
+
+      $consumption_rmd = is_null($consumption->remainder) ? $consumption->points : $consumption->remainder;
+      $consumption_sign = min(1, max(-1, $consumption->points));
+
+      // Only subtract if the consumption
+      // is smaller or equal to the perishable.
+      if (abs($perishable_rmd) >= abs($consumption_rmd)) {
+        $perishable_rmd = (abs($perishable_rmd) - abs($consumption_rmd)) * $perishable_sign;
+
+        // This consumption is nullified.
+        userpoints_userpointsapi(array(
+          'txn_id' => $consumption->txn_id,
+          'remainder' => 0,
+        ));
+      }
+      // Perishable is nullified. Update this
+      // consumption and break loop.
+      else {
+        $perishable_rmd = 0;
+        $consumption_rmd = (abs($consumption_rmd) - abs($perishable_rmd)) * $consumption_sign;
+        userpoints_userpointsapi(array(
+          'txn_id' => $consumption->txn_id,
+          'remainder' => $consumption_rmd,
+        ));
+        break;
+      }
+    }
+
+    // Add expire transaction in any case.
+    userpoints_userpointsapi(array(
+      'points' => $perishable_rmd * (-1),
+      'uid' => $perishable->uid,
       'operation' => 'expiry',
       'description' => $description,
-      'parent_txn_id' => $line->txn_id,
+      'parent_txn_id' => $perishable->txn_id,
       'moderate' => FALSE,
-      'tid' => $line->tid,
-      'time_stamp' => $line->time_stamp,
+      'tid' => $perishable->tid,
+      'time_stamp' => $perishable->time_stamp,
       'expirydate' => 0,
-    );
-    userpoints_userpointsapi($params);
-    // Ok we've expired the entry lets update the original entry to set the
-    // expired flag.
-    $params = array(
-        'txn_id' => $line->txn_id,
+    ));
+    // Ok we've expired the entry lets update the
+    // original entry to set the expired flag.
+    userpoints_userpointsapi(array(
+        'txn_id' => $perishable->txn_id,
         'expired' => 1,
-    );
-    userpoints_userpointsapi($params);
+    ));
   }
 }
 
-- 
1.9.5.msysgit.1

