From e4a9abc315ce862ceb84b65a34a1159a7265d639 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 7 Jun 2019 17:57:46 +0200 Subject: [PATCH] Clean up repositories and cron code. --- .sandstorm/launcher.sh | 2 +- app/Console/Commands/ApplyRules.php | 418 ------------------ .../Commands/Correction/FixAccountTypes.php | 13 +- app/Console/Commands/CreateExport.php | 150 ------- app/Console/Commands/DecryptAttachment.php | 111 ----- app/Console/Commands/EncryptFile.php | 73 --- app/Console/Commands/Import.php | 165 ------- .../CreateCSVImport.php} | 190 ++++---- app/Console/Commands/Tools/ApplyRules.php | 50 +-- app/Console/Commands/{ => Tools}/Cron.php | 11 +- .../Commands/Upgrade/MigrateToRules.php | 2 +- .../Commands/Upgrade/UpgradeDatabase.php | 6 +- app/Events/StoredTransactionGroup.php | 5 +- .../Events/StoredGroupEventHandler.php | 5 +- app/Jobs/CreateRecurringTransactions.php | 149 ++++--- .../RuleGroup/RuleGroupRepository.php | 2 +- app/Support/Cronjobs/RecurringCronjob.php | 56 ++- .../Import/Placeholder/ImportTransaction.php | 117 ++--- app/TransactionRules/Engine/RuleEngine.php | 210 +++++++++ 19 files changed, 547 insertions(+), 1188 deletions(-) delete mode 100644 app/Console/Commands/ApplyRules.php delete mode 100644 app/Console/Commands/CreateExport.php delete mode 100644 app/Console/Commands/DecryptAttachment.php delete mode 100644 app/Console/Commands/EncryptFile.php delete mode 100644 app/Console/Commands/Import.php rename app/Console/Commands/{CreateImport.php => Import/CreateCSVImport.php} (52%) rename app/Console/Commands/{ => Tools}/Cron.php (84%) create mode 100644 app/TransactionRules/Engine/RuleEngine.php diff --git a/.sandstorm/launcher.sh b/.sandstorm/launcher.sh index d203eb5c92..9395e9f7e7 100755 --- a/.sandstorm/launcher.sh +++ b/.sandstorm/launcher.sh @@ -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 diff --git a/app/Console/Commands/ApplyRules.php b/app/Console/Commands/ApplyRules.php deleted file mode 100644 index 425505b959..0000000000 --- a/app/Console/Commands/ApplyRules.php +++ /dev/null @@ -1,418 +0,0 @@ -. - */ - -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(''); - } - - -} diff --git a/app/Console/Commands/Correction/FixAccountTypes.php b/app/Console/Commands/Correction/FixAccountTypes.php index 7fdcb6d14d..c776e74d8c 100644 --- a/app/Console/Commands/Correction/FixAccountTypes.php +++ b/app/Console/Commands/Correction/FixAccountTypes.php @@ -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); } } diff --git a/app/Console/Commands/CreateExport.php b/app/Console/Commands/CreateExport.php deleted file mode 100644 index 75390480f9..0000000000 --- a/app/Console/Commands/CreateExport.php +++ /dev/null @@ -1,150 +0,0 @@ -. - */ - -/** @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; - } -} diff --git a/app/Console/Commands/DecryptAttachment.php b/app/Console/Commands/DecryptAttachment.php deleted file mode 100644 index 450eedd8bd..0000000000 --- a/app/Console/Commands/DecryptAttachment.php +++ /dev/null @@ -1,111 +0,0 @@ -. - */ - -/** @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; - } -} diff --git a/app/Console/Commands/EncryptFile.php b/app/Console/Commands/EncryptFile.php deleted file mode 100644 index 6c0e6d4272..0000000000 --- a/app/Console/Commands/EncryptFile.php +++ /dev/null @@ -1,73 +0,0 @@ -. - */ - -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; - } -} diff --git a/app/Console/Commands/Import.php b/app/Console/Commands/Import.php deleted file mode 100644 index 7ee59ea03b..0000000000 --- a/app/Console/Commands/Import.php +++ /dev/null @@ -1,165 +0,0 @@ -. - */ - -/** @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; - } -} diff --git a/app/Console/Commands/CreateImport.php b/app/Console/Commands/Import/CreateCSVImport.php similarity index 52% rename from app/Console/Commands/CreateImport.php rename to app/Console/Commands/Import/CreateCSVImport.php index 1e54d4782d..cae2732402 100644 --- a/app/Console/Commands/CreateImport.php +++ b/app/Console/Commands/Import/CreateCSVImport.php @@ -1,7 +1,7 @@ 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; diff --git a/app/Console/Commands/Tools/ApplyRules.php b/app/Console/Commands/Tools/ApplyRules.php index 8e6d2d145b..de22ed46fc 100644 --- a/app/Console/Commands/Tools/ApplyRules.php +++ b/app/Console/Commands/Tools/ApplyRules.php @@ -1,7 +1,6 @@ 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(); } diff --git a/app/Console/Commands/Cron.php b/app/Console/Commands/Tools/Cron.php similarity index 84% rename from app/Console/Commands/Cron.php rename to app/Console/Commands/Tools/Cron.php index ac7dfb6ff4..206774f5cb 100644 --- a/app/Console/Commands/Cron.php +++ b/app/Console/Commands/Tools/Cron.php @@ -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) { diff --git a/app/Console/Commands/Upgrade/MigrateToRules.php b/app/Console/Commands/Upgrade/MigrateToRules.php index 502e0669e4..a5569ff028 100644 --- a/app/Console/Commands/Upgrade/MigrateToRules.php +++ b/app/Console/Commands/Upgrade/MigrateToRules.php @@ -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'; } diff --git a/app/Console/Commands/Upgrade/UpgradeDatabase.php b/app/Console/Commands/Upgrade/UpgradeDatabase.php index 737394dc67..881ee2bb29 100644 --- a/app/Console/Commands/Upgrade/UpgradeDatabase.php +++ b/app/Console/Commands/Upgrade/UpgradeDatabase.php @@ -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; } } diff --git a/app/Events/StoredTransactionGroup.php b/app/Events/StoredTransactionGroup.php index 83c82b181b..e473b7d487 100644 --- a/app/Events/StoredTransactionGroup.php +++ b/app/Events/StoredTransactionGroup.php @@ -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; } } diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php index ea56b12126..214c7397ee 100644 --- a/app/Handlers/Events/StoredGroupEventHandler.php +++ b/app/Handlers/Events/StoredGroupEventHandler.php @@ -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); diff --git a/app/Jobs/CreateRecurringTransactions.php b/app/Jobs/CreateRecurringTransactions.php index 70c227faa7..0659711f49 100644 --- a/app/Jobs/CreateRecurringTransactions.php +++ b/app/Jobs/CreateRecurringTransactions.php @@ -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; diff --git a/app/Repositories/RuleGroup/RuleGroupRepository.php b/app/Repositories/RuleGroup/RuleGroupRepository.php index bd2c8fcc82..a29fea2de7 100644 --- a/app/Repositories/RuleGroup/RuleGroupRepository.php +++ b/app/Repositories/RuleGroup/RuleGroupRepository.php @@ -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.*']); } /** diff --git a/app/Support/Cronjobs/RecurringCronjob.php b/app/Support/Cronjobs/RecurringCronjob.php index c8ebdcde32..badc2ca666 100644 --- a/app/Support/Cronjobs/RecurringCronjob.php +++ b/app/Support/Cronjobs/RecurringCronjob.php @@ -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')); } } diff --git a/app/Support/Import/Placeholder/ImportTransaction.php b/app/Support/Import/Placeholder/ImportTransaction.php index 7395d67835..0616ab30c0 100644 --- a/app/Support/Import/Placeholder/ImportTransaction.php +++ b/app/Support/Import/Placeholder/ImportTransaction.php @@ -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; + } + } diff --git a/app/TransactionRules/Engine/RuleEngine.php b/app/TransactionRules/Engine/RuleEngine.php new file mode 100644 index 0000000000..c23a98da5d --- /dev/null +++ b/app/TransactionRules/Engine/RuleEngine.php @@ -0,0 +1,210 @@ +. + */ + +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); + } + +} \ No newline at end of file