diff --git a/app/Support/ParseDateString.php b/app/Support/ParseDateString.php new file mode 100644 index 0000000000..1b45b4defb --- /dev/null +++ b/app/Support/ParseDateString.php @@ -0,0 +1,157 @@ +keywords, true)) { + return $this->parseKeyword($date); + } + + // if regex for YYYY-MM-DD: + $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][\d]|3[01])$/'; + if (preg_match($pattern, $date)) { + return $this->parseDefaultDate($date); + } + + // if + or -: + if (0 === strpos($date, '+') || 0 === strpos($date, '-')) { + return $this->parseRelativeDate($date); + } + + throw new FireflyException('Not recognised.'); + } + + /** + * @param string $date + * + * @return Carbon + */ + private function parseDefaultDate(string $date): Carbon + { + return Carbon::createFromFormat('Y-m-d', $date); + } + + /** + * @param string $keyword + * + * @return Carbon + */ + private function parseKeyword(string $keyword): Carbon + { + $today = Carbon::today()->startOfDay(); + switch ($keyword) { + default: + case 'today': + return $today; + case 'yesterday': + return $today->subDay(); + case 'tomorrow': + return $today->addDay(); + case 'start of this week': + return $today->startOfWeek(); + case 'end of this week': + return $today->endOfWeek(); + case 'start of this month': + return $today->startOfMonth(); + case 'end of this month': + return $today->endOfMonth(); + case 'start of this quarter': + return $today->startOfQuarter(); + case 'end of this quarter': + return $today->endOfQuarter(); + case 'start of this year': + return $today->startOfYear(); + case 'end of this year': + return $today->endOfYear(); + } + } + + /** + * @param string $date + * + * @return Carbon + */ + private function parseRelativeDate(string $date): Carbon + { + Log::debug(sprintf('Now in parseRelativeDate("%s")', $date)); + $parts = explode(' ', $date); + $today = Carbon::today()->startOfDay(); + $functions = [ + [ + 'd' => 'subDays', + 'w' => 'subWeeks', + 'm' => 'subMonths', + 'q' => 'subQuarters', + 'y' => 'subYears', + ], [ + 'd' => 'addDays', + 'w' => 'addWeeks', + 'm' => 'addMonths', + 'q' => 'addQuarters', + 'y' => 'addYears', + ], + ]; + + /** @var string $part */ + foreach ($parts as $part) { + Log::debug(sprintf('Now parsing part "%s"', $part)); + $part = trim($part); + + // verify if correct + $pattern = '/[+-]\d+[wqmdy]/'; + $res = preg_match($pattern, $part); + if (0 === $res || false === $res) { + Log::error(sprintf('Part "%s" does not match regular expression. Will be skipped.', $part)); + continue; + } + $direction = 0 === strpos($part, '+') ? 1 : 0; + $period = $part[strlen($part) - 1]; + $number = (int) substr($part, 1, -1); + if (!isset($functions[$direction][$period])) { + Log::error(sprintf('No method for direction %d and period "%s".', $direction, $period)); + continue; + } + $func = $functions[$direction][$period]; + Log::debug(sprintf('Will now do %s(%d) on %s', $func, $number, $today->format('Y-m-d'))); + $today->$func($number); + Log::debug(sprintf('Resulting date is %s', $today->format('Y-m-d'))); + + } + + return $today; + } + +} diff --git a/app/TransactionRules/Triggers/DateIs.php b/app/TransactionRules/Triggers/DateIs.php new file mode 100644 index 0000000000..6661b4e441 --- /dev/null +++ b/app/TransactionRules/Triggers/DateIs.php @@ -0,0 +1,107 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\TransactionRules\Triggers; + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Support\ParseDateString; +use Log; + +/** + * Class DateIs. + */ +final class DateIs extends AbstractTrigger implements TriggerInterface +{ + /** + * A trigger is said to "match anything", or match any given transaction, + * when the trigger value is very vague or has no restrictions. Easy examples + * are the "AmountMore"-trigger combined with an amount of 0: any given transaction + * has an amount of more than zero! Other examples are all the "Description"-triggers + * which have hard time handling empty trigger values such as "" or "*" (wild cards). + * + * If the user tries to create such a trigger, this method MUST return true so Firefly III + * can stop the storing / updating the trigger. If the trigger is in any way restrictive + * (even if it will still include 99.9% of the users transactions), this method MUST return + * false. + * + * @param mixed $value + * + * @return bool + */ + public static function willMatchEverything($value = null): bool + { + if (null !== $value) { + return false; + } + Log::error(sprintf('Cannot use %s with a null value.', self::class)); + + return true; + } + + /** + * Returns true when category is X. + * + * @param TransactionJournal $journal + * + * @return bool + */ + public function triggered(TransactionJournal $journal): bool + { + /** @var Carbon $date */ + $date = $journal->date; + Log::debug(sprintf('Found date on journal: %s', $date->format('Y-m-d'))); + $dateParser = new ParseDateString(); + + + try { + $ruleDate = $dateParser->parseDate($this->triggerValue); + } catch (FireflyException $e) { + Log::error('Cannot execute rule trigger.'); + Log::error($e->getMessage()); + + return false; + } + if ($ruleDate->isSameDay($date)) { + Log::debug( + sprintf( + '%s is on the same day as %s, so return true.', + $date->format('Y-m-d H:i:s'), + $ruleDate->format('Y-m-d H:i:s'), + ) + ); + + return true; + } + + Log::debug( + sprintf( + '%s is NOT on the same day as %s, so return true.', + $date->format('Y-m-d H:i:s'), + $ruleDate->format('Y-m-d H:i:s'), + ) + ); + + return false; + } +} diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index e8c50939d9..208a6c87f8 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -24,6 +24,7 @@ namespace FireflyIII\Validation; use Config; use DB; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountType; @@ -34,11 +35,13 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Services\Password\Verifier; +use FireflyIII\Support\ParseDateString; use FireflyIII\TransactionRules\Triggers\TriggerInterface; use FireflyIII\User; use Google2FA; use Illuminate\Support\Collection; use Illuminate\Validation\Validator; +use Log; /** * Class FireflyValidator. @@ -333,6 +336,20 @@ class FireflyValidator extends Validator return 1 === $count; } + // if the type is date, the simply try to parse it and throw error when it's bad. + if (in_array($triggerType, ['date_is'], true)) { + /** @var ParseDateString $parser */ + $parser = app(ParseDateString::class); + try { + $parser->parseDate($value); + } catch (FireflyException $e) { + + Log::error($e->getMessage()); + + return false; + } + } + // and finally a "will match everything check": $classes = app('config')->get('firefly.rule-triggers'); /** @var TriggerInterface $class */ diff --git a/config/firefly.php b/config/firefly.php index 7b563c511d..7273dd0333 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -73,18 +73,19 @@ use FireflyIII\TransactionRules\Actions\PrependNotes; use FireflyIII\TransactionRules\Actions\RemoveAllTags; use FireflyIII\TransactionRules\Actions\RemoveTag; use FireflyIII\TransactionRules\Actions\SetBudget; -use FireflyIII\TransactionRules\Actions\UpdatePiggybank; use FireflyIII\TransactionRules\Actions\SetCategory; use FireflyIII\TransactionRules\Actions\SetDescription; use FireflyIII\TransactionRules\Actions\SetDestinationAccount; use FireflyIII\TransactionRules\Actions\SetNotes; use FireflyIII\TransactionRules\Actions\SetSourceAccount; +use FireflyIII\TransactionRules\Actions\UpdatePiggybank; use FireflyIII\TransactionRules\Triggers\AmountExactly; use FireflyIII\TransactionRules\Triggers\AmountLess; use FireflyIII\TransactionRules\Triggers\AmountMore; use FireflyIII\TransactionRules\Triggers\BudgetIs; use FireflyIII\TransactionRules\Triggers\CategoryIs; use FireflyIII\TransactionRules\Triggers\CurrencyIs; +use FireflyIII\TransactionRules\Triggers\DateIs; use FireflyIII\TransactionRules\Triggers\DescriptionContains; use FireflyIII\TransactionRules\Triggers\DescriptionEnds; use FireflyIII\TransactionRules\Triggers\DescriptionIs; @@ -468,6 +469,7 @@ return [ 'description_ends' => DescriptionEnds::class, 'description_contains' => DescriptionContains::class, 'description_is' => DescriptionIs::class, + 'date_is' => DateIs::class, 'transaction_type' => TransactionType::class, 'category_is' => CategoryIs::class, 'budget_is' => BudgetIs::class, @@ -554,6 +556,7 @@ return [ 'notes_start', 'notes_end', 'notes_are', + 'date_is', ], 'test-triggers' => [ diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index d7079a1f00..030edc77aa 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -421,6 +421,10 @@ return [ 'rule_trigger_description_contains' => 'Description contains ":trigger_value"', 'rule_trigger_description_is_choice' => 'Description is..', 'rule_trigger_description_is' => 'Description is ":trigger_value"', + + 'rule_trigger_date_is_choice' => 'Transaction date is..', + 'rule_trigger_date_is' => 'Transaction date is ":trigger_value"', + 'rule_trigger_budget_is_choice' => 'Budget is..', 'rule_trigger_budget_is' => 'Budget is ":trigger_value"', 'rule_trigger_tag_is_choice' => '(A) tag is..',