Skip to content

Instantly share code, notes, and snippets.

@antoniovassell
Created November 26, 2015 17:13
Show Gist options
  • Save antoniovassell/39cc2b6bb33d4f5f683d to your computer and use it in GitHub Desktop.
Save antoniovassell/39cc2b6bb33d4f5f683d to your computer and use it in GitHub Desktop.
<?php
namespace App\Model\Table;
use App\Model\Entity\TransactionItem;
use ArrayObject;
use Cake\Core\Configure;
use Cake\Error\Debugger;
use Cake\Event\Event;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\ORM\TableRegistry;
use Cake\Validation\Validator;
use App\Model\Entity\ForeignExchangeTrait;
use Doctrine\Instantiator\Exception\InvalidArgumentException;
use Assert\Assertion;
/**
* TransactionItems Model
*
* @property \App\Model\Table\TransactionFrequenciesTable $TransactionFrequencies
*/
class TransactionItemsTable extends Table
{
use ForeignExchangeTrait;
/**
* Initialize method
*
* @param array $config The configuration for the Table.
* @return void
*/
public function initialize(array $config)
{
$this->table('transaction_items');
$this->displayField('name');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('Users', [
'foreignKey' => 'user_id'
]);
$this->belongsTo('TransactionTypes', [
'foreignKey' => 'transaction_type_id'
]);
$this->belongsTo('TransactionFrequencies', [
'foreignKey' => 'transaction_frequency_id'
]);
$this->belongsTo('Accounts', [
'foreignKey' => 'account_id'
]);
$this->hasMany('Transactions', [
'foreignKey' => 'transaction_item_id'
]);
$this->belongsTo('TransactionCategories', [
'foreignKey' => 'transaction_category_id'
]);
}
/**
* Default validation rules.
*
* @param \Cake\Validation\Validator $validator Validator instance.
* @return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->add('id', 'valid', ['rule' => 'uuid'])
->allowEmpty('id', 'create')
->requirePresence('name', 'create')
->notEmpty('name')
->add('priority', 'valid', ['rule' => 'numeric'])
->notEmpty('priority')
->add('user_id', 'valid', ['rule' => 'uuid'])
->requirePresence('user_id', 'create')
->notEmpty('user_id')
->add('account_id', 'valid', ['rule' => 'uuid'])
->requirePresence('account_id', 'create')
->notEmpty('account_id')
->add('transaction_type_id', 'valid', ['rule' => 'uuid'])
->requirePresence('transaction_type_id', 'create')
->notEmpty('transaction_type_id')
->add('transaction_frequency_id', 'valid', ['rule' => 'uuid'])
->requirePresence('transaction_frequency_id', function ($context) {
return (isset($context['data']['is_recurrent']) && $context['data']['is_recurrent'] === true);
})
->notEmpty('transaction_frequency_id')
->add('value', 'valid', ['rule' => 'decimal'])
->requirePresence('value', 'create')
->notEmpty('value')
->add('input_value', 'valid', ['rule' => 'decimal'])
->requirePresence('input_value', 'create')
->notEmpty('input_value')
->add('is_recurrent', 'valid', ['rule' => 'boolean'])
->requirePresence('is_recurrent', 'create')
->notEmpty('is_recurrent')
->add('is_ongoing', 'valid', ['rule' => 'boolean'])
->requirePresence('is_ongoing', function ($context) {
return (isset($context['data']['is_recurrent']) && $context['data']['is_recurrent'] === true);
})
->notEmpty('is_ongoing')
->add('date', 'valid', ['rule' => 'date'])
->requirePresence('date', 'create')
->notEmpty('date')
->add('end_date', 'valid', ['rule' => 'date'])
->add('end_date', 'greaterThanStartDate', [
'rule' => function ($value, $context) {
return strtotime($context['data']['date']) <= strtotime($value);
}
])
->requirePresence('end_date', function ($context) {
return (isset($context['data']['is_recurrent']) &&
$context['data']['is_recurrent'] === true &&
isset($context['data']['is_ongoing']) &&
$context['data']['is_ongoing'] === false);
})
->notEmpty('end_date', 'This field is required', function ($context) {
return (isset($context['data']['is_recurrent']) && $context['data']['is_recurrent'] === true);
});
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* @todo: Might need to validate currency as well
* @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
* @return \Cake\ORM\RulesChecker
*/
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->existsIn(['user_id'], 'Users'));
$rules->add($rules->existsIn(['transaction_type_id'], 'TransactionTypes'));
$rules->add($rules->existsIn(['transaction_frequency_id'], 'TransactionFrequencies'));
$rules->add($rules->existsIn(['account_id'], 'Accounts'));
$rules->add($rules->existsIn(['transaction_category_id'], 'TransactionCategories'));
return $rules;
}
/**
* Add new item
*
* @param Entity $postData Posted data
* @return array
*/
public function add($postData)
{
if (!empty($postData->errors())) {
throw new InvalidArgumentException(__('Transaction Item is missing data'));
}
// @todo: cache
$transactionTypes = TableRegistry::get('TransactionTypes')->find('list')->toArray();
$postData->value = $this->getSignedValue(
$postData->value,
$transactionTypes,
$postData->transaction_type_id
);
$postData->input_value = $this->getSignedValue(
$postData->input_value,
$transactionTypes,
$postData->transaction_type_id
);
$transactionsData = $this->calculateTransactions($postData);
$postData->transactions = $this->Transactions->newEntities($transactionsData);
if (empty($postData->errors)) {
if ($this->save($postData)) {
$event = new Event('Model.Transaction.afterUpdate', $this, [
'userId' => $postData->user_id
]);
$this->eventManager()->dispatch($event);
return true;
}
}
return false;
}
/**
* Calculate individual transactions base on transaction item added
*
* @param Entity $postData
* @return array
*/
public function calculateTransactions($postData)
{
$transactionsData = [];
if ($postData->is_recurrent === true) {
$occurrences = $this->TransactionFrequencies->getOccurrences($postData);
} else {
$occurrences = [$postData->date];
}
foreach ($occurrences as $index => $date) {
$transactionsData[] = [
'account_id' => $postData->account_id,
'value' => $postData->value,
'processed' => false,
'priority' => ($postData->priority) ? $postData->priority : 1,
'date' => $date,
'currency' => $postData->currency,
'exchange_rate' => $postData->exchange_rate,
'exchange_rate_to_original' => $postData->exchange_rate_to_original,
'input_value' => $postData->input_value
];
}
return $transactionsData;
}
/**
* Edit method
*
* @param $postData
* @return bool
*/
public function edit($postData)
{
$this->Transactions->deleteAll(['Transactions.transaction_item_id' => $postData->id]);
$this->add($postData);
return true;
}
/**
* Get signed value base on the type of transaction (income(+) or expense (-))
*
* @todo: Income should always be positive, expense always negative
* @param float $value Monetary value
* @param array $transactionTypes Types of transactions (income, expenses, etc)
* @param UUID $transactionTypeId
* @return float Signed monetary value
*/
public function getSignedValue($value, $transactionTypes, $transactionTypeId)
{
$transactionType = null;
Assertion::notEmpty($transactionTypeId, __('Transaction Type ID cannot be empty'));
Assertion::keyIsset($transactionTypes, $transactionTypeId, __('Transaction Type not valid'));
$transactionType = $transactionTypes[$transactionTypeId];
switch ($transactionType) {
case 'Income':
return $value;
case 'Expense':
return abs($value) * (-1);
}
return false;
}
/**
* Find transaction items, find method for pagination
*
* @param Query $query
* @param array $options
* @return Query
*/
public function findTransactionItems(Query $query, array $options)
{
$conditions = [
'TransactionItems.user_id' => $options['options']['user_id'],
'Transactions.date >=' => $options['options']['start_date'],
'Transactions.date <=' => $options['options']['end_date']
];
$query->find('all')
->where($conditions)
->contain(['TransactionItems'])
->order(['Transactions.priority', 'Transactions.date']);
return $query;
}
/**
* Before marshal callback
*
* @param Event $event
* @param ArrayObject $data
* @param ArrayObject $options
*/
public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
{
if (isset($data['date']) && !empty($data['date'])) {
$data['date'] = date('Y-m-d', strtotime($data['date']));
}
if (isset($data['end_date']) && !empty($data['end_date'])) {
$data['end_date'] = date('Y-m-d', strtotime($data['end_date']));
}
$data = $this->setFxValues($data);
}
/**
* Get transaction items per month given a user id
*
* @param UUID $userId Id of user
* @return \Cake\Datasource\ResultSetInterface
*/
public function getItemsPerMonth($userId)
{
$query = $this->find();
$processedTransactions = $query->newExpr()->addCase($query->newExpr()->add(['Transactions.processed' => true]), '`value`', 'integer');
$query
->where(['TransactionItems.user_id' => $userId])
->select([
'TransactionItems.id',
'TransactionItems.name',
'TransactionItems.value',
'TransactionItems.input_value',
'TransactionItems.currency',
'TransactionItems.account_id',
'TransactionItems.transaction_type_id',
'TransactionItems.transaction_frequency_id',
'month_year' => 'CONCAT(MONTHNAME(Transactions.date), ", ", YEAR(Transactions.date))',
'year' => 'YEAR(Transactions.date)',
'transactions_sum' => $query->func()->sum('Transactions.value'),
'transactions_done_sum' => $query->func()->sum($processedTransactions),
'transactions_count' => $query->func()->count('Transactions.id')
])
->leftJoinWith('Transactions')
->hydrate(false)
->group(['YEAR(Transactions.date)', 'MONTH(Transactions.date)', 'TransactionItems.id']);
$result = $query->all();
return $result;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment