 userpoints.admin.inc       |    2 +-
 userpoints.info            |    1 +
 userpoints.module          |  125 +------------
 userpoints.pages.inc       |    4 +-
 userpoints.test            |   24 ++--
 userpoints.transaction.inc |  463 +++++++++++++++++++++-----------------------
 6 files changed, 237 insertions(+), 382 deletions(-)

diff --git a/userpoints.admin.inc b/userpoints.admin.inc
index 2d72abd..d5d81b6 100644
--- a/userpoints.admin.inc
+++ b/userpoints.admin.inc
@@ -27,7 +27,7 @@ function userpoints_admin_txn($form, &$form_state, $mode, $txn = NULL) {
   if ($mode == 'edit') {
     drupal_set_title(t('Edit !points transaction', userpoints_translation()));
     $timestamp = format_date($txn->time_stamp, 'custom', 'Y-m-d H:i:s O');
-    $txn_user = $txn->user;
+    $txn_user = $txn->getUser();
 
     $form['txn'] = array(
       '#type' => 'value',
diff --git a/userpoints.info b/userpoints.info
index 2f5e8dc..9a9331c 100644
--- a/userpoints.info
+++ b/userpoints.info
@@ -8,3 +8,4 @@ files[]=views/userpoints_views_handler_field_category.inc
 files[]=views/userpoints_views_handler_filter_category.inc
 files[]=views/userpoints_views_handler_argument_category.inc
 configure = admin/config/people/userpoints/settings
+dependencies[]=entity
diff --git a/userpoints.module b/userpoints.module
index b4cdf15..e778087 100644
--- a/userpoints.module
+++ b/userpoints.module
@@ -1507,136 +1507,15 @@ function userpoints_entity_info() {
   $return = array(
     'userpoints_transaction' => array(
       'label' => t('Userpoints Transaction'),
+      'entity class' => 'UserpointsTransaction',
       'controller class' => 'UserpointsTransactionController',
       'base table' => 'userpoints_txn',
-      'uri callback' => 'userpoints_transaction_uri_callback',
+      'uri callback' => 'entity_class_uri',
       'fieldable' => FALSE,
       'entity keys' => array(
         'id' => 'txn_id',
       ),
-      'bundles' => array(),
-      'view modes' => array(
-        'full' => array(
-          'label' => t('Full content'),
-          'custom settings' => FALSE,
-        ),
-      ),
     ),
   );
   return $return;
 }
-
-/**
- * Return the path to the correct view location based on the global context.
- *
- * @param $transaction
- *   The userpoints transaction entity.
- * @param bool $prefix_only
- *   If only the url prefix (without /view suffix) should be returned. Defaults to FALSE.
- *
- * @return
- *   An array with the keys path and options.
- */
-function userpoints_transaction_uri_callback($transaction, $prefix_only = FALSE) {
-  global $user;
-  $uri = array(
-    // Default path is displaying it below myuserpoints for the current user.
-    'path' => 'myuserpoints/transaction/' . $transaction->txn_id,
-    'options' => array(),
-  );
-  // When viewing the transaction in the admin UI (but not the add points page, as this might be called for generating
-  // tokens), use the admin link.
-  if (strpos($_GET['q'], 'admin/config/people/userpoints') !== FALSE && strpos($_GET['q'], 'admin/config/people/userpoints/add') === FALSE ) {
-    $uri['path'] = "admin/config/people/userpoints/transaction/$transaction->txn_id";
-  }
-  elseif ($transaction->uid != $user->uid) {
-    // When not in the admin ui but viewing the transaction of someone else, use the path for another user.
-    $uri['path'] = "user/$transaction->uid/points/$transaction->txn_id";
-  }
-
-  if (!$prefix_only) {
-    $uri['path'] .= '/view';
-  }
-
-  return $uri;
-}
-
-/**
- * Userpoints transaction controller.
- */
-class UserpointsTransactionController extends DrupalDefaultEntityController {
-
-  /**
-   * Overrides DrupalDefaultEntityController::load().
-   */
-  public function load($ids = array(), $conditions = array()) {
-    $entities = array();
-
-    // Revisions are not statically cached, and require a different query to
-    // other conditions, so separate the revision id into its own variable.
-    if ($this->revisionKey && isset($conditions[$this->revisionKey])) {
-      $revision_id = $conditions[$this->revisionKey];
-      unset($conditions[$this->revisionKey]);
-    }
-    else {
-      $revision_id = FALSE;
-    }
-
-    // Create a new variable which is either a prepared version of the $ids
-    // array for later comparison with the entity cache, or FALSE if no $ids
-    // were passed. The $ids array is reduced as items are loaded from cache,
-    // and we need to know if it's empty for this reason to avoid querying the
-    // database when all requested entities are loaded from cache.
-    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
-    // Try to load entities from the static cache, if the entity type supports
-    // static caching.
-    if ($this->cache && !$revision_id) {
-      $entities += $this->cacheGet($ids, $conditions);
-      // If any entities were loaded, remove them from the ids still to load.
-      if ($passed_ids) {
-        $ids = array_keys(array_diff_key($passed_ids, $entities));
-      }
-    }
-
-    // Load any remaining entities from the database. This is the case if $ids
-    // is set to FALSE (so we load all entities), if there are any ids left to
-    // load, if loading a revision, or if $conditions was passed without $ids.
-    if ($ids === FALSE || $ids || $revision_id || ($conditions && !$passed_ids)) {
-      // Build the query.
-      $query = $this->buildQuery($ids, $conditions, $revision_id);
-      $queried_entities = array();
-      $result = $query->execute();
-      foreach ($result->fetchAllAssoc($this->idKey, PDO::FETCH_ASSOC) as $row) {
-        $queried_entities[$row[$this->idKey]] = new UserpointsTransaction($row);
-      }
-    }
-
-    // Pass all entities loaded from the database through $this->attachLoad(),
-    // which attaches fields (if supported by the entity type) and calls the
-    // entity type specific load callback, for example hook_node_load().
-    if (!empty($queried_entities)) {
-      $this->attachLoad($queried_entities, $revision_id);
-      $entities += $queried_entities;
-    }
-
-    if ($this->cache) {
-      // Add entities to the cache if we are not loading a revision.
-      if (!empty($queried_entities) && !$revision_id) {
-        $this->cacheSet($queried_entities);
-      }
-    }
-
-    // Ensure that the returned array is ordered the same as the original
-    // $ids array if this was passed in and remove any invalid ids.
-    if ($passed_ids) {
-      // Remove any invalid ids from the array.
-      $passed_ids = array_intersect_key($passed_ids, $entities);
-      foreach ($entities as $entity) {
-        $passed_ids[$entity->{$this->idKey}] = $entity;
-      }
-      $entities = $passed_ids;
-    }
-
-    return $entities;
-  }
-}
diff --git a/userpoints.pages.inc b/userpoints.pages.inc
index 62f7071..fd9be4d 100644
--- a/userpoints.pages.inc
+++ b/userpoints.pages.inc
@@ -245,7 +245,7 @@ function userpoints_view_transaction($transaction) {
   $content['details']['user'] = array(
     '#theme' => 'userpoints_view_item',
     '#title' => t('User'),
-    '#value' => theme('username', array('account' => $transaction->user)),
+    '#value' => theme('username', array('account' => $transaction->getUser())),
     '#weight' => 0,
     '#attributes' => array('class' => array('userpoints-item-user')),
   );
@@ -261,7 +261,7 @@ function userpoints_view_transaction($transaction) {
   $content['details']['category'] = array(
     '#theme' => 'userpoints_view_item',
     '#title' => t('Category'),
-    '#value' => $transaction->category,
+    '#value' => $transaction->tid,
     '#weight' => 20,
     '#attributes' => array('class' => array('userpoints-item-category')),
   );
diff --git a/userpoints.test b/userpoints.test
index 4a59c95..adccfb2 100644
--- a/userpoints.test
+++ b/userpoints.test
@@ -194,8 +194,8 @@ class UserpointsAPITestCase extends UserpointsBaseTestCase {
 
     foreach ($times as $key => $time) {
       $transaction = userpoints_grant_points($key, $points, $this->non_admin_user->uid)
-        ->setExpiryDate($time['time'])
-        ->save();
+        ->setExpiryDate($time['time']);
+      $transaction->save();
       $this->assertTrue((bool)$transaction->getTxnId(), t($time['string'] . " API responded with a successful grant of points"));
       // Check the database to ensure the points were properly saved.
       $sql = "SELECT points FROM {userpoints_txn} WHERE uid = :uid AND points = :points AND expirydate = :date";
@@ -222,13 +222,13 @@ class UserpointsAPITestCase extends UserpointsBaseTestCase {
     $expire_points = rand(1, 100);
 
     $transaction = userpoints_grant_points('must_expire', $expire_points, $this->non_admin_user->uid)
-        ->setExpiryDate(REQUEST_TIME - 100)
-        ->save();
+      ->setExpiryDate(REQUEST_TIME - 100);
+    $transaction->save();
     $this->assertTrue((bool)$transaction->getTxnId(), t("API succesfully added points for expiration"));
 
     $transaction = userpoints_grant_points('must_not_expire', $keep_points, $this->non_admin_user->uid)
-        ->setExpiryDate(REQUEST_TIME + 10000)
-        ->save();
+      ->setExpiryDate(REQUEST_TIME + 10000);
+    $transaction->save();
     $this->assertTrue((bool)$transaction->getTxnId(), t("API succesfully added points for expiration"));
 
     // Load timestamp of the first transaction.
@@ -597,7 +597,7 @@ class UserpointsGrantPointsTestCase extends UserpointsBaseTestCase {
    */
   function testGrantPoints() {
     // Most basic usage, with automated saving.
-    userpoints_grant_points('test', 10, $this->non_admin_user->uid);
+    userpoints_grant_points('test', 10, $this->non_admin_user->uid)->save();
     $this->verifyPoints($this->non_admin_user->uid, 10, 10);
 
     // Negative points, use of save().
@@ -607,8 +607,8 @@ class UserpointsGrantPointsTestCase extends UserpointsBaseTestCase {
 
     // Verify that pending points are not added to the total.
     $transaction = userpoints_grant_points('test', 7, $this->non_admin_user->uid)
-      ->pending()
-      ->save();
+      ->pending();
+    $transaction->save();
     $this->verifyPoints($this->non_admin_user->uid, 5, 10);
 
     // Make sure that loaded transactions can be updated, and after marked as
@@ -630,8 +630,8 @@ class UserpointsGrantPointsTestCase extends UserpointsBaseTestCase {
     }
 
     $transaction = userpoints_grant_points('test', 19, $this->non_admin_user->uid)
-      ->pending()
-      ->save();
+      ->pending();
+    $transaction->save();
     $this->verifyPoints($this->non_admin_user->uid, 12, 12);
 
     // Transactions can be declined.
@@ -650,4 +650,4 @@ class UserpointsGrantPointsTestCase extends UserpointsBaseTestCase {
       $this->pass(t('Changing a declined transaction was denied.'));
     }
   }
-}
\ No newline at end of file
+}
diff --git a/userpoints.transaction.inc b/userpoints.transaction.inc
index 8183541..79974ac 100644
--- a/userpoints.transaction.inc
+++ b/userpoints.transaction.inc
@@ -10,7 +10,7 @@
  *
  * @ingroup userpoints_api
  */
-class UserpointsTransaction {
+class UserpointsTransaction extends Entity {
 
   /**
    * The transaction has been approved.
@@ -29,18 +29,39 @@ class UserpointsTransaction {
   /**
    * The transaction id (primary key) of this transaction
    *
-   * @var int
+   * @var integer
    */
-  protected $txn_id = NULL;
+  public $txn_id = NULL;
 
-  /**
-   * The actual fields of this transaction.
-   *
-   * Stored in this array to simplify the save/update process.
-   *
-   * @var array
-   */
-  protected $fields = array();
+  public $uid = 0;
+
+  public $points;
+
+  public $operation;
+
+  public $status;
+
+  public $tid;
+
+  public $expirydate;
+
+  public $expired;
+
+  public $time_stamp;
+
+  public $changed;
+
+  public $approver_uid = 0;
+
+  public $description;
+
+  public $reference;
+
+  public $parent_txn_id = 0;
+
+  public $entity_type;
+
+  public $entity_id = 0;
 
   /**
    * Deny reasons.
@@ -85,39 +106,60 @@ class UserpointsTransaction {
    * @param $txn_id
    *   Transaction id if an existing transaction should be loaded.
    */
-  function __construct($values = FALSE) {
-    if ($values) {
-
-      // The txn_id is stored in a separate property.
-      $this->txn_id = $values['txn_id'];
-      unset($values['txn_id']);
+  function __construct($values = array()) {
+    parent::__construct($values, 'userpoints_transaction');
 
-      // All other values are stored in the fields property.
-      $this->fields = $values;
-      $this->orig_status = $this->getStatus();
+    // Apply default status.
+    if (!isset($values['status'])) {
+      $this->status = variable_get('userpoints_points_moderation', UserpointsTransaction::STATUS_APPROVED);
     }
     else {
-      // Initalize fields.
-      $this->fields = array(
-        'uid' => 0,
-        'points' => NULL,
-        'operation' => NULL,
-        'status' => variable_get('userpoints_points_moderation', UserpointsTransaction::STATUS_APPROVED),
-        'tid' => userpoints_get_default_tid(),
-        'expirydate' => userpoints_get_default_expiry_date(),
-        'expired' => 0,
-        'time_stamp' => REQUEST_TIME,
-        'changed' => REQUEST_TIME,
-        'approver_uid' => 0,
-        'description' => '',
-        'reference' => '',
-        'parent_txn_id' => 0,
-        'entity_type' => NULL,
-        'entity_id' => 0,
-      );
+      $this->orig_status = $this->getStatus();
+    }
+
+    if (!isset($values['tid'])) {
+      $this->tid = userpoints_get_default_tid();
+    }
+
+    if (!isset($values['expirydate'])) {
+      $this->expirydate = userpoints_get_default_expiry_date();
+    }
+
+    if (!isset($values['time_stamp'])) {
+      $this->time_stamp = REQUEST_TIME;
     }
   }
 
+ /**
+  * Overrides Entity::defaultUri().
+  *
+  * @param bool $prefix_only
+  *   If only the url prefix (without /view suffix) should be returned. Defaults to FALSE.
+  */
+  function defaultUri($prefix_only = FALSE) {
+    global $user;
+    $uri = array(
+      // Default path is displaying it below myuserpoints for the current user.
+      'path' => 'myuserpoints/transaction/' . $this->txn_id,
+      'options' => array(),
+    );
+    // When viewing the transaction in the admin UI (but not the add points page, as this might be called for generating
+    // tokens), use the admin link.
+    if (strpos($_GET['q'], 'admin/config/people/userpoints') !== FALSE && strpos($_GET['q'], 'admin/config/people/userpoints/add') === FALSE ) {
+      $uri['path'] = "admin/config/people/userpoints/transaction/$this->txn_id";
+    }
+    elseif ($this->uid != $user->uid) {
+      // When not in the admin ui but viewing the transaction of someone else, use the path for another user.
+      $uri['path'] = "user/$this->uid/points/$this->txn_id";
+    }
+
+    if (!$prefix_only) {
+      $uri['path'] .= '/view';
+    }
+
+    return $uri;
+  }
+
   /**
    * Marks this transaction as aborted.
    */
@@ -150,8 +192,8 @@ class UserpointsTransaction {
       return $this;
     }
 
-    $this->fields['entity_type'] = $entity_type;
-    $this->fields['entity_id'] = $entity_id;
+    $this->entity_type = $entity_type;
+    $this->entity_id = $entity_id;
     return $this;
   }
 
@@ -166,7 +208,7 @@ class UserpointsTransaction {
   function setReference($reference) {
     $this->checkChange();
 
-    $this->fields['reference'] = $reference;
+    $this->reference = $reference;
     return $this;
   }
 
@@ -186,7 +228,7 @@ class UserpointsTransaction {
   function setDescription($description) {
     $this->checkChange();
 
-    $this->fields['description'] = $description;
+    $this->description = $description;
     return $this;
   }
 
@@ -224,7 +266,7 @@ class UserpointsTransaction {
       $this->orig_status = $this->getStatus();
     }
 
-    $this->fields['status'] = $status;
+    $this->status = $status;
     return $this;
   }
 
@@ -242,7 +284,7 @@ class UserpointsTransaction {
     $this->checkChange();
 
     if ($expirydate > 0 || $expirydate === 0 || $expirydate === NULL) {
-      $this->fields['expirydate'] = (int)$expirydate;
+      $this->expirydate = (int)$expirydate;
     }
     else {
       $this->abort();
@@ -265,10 +307,10 @@ class UserpointsTransaction {
   function setExpired($expired) {
 
     // A transaction can always be expired but this can not be reversed.
-    if (!$expired && $this->fields['expired']) {
+    if (!$expired && $this->expired) {
       $this->checkChange();
     }
-    $this->fields['expired'] = $expired;
+    $this->expired = $expired;
     return $this;
   }
 
@@ -282,7 +324,7 @@ class UserpointsTransaction {
    */
   function setUid($uid) {
     $this->checkChange();
-    $this->fields['uid'] = $uid;
+    $this->uid = $uid;
     return $this;
   }
 
@@ -296,7 +338,7 @@ class UserpointsTransaction {
    */
   function setApproverUid($uid) {
     $this->checkChange();
-    $this->fields['approver_uid'] = (int)$uid;
+    $this->approver_uid = (int)$uid;
     return $this;
   }
 
@@ -318,7 +360,7 @@ class UserpointsTransaction {
       throw new UserpointsInvalidArgumentException();
     }
 
-    $this->fields['points'] = $points;
+    $this->points = $points;
     return $this;
   }
 
@@ -340,7 +382,7 @@ class UserpointsTransaction {
     if (variable_get('userpoints_transaction_timestamp', 1)) {
       return $this;
     }
-    $this->fields['time_stamp'] = $time_stamp;
+    $this->time_stamp = $time_stamp;
     return $this;
   }
 
@@ -358,7 +400,7 @@ class UserpointsTransaction {
   function setParent($txn_id) {
     $this->checkChange();
 
-    $this->fields['parent_txn_id'] = $txn_id;
+    $this->parent_txn_id = $txn_id;
     return $this;
   }
 
@@ -372,7 +414,7 @@ class UserpointsTransaction {
    */
   function setTid($tid) {
     $this->checkChange();
-    $this->fields['tid'] = $tid;
+    $this->tid = $tid;
     return $this;
   }
 
@@ -396,7 +438,7 @@ class UserpointsTransaction {
   function setOperation($operation) {
     $this->checkChange();
 
-    $this->fields['operation'] = $operation;
+    $this->operation = $operation;
     return $this;
   }
 
@@ -423,8 +465,8 @@ class UserpointsTransaction {
    *   An entity object or NULL.
    */
   function getEntity() {
-    if (!empty($this->fields['entity_id']) && !empty($this->fields['entity_type']) && entity_get_info($this->fields['entity_type'])) {
-      return array_shift(entity_load($this->fields['entity_type'], array($this->fields['entity_id'])));
+    if (!empty($this->entity_id) && !empty($this->entity_type) && entity_get_info($this->entity_type)) {
+      return array_shift(entity_load($this->entity_type, array($this->entity_id)));
     }
   }
 
@@ -435,7 +477,7 @@ class UserpointsTransaction {
    *   The entity type as a string.
    */
   function getEntityType() {
-    return $this->fields['entity_type'];
+    return $this->entity_type;
   }
 
   /**
@@ -445,7 +487,7 @@ class UserpointsTransaction {
    *   The entity id as an integer.
    */
   function getEntityId() {
-    return $this->fields['entity_id'];
+    return $this->entity_id;
   }
 
   /**
@@ -457,7 +499,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setReference()
    */
   function getReference() {
-    return $this->fields['reference'];
+    return $this->reference;
   }
 
   /**
@@ -469,7 +511,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setDescription()
    */
   function getDescription() {
-    return $this->fields['description'];
+    return $this->description;
   }
 
   /**
@@ -488,7 +530,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::isDeclined()
    */
   function getStatus() {
-    return $this->fields['status'];
+    return $this->status;
   }
 
   /**
@@ -500,7 +542,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setExpiryDate()
    */
   function getExpiryDate() {
-    return $this->fields['expirydate'];
+    return $this->expirydate;
   }
 
   /**
@@ -512,7 +554,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setExpired()
    */
   function isExpired() {
-    return $this->fields['expired'];
+    return $this->expired;
   }
 
   /**
@@ -524,7 +566,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setUid()
    */
   function getUid() {
-    return $this->fields['uid'];
+    return $this->uid;
   }
 
   /**
@@ -537,7 +579,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::getUid()
    */
   function getUser() {
-    return user_load($this->fields['uid']);
+    return user_load($this->uid);
   }
 
   /**
@@ -549,7 +591,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setApproverUid()
    */
   function getApproverUid() {
-    return $this->fields['approver_uid'];
+    return $this->approver_uid;
   }
 
   /**
@@ -562,7 +604,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::getApproverUid()
    */
   function getApprover() {
-    return user_load($this->fields['approver_uid']);
+    return user_load($this->approver_uid);
   }
 
   /**
@@ -574,7 +616,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setPoints()
    */
   function getPoints() {
-    return $this->fields['points'];
+    return $this->points;
   }
 
   /**
@@ -586,7 +628,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setTimestamp()
    */
   function getTimestamp() {
-    return $this->fields['time_stamp'];
+    return $this->time_stamp;
   }
 
   /**
@@ -596,7 +638,7 @@ class UserpointsTransaction {
    *   Unix timestamp of the changed date.
    */
   function getChanged() {
-    return $this->fields['changed'];
+    return $this->changed;
   }
 
   /**
@@ -608,8 +650,8 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setParent()
    */
   function getParent() {
-    if (!empty($this->fields['parent_txn_id'])) {
-      return userpoints_transaction_load($this->fields['parent_txn_id']);
+    if (!empty($this->parent_txn_id)) {
+      return userpoints_transaction_load($this->parent_txn_id);
     }
   }
 
@@ -625,7 +667,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::getCategory()
    */
   function getTid() {
-    return $this->fields['tid'];
+    return $this->tid;
   }
 
   /**
@@ -637,7 +679,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setOperation()
    */
   function getOperation() {
-    return $this->fields['operation'];
+    return $this->operation;
   }
 
   /**
@@ -737,110 +779,6 @@ class UserpointsTransaction {
   }
 
   /**
-   * Save or update this transaction.
-   *
-   * It is not permitted to update a approved or denied transaction except
-   * marking it as expird. Any attemt to change a property of such a transaction
-   * will result in an immediate exception.
-   */
-  function save() {
-    // Prevent saving when any of the required properties are missing.
-    if (!$this->getPoints() || !$this->getUid() || !$this->getOperation()) {
-      $this->abort();
-      throw new UserpointsTransactionIncompleteException();
-    }
-
-    // Call the before hook to allow modules to change and deny this.
-    module_invoke_all('userpoints_transaction_before', $this);
-
-    // Abort if the transaction has been denied.
-    if ($this->isDenied()) {
-      $this->abort();
-      throw new UserpointsTransactionDeniedException($this->getDenyReasons());
-    }
-
-    if (empty($this->txn_id)) {
-      // This is a new transaction, save.
-      $this->txn_id = db_insert('userpoints_txn')
-          ->fields($this->fields)
-          ->execute();
-    }
-    else {
-      // Existing transaction, update.
-      db_update('userpoints_txn')
-        ->condition('txn_id', $this->txn_id)
-        ->fields($this->fields)
-        ->execute();
-    }
-
-    // Update totals if the transaction is approved and not expired.
-    if ($this->isApproved() && !$this->isExpired()) {
-      $this->updateTotals();
-    }
-
-    // Call the after hook so that modules can react on this.
-    module_invoke_all('userpoints_transaction_after', $this);
-
-    // Display a message unless disabled or no message exists.
-    if ($this->getDisplay() && $message = $this->getMessage()) {
-      drupal_set_message($message);
-    }
-
-    // Reset original status to current one.
-    $this->orig_status = $this->getStatus();
-    return $this;
-  }
-
-  /**
-   * Update the total aggregations of the corresponding user.
-   */
-  protected function updateTotals() {
-    // Update this category.
-    $this->updateTotalsCategory($this->getTid());
-    // Update the total over all categories.
-    $this->updateTotalsCategory('all');
-  }
-
-  /**
-   * Update the totals of a specific category.
-   *
-   * Updating the total of all categories is supported by using 'all' for the
-   * $tid.
-   */
-  protected function updateTotalsCategory($tid) {
-    $table = 'userpoints';
-    if ($tid === 'all') {
-      // Use a different table for the overall total.
-      $table = 'userpoints_total';
-    }
-
-    // Always update the time stamp and the total points.
-    $total = array(
-      'last_update' => REQUEST_TIME,
-      'points' => $this->getPoints() + userpoints_get_current_points($this->getUid(), $tid),
-    );
-    // Update the total max points if necessary.
-    $max_points_total = userpoints_get_max_points($this->getUid(), $tid);
-    if ($total['points'] > $max_points_total) {
-      $total['max_points'] = $total['points'];
-    }
-
-    // The keys for the merge query. The tid is only added when not 'all'.
-    $keys = array(
-      'uid' => $this->getUid(),
-    );
-    if ($tid !== 'all') {
-      $keys['tid'] = $tid;
-    }
-
-    // Save the updates.
-    db_merge($table)
-      ->key($keys)
-      ->fields($total)
-      ->execute();
-  }
-
-  /**
    * Checks if a change is allowed.
    *
    * @param $only_new
@@ -1122,7 +1060,7 @@ class UserpointsTransaction {
     $actions = array();
     $url_options = array('query' => drupal_get_destination());
 
-    $uri = userpoints_transaction_uri_callback($this, TRUE);
+    $uri = $this->defaultUri(TRUE);
     $url_prefix = $uri['path'];
 
     if ($show_view && userpoints_access_view_transaction($this)) {
@@ -1207,77 +1145,15 @@ class UserpointsTransaction {
   }
 
   /**
-   * Automatically called if this object is goes out of scope and is destructed.
+   * Resets the original status to the current one.
    */
-  function __destruct() {
-    // Automatically save new transactions to improve DX.
-    if (!$this->getTxnId() && !$this->isAborted()) {
-      $this->save();
-    }
-  }
-
-  /**
-   * Magic function to allow access to property by name.
-   */
-  function __get($name) {
-
-    // Compatibility with dpm().
-    if (strpos($name, 'krumo') !== FALSE) {
-      if (!isset($this->$name)) {
-        $this->$name = NULL;
-      }
-      return $this->$name;
-    }
-
-    $method = 'get' . str_replace('_', '', $name);
-    if (method_exists($this, $method)) {
-      return $this->$method();
-    }
-    else {
-      throw new UserpointsInvalidPropertyException($name);
-    }
-  }
-
-  /**
-   * Magic function to allow access to property by name.
-   */
-  function __set($name, $value) {
-
-    $method = 'get' . str_replace('_', '', $name);
-    if (method_exists($this, $method)) {
-      return $this->$method($value);
-    }
-    else {
-      // Some modules, e.g rdf.module and devel.module try to set custom
-      // properties on entites. Do not prevent this.
-      return $this->$name = $value;
-    }
+  public function resetOriginalStatus() {
+    $this->orig_status = $this->getStatus();
   }
 }
 
 /**
- * This exception is thrown when a pr{ "rules_userpoints_transaction_after_test_rule" : {
-        "LABEL" : "Userpoints Transaction After Test rule",
-        "PLUGIN" : "reaction rule",
-        "REQUIRES" : [ "rules", "userpoints_rules" ],
-        "ON" : [ "userpoints_event_points_awarded_after" ],
-        "IF" : [
-          { "data_is" : {
-              "data" : [ "userpoints-transaction:operation" ],
-              "value" : "userpoints_rules_trigger_after_rule"
-            }
-          }
-        ],
-        "DO" : [
-          { "data_set" : { "data" : [ "userpoints-transaction:status" ], "value" : "1" } },
-          { "data_set" : {
-              "data" : [ "userpoints-transaction:reference" ],
-              "value" : "Transaction was set to pending through rules."
-            }
-          }
-        ]
-      }
-    }operty is changed after a saved transaction
+ * This exception is thrown when a property is changed after a saved transaction
  * has been approved or declined.
  */
 class UserpointsChangeException extends Exception {
@@ -1307,3 +1183,102 @@ class UserpointsInvalidArgumentException extends Exception {
 class UserpointsTransactionIncompleteException extends Exception {
 
 }
+
+
+/**
+ * Userpoints transaction controller.
+ */
+class UserpointsTransactionController extends EntityAPIController {
+
+  /**
+   * Overrides EntityAPIController::save().
+   *
+   * It is not permitted to update a approved or denied transaction except
+   * marking it as expird. Any attemt to change a property of such a transaction
+   * will result in an immediate exception.
+   */
+  public function save($entity, DatabaseTransaction $transaction = NULL) {
+
+    // Prevent saving when any of the required properties are missing.
+    if (!$entity->getPoints() || !$entity->getUid() || !$entity->getOperation()) {
+      $entity->abort();
+      throw new UserpointsTransactionIncompleteException();
+    }
+
+    // Call the before hook to allow modules to change and deny this.
+    module_invoke_all('userpoints_transaction_before', $entity);
+
+    // Abort if the transaction has been denied.
+    if ($entity->isDenied()) {
+      $entity->abort();
+      return FALSE;
+    }
+
+    $return = parent::save($entity, $transaction);
+
+    // Update totals if the transaction is approved and not expired.
+    if ($entity->isApproved() && !$entity->isExpired()) {
+      $this->updateTotals($entity->tid, $entity->uid, $entity->points);
+    }
+
+    // Display a message unless disabled or no message exists.
+    if ($entity->getDisplay() && $message = $entity->getMessage()) {
+      drupal_set_message($message);
+    }
+
+    // Reset original status to current one
+    $entity->resetOriginalStatus();
+    return $return;
+  }
+
+
+  /**
+   * Update the total aggregations of the corresponding user.
+   */
+  protected function updateTotals($tid, $uid, $points) {
+    // Update this category.
+    $this->updateTotalsCategory($tid, $uid, $points);
+    // Update the total over all categories.
+    $this->updateTotalsCategory('all', $uid, $points);
+  }
+
+  /**
+   * Update the totals of a specific category.
+   *
+   * Updating the total of all categories is supported by using 'all' for the
+   * $tid.
+   */
+  protected function updateTotalsCategory($tid, $uid, $points) {
+    $table = 'userpoints';
+    if ($tid === 'all') {
+      // Use a different table for the overall total.
+      $table = 'userpoints_total';
+    }
+
+    // Always update the time stamp and the total points.
+    $total = array(
+      'last_update' => REQUEST_TIME,
+      'points' => $points+ userpoints_get_current_points($uid, $tid),
+    );
+    // Update the total max points if necessary.
+    $max_points_total = userpoints_get_max_points($uid, $tid);
+    if ($total['points'] > $max_points_total) {
+      $total['max_points'] = $total['points'];
+    }
+
+    // The keys for the merge query. The tid is only added when not 'all'.
+    $keys = array(
+      'uid' => $uid,
+    );
+    if ($tid !== 'all') {
+      $keys['tid'] = $tid;
+    }
+
+    // Save the updates.
+    db_merge($table)
+      ->key($keys)
+      ->fields($total)
+      ->execute();
+  }
+
+}
