 userpoints.admin.inc                        |    2 +-
 userpoints.api.php                          |   13 +-
 userpoints.info                             |    1 +
 userpoints.module                           |  237 +-----------
 userpoints.pages.inc                        |    4 +-
 userpoints.test                             |   24 +-
 userpoints.transaction.inc                  |  579 +++++++++++++++------------
 userpoints_rules/userpoints_rules.module    |    7 -
 userpoints_rules/userpoints_rules.rules.inc |   10 -
 userpoints_rules/userpoints_rules.test      |   22 +-
 10 files changed, 356 insertions(+), 543 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.api.php b/userpoints.api.php
index aa52c7a..db3e0ee 100644
--- a/userpoints.api.php
+++ b/userpoints.api.php
@@ -124,16 +124,5 @@ function hook_userpoints_transaction_before(UserpointsTransaction $transaction)
 }
 
 /**
- * Invoked after a transaction has been saved.
- *
- * @param $transaction
- *   A UserpointsTransaction object which contains all information about the
- *   new or updated transaction.
- */
-function hook_userpoints_transaction_after(UserpointsTransaction $transaction) {
-
-}
-
-/**
  * @}
- */
\ No newline at end of file
+ */
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..c18523a 100644
--- a/userpoints.module
+++ b/userpoints.module
@@ -1357,117 +1357,6 @@ function userpoints_get_list_row($data) {
 }
 
 /**
- * Implements hook_entity_property_info().
- */
-function userpoints_entity_property_info() {
-  $info = array();
-  // Add meta-data about the basic userpoints_transaction properties.
-  $properties = &$info['userpoints_transaction']['properties'];
-  $properties = array(
-    'user' => array(
-      'type' => 'user',
-      'label' => t('User'),
-      'description' => t('The user that will receive the !points', userpoints_translation()),
-      'setter callback' => 'userpoints_transaction_property_set',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-    'points' => array(
-      'type' => 'integer',
-      'label' => t('!Points', userpoints_translation()),
-      'description' => t('Amount of !points to give or take.', userpoints_translation()),
-      'restriction' => 'input',
-      'setter callback' => 'userpoints_transaction_property_set',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-    'points_abs' => array(
-      'label' => t('!Points absolute', userpoints_translation()),
-      'description' => t('The absolute (positive) amount of !points of this transaction.', userpoints_translation()),
-      'type' => 'integer',
-      'getter callback' => 'userpoints_transaction_get_points_absolute',
-    ),
-    'tid' => array(
-      'label' => t('!Points category', userpoints_translation()),
-      'description' => t('The category to which these transaction belongs.'),
-      'type' => 'integer',
-      'options list' => 'userpoints_rules_get_categories',
-      'restriction' => 'input',
-      'setter callback' => 'userpoints_transaction_property_set',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-    'entity' => array(
-      'label' => t('Entity'),
-      'type' => 'entity',
-      'description' => t('The entity to which this transaction refers.'),
-      'restriction' => 'input',
-      'optional' => TRUE,
-      'getter callback' => 'userpoints_transaction_property_get',
-     ),
-    'description' => array(
-      'label' => t('Description'),
-      'type' => 'text',
-      'description' => t('Can contain the reason why the points have been given.'),
-      'restriction' => 'input',
-      'optional' => TRUE,
-      'setter callback' => 'userpoints_transaction_property_set',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-    'reference' => array(
-      'label' => t('Reference'),
-      'type' => 'text',
-      'description' => t('Can contain a reference for this transaction.'),
-      'optional' => TRUE,
-      'setter callback' => 'userpoints_transaction_property_set',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-    'operation' => array(
-      'label' => t('Operation'),
-      'type' => 'text',
-      'description' => t('Describes the operation (Insert/Remove/...).'),
-      'restriction' => 'input',
-      'getter callback' => 'userpoints_transaction_property_get',
-      'setter callback' => 'userpoints_transaction_property_set',
-    ),
-    'reason' => array(
-      'label' => t('Reason'),
-      'type' => 'text',
-      'description' => t('The reason why the points were granted.'),
-      'restriction' => 'input',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-    'time_stamp' => array(
-      'label' => t('Timestamp'),
-      'type' => 'date',
-      'description' => t('Time when the points were given.'),
-      'setter callback' => 'userpoints_transaction_property_set',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-    'expirydate' => array(
-      'label' => t('Expiry date'),
-      'type' => 'date',
-      'description' => t('Time when the points will expire.'),
-      'setter callback' => 'userpoints_transaction_property_set',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-    'display' => array(
-      'label' => t('Display'),
-      'type' => 'boolean',
-      'description' => t('Whether to show a message to the user for this transaction or not.'),
-      'setter callback' => 'userpoints_transaction_property_set',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-    'status' => array(
-      'label' => t('Status'),
-      'type' => 'integer',
-      'description' => t('Status of this transaction.'),
-      'options list' => 'userpoints_txn_status',
-      'setter callback' => 'userpoints_transaction_property_set',
-      'getter callback' => 'userpoints_transaction_property_get',
-    ),
-  );
-  return $info;
-}
-
-/**
  * Gets the property from the UserpointsTransaction object.
  */
 function userpoints_transaction_property_get($data, array $options, $name) {
@@ -1507,136 +1396,16 @@ function userpoints_entity_info() {
   $return = array(
     'userpoints_transaction' => array(
       'label' => t('Userpoints Transaction'),
+      'entity class' => 'UserpointsTransaction',
       'controller class' => 'UserpointsTransactionController',
+      'metadata controller class' => 'UserpointsTransactionMetadataController',
       '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..3e5b038 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.
@@ -20,7 +20,6 @@ class UserpointsTransaction {
    * The transaction is pending for approval.
    */
   const STATUS_PENDING = 1;
-
   /**
    * The transaction has been declined.
    */
@@ -29,18 +28,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,40 +105,61 @@ 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 ($this->status === NULL) {
+      $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 ($this->tid === NULL) {
+      $this->tid = userpoints_get_default_tid();
+    }
+
+    if ($this->expirydate === NULL) {
+      $this->expirydate = userpoints_get_default_expiry_date();
+    }
+
+    if ($this->time_stamp === NULL) {
+      $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.
    */
   function abort() {
@@ -150,8 +191,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 +207,7 @@ class UserpointsTransaction {
   function setReference($reference) {
     $this->checkChange();
 
-    $this->fields['reference'] = $reference;
+    $this->reference = $reference;
     return $this;
   }
 
@@ -186,7 +227,7 @@ class UserpointsTransaction {
   function setDescription($description) {
     $this->checkChange();
 
-    $this->fields['description'] = $description;
+    $this->description = $description;
     return $this;
   }
 
@@ -224,7 +265,7 @@ class UserpointsTransaction {
       $this->orig_status = $this->getStatus();
     }
 
-    $this->fields['status'] = $status;
+    $this->status = $status;
     return $this;
   }
 
@@ -242,7 +283,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 +306,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 +323,7 @@ class UserpointsTransaction {
    */
   function setUid($uid) {
     $this->checkChange();
-    $this->fields['uid'] = $uid;
+    $this->uid = $uid;
     return $this;
   }
 
@@ -296,7 +337,7 @@ class UserpointsTransaction {
    */
   function setApproverUid($uid) {
     $this->checkChange();
-    $this->fields['approver_uid'] = (int)$uid;
+    $this->approver_uid = (int)$uid;
     return $this;
   }
 
@@ -318,7 +359,7 @@ class UserpointsTransaction {
       throw new UserpointsInvalidArgumentException();
     }
 
-    $this->fields['points'] = $points;
+    $this->points = $points;
     return $this;
   }
 
@@ -340,7 +381,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 +399,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 +413,7 @@ class UserpointsTransaction {
    */
   function setTid($tid) {
     $this->checkChange();
-    $this->fields['tid'] = $tid;
+    $this->tid = $tid;
     return $this;
   }
 
@@ -396,7 +437,7 @@ class UserpointsTransaction {
   function setOperation($operation) {
     $this->checkChange();
 
-    $this->fields['operation'] = $operation;
+    $this->operation = $operation;
     return $this;
   }
 
@@ -423,8 +464,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 +476,7 @@ class UserpointsTransaction {
    *   The entity type as a string.
    */
   function getEntityType() {
-    return $this->fields['entity_type'];
+    return $this->entity_type;
   }
 
   /**
@@ -445,7 +486,7 @@ class UserpointsTransaction {
    *   The entity id as an integer.
    */
   function getEntityId() {
-    return $this->fields['entity_id'];
+    return $this->entity_id;
   }
 
   /**
@@ -457,7 +498,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setReference()
    */
   function getReference() {
-    return $this->fields['reference'];
+    return $this->reference;
   }
 
   /**
@@ -469,7 +510,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setDescription()
    */
   function getDescription() {
-    return $this->fields['description'];
+    return $this->description;
   }
 
   /**
@@ -488,7 +529,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::isDeclined()
    */
   function getStatus() {
-    return $this->fields['status'];
+    return $this->status;
   }
 
   /**
@@ -500,7 +541,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setExpiryDate()
    */
   function getExpiryDate() {
-    return $this->fields['expirydate'];
+    return $this->expirydate;
   }
 
   /**
@@ -512,7 +553,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setExpired()
    */
   function isExpired() {
-    return $this->fields['expired'];
+    return $this->expired;
   }
 
   /**
@@ -524,7 +565,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setUid()
    */
   function getUid() {
-    return $this->fields['uid'];
+    return $this->uid;
   }
 
   /**
@@ -537,7 +578,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::getUid()
    */
   function getUser() {
-    return user_load($this->fields['uid']);
+    return user_load($this->uid);
   }
 
   /**
@@ -549,7 +590,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setApproverUid()
    */
   function getApproverUid() {
-    return $this->fields['approver_uid'];
+    return $this->approver_uid;
   }
 
   /**
@@ -562,7 +603,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::getApproverUid()
    */
   function getApprover() {
-    return user_load($this->fields['approver_uid']);
+    return user_load($this->approver_uid);
   }
 
   /**
@@ -574,7 +615,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setPoints()
    */
   function getPoints() {
-    return $this->fields['points'];
+    return $this->points;
   }
 
   /**
@@ -586,7 +627,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setTimestamp()
    */
   function getTimestamp() {
-    return $this->fields['time_stamp'];
+    return $this->time_stamp;
   }
 
   /**
@@ -596,7 +637,7 @@ class UserpointsTransaction {
    *   Unix timestamp of the changed date.
    */
   function getChanged() {
-    return $this->fields['changed'];
+    return $this->changed;
   }
 
   /**
@@ -608,8 +649,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 +666,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::getCategory()
    */
   function getTid() {
-    return $this->fields['tid'];
+    return $this->tid;
   }
 
   /**
@@ -637,7 +678,7 @@ class UserpointsTransaction {
    * @see UserpointsTransaction::setOperation()
    */
   function getOperation() {
-    return $this->fields['operation'];
+    return $this->operation;
   }
 
   /**
@@ -737,110 +778,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
@@ -959,10 +896,10 @@ class UserpointsTransaction {
     // Prepare arguments. They are the same for all string combinations.
     $categories = userpoints_get_categories();
     $arguments = array_merge(userpoints_translation(), array(
-        '!username' => theme('username', array('account' => $this->getUser())),
-        '%total' => userpoints_get_current_points($this->getUid(), $this->getTid()),
-        '%category' => $this->getCategory(),
-      ));
+      '!username' => theme('username', array('account' => $this->getUser())),
+      '%total' => userpoints_get_current_points($this->getUid(), $this->getTid()),
+      '%category' => $this->getCategory(),
+    ));
 
     $view_own_points = user_access('view own userpoints') || user_access('view userpoints') || user_access('administer userpoints');
     $view_all_points = user_access('view userpoints') || user_access('administer userpoints');
@@ -1122,7 +1059,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)) {
@@ -1164,8 +1101,8 @@ class UserpointsTransaction {
       UserpointsTransaction::STATUS_PENDING => 'pending',
     );
     $row = array('class' => array(
-        'userpoints-transaction-row-status-' . $css_stati[$this->getStatus()],
-        'userpoints-transaction-row-category-' . $this->getTid()),
+      'userpoints-transaction-row-status-' . $css_stati[$this->getStatus()],
+      'userpoints-transaction-row-category-' . $this->getTid()),
     );
     if ($settings['show_user']) {
       $row['data'][] = array(
@@ -1207,77 +1144,15 @@ class UserpointsTransaction {
   }
 
   /**
-   * Automatically called if this object is goes out of scope and is destructed.
-   */
-  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.
+   * Resets the original status to the current one.
    */
-  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 +1182,205 @@ 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.
+    // @todo: Rename this hook?
+    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();
+  }
+
+}
+
+class UserpointsTransactionMetadataController extends EntityDefaultMetadataController {
+  public function entityPropertyInfo() {
+    $info = parent::entityPropertyInfo();
+
+    $properties = &$info['userpoints_transaction']['properties'];
+    $properties['user'] = array(
+      'type' => 'user',
+      'label' => t('User'),
+      'description' => t('The user that will receive the !points', userpoints_translation()),
+      'setter callback' => 'userpoints_transaction_property_set',
+      'getter callback' => 'userpoints_transaction_property_get',
+    );
+    $properties['points']['label'] = t('!Points', userpoints_translation());
+    $properties['points']['description'] = t('Amount of !points to give or take.', userpoints_translation());
+
+    $properties['points_abs'] = array(
+      'label' => t('!Points absolute', userpoints_translation()),
+      'description' => t('The absolute (positive) amount of !points of this transaction.', userpoints_translation()),
+      'type' => 'integer',
+      'getter callback' => 'userpoints_transaction_get_points_absolute',
+    );
+
+    $properties['tid'] = array(
+      'label' => t('!Points category', userpoints_translation()),
+      'description' => t('The category to which these transaction belongs.'),
+      'options list' => 'userpoints_rules_get_categories',
+    ) + $properties['tid'];
+
+    $properties['entity'] = array(
+      'label' => t('Entity'),
+      'type' => 'entity',
+      'description' => t('The entity to which this transaction refers.'),
+      'optional' => TRUE,
+      'getter callback' => 'userpoints_transaction_property_get',
+    );
+    $properties['description'] = array(
+      'label' => t('Description'),
+      'description' => t('Can contain the reason why the points have been given.'),
+      'optional' => TRUE,
+    ) + $properties['description'];
+
+    $properties['reference'] = array(
+      'label' => t('Reference'),
+      'description' => t('Can contain a reference for this transaction.'),
+      'optional' => TRUE,
+    ) + $properties['reference'];
+
+    $properties['operation'] = array(
+      'label' => t('Operation'),
+      'description' => t('Describes the operation (Insert/Remove/...).'),
+    ) + $properties['operation'];
+
+    $properties['reason'] = array(
+      'label' => t('Reason'),
+      'type' => 'text',
+      'description' => t('The reason why the points were granted.'),
+      'getter callback' => 'userpoints_transaction_property_get',
+    );
+
+    $properties['time_stamp'] = array(
+      'label' => t('Timestamp'),
+      'type' => 'date',
+      'description' => t('Time when the points were given.'),
+    ) + $properties['time_stamp'];
+
+    $properties['expirydate'] = array(
+      'label' => t('Expiry date'),
+      'type' => 'date',
+      'description' => t('Time when the points will expire.'),
+    ) + $properties['expirydate'];
+
+    $properties['display'] = array(
+      'label' => t('Display'),
+      'type' => 'boolean',
+      'description' => t('Whether to show a message to the user for this transaction or not.'),
+      'setter callback' => 'userpoints_transaction_property_set',
+      'getter callback' => 'userpoints_transaction_property_get',
+    );
+    $properties['status'] = array(
+      'label' => t('Status'),
+      'description' => t('Status of this transaction.'),
+      'options list' => 'userpoints_txn_status',
+    ) + $properties['status'];
+
+    // Add default setter callback.
+    foreach ($properties as $key => &$property) {
+      if (isset($property['setter callback'])) {
+        continue;
+      }
+
+      // Exclude some read only properties.
+      if (in_array($key, array('display', 'reason', 'entity', 'points_abs'))) {
+        continue;
+      }
+
+      $property['setter callback'] = 'entity_property_verbatim_set';
+    }
+
+    return $info;
+  }
+}
diff --git a/userpoints_rules/userpoints_rules.module b/userpoints_rules/userpoints_rules.module
index 292bd0c..60cfbc2 100644
--- a/userpoints_rules/userpoints_rules.module
+++ b/userpoints_rules/userpoints_rules.module
@@ -11,10 +11,3 @@
 function userpoints_rules_userpoints_transaction_before($userpoints_transaction) {
   rules_invoke_event('userpoints_event_points_awarded_before', $userpoints_transaction);
 }
-
-/**
- * Implements hook_userpoints().
- */
-function userpoints_rules_userpoints_transaction_after($userpoints_transaction) {
-  rules_invoke_event('userpoints_event_points_awarded_after', $userpoints_transaction);
-}
\ No newline at end of file
diff --git a/userpoints_rules/userpoints_rules.rules.inc b/userpoints_rules/userpoints_rules.rules.inc
index ccf6f73..87ccf1d 100644
--- a/userpoints_rules/userpoints_rules.rules.inc
+++ b/userpoints_rules/userpoints_rules.rules.inc
@@ -143,16 +143,6 @@ function userpoints_rules_rules_event_info() {
         ),
        'group' => t('!Points', userpoints_translation()),
     ),
-    'userpoints_event_points_awarded_after' => array(
-       'label' => t('User was awarded !points', userpoints_translation()),
-       'variables' => array(
-          'userpoints_transaction' => array(
-            'type' => 'userpoints_transaction',
-            'label' => t('!Points transaction', userpoints_translation()),
-          )
-        ),
-       'group' => t('!Points', userpoints_translation()),
-    ),
   );
 }
 
diff --git a/userpoints_rules/userpoints_rules.test b/userpoints_rules/userpoints_rules.test
index 5381bb3..6bdd9ca 100644
--- a/userpoints_rules/userpoints_rules.test
+++ b/userpoints_rules/userpoints_rules.test
@@ -63,8 +63,8 @@ class UserpointsRulesTestCase extends DrupalWebTestCase {
 
     $this->createEventRules();
 
-    $transaction = userpoints_grant_points('userpoints_rules_trigger_before_rule', 10, $user->uid)
-      ->save();
+    $transaction = userpoints_grant_points('userpoints_rules_trigger_before_rule', 10, $user->uid);
+    $transaction->save();
 
     // Verify that the changes defined in the rules have been done.
     $this->assertTrue($transaction->isDeclined(), t('Transaction has been marked as declined.'));
@@ -76,19 +76,13 @@ class UserpointsRulesTestCase extends DrupalWebTestCase {
     $this->assertEqual('Transaction was declined through rules.', $transaction->getDescription(), t('Transaction description has been set.'));
 
     $transaction = userpoints_grant_points('userpoints_rules_trigger_after_rule', 15, $user->uid)
-      ->pending()
-      ->save();
+      ->pending();
+    $transaction->save();
 
     // Verify that the changes defined in the rules have been done.
-    $this->assertTrue($transaction->isApproved(), t('Transaction has been marked as pending.'));
-    $this->assertEqual('Transaction was set to pending through rules.', $transaction->getReference(), t('Transaction reference has been set.'));
-
-    // Verify that the changes have not be written to the database as this is
-    // the after event.
     $transaction = userpoints_transaction_load($transaction->getTxnId());
-    $this->assertFalse($transaction->isApproved(), t('Transaction has not been marked as pending.'));
-    $this->assertEqual('', $transaction->getReference(), t('Transaction reference has not been saved.'));
-
+    $this->assertTrue($transaction->isApproved(), t('Transaction has been marked as approved.'));
+    $this->assertEqual('Transaction was set to pending through rules.', $transaction->getReference(), t('Transaction reference has been set.'));
   }
 
   /**
@@ -122,7 +116,7 @@ class UserpointsRulesTestCase extends DrupalWebTestCase {
         "LABEL" : "Userpoints Transaction After Test rule",
         "PLUGIN" : "reaction rule",
         "REQUIRES" : [ "rules", "userpoints_rules" ],
-        "ON" : [ "userpoints_event_points_awarded_after" ],
+        "ON" : [ "userpoints_transaction_insert" ],
         "IF" : [
           { "data_is" : {
               "data" : [ "userpoints-transaction:operation" ],
@@ -183,4 +177,4 @@ class UserpointsRulesTestCase extends DrupalWebTestCase {
     cache_clear_all('*', 'cache_rules', TRUE);
   }
 
-}
\ No newline at end of file
+}
