firefly-iii/app/lib/Firefly/Queue/Import.php

649 lines
25 KiB
PHP

<?php
namespace Firefly\Queue;
use Carbon\Carbon;
use Illuminate\Queue\Jobs\Job;
/**
* Class Import
* @package Firefly\Queue
*/
class Import
{
/** @var \Firefly\Storage\Account\AccountRepositoryInterface */
protected $_accounts;
/** @var \Firefly\Storage\Import\ImportRepositoryInterface */
protected $_repository;
/** @var \Firefly\Storage\Budget\BudgetRepositoryInterface */
protected $_budgets;
/** @var \Firefly\Storage\Category\CategoryRepositoryInterface */
protected $_categories;
/** @var \Firefly\Storage\TransactionJournal\TransactionJournalRepositoryInterface */
protected $_journals;
/** @var \Firefly\Storage\Limit\LimitRepositoryInterface */
protected $_limits;
/** @var \Firefly\Storage\Piggybank\PiggybankRepositoryInterface */
protected $_piggybanks;
/** @var \Firefly\Storage\RecurringTransaction\RecurringTransactionRepositoryInterface */
protected $_recurring;
/**
*
*/
public function __construct()
{
$this->_accounts = \App::make('Firefly\Storage\Account\AccountRepositoryInterface');
$this->_repository = \App::make('Firefly\Storage\Import\ImportRepositoryInterface');
$this->_budgets = \App::make('Firefly\Storage\Budget\BudgetRepositoryInterface');
$this->_categories = \App::make('Firefly\Storage\Category\CategoryRepositoryInterface');
$this->_journals = \App::make('Firefly\Storage\TransactionJournal\TransactionJournalRepositoryInterface');
$this->_limits = \App::make('Firefly\Storage\Limit\LimitRepositoryInterface');
$this->_piggybanks = \App::make('Firefly\Storage\Piggybank\PiggybankRepositoryInterface');
$this->_recurring = \App::make('Firefly\Storage\RecurringTransaction\RecurringTransactionRepositoryInterface');
}
/**
* @param Job $job
* @param array $payload
*/
public function cleanImportAccount(Job $job, array $payload)
{
$importAccountType = $this->_accounts->findAccountType('Import account');
$importAccounts = $this->_accounts->getByAccountType($importAccountType);
if (count($importAccounts) == 0) {
$job->delete();
} else if (count($importAccounts) == 1) {
/** @var \Account $importAccount */
$importAccount = $importAccounts[0];
$transactions = $importAccount->transactions()->get();
/** @var \Transaction $transaction */
foreach ($transactions as $transaction) {
$transaction->account()->associate($importAccount);
$transaction->save();
}
\Log::debug('Updated ' . count($transactions) . ' transactions from Import Account to cash.');
}
$job->delete();
}
/**
* @param Job $job
* @param array $payload
* @throws \Firefly\Exception\FireflyException
*/
public function importComponent(Job $job, array $payload)
{
\Log::debug('Going to import component "' . $payload['data']['name'] . '".');
switch ($payload['data']['type']['type']) {
case 'beneficiary':
$payload['class'] = 'Account';
$payload['data']['account_type'] = 'Beneficiary account';
$this->importAccount($job, $payload);
break;
case 'budget':
$this->importBudget($job, $payload);
break;
case 'category':
$this->importCategory($job, $payload);
break;
case 'payer':
$job->delete();
break;
default:
$job->delete();
break;
}
}
/**
* Import a personal account or beneficiary as a new account.
*
* @param Job $job
* @param array $payload
*/
public function importAccount(Job $job, array $payload)
{
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
// maybe we've already imported this account:
$importEntry = $this->_repository->findImportEntry($importMap, 'Account', intval($payload['data']['id']));
// if so, delete job and return:
if (!is_null($importEntry)) {
$job->delete();
return;
}
// if we try to import a beneficiary, Firefly will "merge" already,
// so we don't care:
if (isset($payload['data']['account_type']) && $payload['data']['account_type'] == 'Beneficiary account') {
// store beneficiary
$acct = $this->_accounts->createOrFindBeneficiary($payload['data']['name']);
\Log::debug('Imported ' . $payload['class'] . ' "' . $payload['data']['name'] . '".');
$this->_repository->store($importMap, 'Account', $payload['data']['id'], $acct->id);
$job->delete();
return;
}
// but we cannot merge accounts, so we need to search first:
$acct = $this->_accounts->findByName($payload['data']['name']);
if (is_null($acct)) {
// store new one!
$acct = $this->_accounts->store((array)$payload['data']);
\Log::debug('Imported ' . $payload['class'] . ' "' . $payload['data']['name'] . '".');
$this->_repository->store($importMap, 'Account', $payload['data']['id'], $acct->id);
} else {
// use previous one!
\Log::debug('Already imported ' . $payload['class'] . ' "' . $payload['data']['name'] . '".');
$this->_repository->store($importMap, 'Account', $payload['data']['id'], $acct->id);
}
// and delete the job
$job->delete();
}
/**
* @param \User $user
*/
protected function overruleUser(\User $user)
{
$this->_accounts->overruleUser($user);
$this->_budgets->overruleUser($user);
$this->_categories->overruleUser($user);
$this->_journals->overruleUser($user);
$this->_limits->overruleUser($user);
$this->_repository->overruleUser($user);
$this->_piggybanks->overruleUser($user);
$this->_recurring->overruleUser($user);
}
/**
* Import a budget into Firefly.
*
* @param Job $job
* @param array $payload
*/
public function importBudget(Job $job, array $payload)
{
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
// maybe we've already imported this budget:
$bdg = $this->_budgets->findByName($payload['data']['name']);
if (is_null($bdg)) {
// we have not!
$bdg = $this->_budgets->store((array)$payload['data']);
$this->_repository->store($importMap, 'Budget', $payload['data']['id'], $bdg->id);
\Log::debug('Imported budget "' . $payload['data']['name'] . '".');
} else {
// we have!
$this->_repository->store($importMap, 'Budget', $payload['data']['id'], $bdg->id);
\Log::debug('Already had budget "' . $payload['data']['name'] . '".');
}
// delete job.
$job->delete();
}
/**
* Import a category into Firefly.
*
* @param Job $job
* @param array $payload
*/
public function importCategory(Job $job, array $payload)
{
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
// try to find budget:
$current = $this->_categories->findByName($payload['data']['name']);
if (is_null($current)) {
$cat = $this->_categories->store((array)$payload['data']);
$this->_repository->store($importMap, 'Category', $payload['data']['id'], $cat->id);
\Log::debug('Imported category "' . $payload['data']['name'] . '".');
} else {
$this->_repository->store($importMap, 'Category', $payload['data']['id'], $current->id);
\Log::debug('Already had category "' . $payload['data']['name'] . '".');
}
$job->delete();
}
/**
* @param Job $job
* @param array $payload
*/
public function importComponentTransaction(Job $job, array $payload)
{
if ($job->attempts() > 1) {
\Log::info('importComponentTransaction Job running for ' . $job->attempts() . 'th time!');
}
if ($job->attempts() > 30) {
\Log::error('importComponentTransaction Job running for ' . $job->attempts() . 'th time, so KILL!');
$job->delete();
return;
}
$oldComponentId = intval($payload['data']['component_id']);
$oldTransactionId = intval($payload['data']['transaction_id']);
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
$oldTransactionMap = $this->_repository->findImportEntry($importMap, 'Transaction', $oldTransactionId);
// we don't know what the component is, so we need to search for it in a set
// of possible types (Account / Beneficiary, Budget, Category)
/** @var \Importentry $oldComponentMap */
$oldComponentMap = $this->_repository->findImportComponentMap($importMap, $oldComponentId);
if (is_null($oldComponentMap)) {
\Log::debug('importComponentTransaction Could not run this one, waiting for five minutes...');
$job->release(300);
return;
}
$journal = $this->_journals->find($oldTransactionMap->new);
\Log::debug('Going to update ' . $journal->description);
// find the cash account:
switch ($oldComponentMap->class) {
case 'Budget':
// budget thing link:
$budget = $this->_budgets->find($oldComponentMap->new);
\Log::debug('Updating transactions Budget.');
$journal->budgets()->save($budget);
$journal->save();
\Log::debug('Updated transactions Budget.');
break;
case 'Category':
$category = $this->_categories->find($oldComponentMap->new);
$journal = $this->_journals->find($oldTransactionMap->new);
\Log::info('Updating transactions Category.');
$journal->categories()->save($category);
$journal->save();
\Log::info('Updated transactions Category.');
break;
case 'Account':
\Log::info('Updating transactions Account.');
$account = $this->_accounts->find($oldComponentMap->new);
$journal = $this->_journals->find($oldTransactionMap->new);
if (is_null($account)) {
\Log::debug('Cash account is needed.');
$account = $this->_accounts->getCashAccount();
\Log::info($account);
}
foreach ($journal->transactions as $transaction) {
if ($transaction->account()->first()->account_type_id == 5) {
$transaction->account()->associate($account);
$transaction->save();
\Log::debug('Updated transactions (#' . $journal->id . '), #' . $transaction->id . '\'s Account.');
}
}
break;
}
$job->delete();
}
/**
* @param Job $job
* @param array $payload
*/
public function importLimit(Job $job, array $payload)
{
if ($job->attempts() > 30) {
\Log::error('importLimit Job running for ' . $job->attempts() . 'th time, so KILL!');
$job->delete();
return;
}
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
// find the budget this limit is part of:
$importEntry = $this->_repository->findImportEntry($importMap, 'Budget',
intval($payload['data']['component_id']));
// budget is not yet imported:
if (is_null($importEntry)) {
\Log::debug('importLimit Cannot import limit #' . $payload['data']['id'] .
' because the budget is not here yet. #' . $job->attempts());
$job->release(300);
return;
}
// find similar limit:
\Log::debug('Trying to find budget with ID #' . $importEntry->new . ', based on entry #' . $importEntry->id);
$budget = $this->_budgets->find($importEntry->new);
if (!is_null($budget)) {
$current = $this->_limits->findByBudgetAndDate($budget, new Carbon($payload['data']['date']));
if (is_null($current)) {
// create it!
$payload['data']['budget_id'] = $budget->id;
$payload['data']['startdate'] = $payload['data']['date'];
$payload['data']['period'] = 'monthly';
$lim = $this->_limits->store((array)$payload['data']);
$this->_repository->store($importMap, 'Limit', $payload['data']['id'], $lim->id);
\Event::fire('limits.store', [$lim]);
\Log::debug('Imported ' . $payload['class'] . ', for ' . $budget->name . ' (' . $lim->startdate . ').');
} else {
// already has!
$this->_repository->store($importMap, 'Budget', $payload['data']['id'], $current->id);
\Log::debug('Already had ' . $payload['class'] . ', for ' . $budget->name . ' (' . $current->startdate . ').');
}
} else {
// cannot import component limit, no longer supported.
\Log::error('Cannot import limit for other than budget!');
}
$job->delete();
}
/**
* @param Job $job
* @param array $payload
*/
public function importPiggybank(Job $job, array $payload)
{
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
// try to find related piggybank:
$current = $this->_piggybanks->findByName($payload['data']['name']);
// we need an account to go with this piggy bank:
$set = $this->_accounts->getActiveDefault();
if (count($set) > 0) {
$account = $set[0];
$payload['data']['account_id'] = $account->id;
} else {
\Log::debug('Released job for work in five minutes...');
$job->release(300);
return;
}
if (is_null($current)) {
$payload['data']['targetamount'] = floatval($payload['data']['target']);
$payload['data']['repeats'] = 0;
$payload['data']['rep_every'] = 1;
$payload['data']['reminder_skip'] = 1;
$payload['data']['rep_times'] = 1;
$piggy = $this->_piggybanks->store((array)$payload['data']);
$this->_repository->store($importMap, 'Piggybank', $payload['data']['id'], $piggy->id);
\Log::debug('Imported ' . $payload['class'] . ' "' . $payload['data']['name'] . '".');
\Event::fire('piggybanks.store', [$piggy]);
} else {
$this->_repository->store($importMap, 'Piggybank', $payload['data']['id'], $current->id);
\Log::debug('Already had ' . $payload['class'] . ' "' . $payload['data']['name'] . '".');
}
$job->delete();
}
/**
* @param Job $job
* @param array $payload
*/
public function importPredictable(Job $job, array $payload)
{
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
// try to find related recurring transaction:
$current = $this->_recurring->findByName($payload['data']['description']);
if (is_null($current)) {
$payload['data']['name'] = $payload['data']['description'];
$payload['data']['match'] = join(',', explode(' ', $payload['data']['description']));
$pct = intval($payload['data']['pct']);
$payload['data']['amount_min'] = floatval($payload['data']['amount']) * ($pct / 100) * -1;
$payload['data']['amount_max'] = floatval($payload['data']['amount']) * (1 + ($pct / 100)) * -1;
$payload['data']['date'] = date('Y-m-') . $payload['data']['dom'];
$payload['data']['repeat_freq'] = 'monthly';
$payload['data']['active'] = intval($payload['data']['inactive']) == 1 ? 0 : 1;
$payload['data']['automatch'] = 1;
$recur = $this->_recurring->store((array)$payload['data']);
$this->_repository->store($importMap, 'RecurringTransaction', $payload['data']['id'], $recur->id);
\Log::debug('Imported ' . $payload['class'] . ' "' . $payload['data']['name'] . '".');
} else {
$this->_repository->store($importMap, 'RecurringTransaction', $payload['data']['id'], $current->id);
\Log::debug('Already had ' . $payload['class'] . ' "' . $payload['data']['description'] . '".');
}
$job->delete();
}
/**
* @param Job $job
* @param array $payload
*/
public function importSetting(Job $job, array $payload)
{
switch ($payload['data']['name']) {
default:
$job->delete();
return;
break;
case 'piggyAccount':
// if we have this account, update all piggy banks:
$accountID = intval($payload['data']['value']);
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
$importEntry = $this->_repository->findImportEntry($importMap, 'Account', $accountID);
if ($importEntry) {
$all = $this->_piggybanks->get();
$account = $this->_accounts->find($importEntry->new);
\Log::debug('Updating all piggybanks, found the right setting.');
foreach ($all as $piggy) {
$piggy->account()->associate($account);
unset($piggy->leftInAccount); //??
$piggy->save();
}
} else {
\Log::debug('importSetting wait five minutes and try again...');
$job->release(300);
}
break;
}
$job->delete();
}
/**
* @param Job $job
* @param array $payload
*/
public function importTransaction(Job $job, array $payload)
{
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
// find or create the account type for the import account.
// find or create the account for the import account.
$accountType = $this->_accounts->findAccountType('Import account');
$importAccount = $this->_accounts->createOrFind('Import account', $accountType);
// if amount is more than zero, move from $importAccount
$amount = floatval($payload['data']['amount']);
$accountEntry = $this->_repository->findImportEntry($importMap, 'Account',
intval($payload['data']['account_id']));
$personalAccount = $this->_accounts->find($accountEntry->new);
if ($amount < 0) {
// if amount is less than zero, move to $importAccount
$accountFrom = $personalAccount;
$accountTo = $importAccount;
} else {
$accountFrom = $importAccount;
$accountTo = $personalAccount;
}
$amount = $amount < 0 ? $amount * -1 : $amount;
$date = new Carbon($payload['data']['date']);
// find a journal?
$current = $this->_repository->findImportEntry($importMap, 'Transaction', intval($payload['data']['id']));
if (is_null($current)) {
$journal = $this->_journals->createSimpleJournal($accountFrom, $accountTo,
$payload['data']['description'], $amount, $date);
$this->_repository->store($importMap, 'Transaction', $payload['data']['id'], $journal->id);
\Log::debug('Imported transaction "' . $payload['data']['description'] . '" (' . $journal->date->format('Y-m-d') . ').');
} else {
// do nothing.
\Log::debug('ALREADY imported transaction "' . $payload['data']['description'] . '".');
}
$job->delete();
}
/**
* @param Job $job
* @param array $payload
*/
public function importTransfer(Job $job, array $payload)
{
/** @var \Importmap $importMap */
$importMap = $this->_repository->findImportmap($payload['mapID']);
$user = $importMap->user;
$this->overruleUser($user);
// from account:
$oldFromAccountID = intval($payload['data']['accountfrom_id']);
$oldFromAccountEntry = $this->_repository->findImportEntry($importMap, 'Account', $oldFromAccountID);
$accountFrom = $this->_accounts->find($oldFromAccountEntry->new);
// to account:
$oldToAccountID = intval($payload['data']['accountto_id']);
$oldToAccountEntry = $this->_repository->findImportEntry($importMap, 'Account', $oldToAccountID);
$accountTo = $this->_accounts->find($oldToAccountEntry->new);
if (!is_null($accountFrom) && !is_null($accountTo)) {
$amount = floatval($payload['data']['amount']);
$date = new Carbon($payload['data']['date']);
$journal = $this->_journals->createSimpleJournal($accountFrom, $accountTo, $payload['data']['description'],
$amount, $date);
\Log::debug('Imported transfer "' . $payload['data']['description'] . '".');
$job->delete();
} else {
$job->release(5);
}
}
/**
* @param Job $job
* @param $payload
*/
public function start(Job $job, array $payload)
{
\Log::debug('Start with job "start"');
$user = \User::find($payload['user']);
$filename = $payload['file'];
if (file_exists($filename)) {
// we are able to process the file!
// make an import map. Which is some kind of object because we use queues.
$importMap = new \Importmap;
$importMap->user()->associate($user);
$importMap->file = $filename;
$importMap->save();
// we can now launch a billion jobs importing every little thing into Firefly III
$raw = file_get_contents($filename);
$JSON = json_decode($raw);
$classes = ['accounts', 'components', 'limits', 'piggybanks',
'predictables', 'settings', 'transactions', 'transfers'];
foreach ($classes as $classes_plural) {
$class = ucfirst(\Str::singular($classes_plural));
\Log::debug('Create job to import all ' . $classes_plural);
foreach ($JSON->$classes_plural as $entry) {
\Log::debug('Create job to import single ' . $class);
$fn = 'import' . $class;
$jobFunction = 'Firefly\Queue\Import@' . $fn;
\Queue::push($jobFunction, ['data' => $entry, 'class' => $class, 'mapID' => $importMap->id]);
}
}
// , components, limits, piggybanks, predictables, settings, transactions, transfers
// component_predictables, component_transactions, component_transfers
$count = count($JSON->component_transaction);
foreach ($JSON->component_transaction as $index => $entry) {
\Log::debug('Create job to import components_transaction! Yay! (' . $index . '/' . $count . ') ');
$fn = 'importComponentTransaction';
$jobFunction = 'Firefly\Queue\Import@' . $fn;
\Queue::push($jobFunction, ['data' => $entry, 'mapID' => $importMap->id]);
}
// queue a job to clean up the "import account", it should properly fall back
// to the cash account (which it doesn't always do for some reason).
\Queue::push('Firefly\Queue\Import@cleanImportAccount', ['mapID' => $importMap->id]);
}
\Log::debug('Done with job "start"');
// this is it, close the job:
$job->delete();
}
}