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

View File

@@ -56,9 +56,6 @@ class AccountFactory
*/ */
public function __construct() 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->canHaveVirtual = [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD];
$this->accountRepository = app(AccountRepositoryInterface::class); $this->accountRepository = app(AccountRepositoryInterface::class);
$this->validAssetFields = ['account_role', 'account_number', 'currency_id', 'BIC', 'include_net_worth']; $this->validAssetFields = ['account_role', 'account_number', 'currency_id', 'BIC', 'include_net_worth'];

View File

@@ -34,18 +34,6 @@ use Log;
*/ */
class AccountMetaFactory 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 * @param array $data
* *

View File

@@ -36,21 +36,7 @@ use Log;
*/ */
class AttachmentFactory class AttachmentFactory
{ {
/** @var User */ private User $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)));
}
}
/** /**
* @param array $data * @param array $data

View File

@@ -62,7 +62,7 @@ trait AmountCollection
{ {
$this->query->where( $this->query->where(
function (EloquentBuilder $q) use ($amount) { 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( $this->query->where(
function (EloquentBuilder $q) use ($amount) { 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; 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. * Limit the search to a specific bill.
* *
@@ -185,6 +252,32 @@ trait MetaCollection
return $this; 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. * Will include bill name + ID, if any.
* *
@@ -272,11 +365,20 @@ trait MetaCollection
public function withoutBudget(): GroupCollectorInterface public function withoutBudget(): GroupCollectorInterface
{ {
$this->withBudgetInformation(); $this->withBudgetInformation();
$this->query->where( $this->query->whereNull('budget_transaction_journal.budget_id');
function (EloquentBuilder $q) {
$q->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; return $this;
} }
@@ -289,11 +391,20 @@ trait MetaCollection
public function withoutCategory(): GroupCollectorInterface public function withoutCategory(): GroupCollectorInterface
{ {
$this->withCategoryInformation(); $this->withCategoryInformation();
$this->query->where( $this->query->whereNull('category_transaction_journal.category_id');
function (EloquentBuilder $q) {
$q->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; return $this;
} }

View File

@@ -130,7 +130,7 @@ class GroupCollector implements GroupCollectorInterface
*/ */
public function dumpQuery(): void public function dumpQuery(): void
{ {
echo $this->query->toSql(); echo $this->query->select($this->fields)->toSql();
echo '<pre>'; echo '<pre>';
print_r($this->query->getBindings()); print_r($this->query->getBindings());
echo '</pre>'; echo '</pre>';
@@ -232,6 +232,16 @@ class GroupCollector implements GroupCollectorInterface
return $this; 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. * Limit the result to a specific transaction group.
* *
@@ -326,6 +336,79 @@ class GroupCollector implements GroupCollectorInterface
return $this; 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. * Limit the search to one specific transaction group.
@@ -417,6 +500,20 @@ class GroupCollector implements GroupCollectorInterface
return $array; 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. * Join table to get attachment information.
*/ */
@@ -655,7 +752,7 @@ class GroupCollector implements GroupCollectorInterface
'transactions as source', 'transactions as source',
function (JoinClause $join) { function (JoinClause $join) {
$join->on('source.transaction_journal_id', '=', 'transaction_journals.id') $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')
->where('source.amount', '<', 0); ->where('source.amount', '<', 0);
} }
) )
// join destination transaction // join destination transaction
@@ -663,7 +760,7 @@ class GroupCollector implements GroupCollectorInterface
'transactions as destination', 'transactions as destination',
function (JoinClause $join) { function (JoinClause $join) {
$join->on('destination.transaction_journal_id', '=', 'transaction_journals.id') $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')
->where('destination.amount', '>', 0); ->where('destination.amount', '>', 0);
} }
) )
// left join transaction type. // left join transaction type.

View File

@@ -220,6 +220,15 @@ interface GroupCollectorInterface
*/ */
public function setCurrency(TransactionCurrency $currency): 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. * Set destination accounts.
* *
@@ -284,6 +293,33 @@ interface GroupCollectorInterface
*/ */
public function setSearchWords(array $array): 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. * Set source accounts.
* *
@@ -311,6 +347,16 @@ interface GroupCollectorInterface
*/ */
public function setTags(Collection $tags): 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. * Limit the search to one specific transaction group.
* *
@@ -377,6 +423,13 @@ interface GroupCollectorInterface
*/ */
public function withAttachmentInformation(): GroupCollectorInterface; public function withAttachmentInformation(): GroupCollectorInterface;
/**
* Has attachments
*
* @return GroupCollectorInterface
*/
public function hasAttachments(): GroupCollectorInterface;
/** /**
* Include bill name + ID. * Include bill name + ID.
* *
@@ -405,6 +458,42 @@ interface GroupCollectorInterface
*/ */
public function withNotes(): 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. * Add tag info.
* *
@@ -426,6 +515,20 @@ interface GroupCollectorInterface
*/ */
public function withoutCategory(): 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. * 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\Services\Internal\Update\AccountUpdateService;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use \Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Log; use Log;
use Storage; use Storage;
@@ -346,7 +347,7 @@ class AccountRepository implements AccountRepositoryInterface
return null; return null;
} }
if (1 === $result->count()) { if (1 === $result->count()) {
return (string)$result->first()->data; return (string) $result->first()->data;
} }
return null; return null;
} }
@@ -704,4 +705,38 @@ class AccountRepository implements AccountRepositoryInterface
$account->save(); $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; 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 * @param User $user
*/ */

View File

@@ -47,18 +47,7 @@ use Log;
*/ */
class CurrencyRepository implements CurrencyRepositoryInterface class CurrencyRepository implements CurrencyRepositoryInterface
{ {
/** @var User */ private User $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)));
}
}
/** /**
* @param TransactionCurrency $currency * @param TransactionCurrency $currency

View File

@@ -25,12 +25,16 @@ use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface;
use FireflyIII\User; use FireflyIII\User;
use Gdbots\QueryParser\Node\Field; use Gdbots\QueryParser\Node\Field;
use Gdbots\QueryParser\Node\Node; use Gdbots\QueryParser\Node\Node;
@@ -47,19 +51,22 @@ use Log;
*/ */
class BetterQuerySearch implements SearchInterface class BetterQuerySearch implements SearchInterface
{ {
private AccountRepositoryInterface $accountRepository; private AccountRepositoryInterface $accountRepository;
private BillRepositoryInterface $billRepository; private BillRepositoryInterface $billRepository;
private BudgetRepositoryInterface $budgetRepository; private BudgetRepositoryInterface $budgetRepository;
private CategoryRepositoryInterface $categoryRepository; private CategoryRepositoryInterface $categoryRepository;
private TagRepositoryInterface $tagRepository; private TagRepositoryInterface $tagRepository;
private User $user; private CurrencyRepositoryInterface $currencyRepository;
private ParsedQuery $query; private TransactionTypeRepositoryInterface $typeRepository;
private int $page; private User $user;
private array $words; private ParsedQuery $query;
private array $validOperators; private int $page;
private GroupCollectorInterface $collector; private array $words;
private float $startTime; private array $validOperators;
private Collection $modifiers; private GroupCollectorInterface $collector;
private float $startTime;
private Collection $modifiers; // obsolete
private Collection $operators;
/** /**
* BetterQuerySearch constructor. * BetterQuerySearch constructor.
@@ -68,7 +75,8 @@ class BetterQuerySearch implements SearchInterface
public function __construct() public function __construct()
{ {
Log::debug('Constructed BetterQuerySearch'); Log::debug('Constructed BetterQuerySearch');
$this->modifiers = new Collection; $this->modifiers = new Collection; // obsolete
$this->operators = new Collection;
$this->page = 1; $this->page = 1;
$this->words = []; $this->words = [];
$this->validOperators = array_keys(config('firefly.search.operators')); $this->validOperators = array_keys(config('firefly.search.operators'));
@@ -78,6 +86,8 @@ class BetterQuerySearch implements SearchInterface
$this->budgetRepository = app(BudgetRepositoryInterface::class); $this->budgetRepository = app(BudgetRepositoryInterface::class);
$this->billRepository = app(BillRepositoryInterface::class); $this->billRepository = app(BillRepositoryInterface::class);
$this->tagRepository = app(TagRepositoryInterface::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 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 implode(' ', $this->words);
} }
/**
* @return array
*/
public function getWords(): array
{
return $this->words;
}
/** /**
* @inheritDoc * @inheritDoc
* @codeCoverageIgnore * @codeCoverageIgnore
@@ -154,9 +181,13 @@ class BetterQuerySearch implements SearchInterface
/** /**
* @inheritDoc * @inheritDoc
* @throws FireflyException
*/ */
public function searchTransactions(): LengthAwarePaginator public function searchTransactions(): LengthAwarePaginator
{ {
if (0 === count($this->getWords()) && 0 === count($this->getOperators())) {
throw new FireflyException('Search query is empty.');
}
return $this->collector->getPaginatedGroups(); return $this->collector->getPaginatedGroups();
} }
@@ -197,11 +228,14 @@ class BetterQuerySearch implements SearchInterface
$value = $searchNode->getNode()->getValue(); $value = $searchNode->getNode()->getValue();
// must be valid operator: // must be valid operator:
if (in_array($operator, $this->validOperators, true)) { if (in_array($operator, $this->validOperators, true)) {
$this->updateCollector($operator, $value); if ($this->updateCollector($operator, $value)) {
$this->modifiers->push([ $this->operators->push(
'type' => $operator, [
'value' => $value, 'type' => $operator,
]); 'value' => $value,
]
);
}
} }
break; break;
} }
@@ -211,12 +245,16 @@ class BetterQuerySearch implements SearchInterface
/** /**
* @param string $operator * @param string $operator
* @param string $value * @param string $value
* @return bool
* @throws FireflyException * @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)); Log::debug(sprintf('updateCollector(%s, %s)', $operator, $value));
$allAccounts = new Collection;
// check if alias, replace if necessary:
$operator = $this->getRootOperator($operator);
switch ($operator) { switch ($operator) {
default: default:
Log::error(sprintf('No such operator: %s', $operator)); Log::error(sprintf('No such operator: %s', $operator));
@@ -224,94 +262,228 @@ class BetterQuerySearch implements SearchInterface
// some search operators are ignored, basically: // some search operators are ignored, basically:
case 'user_action': case 'user_action':
Log::info(sprintf('Ignore search operator "%s"', $operator)); Log::info(sprintf('Ignore search operator "%s"', $operator));
return false;
//
// all account related searches:
//
case 'source_account_starts':
$this->searchAccount($value, 1, 1);
break; break;
case 'from_account_starts': case 'source_account_ends':
$this->fromAccountStarts($value); $this->searchAccount($value, 1, 2);
break; break;
case 'from_account_ends': case 'source_account_is':
$this->fromAccountEnds($value); $this->searchAccount($value, 1, 4);
break; break;
case 'from_account_contains': case 'source_account_nr_starts':
case 'from': $this->searchAccountNr($value, 1, 1);
case 'source': break;
// source can only be asset, liability or revenue account: case 'source_account_nr_ends':
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE]; $this->searchAccountNr($value, 1, 2);
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25); break;
if ($accounts->count() > 0) { case 'source_account_nr_is':
$allAccounts = $accounts->merge($allAccounts); $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; break;
case 'to': case 'destination_account_starts':
case 'destination': $this->searchAccount($value, 2, 1);
// source can only be asset, liability or expense account: break;
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE]; case 'destination_account_ends':
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25); $this->searchAccount($value, 2, 2);
if ($accounts->count() > 0) { break;
$allAccounts = $accounts->merge($allAccounts); 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; 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); $result = $this->categoryRepository->searchCategory($value, 25);
if ($result->count() > 0) { if ($result->count() > 0) {
$this->collector->setCategories($result); $this->collector->setCategories($result);
} }
break; 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':
case 'bill_is':
$result = $this->billRepository->searchBill($value, 25); $result = $this->billRepository->searchBill($value, 25);
if ($result->count() > 0) { if ($result->count() > 0) {
$this->collector->setBills($result); $this->collector->setBills($result);
} }
break; break;
//
// tags
//
case 'has_no_tag':
$this->collector->withoutTags();
break;
case 'has_any_tag':
$this->collector->hasAnyTag();
break;
case 'tag': case 'tag':
$result = $this->tagRepository->searchTag($value); $result = $this->tagRepository->searchTag($value);
if ($result->count() > 0) { if ($result->count() > 0) {
$this->collector->setTags($result); $this->collector->setTags($result);
} }
break; break;
case 'budget': //
$result = $this->budgetRepository->searchBudget($value, 25); // notes
if ($result->count() > 0) { //
$this->collector->setBudgets($result); case 'notes_contain':
} $this->collector->notesContain($value);
break; break;
case 'amount_is': case 'notes_start':
case 'amount': $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); $amount = app('steam')->positive((string) $value);
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount));
$this->collector->amountIs($amount); $this->collector->amountIs($amount);
break; break;
case 'amount_max':
case 'amount_less': case 'amount_less':
$amount = app('steam')->positive((string) $value); $amount = app('steam')->positive((string) $value);
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount));
$this->collector->amountLess($amount); $this->collector->amountLess($amount);
break; break;
case 'amount_min':
case 'amount_more': case 'amount_more':
$amount = app('steam')->positive((string) $value); $amount = app('steam')->positive((string) $value);
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount));
$this->collector->amountMore($amount); $this->collector->amountMore($amount);
break; break;
case 'type': //
// transaction type
//
case 'transaction_type':
$this->collector->setTypes([ucfirst($value)]); $this->collector->setTypes([ucfirst($value)]);
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value));
break; break;
case 'date': //
case 'on': // dates
//
case 'date_is':
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value));
$start = new Carbon($value); $start = new Carbon($value);
$this->collector->setRange($start, $start); $this->collector->setRange($start, $start);
break; break;
case 'date_before': case 'date_before':
case 'before':
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value));
$before = new Carbon($value); $before = new Carbon($value);
$this->collector->setBefore($before); $this->collector->setBefore($before);
break; break;
case 'date_after': case 'date_after':
case 'after':
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value));
$after = new Carbon($value); $after = new Carbon($value);
$this->collector->setAfter($after); $this->collector->setAfter($after);
@@ -326,6 +498,9 @@ class BetterQuerySearch implements SearchInterface
$updatedAt = new Carbon($value); $updatedAt = new Carbon($value);
$this->collector->setUpdatedAt($updatedAt); $this->collector->setUpdatedAt($updatedAt);
break; break;
//
// other fields
//
case 'external_id': case 'external_id':
$this->collector->setExternalId($value); $this->collector->setExternalId($value);
break; break;
@@ -333,55 +508,159 @@ class BetterQuerySearch implements SearchInterface
$this->collector->setInternalReference($value); $this->collector->setInternalReference($value);
break; break;
} }
return true;
} }
/** /**
* searchDirection: 1 = source (default), 2 = destination
* stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is
* @param string $value * @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)); Log::debug(sprintf('searchAccount(%s, %d, %d)', $value, $stringPosition, $searchDirection));
// source can only be asset, liability or revenue account:
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE]; // search direction (default): for source accounts
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25); $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()) { if (0 === $accounts->count()) {
Log::debug('Found zero, return.'); Log::debug('Found zero accounts, do nothing.');
return; return;
} }
Log::debug(sprintf('Found %d, filter.', $accounts->count())); Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count()));
$filtered = $accounts->filter(function (Account $account) use ($value) { $filtered = $accounts->filter(function (Account $account) use ($value, $stringMethod) {
return str_starts_with($account->name, $value); 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()) { if (0 === $filtered->count()) {
Log::debug('Left with zero, return.'); Log::debug('Left with zero, return.');
return; return;
} }
Log::debug(sprintf('Left with %d, set.', $accounts->count())); Log::debug('Left with zero accounts, return.');
$this->collector->setSourceAccounts($filtered); $this->collector->$collectorMethod($filtered);
} }
/** /**
* @param string $value * @param string $value
* @return TransactionCurrency|null
*/ */
private function fromAccountEnds(string $value): void private function findCurrency(string $value): ?TransactionCurrency
{ {
Log::debug(sprintf('fromAccountEnds(%s)', $value)); $result = $this->currencyRepository->findByCodeNull($value);
// source can only be asset, liability or revenue account: if (null === $result) {
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE]; $result = $this->currencyRepository->findByNameNull($value);
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25);
if (0 === $accounts->count()) {
Log::debug('Found zero, return.');
return;
} }
Log::debug(sprintf('Found %d, filter.', $accounts->count())); return $result;
$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);
} }
/**
* @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; public function getModifiers(): Collection;
/**
* @return Collection
*/
public function getOperators(): Collection;
/** /**
* @return string * @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). * 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. * Then call process() to make the magic happen.
* *
* @deprecated
*/ */
class RuleEngine class RuleEngine
{ {
@@ -50,18 +51,12 @@ class RuleEngine
public const TRIGGER_UPDATE = 2; public const TRIGGER_UPDATE = 2;
/** @var int */ /** @var int */
public const TRIGGER_BOTH = 3; public const TRIGGER_BOTH = 3;
/** @var bool */ private bool $allRules;
private $allRules; private RuleGroupRepository $ruleGroupRepository;
/** @var RuleGroupRepository */ private Collection $ruleGroups;
private $ruleGroupRepository; private array $rulesToApply;
/** @var Collection */ private int $triggerMode;
private $ruleGroups; private User $user;
/** @var array */
private $rulesToApply;
/** @var int */
private $triggerMode;
/** @var User */
private $user;
/** /**
* RuleEngine constructor. * RuleEngine constructor.
@@ -230,7 +225,7 @@ class RuleEngine
$validTrigger = ('store-journal' === $trigger->trigger_value && self::TRIGGER_STORE === $this->triggerMode) $validTrigger = ('store-journal' === $trigger->trigger_value && self::TRIGGER_STORE === $this->triggerMode)
|| ('update-journal' === $trigger->trigger_value && self::TRIGGER_UPDATE === $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; 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( $app = new Illuminate\Foundation\Application(
realpath(__DIR__ . '/../') realpath(__DIR__ . '/../')
); );

View File

@@ -92,14 +92,6 @@ use FireflyIII\TransactionRules\Triggers\DescriptionEnds;
use FireflyIII\TransactionRules\Triggers\DescriptionIs; use FireflyIII\TransactionRules\Triggers\DescriptionIs;
use FireflyIII\TransactionRules\Triggers\DescriptionStarts; use FireflyIII\TransactionRules\Triggers\DescriptionStarts;
use FireflyIII\TransactionRules\Triggers\ForeignCurrencyIs; 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\HasAnyBudget;
use FireflyIII\TransactionRules\Triggers\HasAnyCategory; use FireflyIII\TransactionRules\Triggers\HasAnyCategory;
use FireflyIII\TransactionRules\Triggers\HasAnyTag; use FireflyIII\TransactionRules\Triggers\HasAnyTag;
@@ -114,14 +106,6 @@ use FireflyIII\TransactionRules\Triggers\NotesEmpty;
use FireflyIII\TransactionRules\Triggers\NotesEnd; use FireflyIII\TransactionRules\Triggers\NotesEnd;
use FireflyIII\TransactionRules\Triggers\NotesStart; use FireflyIII\TransactionRules\Triggers\NotesStart;
use FireflyIII\TransactionRules\Triggers\TagIs; 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\TransactionType;
use FireflyIII\TransactionRules\Triggers\UserAction; use FireflyIII\TransactionRules\Triggers\UserAction;
use FireflyIII\User; use FireflyIII\User;
@@ -498,95 +482,156 @@ return [
'search' => [ 'search' => [
'operators' => [ 'operators' => [
'user_action' => ['alias' => false, 'trigger_class' => UserAction::class, 'needs_context' => true,], 'user_action' => ['alias' => false, 'trigger_class' => UserAction::class, 'needs_context' => true,],
'from_account_starts' => ['alias' => false, 'trigger_class' => FromAccountStarts::class, 'needs_context' => true,], 'description_starts' => ['alias' => false, 'trigger_class' => DescriptionStarts::class, 'needs_context' => true,],
'from_account_ends' => ['alias' => false, 'trigger_class' => FromAccountEnds::class, 'needs_context' => true,], 'description_ends' => ['alias' => false, 'trigger_class' => DescriptionEnds::class, 'needs_context' => true,],
'from_account_contains' => ['alias' => false, 'trigger_class' => FromAccountContains::class, 'needs_context' => true,], 'description_contains' => ['alias' => false, 'trigger_class' => DescriptionContains::class, 'needs_context' => true,],
'from_account_nr_starts' => ['alias' => false, 'trigger_class' => FromAccountNumberStarts::class, 'needs_context' => true,], 'description_is' => ['alias' => false, 'trigger_class' => DescriptionIs::class, 'needs_context' => true,],
'from_account_nr_ends' => ['alias' => false, 'trigger_class' => FromAccountNumberEnds::class, 'needs_context' => true,], 'currency_is' => ['alias' => false, 'trigger_class' => CurrencyIs::class, 'needs_context' => true,],
'from_account_nr_is' => ['alias' => false, 'trigger_class' => FromAccountNumberIs::class, 'needs_context' => true,], 'foreign_currency_is' => ['alias' => false, 'trigger_class' => ForeignCurrencyIs::class, 'needs_context' => true,],
'from_account_nr_contains' => ['alias' => false, 'trigger_class' => FromAccountNumberContains::class, 'needs_context' => true,], 'has_attachments' => ['alias' => false, 'trigger_class' => HasAttachment::class, 'needs_context' => false,],
'to_account_starts' => ['alias' => false, 'trigger_class' => ToAccountStarts::class, 'needs_context' => true,], 'has_no_category' => ['alias' => false, 'trigger_class' => HasNoCategory::class, 'needs_context' => false,],
'to_account_ends' => ['alias' => false, 'trigger_class' => ToAccountEnds::class, 'needs_context' => true,], 'has_any_category' => ['alias' => false, 'trigger_class' => HasAnyCategory::class, 'needs_context' => false,],
'to_account_contains' => ['alias' => false, 'trigger_class' => ToAccountContains::class, 'needs_context' => true,], 'has_no_budget' => ['alias' => false, 'trigger_class' => HasNoBudget::class, 'needs_context' => false,],
'to_account_nr_starts' => ['alias' => false, 'trigger_class' => ToAccountNumberStarts::class, 'needs_context' => true,], 'has_any_budget' => ['alias' => false, 'trigger_class' => HasAnyBudget::class, 'needs_context' => false,],
'to_account_nr_ends' => ['alias' => false, 'trigger_class' => ToAccountNumberEnds::class, 'needs_context' => true,], 'has_no_tag' => ['alias' => false, 'trigger_class' => HasNoTag::class, 'needs_context' => false,],
'to_account_nr_is' => ['alias' => false, 'trigger_class' => ToAccountNumberIs::class, 'needs_context' => true,], 'has_any_tag' => ['alias' => false, 'trigger_class' => HasAnyTag::class, 'needs_context' => false,],
'to_account_nr_contains' => ['alias' => false, 'trigger_class' => ToAccountNumberContains::class, 'needs_context' => true,], 'notes_contain' => ['alias' => false, 'trigger_class' => NotesContain::class, 'needs_context' => true,],
'description_starts' => ['alias' => false, 'trigger_class' => DescriptionStarts::class, 'needs_context' => true,], 'notes_start' => ['alias' => false, 'trigger_class' => NotesStart::class, 'needs_context' => true,],
'description_ends' => ['alias' => false, 'trigger_class' => DescriptionEnds::class, 'needs_context' => true,], 'notes_end' => ['alias' => false, 'trigger_class' => NotesEnd::class, 'needs_context' => true,],
'description_contains' => ['alias' => false, 'trigger_class' => DescriptionContains::class, 'needs_context' => true,], 'notes_are' => ['alias' => false, 'trigger_class' => NotesAre::class, 'needs_context' => true,],
'description_is' => ['alias' => false, 'trigger_class' => DescriptionIs::class, 'needs_context' => true,], 'no_notes' => ['alias' => false, 'trigger_class' => NotesEmpty::class, 'needs_context' => false,],
'currency_is' => ['alias' => false, 'trigger_class' => CurrencyIs::class, 'needs_context' => true,], 'any_notes' => ['alias' => false, 'trigger_class' => NotesAny::class, 'needs_context' => false,],
'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 // exact amount
'amount_exactly' => ['alias' => false, 'trigger_class' => AmountExactly::class, '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_is' => ['alias' => true, 'alias_for' => 'amount_exactly', 'needs_context' => true,],
'amount' => ['alias' => true, 'alias_for' => 'amount_exactly', 'needs_context' => true,], 'amount' => ['alias' => true, 'alias_for' => 'amount_exactly', 'needs_context' => true,],
// is less than // is less than
'amount_less' => ['alias' => false, 'trigger_class' => AmountLess::class, '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,], 'amount_max' => ['alias' => true, 'alias_for' => 'amount_less', 'needs_context' => true,],
// is more than // is more than
'amount_more' => ['alias' => false, 'trigger_class' => AmountMore::class, '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,], 'amount_min' => ['alias' => true, 'alias_for' => 'amount_more', 'needs_context' => true,],
// source account // source account name is + alias:
'from_account_is' => ['alias' => false, 'trigger_class' => FromAccountIs::class, 'needs_context' => true,], 'source_account_is' => ['alias' => false, 'trigger_class' => SourceAccountIs::class, 'needs_context' => true,],
'source' => ['alias' => true, 'alias_for' => 'from_account_is', 'needs_context' => true,], 'from_account_is' => ['alias' => true, 'alias_for' => 'source_account_is', 'needs_context' => true,],
'from' => ['alias' => true, 'alias_for' => 'from_account_is', 'needs_context' => true,],
// destination account // source account name contains + alias
'to_account_is' => ['alias' => false, 'trigger_class' => ToAccountIs::class, 'needs_context' => true,], 'source_account_contains' => ['alias' => false, 'trigger_class' => SourceAccountContains::class, 'needs_context' => true,],
'destination' => ['alias' => true, 'alias_for' => 'to_account_is', 'needs_context' => true,], 'from_account_contains' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true,],
'to' => ['alias' => true, 'alias_for' => 'to_account_is', '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
'category_is' => ['alias' => false, 'trigger_class' => CategoryIs::class, 'needs_context' => true,], 'category_is' => ['alias' => false, 'trigger_class' => CategoryIs::class, 'needs_context' => true,],
'category' => ['alias' => true, 'alias_for' => 'category_is', 'needs_context' => true,], 'category' => ['alias' => true, 'alias_for' => 'category_is', 'needs_context' => true,],
// budget // budget
'budget_is' => ['alias' => false, 'trigger_class' => BudgetIs::class, 'needs_context' => true,], 'budget_is' => ['alias' => false, 'trigger_class' => BudgetIs::class, 'needs_context' => true,],
'budget' => ['alias' => true, 'alias_for' => 'budget_is', 'needs_context' => true,], 'budget' => ['alias' => true, 'alias_for' => 'budget_is', 'needs_context' => true,],
// bill // bill
'bill_is' => ['alias' => false, 'trigger_class' => BillIs::class, 'needs_context' => true,], // TODO 'bill_is' => ['alias' => false, 'trigger_class' => BillIs::class, 'needs_context' => true,], // TODO
'bill' => ['alias' => true, 'alias_for' => 'bill_is', 'needs_context' => true,], 'bill' => ['alias' => true, 'alias_for' => 'bill_is', 'needs_context' => true,],
// type // type
'transaction_type' => ['alias' => false, 'trigger_class' => TransactionType::class, 'needs_context' => true,], 'transaction_type' => ['alias' => false, 'trigger_class' => TransactionType::class, 'needs_context' => true,],
'type' => ['alias' => true, 'alias_for' => 'transaction_type', 'needs_context' => true,], 'type' => ['alias' => true, 'alias_for' => 'transaction_type', 'needs_context' => true,],
// date: // date:
'date_is' => ['alias' => false, 'trigger_class' => DateIs::class, 'needs_context' => true,], 'date_is' => ['alias' => false, 'trigger_class' => DateIs::class, 'needs_context' => true,],
'date' => ['alias' => true, 'alias_for' => 'date_is', 'needs_context' => true,], 'date' => ['alias' => true, 'alias_for' => 'date_is', 'needs_context' => true,],
'on' => ['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,], 'date_before' => ['alias' => false, 'trigger_class' => DateBefore::class, 'needs_context' => true,],
'before' => ['alias' => true, 'alias_for' => 'date_before', 'needs_context' => true,], 'before' => ['alias' => true, 'alias_for' => 'date_before', 'needs_context' => true,],
'date_after' => ['alias' => false, 'trigger_class' => DateAfter::class, 'needs_context' => true,], 'date_after' => ['alias' => false, 'trigger_class' => DateAfter::class, 'needs_context' => true,],
'after' => ['alias' => true, 'alias_for' => 'date_after', 'needs_context' => true,], 'after' => ['alias' => true, 'alias_for' => 'date_after', 'needs_context' => true,],
// other interesting fields // other interesting fields
'tag_is' => ['alias' => false, 'trigger_class' => TagIs::class, 'needs_context' => true,], 'tag_is' => ['alias' => false, 'trigger_class' => TagIs::class, 'needs_context' => true,],
'tag' => ['alias' => true, 'alias_for' => 'tag', '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 'created_on' => ['alias' => false, 'trigger_class' => CreatedOn::class, 'needs_context' => true,],
'external_id' => ['alias' => false, 'trigger_class' => ExternalId::class, 'needs_context' => true,], // TODO 'created_at' => ['alias' => true, 'alias_for' => 'created_on', 'needs_context' => true,],
'internal_reference' => ['alias' => false, 'trigger_class' => InternalReference::class, 'needs_context' => true,], // TODO
'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"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- <!--
~ phpunit.xml ~ phpunit.xml
~ Copyright (c) 2020 james@firefly-iii.org ~ Copyright (c) 2020 james@firefly-iii.org
@@ -20,20 +18,27 @@
~ You should have received a copy of the GNU Affero General Public License ~ 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/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<phpunit backupGlobals="false" backupGlobals="false"
backupStaticAttributes="false" backupStaticAttributes="false"
bootstrap="vendor/autoload.php" bootstrap="vendor/autoload.php"
colors="true" colors="true"
convertErrorsToExceptions="true" convertErrorsToExceptions="true"
stopOnFailure="true"
convertNoticesToExceptions="true" convertNoticesToExceptions="true"
convertWarningsToExceptions="true" convertWarningsToExceptions="true"
processIsolation="false"> processIsolation="false"
<listeners> xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener" /> <coverage processUncoveredFiles="true">
</listeners> <include>
<testsuites> <directory suffix=".php">./app</directory>
<!-- </include>
</coverage>
<listeners>
<listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener"/>
</listeners>
<testsuites>
<!--
<testsuite name="Api"> <testsuite name="Api">
<directory suffix="Test.php">./tests/Api</directory> <directory suffix="Test.php">./tests/Api</directory>
</testsuite> </testsuite>
@@ -41,19 +46,14 @@
<directory suffix="Test.php">./tests/Feature</directory> <directory suffix="Test.php">./tests/Feature</directory>
</testsuite> </testsuite>
--> -->
<testsuite name="Unit"> <testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory> <directory suffix="Test.php">./tests/Unit</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<filter> <php>
<whitelist processUncoveredFilesFromWhitelist="true"> <env name="APP_ENV" value="testing"/>
<directory suffix=".php">./app</directory> <env name="CACHE_DRIVER" value="array"/>
</whitelist> <env name="SESSION_DRIVER" value="array"/>
</filter> <env name="QUEUE_DRIVER" value="sync"/>
<php> </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> </phpunit>

View File

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

File diff suppressed because it is too large Load Diff