Clean up repositories and cron code.

This commit is contained in:
James Cole 2019-06-07 17:57:46 +02:00
parent a845cb9af9
commit e4a9abc315
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
19 changed files with 547 additions and 1188 deletions

View File

@ -11,7 +11,7 @@ mkdir -p /var/log
mkdir -p /var/log/mysql
mkdir -p /var/log/nginx
# Wipe /var/run, since pidfiles and socket files from previous launches should go away
# TODO someday: I'd prefer a tmpfs for these.
# Someday: I'd prefer a tmpfs for these.
rm -rf /var/run
mkdir -p /var/run
rm -rf /var/tmp

View File

@ -1,418 +0,0 @@
<?php
/**
* ApplyRules.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands;
use Carbon\Carbon;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleGroup;
use FireflyIII\Models\Transaction;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\TransactionRules\Processor;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
/**
*
* Class ApplyRules
*/
class ApplyRules extends Command
{
use VerifiesAccessToken;
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command will apply your rules and rule groups on a selection of your transactions.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature
= 'firefly:apply-rules
{--user=1 : The user ID that the import should import for.}
{--token= : The user\'s access token.}
{--accounts= : A comma-separated list of asset accounts or liabilities to apply your rules to.}
{--rule_groups= : A comma-separated list of rule groups to apply. Take the ID\'s of these rule groups from the Firefly III interface.}
{--rules= : A comma-separated list of rules to apply. Take the ID\'s of these rules from the Firefly III interface. Using this option overrules the option that selects rule groups.}
{--all_rules : If set, will overrule both settings and simply apply ALL of your rules.}
{--start_date= : The date of the earliest transaction to be included (inclusive). If omitted, will be your very first transaction ever. Format: YYYY-MM-DD}
{--end_date= : The date of the latest transaction to be included (inclusive). If omitted, will be your latest transaction ever. Format: YYYY-MM-DD}';
/** @var Collection */
private $accounts;
/** @var Carbon */
private $endDate;
/** @var Collection */
private $results;
/** @var Collection */
private $ruleGroups;
/** @var Collection */
private $rules;
/** @var Carbon */
private $startDate;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->accounts = new Collection;
$this->rules = new Collection;
$this->ruleGroups = new Collection;
$this->results = new Collection;
}
/**
* Execute the console command.
*
* @return int
* @throws \FireflyIII\Exceptions\FireflyException
*/
public function handle(): int
{
if (!$this->verifyAccessToken()) {
$this->error('Invalid access token.');
return 1;
}
$result = $this->verifyInput();
if (false === $result) {
return 1;
}
return 1;
// get transactions from asset accounts.
/** @var TODO REPLACE $collector */
//$collector = app();
$collector->setUser($this->getUser());
$collector->setAccounts($this->accounts);
$collector->setRange($this->startDate, $this->endDate);
$transactions = $collector->getTransactions();
$count = $transactions->count();
// first run all rule groups:
/** @var RuleGroupRepositoryInterface $ruleGroupRepos */
$ruleGroupRepos = app(RuleGroupRepositoryInterface::class);
$ruleGroupRepos->setUser($this->getUser());
/** @var RuleGroup $ruleGroup */
foreach ($this->ruleGroups as $ruleGroup) {
$this->line(sprintf('Going to apply rule group "%s" to %d transaction(s).', $ruleGroup->title, $count));
$rules = $ruleGroupRepos->getActiveStoreRules($ruleGroup);
$this->applyRuleSelection($rules, $transactions, true);
}
// then run all rules (rule groups should be empty).
if ($this->rules->count() > 0) {
$this->line(sprintf('Will apply %d rule(s) to %d transaction(s)', $this->rules->count(), $transactions->count()));
$this->applyRuleSelection($this->rules, $transactions, false);
}
// filter results:
$this->results = $this->results->unique(
function (Transaction $transaction) {
return (int)$transaction->journal_id;
}
);
$this->line('');
if (0 === $this->results->count()) {
$this->line('The rules were fired but did not influence any transactions.');
}
if ($this->results->count() > 0) {
$this->line(sprintf('The rule(s) was/were fired, and influenced %d transaction(s).', $this->results->count()));
foreach ($this->results as $result) {
$this->line(
vsprintf(
'Transaction #%d: "%s" (%s %s)',
[
$result->journal_id,
$result->description,
$result->transaction_currency_code,
round($result->transaction_amount, $result->transaction_currency_dp),
]
)
);
}
}
return 0;
}
/**
* @return bool
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function verifyInput(): bool
{
// verify account.
$result = $this->verifyInputAccounts();
if (false === $result) {
return $result;
}
// verify rule groups.
$result = $this->verifyRuleGroups();
if (false === $result) {
return $result;
}
// verify rules.
$result = $this->verifyRules();
if (false === $result) {
return $result;
}
$this->grabAllRules();
$this->parseDates();
//$this->line('Number of rules found: ' . $this->rules->count());
$this->line('Start date is ' . $this->startDate->format('Y-m-d'));
$this->line('End date is ' . $this->endDate->format('Y-m-d'));
return true;
}
/**
* @return bool
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function verifyInputAccounts(): bool
{
$accountString = $this->option('accounts');
if (null === $accountString || '' === $accountString) {
$this->error('Please use the --accounts to indicate the accounts to apply rules to.');
return false;
}
$finalList = new Collection;
$accountList = explode(',', $accountString);
if (0 === count($accountList)) {
$this->error('Please use the --accounts to indicate the accounts to apply rules to.');
return false;
}
/** @var AccountRepositoryInterface $accountRepository */
$accountRepository = app(AccountRepositoryInterface::class);
$accountRepository->setUser($this->getUser());
foreach ($accountList as $accountId) {
$accountId = (int)$accountId;
$account = $accountRepository->findNull($accountId);
if (null !== $account
&& \in_array(
$account->accountType->type, [AccountType::DEFAULT, AccountType::DEBT, AccountType::ASSET, AccountType::LOAN, AccountType::MORTGAGE], true
)) {
$finalList->push($account);
}
}
if (0 === $finalList->count()) {
$this->error('Please make sure all accounts in --accounts are asset accounts or liabilities.');
return false;
}
$this->accounts = $finalList;
return true;
}
/**
* @return bool
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function verifyRuleGroups(): bool
{
$ruleGroupString = $this->option('rule_groups');
if (null === $ruleGroupString || '' === $ruleGroupString) {
// can be empty.
return true;
}
$ruleGroupList = explode(',', $ruleGroupString);
if (0 === count($ruleGroupList)) {
// can be empty.
return true;
}
/** @var RuleGroupRepositoryInterface $ruleGroupRepos */
$ruleGroupRepos = app(RuleGroupRepositoryInterface::class);
$ruleGroupRepos->setUser($this->getUser());
foreach ($ruleGroupList as $ruleGroupId) {
$ruleGroupId = (int)$ruleGroupId;
$ruleGroup = $ruleGroupRepos->find($ruleGroupId);
$this->ruleGroups->push($ruleGroup);
}
return true;
}
/**
* @return bool
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function verifyRules(): bool
{
$ruleString = $this->option('rules');
if (null === $ruleString || '' === $ruleString) {
// can be empty.
return true;
}
$finalList = new Collection;
$ruleList = explode(',', $ruleString);
if (0 === count($ruleList)) {
// can be empty.
return true;
}
/** @var RuleRepositoryInterface $ruleRepos */
$ruleRepos = app(RuleRepositoryInterface::class);
$ruleRepos->setUser($this->getUser());
foreach ($ruleList as $ruleId) {
$ruleId = (int)$ruleId;
$rule = $ruleRepos->find($ruleId);
if (null !== $rule) {
$finalList->push($rule);
}
}
if ($finalList->count() > 0) {
// reset rule groups.
$this->ruleGroups = new Collection;
$this->rules = $finalList;
}
return true;
}
/**
*
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function grabAllRules(): void
{
if (true === $this->option('all_rules')) {
/** @var RuleRepositoryInterface $ruleRepos */
$ruleRepos = app(RuleRepositoryInterface::class);
$ruleRepos->setUser($this->getUser());
$this->rules = $ruleRepos->getAll();
// reset rule groups.
$this->ruleGroups = new Collection;
}
}
/**
*
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function parseDates(): void
{
// parse start date.
$startDate = Carbon::now()->startOfMonth();
$startString = $this->option('start_date');
if (null === $startString) {
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$repository->setUser($this->getUser());
$first = $repository->firstNull();
if (null !== $first) {
$startDate = $first->date;
}
}
if (null !== $startString && '' !== $startString) {
$startDate = Carbon::createFromFormat('Y-m-d', $startString);
}
// parse end date
$endDate = Carbon::now();
$endString = $this->option('end_date');
if (null !== $endString && '' !== $endString) {
$endDate = Carbon::createFromFormat('Y-m-d', $endString);
}
if ($startDate > $endDate) {
[$endDate, $startDate] = [$startDate, $endDate];
}
$this->startDate = $startDate;
$this->endDate = $endDate;
}
/**
* @param Collection $rules
* @param Collection $transactions
* @param bool $breakProcessing
*
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function applyRuleSelection(Collection $rules, Collection $transactions, bool $breakProcessing): void
{
$bar = $this->output->createProgressBar($rules->count() * $transactions->count());
/** @var Rule $rule */
foreach ($rules as $rule) {
/** @var Processor $processor */
$processor = app(Processor::class);
$processor->make($rule, true);
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
/** @noinspection DisconnectedForeachInstructionInspection */
$bar->advance();
$result = $processor->handleTransaction($transaction);
if (true === $result) {
$this->results->push($transaction);
}
}
if (true === $rule->stop_processing && true === $breakProcessing) {
$this->line('');
$this->line(sprintf('Rule #%d ("%s") says to stop processing.', $rule->id, $rule->title));
return;
}
}
$this->line('');
}
}

View File

@ -21,6 +21,7 @@
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\AccountFactory;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction;
@ -54,8 +55,10 @@ class FixAccountTypes extends Command
/**
* @param TransactionJournal $journal
*
* @throws \FireflyIII\Exceptions\FireflyException
* @param string $type
* @param Transaction $source
* @param Transaction $dest
* @throws FireflyException
*/
public function fixJournal(TransactionJournal $journal, string $type, Transaction $source, Transaction $dest): void
{
@ -131,7 +134,7 @@ class FixAccountTypes extends Command
* Execute the console command.
*
* @return int
* @throws \FireflyIII\Exceptions\FireflyException
* @throws FireflyException
*/
public function handle(): int
{
@ -192,7 +195,7 @@ class FixAccountTypes extends Command
/**
* @param TransactionJournal $journal
*
* @throws \FireflyIII\Exceptions\FireflyException
* @throws FireflyException
*/
private function inspectJournal(TransactionJournal $journal): void
{
@ -220,7 +223,7 @@ class FixAccountTypes extends Command
return;
}
$expectedTypes = $this->expected[$type][$sourceAccountType];
if (!\in_array($destAccountType, $expectedTypes, true)) {
if (!in_array($destAccountType, $expectedTypes, true)) {
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
}
}

View File

@ -1,150 +0,0 @@
<?php
/**
* CreateExport.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
/** @noinspection MultipleReturnStatementsInspection */
declare(strict_types=1);
namespace FireflyIII\Console\Commands;
use Carbon\Carbon;
use FireflyIII\Export\ProcessorInterface;
use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\ExportJob\ExportJobRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
/**
* Class CreateExport.
*
* Generates export from the command line.
*
* @codeCoverageIgnore
*/
class CreateExport extends Command
{
use VerifiesAccessToken;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Use this command to create a new import. Your user ID can be found on the /profile page.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature
= 'firefly:create-export
{--user= : The user ID that the import should import for.}
{--token= : The user\'s access token.}
{--with_attachments : Include user\'s attachments?}
{--with_uploads : Include user\'s uploads?}';
/**
* Execute the console command.
*
* @return int
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function handle(): int
{
if (!$this->verifyAccessToken()) {
$this->error('Invalid access token.');
return 1;
}
$this->line('Full export is running...');
// make repositories
/** @var UserRepositoryInterface $userRepository */
$userRepository = app(UserRepositoryInterface::class);
/** @var ExportJobRepositoryInterface $jobRepository */
$jobRepository = app(ExportJobRepositoryInterface::class);
/** @var AccountRepositoryInterface $accountRepository */
$accountRepository = app(AccountRepositoryInterface::class);
/** @var JournalRepositoryInterface $journalRepository */
$journalRepository = app(JournalRepositoryInterface::class);
// set user
$user = $userRepository->findNull((int)$this->option('user'));
if (null === $user) {
return 1;
}
$jobRepository->setUser($user);
$journalRepository->setUser($user);
$accountRepository->setUser($user);
// first date
$firstJournal = $journalRepository->firstNull();
$first = new Carbon;
if (null !== $firstJournal) {
$first = $firstJournal->date;
}
// create job and settings.
$job = $jobRepository->create();
$settings = [
'accounts' => $accountRepository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]),
'startDate' => $first,
'endDate' => new Carbon,
'exportFormat' => 'csv',
'includeAttachments' => $this->option('with_attachments'),
'includeOldUploads' => $this->option('with_uploads'),
'job' => $job,
];
/** @var ProcessorInterface $processor */
$processor = app(ProcessorInterface::class);
$processor->setSettings($settings);
$processor->collectJournals();
$processor->convertJournals();
$processor->exportJournals();
if ($settings['includeAttachments']) {
$processor->collectAttachments();
}
if ($settings['includeOldUploads']) {
$processor->collectOldUploads();
}
$processor->createZipFile();
$disk = Storage::disk('export');
$fileName = sprintf('export-%s.zip', date('Y-m-d_H-i-s'));
$localPath = storage_path('export') . '/' . $job->key . '.zip';
// "move" from local to export disk
$disk->put($fileName, file_get_contents($localPath));
unlink($localPath);
$this->line('The export has finished! You can find the ZIP file in export disk with file name:');
$this->line($fileName);
return 0;
}
}

View File

@ -1,111 +0,0 @@
<?php
/**
* DecryptAttachment.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
/** @noinspection MultipleReturnStatementsInspection */
declare(strict_types=1);
namespace FireflyIII\Console\Commands;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use Illuminate\Console\Command;
use Log;
/**
* Class DecryptAttachment.
*
* @codeCoverageIgnore
*/
class DecryptAttachment extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Decrypts an attachment and dumps the content in a file in the given directory.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature
= 'firefly:decrypt-attachment {id:The ID of the attachment.} {name:The file name of the attachment.}
{directory:Where the file must be stored.}';
/**
* Execute the console command.
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*
* @return int
*/
public function handle(): int
{
/** @var AttachmentRepositoryInterface $repository */
$repository = app(AttachmentRepositoryInterface::class);
$attachmentId = (int)$this->argument('id');
$attachment = $repository->findWithoutUser($attachmentId);
$attachmentName = trim((string)$this->argument('name'));
$storagePath = realpath(trim((string)$this->argument('directory')));
if (null === $attachment) {
$this->error(sprintf('No attachment with id #%d', $attachmentId));
Log::error(sprintf('DecryptAttachment: No attachment with id #%d', $attachmentId));
return 1;
}
if ($attachmentName !== $attachment->filename) {
$this->error('File name does not match.');
Log::error('DecryptAttachment: File name does not match.');
return 1;
}
if (!is_dir($storagePath)) {
$this->error(sprintf('Path "%s" is not a directory.', $storagePath));
Log::error(sprintf('DecryptAttachment: Path "%s" is not a directory.', $storagePath));
return 1;
}
if (!is_writable($storagePath)) {
$this->error(sprintf('Path "%s" is not writable.', $storagePath));
Log::error(sprintf('DecryptAttachment: Path "%s" is not writable.', $storagePath));
return 1;
}
$fullPath = $storagePath . DIRECTORY_SEPARATOR . $attachment->filename;
$content = $repository->getContent($attachment);
$this->line(sprintf('Going to write content for attachment #%d into file "%s"', $attachment->id, $fullPath));
$result = file_put_contents($fullPath, $content);
if (false === $result) {
$this->error('Could not write to file.');
return 1;
}
$this->info(sprintf('%d bytes written. Exiting now..', $result));
return 0;
}
}

View File

@ -1,73 +0,0 @@
<?php
/**
* EncryptFile.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Services\Internal\File\EncryptService;
use Illuminate\Console\Command;
/**
* Class EncryptFile.
*
* @codeCoverageIgnore
*/
class EncryptFile extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Encrypts a file and places it in the upload disk.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly:encrypt-file {file} {key}';
/**
* Execute the console command.
*
* @throws \Illuminate\Contracts\Encryption\EncryptException
*/
public function handle(): int
{
$code = 0;
$file = (string)$this->argument('file');
$key = (string)$this->argument('key');
/** @var EncryptService $service */
$service = app(EncryptService::class);
try {
$service->encrypt($file, $key);
} catch (FireflyException $e) {
$this->error($e->getMessage());
$code = 1;
}
return $code;
}
}

View File

@ -1,165 +0,0 @@
<?php
/**
* Import.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
/** @noinspection MultipleReturnStatementsInspection */
/** @noinspection PhpDynamicAsStaticMethodCallInspection */
declare(strict_types=1);
namespace FireflyIII\Console\Commands;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Import\Routine\RoutineInterface;
use FireflyIII\Models\ImportJob;
use FireflyIII\Models\Tag;
use Illuminate\Console\Command;
use Log;
/**
* Class Import.
*
* @codeCoverageIgnore
*/
class Import extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'This will start a new import.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly:start-import {key}';
/**
* Run the import routine.
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*
* @throws FireflyException
*/
public function handle(): int
{
Log::debug('Start start-import command');
$jobKey = (string)$this->argument('key');
/** @var ImportJob $job */
$job = ImportJob::where('key', $jobKey)->first();
if (null === $job) {
$this->errorLine(sprintf('No job found with key "%s"', $jobKey));
return 1;
}
if (!$this->isValid($job)) {
$this->errorLine('Job is not valid for some reason. Exit.');
return 1;
}
$this->infoLine(sprintf('Going to import job with key "%s" of type "%s"', $job->key, $job->file_type));
// actually start job:
$type = 'csv' === $job->file_type ? 'file' : $job->file_type;
$key = sprintf('import.routine.%s', $type);
$className = config($key);
if (null === $className || !class_exists($className)) {
throw new FireflyException(sprintf('Cannot find import routine class for job of type "%s".', $type)); // @codeCoverageIgnore
}
/** @var RoutineInterface $routine */
$routine = app($className);
$routine->setImportJob($job);
$routine->run();
/**
* @var int $index
* @var string $error
*/
foreach ($job->errors as $index => $error) {
$this->errorLine(sprintf('Error importing line #%d: %s', $index, $error));
}
/** @var Tag $tag */
$tag = $job->tag()->first();
$count = 0;
if (null === $tag) {
$count = $tag->transactionJournals()->count();
}
$this->infoLine(sprintf('The import has finished. %d transactions have been imported.', $count));
return 0;
}
/**
* Displays an error.
*
* @param string $message
* @param array|null $data
*/
private function errorLine(string $message, array $data = null): void
{
Log::error($message, $data ?? []);
$this->error($message);
}
/**
* Displays an informational message.
*
* @param string $message
* @param array $data
*/
private function infoLine(string $message, array $data = null): void
{
Log::info($message, $data ?? []);
$this->line($message);
}
/**
* Check if job is valid to be imported.
*
* @param ImportJob $job
*
* @return bool
*/
private function isValid(ImportJob $job): bool
{
if (null === $job) {
$this->errorLine('This job does not seem to exist.');
return false;
}
if ('configured' !== $job->status) {
Log::error(sprintf('This job is not ready to be imported (status is %s).', $job->status));
$this->errorLine('This job is not ready to be imported.');
return false;
}
return true;
}
}

View File

@ -1,7 +1,7 @@
<?php
/**
* CreateImport.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
* CreateCSVImport.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
@ -23,9 +23,10 @@
declare(strict_types=1);
namespace FireflyIII\Console\Commands;
namespace FireflyIII\Console\Commands\Import;
use Exception;
use FireflyIII\Console\Commands\VerifiesAccessToken;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Import\Prerequisites\PrerequisitesInterface;
use FireflyIII\Import\Routine\RoutineInterface;
@ -36,11 +37,11 @@ use Illuminate\Console\Command;
use Log;
/**
* Class CreateImport.
* Class CreateCSVImport.
*
* @codeCoverageIgnore
*/
class CreateImport extends Command
class CreateCSVImport extends Command
{
use VerifiesAccessToken;
/**
@ -48,7 +49,7 @@ class CreateImport extends Command
*
* @var string
*/
protected $description = 'Use this command to create a new import. Your user ID can be found on the /profile page.';
protected $description = 'Use this command to create a new CSV file import.';
/**
* The name and signature of the console command.
@ -56,14 +57,11 @@ class CreateImport extends Command
* @var string
*/
protected $signature
= 'firefly:create-import
{file? : The file to import.}
= 'firefly-iii:csv-import
{file? : The CSV file to import.}
{configuration? : The configuration file to use for the import.}
{--type=csv : The file type of the import.}
{--provider=file : The file type of the import.}
{--user=1 : The user ID that the import should import for.}
{--token= : The user\'s access token.}
{--start : Starts the job immediately.}';
{--token= : The user\'s access token.}';
/**
* Run the command.
@ -83,7 +81,6 @@ class CreateImport extends Command
$configuration = (string)$this->argument('configuration');
$user = $userRepository->findNull((int)$this->option('user'));
$cwd = getcwd();
$provider = strtolower((string)$this->option('provider'));
$configurationData = [];
if (null === $user) {
@ -110,26 +107,25 @@ class CreateImport extends Command
$this->infoLine(sprintf('Going to create a job to import file: %s', $file));
$this->infoLine(sprintf('Using configuration file: %s', $configuration));
$this->infoLine(sprintf('Import into user: #%d (%s)', $user->id, $user->email));
$this->infoLine(sprintf('Type of import: %s', $provider));
/** @var ImportJobRepositoryInterface $jobRepository */
$jobRepository = app(ImportJobRepositoryInterface::class);
$jobRepository->setUser($user);
$importJob = $jobRepository->create($provider);
$importJob = $jobRepository->create('file');
$this->infoLine(sprintf('Created job "%s"', $importJob->key));
// make sure that job has no prerequisites.
if ((bool)config(sprintf('import.has_prereq.%s', $provider))) {
if ((bool)config('import.has_prereq.csv')) {
// make prerequisites thing.
$class = (string)config(sprintf('import.prerequisites.%s', $provider));
$class = (string)config('import.prerequisites.csv');
if (!class_exists($class)) {
throw new FireflyException(sprintf('No class to handle prerequisites for "%s".', $provider)); // @codeCoverageIgnore
throw new FireflyException('No class to handle prerequisites for CSV.'); // @codeCoverageIgnore
}
/** @var PrerequisitesInterface $object */
$object = app($class);
$object->setUser($user);
if (!$object->isComplete()) {
$this->errorLine(sprintf('Import provider "%s" has prerequisites that can only be filled in using the browser.', $provider));
$this->errorLine('CSV Import provider has prerequisites that can only be filled in using the browser.');
return 1;
}
@ -151,85 +147,82 @@ class CreateImport extends Command
$jobRepository->setStatus($importJob, 'ready_to_run');
if (true === $this->option('start')) {
$this->infoLine('The import routine has started. The process is not visible. Please wait.');
Log::debug('Go for import!');
$this->infoLine('The import routine has started. The process is not visible. Please wait.');
Log::debug('Go for import!');
// run it!
$key = sprintf('import.routine.%s', $provider);
$className = config($key);
if (null === $className || !class_exists($className)) {
// @codeCoverageIgnoreStart
$this->errorLine(sprintf('No routine for provider "%s"', $provider));
// run it!
$className = config('import.routine.file');
if (null === $className || !class_exists($className)) {
// @codeCoverageIgnoreStart
$this->errorLine('No routine for file provider.');
return 1;
// @codeCoverageIgnoreEnd
}
// keep repeating this call until job lands on "provider_finished"
$valid = ['provider_finished'];
$count = 0;
while (!in_array($importJob->status, $valid, true) && $count < 6) {
Log::debug(sprintf('Now in loop #%d.', $count + 1));
/** @var RoutineInterface $routine */
$routine = app($className);
$routine->setImportJob($importJob);
try {
$routine->run();
} catch (FireflyException|Exception $e) {
$message = 'The import routine crashed: ' . $e->getMessage();
Log::error($message);
Log::error($e->getTraceAsString());
// set job errored out:
$jobRepository->setStatus($importJob, 'error');
$this->errorLine($message);
return 1;
// @codeCoverageIgnoreEnd
}
$count++;
}
if ('provider_finished' === $importJob->status) {
$this->infoLine('Import has finished. Please wait for storage of data.');
// set job to be storing data:
$jobRepository->setStatus($importJob, 'storing_data');
// keep repeating this call until job lands on "provider_finished"
$valid = ['provider_finished'];
$count = 0;
while (!\in_array($importJob->status, $valid, true) && $count < 6) {
Log::debug(sprintf('Now in loop #%d.', $count + 1));
/** @var RoutineInterface $routine */
$routine = app($className);
$routine->setImportJob($importJob);
try {
$routine->run();
} catch (FireflyException|Exception $e) {
$message = 'The import routine crashed: ' . $e->getMessage();
Log::error($message);
Log::error($e->getTraceAsString());
/** @var ImportArrayStorage $storage */
$storage = app(ImportArrayStorage::class);
$storage->setImportJob($importJob);
// set job errored out:
$jobRepository->setStatus($importJob, 'error');
$this->errorLine($message);
try {
$storage->store();
} catch (FireflyException|Exception $e) {
$message = 'The import routine crashed: ' . $e->getMessage();
Log::error($message);
Log::error($e->getTraceAsString());
return 1;
}
$count++;
// set job errored out:
$jobRepository->setStatus($importJob, 'error');
$this->errorLine($message);
return 1;
}
if ('provider_finished' === $importJob->status) {
$this->infoLine('Import has finished. Please wait for storage of data.');
// set job to be storing data:
$jobRepository->setStatus($importJob, 'storing_data');
// set storage to be finished:
$jobRepository->setStatus($importJob, 'storage_finished');
}
/** @var ImportArrayStorage $storage */
$storage = app(ImportArrayStorage::class);
$storage->setImportJob($importJob);
// give feedback:
$this->infoLine('Job has finished.');
if (null !== $importJob->tag) {
$this->infoLine(sprintf('%d transaction(s) have been imported.', $importJob->tag->transactionJournals->count()));
$this->infoLine(sprintf('You can find your transactions under tag "%s"', $importJob->tag->tag));
}
try {
$storage->store();
} catch (FireflyException|Exception $e) {
$message = 'The import routine crashed: ' . $e->getMessage();
Log::error($message);
Log::error($e->getTraceAsString());
// set job errored out:
$jobRepository->setStatus($importJob, 'error');
$this->errorLine($message);
return 1;
}
// set storage to be finished:
$jobRepository->setStatus($importJob, 'storage_finished');
}
// give feedback:
$this->infoLine('Job has finished.');
if (null !== $importJob->tag) {
$this->infoLine(sprintf('%d transaction(s) have been imported.', $importJob->tag->transactionJournals->count()));
$this->infoLine(sprintf('You can find your transactions under tag "%s"', $importJob->tag->tag));
}
if (null === $importJob->tag) {
$this->errorLine('No transactions have been imported :(.');
}
if (count($importJob->errors) > 0) {
$this->infoLine(sprintf('%d error(s) occurred:', count($importJob->errors)));
foreach ($importJob->errors as $err) {
$this->errorLine('- ' . $err);
}
if (null === $importJob->tag) {
$this->errorLine('No transactions have been imported :(.');
}
if (count($importJob->errors) > 0) {
$this->infoLine(sprintf('%d error(s) occurred:', count($importJob->errors)));
foreach ($importJob->errors as $err) {
$this->errorLine('- ' . $err);
}
}
// clear cache for user:
@ -239,7 +232,7 @@ class CreateImport extends Command
}
/**
* @param string $message
* @param string $message
* @param array|null $data
*/
private function errorLine(string $message, array $data = null): void
@ -251,7 +244,7 @@ class CreateImport extends Command
/**
* @param string $message
* @param array $data
* @param array $data
*/
private function infoLine(string $message, array $data = null): void
{
@ -270,30 +263,21 @@ class CreateImport extends Command
$file = (string)$this->argument('file');
$configuration = (string)$this->argument('configuration');
$cwd = getcwd();
$validTypes = config('import.options.file.import_formats');
$type = strtolower($this->option('type'));
$provider = strtolower($this->option('provider'));
$enabled = (bool)config(sprintf('import.enabled.%s', $provider));
$enabled = (bool)config('import.enabled.file');
if (false === $enabled) {
$this->errorLine(sprintf('Provider "%s" is not enabled.', $provider));
$this->errorLine('CSV Provider is not enabled.');
return false;
}
if ('file' === $provider && !\in_array($type, $validTypes, true)) {
$this->errorLine(sprintf('Cannot import file of type "%s"', $type));
return false;
}
if ('file' === $provider && !file_exists($file)) {
if (!file_exists($file)) {
$this->errorLine(sprintf('Firefly III cannot find file "%s" (working directory: "%s").', $file, $cwd));
return false;
}
if ('file' === $provider && !file_exists($configuration)) {
if (!file_exists($configuration)) {
$this->errorLine(sprintf('Firefly III cannot find configuration file "%s" (working directory: "%s").', $configuration, $cwd));
return false;

View File

@ -1,7 +1,6 @@
<?php
/**
* ApplyRules.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
@ -36,6 +35,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\TransactionRules\Engine\RuleEngine;
use FireflyIII\TransactionRules\Processor;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
@ -138,17 +138,25 @@ class ApplyRules extends Command
$this->allRules = $this->option('all_rules');
// always get all the rules of the user.
$this->grabAllRules();
// loop all groups and rules and indicate if they're included:
$count = 0;
$count = 0;
$rulesToApply = [];
/** @var RuleGroup $group */
foreach ($this->groups as $group) {
/** @var Rule $rule */
foreach ($group->rules as $rule) {
// if in rule selection, or group in selection or all rules, it's included.
if ($this->includeRule($rule, $group)) {
$test = $this->includeRule($rule, $group);
if (true === $test) {
Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title));
$count++;
$rulesToApply[] = $rule->id;
}
if (false === $test) {
Log::debug(sprintf('Will not include rule #%d "%s"', $rule->id, $rule->title));
}
}
}
@ -160,7 +168,6 @@ class ApplyRules extends Command
$this->warn(' --all_rules');
}
// get transactions from asset accounts.
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
@ -173,40 +180,17 @@ class ApplyRules extends Command
$this->line(sprintf('Will apply %d rules to %d transactions.', $count, count($journals)));
// start looping.
/** @var RuleEngine $ruleEngine */
$ruleEngine = app(RuleEngine::class);
$ruleEngine->setUser($this->getUser());
$ruleEngine->setRulesToApply($rulesToApply);
$bar = $this->output->createProgressBar(count($journals));
Log::debug(sprintf('Now looping %d transactions.', count($journals)));
/** @var array $journal */
foreach ($journals as $journal) {
Log::debug('Start of new journal.');
foreach ($this->groups as $group) {
$groupTriggered = false;
/** @var Rule $rule */
foreach ($group->rules as $rule) {
$ruleTriggered = false;
// if in rule selection, or group in selection or all rules, it's included.
if ($this->includeRule($rule, $group)) {
/** @var Processor $processor */
$processor = app(Processor::class);
$processor->make($rule, true);
$ruleTriggered = $processor->handleJournalArray($journal);
if ($ruleTriggered) {
$groupTriggered = true;
}
}
// if the rule is triggered and stop processing is true, cancel the entire group.
if ($ruleTriggered && $rule->stop_processing) {
Log::info('Break out group because rule was triggered.');
break;
}
}
// if group is triggered and stop processing is true, cancel the whole thing.
if ($groupTriggered && $group->stop_processing) {
Log::info('Break out ALL because group was triggered.');
break;
}
}
$ruleEngine->processJournalArray($journal);
Log::debug('Done with all rules for this group + done with journal.');
$bar->advance();
}

View File

@ -2,7 +2,7 @@
/**
* Cron.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
@ -22,7 +22,7 @@
declare(strict_types=1);
namespace FireflyIII\Console\Commands;
namespace FireflyIII\Console\Commands\Tools;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Support\Cronjobs\RecurringCronjob;
@ -46,7 +46,10 @@ class Cron extends Command
*
* @var string
*/
protected $signature = 'firefly:cron';
protected $signature = 'firefly-iii:cron
{--F|force : Force the cron job(s) to execute.}
{--date= : Set the date in YYYY-MM-DD to make Firefly III think that\'s the current date.}
';
/**
* Execute the console command.
@ -55,7 +58,9 @@ class Cron extends Command
*/
public function handle(): int
{
$recurring = new RecurringCronjob;
$recurring->setForce($this->option('force'));
try {
$result = $recurring->fire();
} catch (FireflyException $e) {

View File

@ -87,7 +87,7 @@ class MigrateToRules extends Command
$currencyCode = $this->tryDecrypt($currencyPreference->data);
// try json decrypt just in case.
if (\strlen($currencyCode) > 3) {
if (strlen($currencyCode) > 3) {
$currencyCode = json_decode($currencyCode) ?? 'EUR';
}

View File

@ -57,9 +57,9 @@ class UpgradeDatabase extends Command
/**
* Execute the console command.
*
* @return mixed
* @return int
*/
public function handle()
public function handle(): int
{
$commands = [
'firefly-iii:transaction-identifiers',
@ -83,5 +83,7 @@ class UpgradeDatabase extends Command
$result = Artisan::output();
echo $result;
}
return 0;
}
}

View File

@ -39,13 +39,16 @@ class StoredTransactionGroup extends Event
/** @var TransactionGroup The group that was stored. */
public $transactionGroup;
public $applyRules;
/**
* Create a new event instance.
*
* @param TransactionGroup $transactionGroup
*/
public function __construct(TransactionGroup $transactionGroup)
public function __construct(TransactionGroup $transactionGroup, bool $applyRules = true)
{
$this->transactionGroup = $transactionGroup;
$this->applyRules = $applyRules;
}
}

View File

@ -44,7 +44,10 @@ class StoredGroupEventHandler
public function processRules(StoredTransactionGroup $storedJournalEvent): bool
{
$journals = $storedJournalEvent->transactionGroup->transactionJournals;
if(false === $storedJournalEvent->applyRules) {
return true;
}
die('cannot apply rules yet');
// create objects:
/** @var RuleGroupRepositoryInterface $ruleGroupRepos */
$ruleGroupRepos = app(RuleGroupRepositoryInterface::class);

View File

@ -56,10 +56,13 @@ use FireflyIII\Models\RecurrenceMeta;
use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\Rule;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\TransactionRules\Engine\RuleEngine;
use FireflyIII\TransactionRules\Processor;
use FireflyIII\User;
use Illuminate\Bus\Queueable;
@ -83,10 +86,14 @@ class CreateRecurringTransactions implements ShouldQueue
private $date;
/** @var JournalRepositoryInterface Journal repository */
private $journalRepository;
/** @var TransactionGroupRepositoryInterface */
private $groupRepository;
/** @var RecurringRepositoryInterface Recurring transactions repository. */
private $repository;
/** @var array The users rules. */
private $rules = [];
/** @var bool Force the transaction to be created no matter what. */
private $force;
/**
* Create a new job instance.
@ -99,13 +106,23 @@ class CreateRecurringTransactions implements ShouldQueue
$this->date = $date;
$this->repository = app(RecurringRepositoryInterface::class);
$this->journalRepository = app(JournalRepositoryInterface::class);
$this->groupRepository = app(TransactionGroupRepositoryInterface::class);
$this->force = false;
}
/**
* @param bool $force
*/
public function setForce(bool $force): void
{
$this->force = $force;
}
/**
* Execute the job.
*
* @throws \FireflyIII\Exceptions\FireflyException
* @throws FireflyException
*/
public function handle(): void
{
@ -118,7 +135,6 @@ class CreateRecurringTransactions implements ShouldQueue
$filtered = $recurrences->filter(
function (Recurrence $recurrence) {
return $this->validRecurrence($recurrence);
}
);
Log::debug(sprintf('Left after filtering is %d', $filtered->count()));
@ -129,13 +145,11 @@ class CreateRecurringTransactions implements ShouldQueue
}
$this->repository->setUser($recurrence->user);
$this->journalRepository->setUser($recurrence->user);
$this->groupRepository->setUser($recurrence->user);
Log::debug(sprintf('Now at recurrence #%d', $recurrence->id));
$created = $this->handleRepetitions($recurrence);
Log::debug(sprintf('Done with recurrence #%d', $recurrence->id));
$result[$recurrence->user_id] = $result[$recurrence->user_id]->merge($created);
// apply rules:
$this->applyRules($recurrence->user, $created);
}
Log::debug('Now running report thing.');
@ -165,33 +179,29 @@ class CreateRecurringTransactions implements ShouldQueue
/**
* Apply the users rules to newly created journals.
*
* @param User $user
* @param Collection $journals
* @param User $user
* @param Collection $groups
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function applyRules(User $user, Collection $journals): void
private function applyRules(User $user, Collection $groups): void
{
$userId = $user->id;
if (!isset($this->rules[$userId])) {
$this->rules[$userId] = $this->getRules($user);
}
/** @var RuleEngine $ruleEngine */
$ruleEngine = app(RuleEngine::class);
$ruleEngine->setUser($user);
$ruleEngine->setAllRules(true);
// run the rules:
if ($this->rules[$userId]->count() > 0) {
/** @var TransactionGroup $group */
foreach ($groups as $group) {
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$this->rules[$userId]->each(
function (Rule $rule) use ($journal) {
Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id));
/** @var Processor $processor */
$processor = app(Processor::class);
$processor->make($rule);
$processor->handleTransactionJournal($journal);
if ($rule->stop_processing) {
return;
}
}
);
foreach ($group->transactionJournals as $journal) {
//$ruleEngine->processTransactionJournal($journal);
}
}
}
@ -232,7 +242,7 @@ class CreateRecurringTransactions implements ShouldQueue
}
/**
* Get the users rules.
* Get the users rule groups.
*
* @param User $user
*
@ -271,19 +281,23 @@ class CreateRecurringTransactions implements ShouldQueue
* Get transaction information from a recurring transaction.
*
* @param Recurrence $recurrence
* @param Carbon $date
*
* @return array
*/
private function getTransactionData(Recurrence $recurrence): array
private function getTransactionData(Recurrence $recurrence, Carbon $date): array
{
$transactions = $recurrence->recurrenceTransactions()->get();
$return = [];
/** @var RecurrenceTransaction $transaction */
foreach ($transactions as $index => $transaction) {
$single = [
'type' => strtolower($recurrence->transactionType->type),
'date' => $date,
'user' => $recurrence->user_id,
'currency_id' => (int)$transaction->transaction_currency_id,
'currency_code' => null,
'description' => null,
'description' => $recurrence->recurrenceTransactions()->first()->description,
'amount' => $transaction->amount,
'budget_id' => $this->repository->getBudget($transaction),
'budget_name' => null,
@ -298,6 +312,14 @@ class CreateRecurringTransactions implements ShouldQueue
'foreign_amount' => $transaction->foreign_amount,
'reconciled' => false,
'identifier' => $index,
'recurrence_id' => (int)$recurrence->id,
'order' => $index,
'notes' => (string)trans('firefly.created_from_recurrence', ['id' => $recurrence->id, 'title' => $recurrence->title]),
'tags' => $this->repository->getTags($recurrence),
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'bill_id' => null,
'bill_name' => null,
];
$return[] = $single;
}
@ -309,7 +331,7 @@ class CreateRecurringTransactions implements ShouldQueue
* Check if the occurences should be executed.
*
* @param Recurrence $recurrence
* @param array $occurrences
* @param array $occurrences
*
* @return Collection
* @throws \FireflyIII\Exceptions\FireflyException
@ -318,7 +340,6 @@ class CreateRecurringTransactions implements ShouldQueue
*/
private function handleOccurrences(Recurrence $recurrence, array $occurrences): Collection
{
throw new FireflyException('Needs refactor');
$collection = new Collection;
/** @var Carbon $date */
foreach ($occurrences as $date) {
@ -332,55 +353,58 @@ class CreateRecurringTransactions implements ShouldQueue
// count created journals on THIS day.
$journalCount = $this->repository->getJournalCount($recurrence, $date, $date);
if ($journalCount > 0) {
if ($journalCount > 0 && false === $this->force) {
Log::info(sprintf('Already created %d journal(s) for date %s', $journalCount, $date->format('Y-m-d')));
continue;
}
if ($journalCount > 0 && true === $this->force) {
Log::warning(sprintf('Already created %d groups for date %s but FORCED to continue.', $journalCount, $date->format('Y-m-d')));
}
// create transaction array and send to factory.
$array = [
'type' => $recurrence->transactionType->type,
'date' => $date,
'tags' => $this->repository->getTags($recurrence),
'user' => $recurrence->user_id,
'notes' => (string)trans('firefly.created_from_recurrence', ['id' => $recurrence->id, 'title' => $recurrence->title]),
// journal data:
'description' => $recurrence->recurrenceTransactions()->first()->description,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'bill_id' => null,
'bill_name' => null,
'recurrence_id' => (int)$recurrence->id,
// transaction data:
'transactions' => $this->getTransactionData($recurrence),
$groupTitle = null;
if ($recurrence->recurrenceTransactions->count() > 0) {
/** @var RecurrenceTransaction $first */
$first = $recurrence->recurrenceTransactions()->first();
$groupTitle = $first->description;
}
$array = [
'user' => $recurrence->user_id,
'group_title' => $groupTitle,
'transactions' => $this->getTransactionData($recurrence, $date),
];
$journal = $this->journalRepository->store($array);
Log::info(sprintf('Created new journal #%d', $journal->id));
/** @var TransactionGroup $group */
$group = $this->groupRepository->store($array);
Log::info(sprintf('Created new transaction group #%d', $group->id));
// get piggy bank ID from meta data:
$piggyBankId = $this->getPiggyId($recurrence);
Log::debug(sprintf('Piggy bank ID for recurrence #%d is #%d', $recurrence->id, $piggyBankId));
/** @var TransactionJournal $journal */
foreach ($group->transactionJournals as $journal) {
// get piggy bank ID from meta data:
$piggyBankId = $this->getPiggyId($recurrence);
Log::debug(sprintf('Piggy bank ID for recurrence #%d is #%d', $recurrence->id, $piggyBankId));
// trigger event:
event(new StoredTransactionGroup($journal));
// link to piggy bank:
/** @var PiggyBankFactory $factory */
$factory = app(PiggyBankFactory::class);
$factory->setUser($recurrence->user);
// link to piggy bank:
/** @var PiggyBankFactory $factory */
$factory = app(PiggyBankFactory::class);
$factory->setUser($recurrence->user);
$piggyBank = $factory->find($piggyBankId, null);
if (null !== $piggyBank) {
/** @var PiggyBankEventFactory $factory */
$factory = app(PiggyBankEventFactory::class);
$factory->create($journal, $piggyBank);
}
$piggyBank = $factory->find($piggyBankId, null);
if (null !== $piggyBank) {
/** @var PiggyBankEventFactory $factory */
$factory = app(PiggyBankEventFactory::class);
$factory->create($journal, $piggyBank);
}
$collection->push($journal);
// trigger event:
event(new StoredTransactionGroup($group, $recurrence->apply_rules));
// update recurring thing:
$recurrence->latest_date = $date;
$recurrence->save();
$collection->push($group);
}
return $collection;
@ -489,12 +513,13 @@ class CreateRecurringTransactions implements ShouldQueue
// has repeated X times.
$journalCount = $this->repository->getJournalCount($recurrence);
if (0 !== $recurrence->repetitions && $journalCount >= $recurrence->repetitions) {
if (0 !== $recurrence->repetitions && $journalCount >= $recurrence->repetitions && false === $this->force) {
Log::info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $recurrence->repetitions));
return false;
}
// is no longer running
if ($this->repeatUntilHasPassed($recurrence)) {
Log::info(
@ -524,7 +549,7 @@ class CreateRecurringTransactions implements ShouldQueue
}
// already fired today (with success):
if ($this->hasFiredToday($recurrence)) {
if ($this->hasFiredToday($recurrence) && false === $this->force) {
Log::info(sprintf('Recurrence #%d has already fired today. Skipped.', $recurrence->id));
return false;

View File

@ -156,7 +156,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface
*/
public function getActiveGroups(): Collection
{
return $this->user->ruleGroups()->where('rule_groups.active', 1)->orderBy('order', 'ASC')->get(['rule_groups.*']);
return $this->user->ruleGroups()->with(['rules'])->where('rule_groups.active', 1)->orderBy('order', 'ASC')->get(['rule_groups.*']);
}
/**

View File

@ -28,13 +28,43 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Jobs\CreateRecurringTransactions;
use FireflyIII\Models\Configuration;
use Log;
use Preferences;
/**
* Class RecurringCronjob
*/
class RecurringCronjob extends AbstractCronjob
{
/** @var bool */
private $force;
/** @var Carbon */
private $date;
/**
* RecurringCronjob constructor.
* @throws \Exception
*/
public function __construct()
{
$this->force = false;
$this->date = new Carbon;
}
/**
* @param bool $force
*/
public function setForce(bool $force): void
{
$this->force = $force;
}
/**
* @param Carbon $date
*/
public function setDate(Carbon $date): void
{
$this->date = $date;
}
/**
* @return bool
@ -48,17 +78,25 @@ class RecurringCronjob extends AbstractCronjob
$diff = time() - $lastTime;
$diffForHumans = Carbon::now()->diffForHumans(Carbon::createFromTimestamp($lastTime), true);
if (0 === $lastTime) {
Log::info('Recurring transactions cronjob has never fired before.');
Log::info('Recurring transactions cron-job has never fired before.');
}
// less than half a day ago:
if ($lastTime > 0 && $diff <= 43200) {
Log::info(sprintf('It has been %s since the recurring transactions cronjob has fired. It will not fire now.', $diffForHumans));
Log::info(sprintf('It has been %s since the recurring transactions cron-job has fired.', $diffForHumans));
if (false === $this->force) {
Log::info('The cron-job will not fire now.');
return false;
return false;
}
// fire job regardless.
if (true === $this->force) {
Log::info('Execution of the recurring transaction cron-job has been FORCED.');
}
}
if ($lastTime > 0 && $diff > 43200) {
Log::info(sprintf('It has been %s since the recurring transactions cronjob has fired. It will fire now!', $diffForHumans));
Log::info(sprintf('It has been %s since the recurring transactions cron-job has fired. It will fire now!', $diffForHumans));
}
try {
@ -68,7 +106,8 @@ class RecurringCronjob extends AbstractCronjob
Log::error($e->getTraceAsString());
throw new FireflyException(sprintf('Could not run recurring transaction cron job: %s', $e->getMessage()));
}
Preferences::mark();
app('preferences')->mark();
return true;
}
@ -79,8 +118,9 @@ class RecurringCronjob extends AbstractCronjob
*/
private function fireRecurring(): void
{
$job = new CreateRecurringTransactions(new Carbon);
$job = new CreateRecurringTransactions($this->date);
$job->setForce($this->force);
$job->handle();
app('fireflyconfig')->set('last_rt_job', time());
app('fireflyconfig')->set('last_rt_job', $this->date->format('U'));
}
}

View File

@ -161,9 +161,26 @@ class ImportTransaction
'opposing-number' => 'opposingNumber',
];
// overrule some old role values.
if ('original-source' === $role) {
$role = 'original_source';
$replaceOldRoles = [
'original-source' => 'original_source',
'sepa-cc' => 'sepa_cc',
'sepa-ct-op' => 'sepa_ct_op',
'sepa-ct-id' => 'sepa_ct_id',
'sepa-db' => 'sepa_db',
'sepa-country' => 'sepa_country',
'sepa-ep' => 'sepa_ep',
'sepa-ci' => 'sepa_ci',
'sepa-batch-id' => 'sepa_batch_id',
'internal-reference' => 'internal_reference',
'date-interest' => 'date_interest',
'date-invoice' => 'date_invoice',
'date-book' => 'date_book',
'date-payment' => 'date_payment',
'date-process' => 'date_process',
'date-due' => 'date_due',
];
if (in_array($role, array_keys($replaceOldRoles))) {
$role = $replaceOldRoles[$role];
}
if (isset($basics[$role])) {
@ -201,7 +218,7 @@ class ImportTransaction
return;
}
$modifiers = ['generic-debit-credit'];
$modifiers = ['generic-debit-credit', 'ing-debit-credit', 'rabo-debit-credit'];
if (in_array($role, $modifiers, true)) {
$this->modifiers[$role] = $columnValue->getValue();
@ -235,18 +252,6 @@ class ImportTransaction
}
}
/**
* Returns the mapped value if it exists in the ColumnValue object.
*
* @param ColumnValue $columnValue
*
* @return int
*/
private function getMappedValue(ColumnValue $columnValue): int
{
return $columnValue->getMappedValue() > 0 ? $columnValue->getMappedValue() : (int)$columnValue->getValue();
}
/**
* Calculate the amount of this transaction.
*
@ -294,40 +299,6 @@ class ImportTransaction
return $result;
}
/**
* This methods decides which input value to use for the amount calculation.
*
* @return array
*/
private function selectAmountInput(): array
{
$info = [];
$converterClass = '';
if (null !== $this->amount) {
Log::debug('Amount value is not NULL, assume this is the correct value.');
$converterClass = Amount::class;
$info['amount'] = $this->amount;
}
if (null !== $this->amountDebit) {
Log::debug('Amount DEBIT value is not NULL, assume this is the correct value (overrules Amount).');
$converterClass = AmountDebit::class;
$info['amount'] = $this->amountDebit;
}
if (null !== $this->amountCredit) {
Log::debug('Amount CREDIT value is not NULL, assume this is the correct value (overrules Amount and AmountDebit).');
$converterClass = AmountCredit::class;
$info['amount'] = $this->amountCredit;
}
if (null !== $this->amountNegated) {
Log::debug('Amount NEGATED value is not NULL, assume this is the correct value (overrules Amount and AmountDebit and AmountCredit).');
$converterClass = AmountNegated::class;
$info['amount'] = $this->amountNegated;
}
$info['class'] = $converterClass;
return $info;
}
/**
* The method that calculates the foreign amount isn't nearly as complex,\
* because Firefly III only supports one foreign amount field. So the foreign amount is there
@ -424,4 +395,50 @@ class ImportTransaction
];
}
/**
* Returns the mapped value if it exists in the ColumnValue object.
*
* @param ColumnValue $columnValue
*
* @return int
*/
private function getMappedValue(ColumnValue $columnValue): int
{
return $columnValue->getMappedValue() > 0 ? $columnValue->getMappedValue() : (int)$columnValue->getValue();
}
/**
* This methods decides which input value to use for the amount calculation.
*
* @return array
*/
private function selectAmountInput(): array
{
$info = [];
$converterClass = '';
if (null !== $this->amount) {
Log::debug('Amount value is not NULL, assume this is the correct value.');
$converterClass = Amount::class;
$info['amount'] = $this->amount;
}
if (null !== $this->amountDebit) {
Log::debug('Amount DEBIT value is not NULL, assume this is the correct value (overrules Amount).');
$converterClass = AmountDebit::class;
$info['amount'] = $this->amountDebit;
}
if (null !== $this->amountCredit) {
Log::debug('Amount CREDIT value is not NULL, assume this is the correct value (overrules Amount and AmountDebit).');
$converterClass = AmountCredit::class;
$info['amount'] = $this->amountCredit;
}
if (null !== $this->amountNegated) {
Log::debug('Amount NEGATED value is not NULL, assume this is the correct value (overrules Amount and AmountDebit and AmountCredit).');
$converterClass = AmountNegated::class;
$info['amount'] = $this->amountNegated;
}
$info['class'] = $converterClass;
return $info;
}
}

View File

@ -0,0 +1,210 @@
<?php
/**
* RuleEngine.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\TransactionRules\Engine;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepository;
use FireflyIII\TransactionRules\Processor;
use FireflyIII\User;
use Illuminate\Support\Collection;
use Log;
/**
* Class RuleEngine
*
* Set the user, then apply an array to setRulesToApply(array) or call addRuleIdToApply(int) or addRuleToApply(Rule).
* Then call process() to make the magic happen.
*
*/
class RuleEngine
{
/** @var Collection */
private $ruleGroups;
/** @var array */
private $rulesToApply;
/** @var bool */
private $allRules;
/** @var User */
private $user;
/** @var RuleGroupRepository */
private $ruleGroupRepository;
/**
* RuleEngine constructor.
*/
public function __construct()
{
Log::debug('Created RuleEngine');
$this->ruleGroups = new Collection;
$this->rulesToApply = [];
$this->allRules = false;
$this->ruleGroupRepository = app(RuleGroupRepository::class);
}
/**
* @param bool $allRules
*/
public function setAllRules(bool $allRules): void
{
Log::debug('RuleEngine will apply ALL rules.');
$this->allRules = $allRules;
}
/**
* @param array $rulesToApply
*/
public function setRulesToApply(array $rulesToApply): void
{
Log::debug('RuleEngine will try rules', $rulesToApply);
$this->rulesToApply = $rulesToApply;
}
/**
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
$this->ruleGroupRepository->setUser($user);
$this->ruleGroups = $this->ruleGroupRepository->getActiveGroups();
}
/**
* @param TransactionJournal $transactionJournal
*/
public function processTransactionJournal(TransactionJournal $transactionJournal): void
{
Log::debug(sprintf('Will process transaction journal #%d ("%s")', $transactionJournal->id, $transactionJournal->description));
/** @var RuleGroup $group */
foreach ($this->ruleGroups as $group) {
Log::debug(sprintf('Now at rule group #%d', $group->id));
$groupTriggered = false;
/** @var Rule $rule */
foreach ($group->rules as $rule) {
Log::debug(sprintf('Now at rule #%d from rule group #%d', $rule->id, $group->id));
$ruleTriggered = false;
// if in rule selection, or group in selection or all rules, it's included.
if ($this->includeRule($rule)) {
Log::debug(sprintf('Rule #%d is included.', $rule->id));
/** @var Processor $processor */
$processor = app(Processor::class);
$ruleTriggered = false;
try {
$processor->make($rule, true);
$ruleTriggered = $processor->handleTransactionJournal($transactionJournal);
} catch (FireflyException $e) {
Log::error($e->getMessage());
}
if ($ruleTriggered) {
Log::debug('The rule was triggered, so the group is as well!');
$groupTriggered = true;
}
}
if (!$this->includeRule($rule)) {
Log::debug(sprintf('Rule #%d is not included.', $rule->id));
}
// if the rule is triggered and stop processing is true, cancel the entire group.
if ($ruleTriggered && $rule->stop_processing) {
Log::info(sprintf('Break out group #%d because rule #%d was triggered.', $group->id, $rule->id));
break;
}
}
// if group is triggered and stop processing is true, cancel the whole thing.
if ($groupTriggered && $group->stop_processing) {
Log::info(sprintf('Break out ALL because group #%d was triggered.', $group->id));
break;
}
}
Log::debug('Done processing this transaction journal.');
}
/**
* @param array $journal
*/
public function processJournalArray(array $journal): void
{
Log::debug(sprintf('Will process transaction journal #%d ("%s")', $journal['id'], $journal['description']));
/** @var RuleGroup $group */
foreach ($this->ruleGroups as $group) {
Log::debug(sprintf('Now at rule group #%d', $group->id));
$groupTriggered = false;
/** @var Rule $rule */
foreach ($group->rules as $rule) {
Log::debug(sprintf('Now at rule #%d from rule group #%d', $rule->id, $group->id));
$ruleTriggered = false;
// if in rule selection, or group in selection or all rules, it's included.
if ($this->includeRule($rule)) {
Log::debug(sprintf('Rule #%d is included.', $rule->id));
/** @var Processor $processor */
$processor = app(Processor::class);
$ruleTriggered = false;
try {
$processor->make($rule, true);
$ruleTriggered = $processor->handleJournalArray($journal);
} catch (FireflyException $e) {
Log::error($e->getMessage());
}
if ($ruleTriggered) {
Log::debug('The rule was triggered, so the group is as well!');
$groupTriggered = true;
}
}
if (!$this->includeRule($rule)) {
Log::debug(sprintf('Rule #%d is not included.', $rule->id));
}
// if the rule is triggered and stop processing is true, cancel the entire group.
if ($ruleTriggered && $rule->stop_processing) {
Log::info(sprintf('Break out group #%d because rule #%d was triggered.', $group->id, $rule->id));
break;
}
}
// if group is triggered and stop processing is true, cancel the whole thing.
if ($groupTriggered && $group->stop_processing) {
Log::info(sprintf('Break out ALL because group #%d was triggered.', $group->id));
break;
}
}
Log::debug('Done processing this transaction journal.');
}
/**
* @param Rule $rule
* @return bool
*/
private function includeRule(Rule $rule): bool
{
return $this->allRules || in_array($rule->id, $this->rulesToApply, true);
}
}