_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 Firefly tries to import a beneficiary, Firefly will "merge" already existing ones, // 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 (old id is #' . $oldComponentMap->old . ').'); if (!is_null($category)) { $journal->categories()->save($category); $journal->save(); \Log::info('Updated transactions category.'); } else { \Log::error('No category mapping to old id #' . $oldComponentMap->old . ' found. Release for 5m!'); $job->release(300); return; } 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(); } }