Expand search.

This commit is contained in:
James Cole
2020-08-22 12:24:01 +02:00
parent d69934ca8f
commit ffca935ced
21 changed files with 3514 additions and 322 deletions

View File

@@ -98,6 +98,7 @@ class ApplyRules extends Command
*/
public function handle(): int
{
$start = microtime(true);
$this->stupidLaravel();
// @codeCoverageIgnoreStart
if (!$this->verifyAccessToken()) {
@@ -152,6 +153,8 @@ class ApplyRules extends Command
$ruleEngine->setUser($this->getUser());
$ruleEngine->setRulesToApply($rulesToApply);
app('telemetry')->feature('system.command.executed', $this->signature);
// for this call, the rule engine only includes "store" rules:
$ruleEngine->setTriggerMode(RuleEngine::TRIGGER_STORE);
@@ -166,9 +169,9 @@ class ApplyRules extends Command
$bar->advance();
}
$this->line('');
$this->line('Done!');
$end = round(microtime(true) - $start, 2);
$this->line(sprintf('Done in %s seconds!', $end));
app('telemetry')->feature('system.command.executed', $this->signature);
return 0;
}

View File

@@ -56,9 +56,6 @@ class AccountFactory
*/
public function __construct()
{
if ('testing' === config('app.env')) {
Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this)));
}
$this->canHaveVirtual = [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD];
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->validAssetFields = ['account_role', 'account_number', 'currency_id', 'BIC', 'include_net_worth'];

View File

@@ -34,18 +34,6 @@ use Log;
*/
class AccountMetaFactory
{
/**
* Constructor.
*
* @codeCoverageIgnore
*/
public function __construct()
{
if ('testing' === config('app.env')) {
Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this)));
}
}
/**
* @param array $data
*

View File

@@ -36,21 +36,7 @@ use Log;
*/
class AttachmentFactory
{
/** @var User */
private $user;
/**
* Constructor.
*
* @codeCoverageIgnore
*/
public function __construct()
{
if ('testing' === config('app.env')) {
Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this)));
}
}
private User $user;
/**
* @param array $data

View File

@@ -62,7 +62,7 @@ trait AmountCollection
{
$this->query->where(
function (EloquentBuilder $q) use ($amount) {
$q->where('destination.amount', '<', app('steam')->positive($amount));
$q->where('destination.amount', '<=', app('steam')->positive($amount));
}
);
@@ -80,7 +80,7 @@ trait AmountCollection
{
$this->query->where(
function (EloquentBuilder $q) use ($amount) {
$q->where('destination.amount', '>', app('steam')->positive($amount));
$q->where('destination.amount', '>=', app('steam')->positive($amount));
}
);

View File

@@ -61,6 +61,73 @@ trait MetaCollection
return $this;
}
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function notesContain(string $value): GroupCollectorInterface
{
$this->withNotes();
$this->query->where('notes', 'LIKE', sprintf('%%%s%%', $value));
return $this;
}
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function notesEndWith(string $value): GroupCollectorInterface
{
$this->withNotes();
$this->query->where('notes', 'LIKE', sprintf('%%%s', $value));
return $this;
}
/**
* @return GroupCollectorInterface
*/
public function withoutNotes(): GroupCollectorInterface
{
$this->withNotes();
$this->query->whereNull('notes');
return $this;
}
/**
* @return GroupCollectorInterface
*/
public function withAnyNotes(): GroupCollectorInterface
{
$this->withNotes();
$this->query->whereNotNull('notes');
return $this;
}
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function notesExactly(string $value): GroupCollectorInterface
{
$this->withNotes();
$this->query->where('notes', '=', sprintf('%s', $value));
return $this;
}
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function notesStartWith(string $value): GroupCollectorInterface
{
$this->withNotes();
$this->query->where('notes', 'LIKE', sprintf('%s%%', $value));
return $this;
}
/**
* Limit the search to a specific bill.
*
@@ -185,6 +252,32 @@ trait MetaCollection
return $this;
}
/**
* Where has no tags.
*
* @return GroupCollectorInterface
*/
public function withoutTags(): GroupCollectorInterface
{
$this->withTagInformation();
$this->query->whereNull('tag_transaction_journal.tag_id');
return $this;
}
/**
* Where has no tags.
*
* @return GroupCollectorInterface
*/
public function hasAnyTag(): GroupCollectorInterface
{
$this->withTagInformation();
$this->query->whereNotNull('tag_transaction_journal.tag_id');
return $this;
}
/**
* Will include bill name + ID, if any.
*
@@ -272,11 +365,20 @@ trait MetaCollection
public function withoutBudget(): GroupCollectorInterface
{
$this->withBudgetInformation();
$this->query->where(
function (EloquentBuilder $q) {
$q->whereNull('budget_transaction_journal.budget_id');
}
);
$this->query->whereNull('budget_transaction_journal.budget_id');
return $this;
}
/**
* Limit results to a transactions without a budget..
*
* @return GroupCollectorInterface
*/
public function withBudget(): GroupCollectorInterface
{
$this->withBudgetInformation();
$this->query->whereNotNull('budget_transaction_journal.budget_id');
return $this;
}
@@ -289,11 +391,20 @@ trait MetaCollection
public function withoutCategory(): GroupCollectorInterface
{
$this->withCategoryInformation();
$this->query->where(
function (EloquentBuilder $q) {
$q->whereNull('category_transaction_journal.category_id');
}
);
$this->query->whereNull('category_transaction_journal.category_id');
return $this;
}
/**
* Limit results to a transactions without a category.
*
* @return GroupCollectorInterface
*/
public function withCategory(): GroupCollectorInterface
{
$this->withCategoryInformation();
$this->query->whereNotNull('category_transaction_journal.category_id');
return $this;
}

View File

@@ -130,7 +130,7 @@ class GroupCollector implements GroupCollectorInterface
*/
public function dumpQuery(): void
{
echo $this->query->toSql();
echo $this->query->select($this->fields)->toSql();
echo '<pre>';
print_r($this->query->getBindings());
echo '</pre>';
@@ -232,6 +232,16 @@ class GroupCollector implements GroupCollectorInterface
return $this;
}
/**
* @inheritDoc
*/
public function setForeignCurrency(TransactionCurrency $currency): GroupCollectorInterface
{
$this->query->where('source.foreign_currency_id', $currency->id);
return $this;
}
/**
* Limit the result to a specific transaction group.
*
@@ -326,6 +336,79 @@ class GroupCollector implements GroupCollectorInterface
return $this;
}
/**
* @inheritDoc
*/
public function descriptionStarts(array $array): GroupCollectorInterface
{
$this->query->where(
static function (EloquentBuilder $q) use ($array) {
$q->where(
static function (EloquentBuilder $q1) use ($array) {
foreach ($array as $word) {
$keyword = sprintf('%s%%', $word);
$q1->where('transaction_journals.description', 'LIKE', $keyword);
}
}
);
$q->orWhere(
static function (EloquentBuilder $q2) use ($array) {
foreach ($array as $word) {
$keyword = sprintf('%s%%', $word);
$q2->where('transaction_groups.title', 'LIKE', $keyword);
}
}
);
}
);
return $this;
}
/**
* @inheritDoc
*/
public function descriptionEnds(array $array): GroupCollectorInterface
{
$this->query->where(
static function (EloquentBuilder $q) use ($array) {
$q->where(
static function (EloquentBuilder $q1) use ($array) {
foreach ($array as $word) {
$keyword = sprintf('%%%s', $word);
$q1->where('transaction_journals.description', 'LIKE', $keyword);
}
}
);
$q->orWhere(
static function (EloquentBuilder $q2) use ($array) {
foreach ($array as $word) {
$keyword = sprintf('%%%s', $word);
$q2->where('transaction_groups.title', 'LIKE', $keyword);
}
}
);
}
);
return $this;
}
/**
* @inheritDoc
*/
public function descriptionIs(string $value): GroupCollectorInterface
{
$this->query->where(
static function (EloquentBuilder $q) use ($value) {
$q->where('transaction_journals.description', '=', $value);
$q->orWhere('transaction_groups.title', '=', $value);
}
);
return $this;
}
/**
* Limit the search to one specific transaction group.
@@ -417,6 +500,20 @@ class GroupCollector implements GroupCollectorInterface
return $array;
}
/**
* Has attachments
*
* @return GroupCollectorInterface
*/
public function hasAttachments(): GroupCollectorInterface
{
Log::debug('Add filter on attachment ID.');
$this->joinAttachmentTables();
$this->query->whereNotNull('attachments.attachable_id');
return $this;
}
/**
* Join table to get attachment information.
*/
@@ -655,7 +752,7 @@ class GroupCollector implements GroupCollectorInterface
'transactions as source',
function (JoinClause $join) {
$join->on('source.transaction_journal_id', '=', 'transaction_journals.id')
->where('source.amount', '<', 0);
->where('source.amount', '<', 0);
}
)
// join destination transaction
@@ -663,7 +760,7 @@ class GroupCollector implements GroupCollectorInterface
'transactions as destination',
function (JoinClause $join) {
$join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')
->where('destination.amount', '>', 0);
->where('destination.amount', '>', 0);
}
)
// left join transaction type.

View File

@@ -220,6 +220,15 @@ interface GroupCollectorInterface
*/
public function setCurrency(TransactionCurrency $currency): GroupCollectorInterface;
/**
* Limit results to a specific foreign currency.
*
* @param TransactionCurrency $currency
*
* @return GroupCollectorInterface
*/
public function setForeignCurrency(TransactionCurrency $currency): GroupCollectorInterface;
/**
* Set destination accounts.
*
@@ -284,6 +293,33 @@ interface GroupCollectorInterface
*/
public function setSearchWords(array $array): GroupCollectorInterface;
/**
* Beginning of the description must match:
*
* @param array $array
*
* @return GroupCollectorInterface
*/
public function descriptionStarts(array $array): GroupCollectorInterface;
/**
* End of the description must match:
*
* @param array $array
*
* @return GroupCollectorInterface
*/
public function descriptionEnds(array $array): GroupCollectorInterface;
/**
* Description must be:
*
* @param string $value
*
* @return GroupCollectorInterface
*/
public function descriptionIs(string $value): GroupCollectorInterface;
/**
* Set source accounts.
*
@@ -311,6 +347,16 @@ interface GroupCollectorInterface
*/
public function setTags(Collection $tags): GroupCollectorInterface;
/**
* @return GroupCollectorInterface
*/
public function withoutTags(): GroupCollectorInterface;
/**
* @return GroupCollectorInterface
*/
public function hasAnyTag(): GroupCollectorInterface;
/**
* Limit the search to one specific transaction group.
*
@@ -377,6 +423,13 @@ interface GroupCollectorInterface
*/
public function withAttachmentInformation(): GroupCollectorInterface;
/**
* Has attachments
*
* @return GroupCollectorInterface
*/
public function hasAttachments(): GroupCollectorInterface;
/**
* Include bill name + ID.
*
@@ -405,6 +458,42 @@ interface GroupCollectorInterface
*/
public function withNotes(): GroupCollectorInterface;
/**
* Any notes, no matter what.
*
* @return GroupCollectorInterface
*/
public function withAnyNotes(): GroupCollectorInterface;
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function notesContain(string $value): GroupCollectorInterface;
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function withoutNotes(): GroupCollectorInterface;
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function notesStartWith(string $value): GroupCollectorInterface;
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function notesEndWith(string $value): GroupCollectorInterface;
/**
* @param string $value
* @return GroupCollectorInterface
*/
public function notesExactly(string $value): GroupCollectorInterface;
/**
* Add tag info.
*
@@ -426,6 +515,20 @@ interface GroupCollectorInterface
*/
public function withoutCategory(): GroupCollectorInterface;
/**
* Limit results to a transactions with a category.
*
* @return GroupCollectorInterface
*/
public function withCategory(): GroupCollectorInterface;
/**
* Limit results to a transactions with a budget.
*
* @return GroupCollectorInterface
*/
public function withBudget(): GroupCollectorInterface;
/**
* Look for specific external ID's.
*

View File

@@ -38,6 +38,7 @@ use FireflyIII\Services\Internal\Destroy\AccountDestroyService;
use FireflyIII\Services\Internal\Update\AccountUpdateService;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Relations\HasMany;
use \Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Support\Collection;
use Log;
use Storage;
@@ -346,7 +347,7 @@ class AccountRepository implements AccountRepositoryInterface
return null;
}
if (1 === $result->count()) {
return (string)$result->first()->data;
return (string) $result->first()->data;
}
return null;
}
@@ -704,4 +705,38 @@ class AccountRepository implements AccountRepositoryInterface
$account->save();
}
}
/**
* @inheritDoc
*/
public function searchAccountNr(string $query, array $types, int $limit): Collection
{
$dbQuery = $this->user->accounts()->distinct()
->leftJoin('account_meta', 'accounts.id', 'account_meta.account_id')
->where('accounts.active', 1)
->orderBy('accounts.order', 'ASC')
->orderBy('accounts.account_type_id', 'ASC')
->orderBy('accounts.name', 'ASC')
->with(['accountType', 'accountMeta']);
if ('' !== $query) {
// split query on spaces just in case:
$parts = explode(' ', $query);
foreach ($parts as $part) {
$search = sprintf('%%%s%%', $part);
$dbQuery->where(function (EloquentBuilder $q1) use ($search) {
$q1->where('accounts.iban', 'LIKE', $search);
$q1->orWhere(function (EloquentBuilder $q2) use ($search) {
$q2->where('account_meta.name', '=', 'account_number');
$q2->where('account_meta.data', 'LIKE', $search);
});
});
}
}
if (count($types) > 0) {
$dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
$dbQuery->whereIn('account_types.type', $types);
}
return $dbQuery->take($limit)->get(['accounts.*']);
}
}

View File

@@ -289,6 +289,15 @@ interface AccountRepositoryInterface
*/
public function searchAccount(string $query, array $types, int $limit): Collection;
/**
* @param string $query
* @param array $types
* @param int $limit
*
* @return Collection
*/
public function searchAccountNr(string $query, array $types, int $limit): Collection;
/**
* @param User $user
*/

View File

@@ -47,18 +47,7 @@ use Log;
*/
class CurrencyRepository implements CurrencyRepositoryInterface
{
/** @var User */
private $user;
/**
* Constructor.
*/
public function __construct()
{
if ('testing' === config('app.env')) {
Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this)));
}
}
private User $user;
/**
* @param TransactionCurrency $currency

View File

@@ -25,12 +25,16 @@ use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface;
use FireflyIII\User;
use Gdbots\QueryParser\Node\Field;
use Gdbots\QueryParser\Node\Node;
@@ -47,19 +51,22 @@ use Log;
*/
class BetterQuerySearch implements SearchInterface
{
private AccountRepositoryInterface $accountRepository;
private BillRepositoryInterface $billRepository;
private BudgetRepositoryInterface $budgetRepository;
private CategoryRepositoryInterface $categoryRepository;
private TagRepositoryInterface $tagRepository;
private User $user;
private ParsedQuery $query;
private int $page;
private array $words;
private array $validOperators;
private GroupCollectorInterface $collector;
private float $startTime;
private Collection $modifiers;
private AccountRepositoryInterface $accountRepository;
private BillRepositoryInterface $billRepository;
private BudgetRepositoryInterface $budgetRepository;
private CategoryRepositoryInterface $categoryRepository;
private TagRepositoryInterface $tagRepository;
private CurrencyRepositoryInterface $currencyRepository;
private TransactionTypeRepositoryInterface $typeRepository;
private User $user;
private ParsedQuery $query;
private int $page;
private array $words;
private array $validOperators;
private GroupCollectorInterface $collector;
private float $startTime;
private Collection $modifiers; // obsolete
private Collection $operators;
/**
* BetterQuerySearch constructor.
@@ -68,7 +75,8 @@ class BetterQuerySearch implements SearchInterface
public function __construct()
{
Log::debug('Constructed BetterQuerySearch');
$this->modifiers = new Collection;
$this->modifiers = new Collection; // obsolete
$this->operators = new Collection;
$this->page = 1;
$this->words = [];
$this->validOperators = array_keys(config('firefly.search.operators'));
@@ -78,6 +86,8 @@ class BetterQuerySearch implements SearchInterface
$this->budgetRepository = app(BudgetRepositoryInterface::class);
$this->billRepository = app(BillRepositoryInterface::class);
$this->tagRepository = app(TagRepositoryInterface::class);
$this->currencyRepository = app(CurrencyRepositoryInterface::class);
$this->typeRepository = app(TransactionTypeRepositoryInterface::class);
}
/**
@@ -86,7 +96,16 @@ class BetterQuerySearch implements SearchInterface
*/
public function getModifiers(): Collection
{
return $this->modifiers;
die(__METHOD__);
}
/**
* @inheritDoc
* @codeCoverageIgnore
*/
public function getOperators(): Collection
{
return $this->operators;
}
/**
@@ -98,6 +117,14 @@ class BetterQuerySearch implements SearchInterface
return implode(' ', $this->words);
}
/**
* @return array
*/
public function getWords(): array
{
return $this->words;
}
/**
* @inheritDoc
* @codeCoverageIgnore
@@ -154,9 +181,13 @@ class BetterQuerySearch implements SearchInterface
/**
* @inheritDoc
* @throws FireflyException
*/
public function searchTransactions(): LengthAwarePaginator
{
if (0 === count($this->getWords()) && 0 === count($this->getOperators())) {
throw new FireflyException('Search query is empty.');
}
return $this->collector->getPaginatedGroups();
}
@@ -197,11 +228,14 @@ class BetterQuerySearch implements SearchInterface
$value = $searchNode->getNode()->getValue();
// must be valid operator:
if (in_array($operator, $this->validOperators, true)) {
$this->updateCollector($operator, $value);
$this->modifiers->push([
'type' => $operator,
'value' => $value,
]);
if ($this->updateCollector($operator, $value)) {
$this->operators->push(
[
'type' => $operator,
'value' => $value,
]
);
}
}
break;
}
@@ -211,12 +245,16 @@ class BetterQuerySearch implements SearchInterface
/**
* @param string $operator
* @param string $value
* @return bool
* @throws FireflyException
*/
private function updateCollector(string $operator, string $value): void
private function updateCollector(string $operator, string $value): bool
{
Log::debug(sprintf('updateCollector(%s, %s)', $operator, $value));
$allAccounts = new Collection;
// check if alias, replace if necessary:
$operator = $this->getRootOperator($operator);
switch ($operator) {
default:
Log::error(sprintf('No such operator: %s', $operator));
@@ -224,94 +262,228 @@ class BetterQuerySearch implements SearchInterface
// some search operators are ignored, basically:
case 'user_action':
Log::info(sprintf('Ignore search operator "%s"', $operator));
return false;
//
// all account related searches:
//
case 'source_account_starts':
$this->searchAccount($value, 1, 1);
break;
case 'from_account_starts':
$this->fromAccountStarts($value);
case 'source_account_ends':
$this->searchAccount($value, 1, 2);
break;
case 'from_account_ends':
$this->fromAccountEnds($value);
case 'source_account_is':
$this->searchAccount($value, 1, 4);
break;
case 'from_account_contains':
case 'from':
case 'source':
// source can only be asset, liability or revenue account:
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE];
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25);
if ($accounts->count() > 0) {
$allAccounts = $accounts->merge($allAccounts);
case 'source_account_nr_starts':
$this->searchAccountNr($value, 1, 1);
break;
case 'source_account_nr_ends':
$this->searchAccountNr($value, 1, 2);
break;
case 'source_account_nr_is':
$this->searchAccountNr($value, 1, 4);
break;
case 'source_account_nr_contains':
$this->searchAccountNr($value, 1, 3);
break;
case 'source_account_contains':
$this->searchAccount($value, 1, 3);
break;
case 'source_account_id':
$account = $this->accountRepository->findNull((int)$value);
if(null !== $account) {
$this->collector->setSourceAccounts(new Collection([$account]));
}
$this->collector->setSourceAccounts($allAccounts);
break;
case 'to':
case 'destination':
// source can only be asset, liability or expense account:
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE];
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25);
if ($accounts->count() > 0) {
$allAccounts = $accounts->merge($allAccounts);
case 'destination_account_starts':
$this->searchAccount($value, 2, 1);
break;
case 'destination_account_ends':
$this->searchAccount($value, 2, 2);
break;
case 'destination_account_nr_starts':
$this->searchAccountNr($value, 2, 1);
break;
case 'destination_account_nr_ends':
$this->searchAccountNr($value, 2, 2);
break;
case 'destination_account_nr_is':
$this->searchAccountNr($value, 2, 4);
break;
case 'destination_account_is':
$this->searchAccount($value, 2, 4);
break;
case 'destination_account_nr_contains':
$this->searchAccountNr($value, 2, 3);
break;
case 'destination_account_contains':
$this->searchAccount($value, 2, 3);
break;
case 'destination_account_id':
$account = $this->accountRepository->findNull((int)$value);
if(null !== $account) {
$this->collector->setDestinationAccounts(new Collection([$account]));
}
$this->collector->setDestinationAccounts($allAccounts);
break;
case 'category':
case 'account_id':
$account = $this->accountRepository->findNull((int)$value);
if(null !== $account) {
$this->collector->setAccounts(new Collection([$account]));
}
break;
//
// description
//
case 'description_starts':
$this->collector->descriptionStarts([$value]);
break;
case 'description_ends':
$this->collector->descriptionEnds([$value]);
break;
case 'description_contains':
$this->words[] = $value;
return false;
case 'description_is':
$this->collector->descriptionIs($value);
break;
//
// currency
//
case 'currency_is':
$currency = $this->findCurrency($value);
if (null !== $currency) {
$this->collector->setCurrency($currency);
}
break;
case 'foreign_currency_is':
$currency = $this->findCurrency($value);
if (null !== $currency) {
$this->collector->setForeignCurrency($currency);
}
break;
//
// attachments
//
case 'has_attachments':
Log::debug('Set collector to filter on attachments.');
$this->collector->hasAttachments();
break;
//
// categories
case 'has_no_category':
$this->collector->withoutCategory();
break;
case 'has_any_category':
$this->collector->withCategory();
break;
case 'category_is':
$result = $this->categoryRepository->searchCategory($value, 25);
if ($result->count() > 0) {
$this->collector->setCategories($result);
}
break;
//
// budgets
//
case 'has_no_budget':
$this->collector->withoutBudget();
break;
case 'has_any_budget':
$this->collector->withBudget();
break;
case 'budget':
case 'budget_is':
$result = $this->budgetRepository->searchBudget($value, 25);
if ($result->count() > 0) {
$this->collector->setBudgets($result);
}
break;
//
// bill
//
case 'bill':
case 'bill_is':
$result = $this->billRepository->searchBill($value, 25);
if ($result->count() > 0) {
$this->collector->setBills($result);
}
break;
//
// tags
//
case 'has_no_tag':
$this->collector->withoutTags();
break;
case 'has_any_tag':
$this->collector->hasAnyTag();
break;
case 'tag':
$result = $this->tagRepository->searchTag($value);
if ($result->count() > 0) {
$this->collector->setTags($result);
}
break;
case 'budget':
$result = $this->budgetRepository->searchBudget($value, 25);
if ($result->count() > 0) {
$this->collector->setBudgets($result);
}
//
// notes
//
case 'notes_contain':
$this->collector->notesContain($value);
break;
case 'amount_is':
case 'amount':
case 'notes_start':
$this->collector->notesStartWith($value);
break;
case 'notes_end':
$this->collector->notesEndWith($value);
break;
case 'notes_are':
$this->collector->notesExactly($value);
break;
case 'no_notes':
$this->collector->withoutNotes();
break;
case 'any_notes':
$this->collector->withAnyNotes();
break;
//
// amount
//
case 'amount_exactly':
$amount = app('steam')->positive((string) $value);
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount));
$this->collector->amountIs($amount);
break;
case 'amount_max':
case 'amount_less':
$amount = app('steam')->positive((string) $value);
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount));
$this->collector->amountLess($amount);
break;
case 'amount_min':
case 'amount_more':
$amount = app('steam')->positive((string) $value);
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount));
$this->collector->amountMore($amount);
break;
case 'type':
//
// transaction type
//
case 'transaction_type':
$this->collector->setTypes([ucfirst($value)]);
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value));
break;
case 'date':
case 'on':
//
// dates
//
case 'date_is':
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value));
$start = new Carbon($value);
$this->collector->setRange($start, $start);
break;
case 'date_before':
case 'before':
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value));
$before = new Carbon($value);
$this->collector->setBefore($before);
break;
case 'date_after':
case 'after':
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value));
$after = new Carbon($value);
$this->collector->setAfter($after);
@@ -326,6 +498,9 @@ class BetterQuerySearch implements SearchInterface
$updatedAt = new Carbon($value);
$this->collector->setUpdatedAt($updatedAt);
break;
//
// other fields
//
case 'external_id':
$this->collector->setExternalId($value);
break;
@@ -333,55 +508,159 @@ class BetterQuerySearch implements SearchInterface
$this->collector->setInternalReference($value);
break;
}
return true;
}
/**
* searchDirection: 1 = source (default), 2 = destination
* stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is
* @param string $value
* @param int $searchDirection
* @param int $stringPosition
*/
private function fromAccountStarts(string $value): void
private function searchAccount(string $value, int $searchDirection, int $stringPosition): void
{
Log::debug(sprintf('fromAccountStarts(%s)', $value));
// source can only be asset, liability or revenue account:
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE];
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25);
Log::debug(sprintf('searchAccount(%s, %d, %d)', $value, $stringPosition, $searchDirection));
// search direction (default): for source accounts
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE];
$collectorMethod = 'setSourceAccounts';
// search direction: for destination accounts
if (2 === $searchDirection) {
// destination can be
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE];
$collectorMethod = 'setDestinationAccounts';
}
// string position (default): starts with:
$stringMethod = 'str_starts_with';
// string position: ends with:
if (2 === $stringPosition) {
$stringMethod = 'str_ends_with';
}
if (3 === $stringPosition) {
$stringMethod = 'str_contains';
}
if (4 === $stringPosition) {
$stringMethod = 'str_is_equal';
}
// get accounts:
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25);
if (0 === $accounts->count()) {
Log::debug('Found zero, return.');
Log::debug('Found zero accounts, do nothing.');
return;
}
Log::debug(sprintf('Found %d, filter.', $accounts->count()));
$filtered = $accounts->filter(function (Account $account) use ($value) {
return str_starts_with($account->name, $value);
Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count()));
$filtered = $accounts->filter(function (Account $account) use ($value, $stringMethod) {
return $stringMethod(strtolower($account->name), strtolower($value));
});
if (0 === $filtered->count()) {
Log::debug('Left with zero accounts, return.');
return;
}
Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod));
$this->collector->$collectorMethod($filtered);
}
/**
* searchDirection: 1 = source (default), 2 = destination
* stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is
* @param string $value
* @param int $searchDirection
* @param int $stringPosition
*/
private function searchAccountNr(string $value, int $searchDirection, int $stringPosition): void
{
Log::debug(sprintf('searchAccountNr(%s, %d, %d)', $value, $searchDirection, $stringPosition));
// search direction (default): for source accounts
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE];
$collectorMethod = 'setSourceAccounts';
// search direction: for destination accounts
if (2 === $searchDirection) {
// destination can be
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE];
$collectorMethod = 'setDestinationAccounts';
}
// string position (default): starts with:
$stringMethod = 'str_starts_with';
// string position: ends with:
if (2 === $stringPosition) {
$stringMethod = 'str_ends_with';
}
if (3 === $stringPosition) {
$stringMethod = 'str_contains';
}
if (4 === $stringPosition) {
$stringMethod = 'str_is_equal';
}
// search for accounts:
$accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 25);
if (0 === $accounts->count()) {
Log::debug('Found zero accounts, do nothing.');
return;
}
// if found, do filter
Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count()));
$filtered = $accounts->filter(function (Account $account) use ($value, $stringMethod) {
// either IBAN or account number!
$ibanMatch = $stringMethod(strtolower($account->iban), strtolower($value));
$accountNrMatch = false;
/** @var AccountMeta $meta */
foreach ($account->accountMeta as $meta) {
if ('account_number' === $meta->name && $stringMethod(strtolower($meta->data), strtolower($value))) {
$accountNrMatch = true;
}
}
return $ibanMatch || $accountNrMatch;
});
if (0 === $filtered->count()) {
Log::debug('Left with zero, return.');
return;
}
Log::debug(sprintf('Left with %d, set.', $accounts->count()));
$this->collector->setSourceAccounts($filtered);
Log::debug('Left with zero accounts, return.');
$this->collector->$collectorMethod($filtered);
}
/**
* @param string $value
* @return TransactionCurrency|null
*/
private function fromAccountEnds(string $value): void
private function findCurrency(string $value): ?TransactionCurrency
{
Log::debug(sprintf('fromAccountEnds(%s)', $value));
// source can only be asset, liability or revenue account:
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE];
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25);
if (0 === $accounts->count()) {
Log::debug('Found zero, return.');
return;
$result = $this->currencyRepository->findByCodeNull($value);
if (null === $result) {
$result = $this->currencyRepository->findByNameNull($value);
}
Log::debug(sprintf('Found %d, filter.', $accounts->count()));
$filtered = $accounts->filter(function (Account $account) use ($value) {
return str_ends_with($account->name, $value);
});
if (0 === $filtered->count()) {
Log::debug('Left with zero, return.');
return;
}
Log::debug(sprintf('Left with %d, set.', $accounts->count()));
$this->collector->setSourceAccounts($filtered);
return $result;
}
/**
* @param string $operator
* @return string
*/
private function getRootOperator(string $operator): string
{
$config = config(sprintf('firefly.search.operators.%s', $operator));
if (null === $config) {
throw new FireflyException(sprintf('No configuration for search operator "%s"', $operator));
}
if (true === $config['alias']) {
Log::debug(sprintf('"%s" is an alias for "%s", so return that instead.', $operator, $config['alias_for']));
return $config['alias_for'];
}
Log::debug(sprintf('"%s" is not an alias.', $operator));
return $operator;
}
}

View File

@@ -36,6 +36,11 @@ interface SearchInterface
*/
public function getModifiers(): Collection;
/**
* @return Collection
*/
public function getOperators(): Collection;
/**
* @return string
*/

View File

@@ -41,6 +41,7 @@ use Log;
* 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.
*
* @deprecated
*/
class RuleEngine
{
@@ -50,18 +51,12 @@ class RuleEngine
public const TRIGGER_UPDATE = 2;
/** @var int */
public const TRIGGER_BOTH = 3;
/** @var bool */
private $allRules;
/** @var RuleGroupRepository */
private $ruleGroupRepository;
/** @var Collection */
private $ruleGroups;
/** @var array */
private $rulesToApply;
/** @var int */
private $triggerMode;
/** @var User */
private $user;
private bool $allRules;
private RuleGroupRepository $ruleGroupRepository;
private Collection $ruleGroups;
private array $rulesToApply;
private int $triggerMode;
private User $user;
/**
* RuleEngine constructor.
@@ -230,7 +225,7 @@ class RuleEngine
$validTrigger = ('store-journal' === $trigger->trigger_value && self::TRIGGER_STORE === $this->triggerMode)
|| ('update-journal' === $trigger->trigger_value && self::TRIGGER_UPDATE === $this->triggerMode)
|| $this->triggerMode === self::TRIGGER_BOTH;
|| $this->triggerMode === self::TRIGGER_BOTH;
return $validTrigger && ($this->allRules || in_array($rule->id, $this->rulesToApply, true)) && true === $rule->active;
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* RuleEngineInterface.php
* Copyright (c) 2020 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\TransactionRules\Engine;
use Illuminate\Support\Collection;
/**
* Interface RuleEngineInterface
*/
interface RuleEngineInterface
{
public function setRules(Collection $rules): void;
public function setRuleGroups(Collection $ruleGroups): void;
}

View File

@@ -52,6 +52,18 @@ if (!function_exists('envNonEmpty')) {
}
}
if (!function_exists('str_is_equal')) {
/**
* @param string $left
* @param string $right
* @return bool
*/
function str_is_equal(string $left, string $right): bool
{
return $left === $right;
}
}
$app = new Illuminate\Foundation\Application(
realpath(__DIR__ . '/../')
);

View File

@@ -92,14 +92,6 @@ use FireflyIII\TransactionRules\Triggers\DescriptionEnds;
use FireflyIII\TransactionRules\Triggers\DescriptionIs;
use FireflyIII\TransactionRules\Triggers\DescriptionStarts;
use FireflyIII\TransactionRules\Triggers\ForeignCurrencyIs;
use FireflyIII\TransactionRules\Triggers\FromAccountContains;
use FireflyIII\TransactionRules\Triggers\FromAccountEnds;
use FireflyIII\TransactionRules\Triggers\FromAccountIs;
use FireflyIII\TransactionRules\Triggers\FromAccountNumberContains;
use FireflyIII\TransactionRules\Triggers\FromAccountNumberEnds;
use FireflyIII\TransactionRules\Triggers\FromAccountNumberIs;
use FireflyIII\TransactionRules\Triggers\FromAccountNumberStarts;
use FireflyIII\TransactionRules\Triggers\FromAccountStarts;
use FireflyIII\TransactionRules\Triggers\HasAnyBudget;
use FireflyIII\TransactionRules\Triggers\HasAnyCategory;
use FireflyIII\TransactionRules\Triggers\HasAnyTag;
@@ -114,14 +106,6 @@ use FireflyIII\TransactionRules\Triggers\NotesEmpty;
use FireflyIII\TransactionRules\Triggers\NotesEnd;
use FireflyIII\TransactionRules\Triggers\NotesStart;
use FireflyIII\TransactionRules\Triggers\TagIs;
use FireflyIII\TransactionRules\Triggers\ToAccountContains;
use FireflyIII\TransactionRules\Triggers\ToAccountEnds;
use FireflyIII\TransactionRules\Triggers\ToAccountIs;
use FireflyIII\TransactionRules\Triggers\ToAccountNumberContains;
use FireflyIII\TransactionRules\Triggers\ToAccountNumberEnds;
use FireflyIII\TransactionRules\Triggers\ToAccountNumberIs;
use FireflyIII\TransactionRules\Triggers\ToAccountNumberStarts;
use FireflyIII\TransactionRules\Triggers\ToAccountStarts;
use FireflyIII\TransactionRules\Triggers\TransactionType;
use FireflyIII\TransactionRules\Triggers\UserAction;
use FireflyIII\User;
@@ -498,95 +482,156 @@ return [
'search' => [
'operators' => [
'user_action' => ['alias' => false, 'trigger_class' => UserAction::class, 'needs_context' => true,],
'from_account_starts' => ['alias' => false, 'trigger_class' => FromAccountStarts::class, 'needs_context' => true,],
'from_account_ends' => ['alias' => false, 'trigger_class' => FromAccountEnds::class, 'needs_context' => true,],
'from_account_contains' => ['alias' => false, 'trigger_class' => FromAccountContains::class, 'needs_context' => true,],
'from_account_nr_starts' => ['alias' => false, 'trigger_class' => FromAccountNumberStarts::class, 'needs_context' => true,],
'from_account_nr_ends' => ['alias' => false, 'trigger_class' => FromAccountNumberEnds::class, 'needs_context' => true,],
'from_account_nr_is' => ['alias' => false, 'trigger_class' => FromAccountNumberIs::class, 'needs_context' => true,],
'from_account_nr_contains' => ['alias' => false, 'trigger_class' => FromAccountNumberContains::class, 'needs_context' => true,],
'to_account_starts' => ['alias' => false, 'trigger_class' => ToAccountStarts::class, 'needs_context' => true,],
'to_account_ends' => ['alias' => false, 'trigger_class' => ToAccountEnds::class, 'needs_context' => true,],
'to_account_contains' => ['alias' => false, 'trigger_class' => ToAccountContains::class, 'needs_context' => true,],
'to_account_nr_starts' => ['alias' => false, 'trigger_class' => ToAccountNumberStarts::class, 'needs_context' => true,],
'to_account_nr_ends' => ['alias' => false, 'trigger_class' => ToAccountNumberEnds::class, 'needs_context' => true,],
'to_account_nr_is' => ['alias' => false, 'trigger_class' => ToAccountNumberIs::class, 'needs_context' => true,],
'to_account_nr_contains' => ['alias' => false, 'trigger_class' => ToAccountNumberContains::class, 'needs_context' => true,],
'description_starts' => ['alias' => false, 'trigger_class' => DescriptionStarts::class, 'needs_context' => true,],
'description_ends' => ['alias' => false, 'trigger_class' => DescriptionEnds::class, 'needs_context' => true,],
'description_contains' => ['alias' => false, 'trigger_class' => DescriptionContains::class, 'needs_context' => true,],
'description_is' => ['alias' => false, 'trigger_class' => DescriptionIs::class, 'needs_context' => true,],
'currency_is' => ['alias' => false, 'trigger_class' => CurrencyIs::class, 'needs_context' => true,],
'foreign_currency_is' => ['alias' => false, 'trigger_class' => ForeignCurrencyIs::class, 'needs_context' => true,],
'has_attachments' => ['alias' => false, 'trigger_class' => HasAttachment::class, 'needs_context' => false,],
'has_no_category' => ['alias' => false, 'trigger_class' => HasNoCategory::class, 'needs_context' => false,],
'has_any_category' => ['alias' => false, 'trigger_class' => HasAnyCategory::class, 'needs_context' => false,],
'has_no_budget' => ['alias' => false, 'trigger_class' => HasNoBudget::class, 'needs_context' => false,],
'has_any_budget' => ['alias' => false, 'trigger_class' => HasAnyBudget::class, 'needs_context' => false,],
'has_no_tag' => ['alias' => false, 'trigger_class' => HasNoTag::class, 'needs_context' => false,],
'has_any_tag' => ['alias' => false, 'trigger_class' => HasAnyTag::class, 'needs_context' => false,],
'notes_contain' => ['alias' => false, 'trigger_class' => NotesContain::class, 'needs_context' => true,],
'notes_start' => ['alias' => false, 'trigger_class' => NotesStart::class, 'needs_context' => true,],
'notes_end' => ['alias' => false, 'trigger_class' => NotesEnd::class, 'needs_context' => true,],
'notes_are' => ['alias' => false, 'trigger_class' => NotesAre::class, 'needs_context' => true,],
'no_notes' => ['alias' => false, 'trigger_class' => NotesEmpty::class, 'needs_context' => false,],
'any_notes' => ['alias' => false, 'trigger_class' => NotesAny::class, 'needs_context' => false,],
'user_action' => ['alias' => false, 'trigger_class' => UserAction::class, 'needs_context' => true,],
'description_starts' => ['alias' => false, 'trigger_class' => DescriptionStarts::class, 'needs_context' => true,],
'description_ends' => ['alias' => false, 'trigger_class' => DescriptionEnds::class, 'needs_context' => true,],
'description_contains' => ['alias' => false, 'trigger_class' => DescriptionContains::class, 'needs_context' => true,],
'description_is' => ['alias' => false, 'trigger_class' => DescriptionIs::class, 'needs_context' => true,],
'currency_is' => ['alias' => false, 'trigger_class' => CurrencyIs::class, 'needs_context' => true,],
'foreign_currency_is' => ['alias' => false, 'trigger_class' => ForeignCurrencyIs::class, 'needs_context' => true,],
'has_attachments' => ['alias' => false, 'trigger_class' => HasAttachment::class, 'needs_context' => false,],
'has_no_category' => ['alias' => false, 'trigger_class' => HasNoCategory::class, 'needs_context' => false,],
'has_any_category' => ['alias' => false, 'trigger_class' => HasAnyCategory::class, 'needs_context' => false,],
'has_no_budget' => ['alias' => false, 'trigger_class' => HasNoBudget::class, 'needs_context' => false,],
'has_any_budget' => ['alias' => false, 'trigger_class' => HasAnyBudget::class, 'needs_context' => false,],
'has_no_tag' => ['alias' => false, 'trigger_class' => HasNoTag::class, 'needs_context' => false,],
'has_any_tag' => ['alias' => false, 'trigger_class' => HasAnyTag::class, 'needs_context' => false,],
'notes_contain' => ['alias' => false, 'trigger_class' => NotesContain::class, 'needs_context' => true,],
'notes_start' => ['alias' => false, 'trigger_class' => NotesStart::class, 'needs_context' => true,],
'notes_end' => ['alias' => false, 'trigger_class' => NotesEnd::class, 'needs_context' => true,],
'notes_are' => ['alias' => false, 'trigger_class' => NotesAre::class, 'needs_context' => true,],
'no_notes' => ['alias' => false, 'trigger_class' => NotesEmpty::class, 'needs_context' => false,],
'any_notes' => ['alias' => false, 'trigger_class' => NotesAny::class, 'needs_context' => false,],
// exact amount
'amount_exactly' => ['alias' => false, 'trigger_class' => AmountExactly::class, 'needs_context' => true,],
'amount_is' => ['alias' => true, 'alias_for' => 'amount_exactly', 'needs_context' => true,],
'amount' => ['alias' => true, 'alias_for' => 'amount_exactly', 'needs_context' => true,],
'amount_exactly' => ['alias' => false, 'trigger_class' => AmountExactly::class, 'needs_context' => true,],
'amount_is' => ['alias' => true, 'alias_for' => 'amount_exactly', 'needs_context' => true,],
'amount' => ['alias' => true, 'alias_for' => 'amount_exactly', 'needs_context' => true,],
// is less than
'amount_less' => ['alias' => false, 'trigger_class' => AmountLess::class, 'needs_context' => true,],
'amount_max' => ['alias' => true, 'alias_for' => 'amount_less', 'needs_context' => true,],
'amount_less' => ['alias' => false, 'trigger_class' => AmountLess::class, 'needs_context' => true,],
'amount_max' => ['alias' => true, 'alias_for' => 'amount_less', 'needs_context' => true,],
// is more than
'amount_more' => ['alias' => false, 'trigger_class' => AmountMore::class, 'needs_context' => true,],
'amount_min' => ['alias' => true, 'alias_for' => 'amount_more', 'needs_context' => true,],
'amount_more' => ['alias' => false, 'trigger_class' => AmountMore::class, 'needs_context' => true,],
'amount_min' => ['alias' => true, 'alias_for' => 'amount_more', 'needs_context' => true,],
// source account
'from_account_is' => ['alias' => false, 'trigger_class' => FromAccountIs::class, 'needs_context' => true,],
'source' => ['alias' => true, 'alias_for' => 'from_account_is', 'needs_context' => true,],
'from' => ['alias' => true, 'alias_for' => 'from_account_is', 'needs_context' => true,],
// source account name is + alias:
'source_account_is' => ['alias' => false, 'trigger_class' => SourceAccountIs::class, 'needs_context' => true,],
'from_account_is' => ['alias' => true, 'alias_for' => 'source_account_is', 'needs_context' => true,],
// destination account
'to_account_is' => ['alias' => false, 'trigger_class' => ToAccountIs::class, 'needs_context' => true,],
'destination' => ['alias' => true, 'alias_for' => 'to_account_is', 'needs_context' => true,],
'to' => ['alias' => true, 'alias_for' => 'to_account_is', 'needs_context' => true,],
// source account name contains + alias
'source_account_contains' => ['alias' => false, 'trigger_class' => SourceAccountContains::class, 'needs_context' => true,],
'from_account_contains' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true,],
'source' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true,],
'from' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true,],
// source account name starts with + alias
'source_account_starts' => ['alias' => false, 'trigger_class' => SourceAccountStarts::class, 'needs_context' => true,],
'from_account_starts' => ['alias' => true, 'alias_for' => 'source_account_starts', 'needs_context' => true,],
// source account name ends with + alias
'source_account_ends' => ['alias' => false, 'trigger_class' => SourceAccountEnds::class, 'needs_context' => true,],
'from_account_ends' => ['alias' => true, 'alias_for' => 'source_account_ends', 'needs_context' => true,],
// source account ID + alias
'source_account_id' => ['alias' => false, 'trigger_class' => SourceAccountIdIs::class, 'needs_context' => true,],
'from_account_id' => ['alias' => true, 'alias_for' => 'source_account_id', 'needs_context' => true,],
// source account number is
'source_account_nr_is' => ['alias' => false, 'trigger_class' => SourceAccountNumberIs::class, 'needs_context' => true,],
'from_account_nr_is' => ['alias' => true, 'alias_for' => 'source_account_nr_is', 'needs_context' => true,],
// source account number contains
'source_account_nr_contains' => ['alias' => false, 'trigger_class' => SourceAccountNumberContains::class, 'needs_context' => true,],
'from_account_nr_contains' => ['alias' => true, 'alias_for' => 'source_account_nr_contains', 'needs_context' => true,],
// source account number starts with
'source_account_nr_starts' => ['alias' => false, 'trigger_class' => SourceAccountNumberStarts::class, 'needs_context' => true,],
'from_account_nr_starts' => ['alias' => true, 'alias_for' => 'source_account_nr_starts', 'needs_context' => true,],
// source account number ends with
'source_account_nr_ends' => ['alias' => false, 'trigger_class' => SourceAccountNumberEnds::class, 'needs_context' => true,],
'from_account_nr_ends' => ['alias' => true, 'alias_for' => 'source_account_nr_ends', 'needs_context' => true,],
// destination account name is + alias
'destination_account_is' => ['alias' => false, 'trigger_class' => DestinationAccountIs::class, 'needs_context' => true,],
'to_account_is' => ['alias' => true, 'alias_for' => 'destination_account_is', 'needs_context' => true,],
// destination account name contains + alias
'destination_account_contains' => ['alias' => false, 'trigger_class' => DestinationAccountContains::class, 'needs_context' => true,],
'to_account_contains' => ['alias' => true, 'alias_for' => 'destination_account_contains', 'needs_context' => true,],
'destination' => ['alias' => true, 'alias_for' => 'destination_account_contains', 'needs_context' => true,],
'to' => ['alias' => true, 'alias_for' => 'destination_account_contains', 'needs_context' => true,],
// destination account name starts with + alias
'destination_account_starts' => ['alias' => false, 'trigger_class' => DestinationAccountStarts::class, 'needs_context' => true,],
'to_account_starts' => ['alias' => true, 'alias_for' => 'destination_account_starts', 'needs_context' => true,],
// destination account name ends with + alias
'destination_account_ends' => ['alias' => false, 'trigger_class' => DestinationAccountEnds::class, 'needs_context' => true,],
'to_account_ends' => ['alias' => true, 'alias_for' => 'destination_account_ends', 'needs_context' => true,],
// destination account ID + alias
'destination_account_id' => ['alias' => false, 'trigger_class' => DestinationAccountIdIs::class, 'needs_context' => true,],
'to_account_id' => ['alias' => true, 'alias_for' => 'destination_account_id', 'needs_context' => true,],
// destination account number is
'destination_account_nr_is' => ['alias' => false, 'trigger_class' => DestinationAccountNumberIs::class, 'needs_context' => true,],
'to_account_nr_is' => ['alias' => true, 'alias_for' => 'destination_account_nr_is', 'needs_context' => true,],
// destination account number contains
'destination_account_nr_contains' => ['alias' => false, 'trigger_class' => DestinationAccountNumberContains::class, 'needs_context' => true,],
'to_account_nr_contains' => ['alias' => true, 'alias_for' => 'destination_account_nr_contains', 'needs_context' => true,],
// destination account number starts with
'destination_account_nr_starts' => ['alias' => false, 'trigger_class' => DestinationAccountNumberStarts::class, 'needs_context' => true,],
'to_account_nr_starts' => ['alias' => true, 'alias_for' => 'destination_account_nr_starts', 'needs_context' => true,],
// destination account number ends with
'destination_account_nr_ends' => ['alias' => false, 'trigger_class' => DestinationAccountNumberEnds::class, 'needs_context' => true,],
'to_account_nr_ends' => ['alias' => true, 'alias_for' => 'destination_account_nr_ends', 'needs_context' => true,],
// any account id is
'account_id' => ['alias' => false, 'trigger_class' => AccountIdIs::class, 'needs_context' => true,], // TODO
// category
'category_is' => ['alias' => false, 'trigger_class' => CategoryIs::class, 'needs_context' => true,],
'category' => ['alias' => true, 'alias_for' => 'category_is', 'needs_context' => true,],
'category_is' => ['alias' => false, 'trigger_class' => CategoryIs::class, 'needs_context' => true,],
'category' => ['alias' => true, 'alias_for' => 'category_is', 'needs_context' => true,],
// budget
'budget_is' => ['alias' => false, 'trigger_class' => BudgetIs::class, 'needs_context' => true,],
'budget' => ['alias' => true, 'alias_for' => 'budget_is', 'needs_context' => true,],
'budget_is' => ['alias' => false, 'trigger_class' => BudgetIs::class, 'needs_context' => true,],
'budget' => ['alias' => true, 'alias_for' => 'budget_is', 'needs_context' => true,],
// bill
'bill_is' => ['alias' => false, 'trigger_class' => BillIs::class, 'needs_context' => true,], // TODO
'bill' => ['alias' => true, 'alias_for' => 'bill_is', 'needs_context' => true,],
'bill_is' => ['alias' => false, 'trigger_class' => BillIs::class, 'needs_context' => true,], // TODO
'bill' => ['alias' => true, 'alias_for' => 'bill_is', 'needs_context' => true,],
// type
'transaction_type' => ['alias' => false, 'trigger_class' => TransactionType::class, 'needs_context' => true,],
'type' => ['alias' => true, 'alias_for' => 'transaction_type', 'needs_context' => true,],
'transaction_type' => ['alias' => false, 'trigger_class' => TransactionType::class, 'needs_context' => true,],
'type' => ['alias' => true, 'alias_for' => 'transaction_type', 'needs_context' => true,],
// date:
'date_is' => ['alias' => false, 'trigger_class' => DateIs::class, 'needs_context' => true,],
'date' => ['alias' => true, 'alias_for' => 'date_is', 'needs_context' => true,],
'on' => ['alias' => true, 'alias_for' => 'date_is', 'needs_context' => true,],
'date_before' => ['alias' => false, 'trigger_class' => DateBefore::class, 'needs_context' => true,],
'before' => ['alias' => true, 'alias_for' => 'date_before', 'needs_context' => true,],
'date_after' => ['alias' => false, 'trigger_class' => DateAfter::class, 'needs_context' => true,],
'after' => ['alias' => true, 'alias_for' => 'date_after', 'needs_context' => true,],
'date_is' => ['alias' => false, 'trigger_class' => DateIs::class, 'needs_context' => true,],
'date' => ['alias' => true, 'alias_for' => 'date_is', 'needs_context' => true,],
'on' => ['alias' => true, 'alias_for' => 'date_is', 'needs_context' => true,],
'date_before' => ['alias' => false, 'trigger_class' => DateBefore::class, 'needs_context' => true,],
'before' => ['alias' => true, 'alias_for' => 'date_before', 'needs_context' => true,],
'date_after' => ['alias' => false, 'trigger_class' => DateAfter::class, 'needs_context' => true,],
'after' => ['alias' => true, 'alias_for' => 'date_after', 'needs_context' => true,],
// other interesting fields
'tag_is' => ['alias' => false, 'trigger_class' => TagIs::class, 'needs_context' => true,],
'tag' => ['alias' => true, 'alias_for' => 'tag', 'needs_context' => true,],
'created_on' => ['alias' => false, 'trigger_class' => CreatedOn::class, 'needs_context' => true,], // TODO
'updated_on' => ['alias' => false, 'trigger_class' => UpdatedOn::class, 'needs_context' => true,], // TODO
'external_id' => ['alias' => false, 'trigger_class' => ExternalId::class, 'needs_context' => true,], // TODO
'internal_reference' => ['alias' => false, 'trigger_class' => InternalReference::class, 'needs_context' => true,], // TODO
'tag_is' => ['alias' => false, 'trigger_class' => TagIs::class, 'needs_context' => true,],
'tag' => ['alias' => true, 'alias_for' => 'tag', 'needs_context' => true,],
'created_on' => ['alias' => false, 'trigger_class' => CreatedOn::class, 'needs_context' => true,],
'created_at' => ['alias' => true, 'alias_for' => 'created_on', 'needs_context' => true,],
'updated_on' => ['alias' => false, 'trigger_class' => UpdatedOn::class, 'needs_context' => true,],
'updated_at' => ['alias' => true, 'alias_for' => 'updated_on', 'needs_context' => true,],
'external_id' => ['alias' => false, 'trigger_class' => ExternalId::class, 'needs_context' => true,],
'internal_reference' => ['alias' => false, 'trigger_class' => InternalReference::class, 'needs_context' => true,],
],
],

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false">
<listeners>
<listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener" />
</listeners>
<logging>
<log type="coverage-clover" target="./storage/build/clover-all.xml" />
</logging>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
</php>
<testsuites>
<!--
<testsuite name="Api">
<directory suffix="Test.php">./tests/Api</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
-->
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ phpunit.xml
~ Copyright (c) 2020 james@firefly-iii.org
@@ -20,20 +18,27 @@
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<phpunit backupGlobals="false"
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
stopOnFailure="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false">
<listeners>
<listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener" />
</listeners>
<testsuites>
<!--
processIsolation="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
<listeners>
<listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener"/>
</listeners>
<testsuites>
<!--
<testsuite name="Api">
<directory suffix="Test.php">./tests/Api</directory>
</testsuite>
@@ -41,19 +46,14 @@
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
-->
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
</php>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
</php>
</phpunit>

View File

@@ -40,6 +40,9 @@ class BillFactoryTest extends TestCase
*/
public function setUp(): void
{
self::markTestIncomplete('Incomplete for refactor.');
return;
parent::setUp();
Log::info(sprintf('Now in %s.', get_class($this)));
}

File diff suppressed because it is too large Load Diff