diff --git a/app/Helpers/Collector/GroupCollector.php b/app/Helpers/Collector/GroupCollector.php index 5030440e54..b5187c4729 100644 --- a/app/Helpers/Collector/GroupCollector.php +++ b/app/Helpers/Collector/GroupCollector.php @@ -162,166 +162,6 @@ class GroupCollector implements GroupCollectorInterface } - /** - * @param Collection $collection - * - * @return Collection - * @throws Exception - */ - private function parseArray(Collection $collection): Collection - { - $groups = []; - /** @var TransactionGroup $augmentedGroup */ - foreach ($collection as $augmentedGroup) { - $groupId = $augmentedGroup->transaction_group_id; - if (!isset($groups[$groupId])) { - // make new array - $groupArray = [ - 'id' => $augmentedGroup->transaction_group_id, - 'user_id' => $augmentedGroup->user_id, - 'title' => $augmentedGroup->transaction_group_title, - 'count' => 1, - 'sums' => [], - 'transactions' => [], - ]; - $journalId = (int)$augmentedGroup->transaction_journal_id; - $groupArray['transactions'][$journalId] = $this->parseAugmentedGroup($augmentedGroup); - $groups[$groupId] = $groupArray; - continue; - } - // or parse the rest. - $journalId = (int)$augmentedGroup->transaction_journal_id; - $groups[$groupId]['count']++; - - if (isset($groups[$groupId]['transactions'][$journalId])) { - $groups[$groupId]['transactions'][$journalId] = - $this->mergeTags($groups[$groupId]['transactions'][$journalId], $augmentedGroup); - } - - if (!isset($groups[$groupId]['transactions'][$journalId])) { - $groups[$groupId]['transactions'][$journalId] = $this->parseAugmentedGroup($augmentedGroup); - } - - } - $groups = $this->parseSums($groups); - - return new Collection($groups); - } - - /** - * @param TransactionGroup $augmentedGroup - * - * @return array - * @throws Exception - */ - private function parseAugmentedGroup(TransactionGroup $augmentedGroup): array - { - $result = $augmentedGroup->toArray(); - $result['tags'] = []; - $result['date'] = new Carbon($result['date']); - $result['created_at'] = new Carbon($result['created_at']); - $result['updated_at'] = new Carbon($result['updated_at']); - $result['reconciled'] = 1 === (int)$result['reconciled']; - if (isset($augmentedGroup['tag_id'])) { // assume the other fields are present as well. - $tagId = (int)$augmentedGroup['tag_id']; - $tagDate = null; - try { - $tagDate = Carbon::parse($augmentedGroup['tag_date']); - } catch (InvalidDateException $e) { - Log::debug(sprintf('Could not parse date: %s', $e->getMessage())); - } - - $result['tags'][$tagId] = [ - 'id' => (int)$result['tag_id'], - 'name' => $result['tag_name'], - 'date' => $tagDate, - 'description' => $result['tag_description'], - 'latitude' => $result['tag_latitude'], - 'longitude' => $result['tag_longitude'], - 'zoom_level' => $result['tag_zoom_level'], - ]; - } - - return $result; - } - - /** - * @param array $existingJournal - * @param TransactionGroup $newGroup - * @return array - */ - private function mergeTags(array $existingJournal, TransactionGroup $newGroup): array - { - $newArray = $newGroup->toArray(); - if (isset($newArray['tag_id'])) { // assume the other fields are present as well. - $tagId = (int)$newGroup['tag_id']; - - $tagDate = null; - try { - $tagDate = Carbon::parse($newArray['tag_date']); - } catch (InvalidDateException $e) { - Log::debug(sprintf('Could not parse date: %s', $e->getMessage())); - } - - $existingJournal['tags'][$tagId] = [ - 'id' => (int)$newArray['tag_id'], - 'name' => $newArray['tag_name'], - 'date' => $tagDate, - 'description' => $newArray['tag_description'], - 'latitude' => $newArray['tag_latitude'], - 'longitude' => $newArray['tag_longitude'], - 'zoom_level' => $newArray['tag_zoom_level'], - ]; - } - - return $existingJournal; - } - - /** - * @param array $groups - * - * @return array - */ - private function parseSums(array $groups): array - { - /** - * @var int $groudId - * @var array $group - */ - foreach ($groups as $groudId => $group) { - /** @var array $transaction */ - foreach ($group['transactions'] as $transaction) { - $currencyId = (int)$transaction['currency_id']; - - // set default: - if (!isset($groups[$groudId]['sums'][$currencyId])) { - $groups[$groudId]['sums'][$currencyId]['currency_id'] = $currencyId; - $groups[$groudId]['sums'][$currencyId]['currency_code'] = $transaction['currency_code']; - $groups[$groudId]['sums'][$currencyId]['currency_symbol'] = $transaction['currency_symbol']; - $groups[$groudId]['sums'][$currencyId]['currency_decimal_places'] = $transaction['currency_decimal_places']; - $groups[$groudId]['sums'][$currencyId]['amount'] = '0'; - } - $groups[$groudId]['sums'][$currencyId]['amount'] = bcadd($groups[$groudId]['sums'][$currencyId]['amount'], $transaction['amount']); - - if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) { - $currencyId = (int)$transaction['foreign_currency_id']; - - // set default: - if (!isset($groups[$groudId]['sums'][$currencyId])) { - $groups[$groudId]['sums'][$currencyId]['currency_id'] = $currencyId; - $groups[$groudId]['sums'][$currencyId]['currency_code'] = $transaction['foreign_currency_code']; - $groups[$groudId]['sums'][$currencyId]['currency_symbol'] = $transaction['foreign_currency_symbol']; - $groups[$groudId]['sums'][$currencyId]['currency_decimal_places'] = $transaction['foreign_currency_decimal_places']; - $groups[$groudId]['sums'][$currencyId]['amount'] = '0'; - } - $groups[$groudId]['sums'][$currencyId]['amount'] = bcadd($groups[$groudId]['sums'][$currencyId]['amount'], $transaction['foreign_amount']); - } - } - } - - return $groups; - } - /** * Define which accounts can be part of the source and destination transactions. * @@ -355,12 +195,46 @@ class GroupCollector implements GroupCollectorInterface */ public function setBill(Bill $bill): GroupCollectorInterface { - $this->withBudgetInformation(); + $this->withBillInformation(); $this->query->where('transaction_journals.bill_id', '=', $bill->id); return $this; } + /** + * Will include bill name + ID, if any. + * + * @return GroupCollectorInterface + */ + public function withBillInformation(): GroupCollectorInterface + { + if (false === $this->hasBillInformation) { + // join bill table + $this->query->leftJoin('bills', 'bills.id', '=', 'transaction_journals.bill_id'); + // add fields + $this->fields[] = 'bills.id as bill_id'; + $this->fields[] = 'bills.name as bill_name'; + $this->hasBillInformation = true; + } + + return $this; + } + + /** + * Limit the search to a specific budget. + * + * @param Budget $budget + * + * @return GroupCollectorInterface + */ + public function setBudget(Budget $budget): GroupCollectorInterface + { + $this->withBudgetInformation(); + $this->query->where('budgets.id', $budget->id); + + return $this; + } + /** * Will include budget ID + name, if any. * @@ -382,21 +256,6 @@ class GroupCollector implements GroupCollectorInterface return $this; } - /** - * Limit the search to a specific budget. - * - * @param Budget $budget - * - * @return GroupCollectorInterface - */ - public function setBudget(Budget $budget): GroupCollectorInterface - { - $this->withBudgetInformation(); - $this->query->where('budgets.id', $budget->id); - - return $this; - } - /** * Limit the search to a specific set of budgets. * @@ -554,26 +413,6 @@ class GroupCollector implements GroupCollectorInterface return $this; } - /** - * Join table to get tag information. - */ - private function joinTagTables(): void - { - if (false === $this->hasJoinedTagTables) { - // join some extra tables: - $this->hasJoinedTagTables = true; - $this->query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); - $this->query->leftJoin('tags', 'tag_transaction_journal.tag_id', '=', 'tags.id'); - $this->fields[] = 'tags.id as tag_id'; - $this->fields[] = 'tags.tag as tag_name'; - $this->fields[] = 'tags.date as tag_date'; - $this->fields[] = 'tags.description as tag_description'; - $this->fields[] = 'tags.latitude as tag_latitude'; - $this->fields[] = 'tags.longitude as tag_longitude'; - $this->fields[] = 'tags.zoomLevel as tag_zoom_level'; - } - } - /** * Limit the search to one specific transaction group. * @@ -617,44 +456,6 @@ class GroupCollector implements GroupCollectorInterface return $this; } - /** - * Build the query. - */ - private function startQuery(): void - { - app('log')->debug('GroupCollector::startQuery'); - $this->query = $this->user - ->transactionGroups() - ->leftJoin('transaction_journals', 'transaction_journals.transaction_group_id', 'transaction_groups.id') - // join source transaction. - ->leftJoin( - 'transactions as source', function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id') - ->where('source.amount', '<', 0); - } - ) - // join destination transaction - ->leftJoin( - 'transactions as destination', function (JoinClause $join) { - $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id') - ->where('destination.amount', '>', 0); - } - ) - // left join transaction type. - ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->leftJoin('transaction_currencies as currency', 'currency.id', '=', 'source.transaction_currency_id') - ->leftJoin('transaction_currencies as foreign_currency', 'foreign_currency.id', '=', 'source.foreign_currency_id') - ->whereNull('transaction_groups.deleted_at') - ->whereNull('transaction_journals.deleted_at') - ->whereNull('source.deleted_at') - ->whereNull('destination.deleted_at') - ->orderBy('transaction_journals.date', 'DESC') - ->orderBy('transaction_journals.order', 'ASC') - ->orderBy('transaction_journals.id', 'DESC') - ->orderBy('transaction_journals.description', 'DESC') - ->orderBy('source.amount', 'DESC'); - } - /** * Automatically include all stuff required to make API calls work. * @@ -708,25 +509,6 @@ class GroupCollector implements GroupCollectorInterface return $this; } - /** - * Will include bill name + ID, if any. - * - * @return GroupCollectorInterface - */ - public function withBillInformation(): GroupCollectorInterface - { - if (false === $this->hasBillInformation) { - // join bill table - $this->query->leftJoin('bills', 'bills.id', '=', 'transaction_journals.bill_id'); - // add fields - $this->fields[] = 'bills.id as bill_id'; - $this->fields[] = 'bills.name as bill_name'; - $this->hasBillInformation = true; - } - - return $this; - } - /** * Limit the search to a specific bunch of categories. * @@ -793,6 +575,18 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * + */ + public function dumpQuery(): void + { + + echo $this->query->toSql(); + echo '
'; + print_r($this->query->getBindings()); + echo ''; + } + /** * Return the sum of all journals. * @@ -830,4 +624,370 @@ class GroupCollector implements GroupCollectorInterface return $return; } + + /** + * Search for words in descriptions. + * + * @param array $array + * + * @return GroupCollectorInterface + */ + public function setSearchWords(array $array): GroupCollectorInterface + { + $this->query->where( + function (EloquentBuilder $q) use ($array) { + $q->where( + function (EloquentBuilder $q1) use ($array) { + foreach ($array as $word) { + $keyword = sprintf('%%%s%%', $word); + $q1->where('transaction_journals.description', 'LIKE', $keyword); + } + } + ); + $q->orWhere( + function (EloquentBuilder $q2) use ($array) { + foreach ($array as $word) { + $keyword = sprintf('%%%s%%', $word); + $q2->where('transaction_groups.title', 'LIKE', $keyword); + } + } + ); + } + ); + + return $this; + } + + /** + * Limit the result to a specific transaction group. + * + * @param TransactionGroup $transactionGroup + * + * @return GroupCollectorInterface + */ + public function setGroup(TransactionGroup $transactionGroup): GroupCollectorInterface + { + $this->query->where('transaction_groups.id', $transactionGroup->id); + + return $this; + } + + /** + * Limit the search to a specific set of bills. + * + * @param Collection $bills + * + * @return GroupCollectorInterface + */ + public function setBills(Collection $bills): GroupCollectorInterface + { + $this->withBillInformation(); + $this->query->whereIn('transaction_journals.bill_id', $bills->pluck('id')->toArray()); + + return $this; + } + + /** + * Get transactions with a specific amount. + * + * @param string $amount + * + * @return GroupCollectorInterface + */ + public function amountIs(string $amount): GroupCollectorInterface + { + $this->query->where( + function (EloquentBuilder $q) use ($amount) { + $q->where('source.amount', app('steam')->negative($amount)); + } + ); + + return $this; + } + + /** + * Get transactions where the amount is less than. + * + * @param string $amount + * + * @return GroupCollectorInterface + */ + public function amountLess(string $amount): GroupCollectorInterface + { + $this->query->where( + function (EloquentBuilder $q) use ($amount) { + $q->where('destination.amount', '<', app('steam')->positive($amount)); + } + ); + + return $this; + } + + /** + * Get transactions where the amount is more than. + * + * @param string $amount + * + * @return GroupCollectorInterface + */ + public function amountMore(string $amount): GroupCollectorInterface + { + $this->query->where( + function (EloquentBuilder $q) use ($amount) { + $q->where('destination.amount', '>', app('steam')->positive($amount)); + } + ); + + return $this; + } + + /** + * Collect transactions before a specific date. + * + * @param Carbon $date + * + * @return GroupCollectorInterface + */ + public function setBefore(Carbon $date): GroupCollectorInterface + { + $beforeStr = $date->format('Y-m-d 00:00:00'); + $this->query->where('transaction_journals.date', '<=', $beforeStr); + Log::debug(sprintf('GroupCollector range is now before %s (inclusive)', $beforeStr)); + + return $this; + } + + /** + * Collect transactions after a specific date. + * + * @param Carbon $date + * + * @return GroupCollectorInterface + */ + public function setAfter(Carbon $date): GroupCollectorInterface + { + $afterStr = $date->format('Y-m-d 00:00:00'); + $this->query->where('transaction_journals.date', '>=', $afterStr); + Log::debug(sprintf('GroupCollector range is now after %s (inclusive)', $afterStr)); + + return $this; + } + + /** + * @param Collection $collection + * + * @return Collection + * @throws Exception + */ + private function parseArray(Collection $collection): Collection + { + $groups = []; + /** @var TransactionGroup $augmentedGroup */ + foreach ($collection as $augmentedGroup) { + $groupId = $augmentedGroup->transaction_group_id; + if (!isset($groups[$groupId])) { + // make new array + $groupArray = [ + 'id' => $augmentedGroup->transaction_group_id, + 'user_id' => $augmentedGroup->user_id, + 'title' => $augmentedGroup->transaction_group_title, + 'count' => 1, + 'sums' => [], + 'transactions' => [], + ]; + $journalId = (int)$augmentedGroup->transaction_journal_id; + $groupArray['transactions'][$journalId] = $this->parseAugmentedGroup($augmentedGroup); + $groups[$groupId] = $groupArray; + continue; + } + // or parse the rest. + $journalId = (int)$augmentedGroup->transaction_journal_id; + $groups[$groupId]['count']++; + + if (isset($groups[$groupId]['transactions'][$journalId])) { + $groups[$groupId]['transactions'][$journalId] = + $this->mergeTags($groups[$groupId]['transactions'][$journalId], $augmentedGroup); + } + + if (!isset($groups[$groupId]['transactions'][$journalId])) { + $groups[$groupId]['transactions'][$journalId] = $this->parseAugmentedGroup($augmentedGroup); + } + + } + $groups = $this->parseSums($groups); + + return new Collection($groups); + } + + /** + * @param TransactionGroup $augmentedGroup + * + * @return array + * @throws Exception + */ + private function parseAugmentedGroup(TransactionGroup $augmentedGroup): array + { + $result = $augmentedGroup->toArray(); + $result['tags'] = []; + $result['date'] = new Carbon($result['date']); + $result['created_at'] = new Carbon($result['created_at']); + $result['updated_at'] = new Carbon($result['updated_at']); + $result['reconciled'] = 1 === (int)$result['reconciled']; + if (isset($augmentedGroup['tag_id'])) { // assume the other fields are present as well. + $tagId = (int)$augmentedGroup['tag_id']; + $tagDate = null; + try { + $tagDate = Carbon::parse($augmentedGroup['tag_date']); + } catch (InvalidDateException $e) { + Log::debug(sprintf('Could not parse date: %s', $e->getMessage())); + } + + $result['tags'][$tagId] = [ + 'id' => (int)$result['tag_id'], + 'name' => $result['tag_name'], + 'date' => $tagDate, + 'description' => $result['tag_description'], + 'latitude' => $result['tag_latitude'], + 'longitude' => $result['tag_longitude'], + 'zoom_level' => $result['tag_zoom_level'], + ]; + } + + return $result; + } + + /** + * @param array $existingJournal + * @param TransactionGroup $newGroup + * @return array + */ + private function mergeTags(array $existingJournal, TransactionGroup $newGroup): array + { + $newArray = $newGroup->toArray(); + if (isset($newArray['tag_id'])) { // assume the other fields are present as well. + $tagId = (int)$newGroup['tag_id']; + + $tagDate = null; + try { + $tagDate = Carbon::parse($newArray['tag_date']); + } catch (InvalidDateException $e) { + Log::debug(sprintf('Could not parse date: %s', $e->getMessage())); + } + + $existingJournal['tags'][$tagId] = [ + 'id' => (int)$newArray['tag_id'], + 'name' => $newArray['tag_name'], + 'date' => $tagDate, + 'description' => $newArray['tag_description'], + 'latitude' => $newArray['tag_latitude'], + 'longitude' => $newArray['tag_longitude'], + 'zoom_level' => $newArray['tag_zoom_level'], + ]; + } + + return $existingJournal; + } + + /** + * @param array $groups + * + * @return array + */ + private function parseSums(array $groups): array + { + /** + * @var int $groudId + * @var array $group + */ + foreach ($groups as $groudId => $group) { + /** @var array $transaction */ + foreach ($group['transactions'] as $transaction) { + $currencyId = (int)$transaction['currency_id']; + + // set default: + if (!isset($groups[$groudId]['sums'][$currencyId])) { + $groups[$groudId]['sums'][$currencyId]['currency_id'] = $currencyId; + $groups[$groudId]['sums'][$currencyId]['currency_code'] = $transaction['currency_code']; + $groups[$groudId]['sums'][$currencyId]['currency_symbol'] = $transaction['currency_symbol']; + $groups[$groudId]['sums'][$currencyId]['currency_decimal_places'] = $transaction['currency_decimal_places']; + $groups[$groudId]['sums'][$currencyId]['amount'] = '0'; + } + $groups[$groudId]['sums'][$currencyId]['amount'] = bcadd($groups[$groudId]['sums'][$currencyId]['amount'], $transaction['amount']); + + if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) { + $currencyId = (int)$transaction['foreign_currency_id']; + + // set default: + if (!isset($groups[$groudId]['sums'][$currencyId])) { + $groups[$groudId]['sums'][$currencyId]['currency_id'] = $currencyId; + $groups[$groudId]['sums'][$currencyId]['currency_code'] = $transaction['foreign_currency_code']; + $groups[$groudId]['sums'][$currencyId]['currency_symbol'] = $transaction['foreign_currency_symbol']; + $groups[$groudId]['sums'][$currencyId]['currency_decimal_places'] = $transaction['foreign_currency_decimal_places']; + $groups[$groudId]['sums'][$currencyId]['amount'] = '0'; + } + $groups[$groudId]['sums'][$currencyId]['amount'] = bcadd($groups[$groudId]['sums'][$currencyId]['amount'], $transaction['foreign_amount']); + } + } + } + + return $groups; + } + + /** + * Join table to get tag information. + */ + private function joinTagTables(): void + { + if (false === $this->hasJoinedTagTables) { + // join some extra tables: + $this->hasJoinedTagTables = true; + $this->query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); + $this->query->leftJoin('tags', 'tag_transaction_journal.tag_id', '=', 'tags.id'); + $this->fields[] = 'tags.id as tag_id'; + $this->fields[] = 'tags.tag as tag_name'; + $this->fields[] = 'tags.date as tag_date'; + $this->fields[] = 'tags.description as tag_description'; + $this->fields[] = 'tags.latitude as tag_latitude'; + $this->fields[] = 'tags.longitude as tag_longitude'; + $this->fields[] = 'tags.zoomLevel as tag_zoom_level'; + } + } + + /** + * Build the query. + */ + private function startQuery(): void + { + app('log')->debug('GroupCollector::startQuery'); + $this->query = $this->user + ->transactionGroups() + ->leftJoin('transaction_journals', 'transaction_journals.transaction_group_id', 'transaction_groups.id') + // join source transaction. + ->leftJoin( + 'transactions as source', function (JoinClause $join) { + $join->on('source.transaction_journal_id', '=', 'transaction_journals.id') + ->where('source.amount', '<', 0); + } + ) + // join destination transaction + ->leftJoin( + 'transactions as destination', function (JoinClause $join) { + $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id') + ->where('destination.amount', '>', 0); + } + ) + // left join transaction type. + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin('transaction_currencies as currency', 'currency.id', '=', 'source.transaction_currency_id') + ->leftJoin('transaction_currencies as foreign_currency', 'foreign_currency.id', '=', 'source.foreign_currency_id') + ->whereNull('transaction_groups.deleted_at') + ->whereNull('transaction_journals.deleted_at') + ->whereNull('source.deleted_at') + ->whereNull('destination.deleted_at') + ->orderBy('transaction_journals.date', 'DESC') + ->orderBy('transaction_journals.order', 'ASC') + ->orderBy('transaction_journals.id', 'DESC') + ->orderBy('transaction_journals.description', 'DESC') + ->orderBy('source.amount', 'DESC'); + } } \ No newline at end of file diff --git a/app/Helpers/Collector/GroupCollectorInterface.php b/app/Helpers/Collector/GroupCollectorInterface.php index cb10f517c4..e9e358f73f 100644 --- a/app/Helpers/Collector/GroupCollectorInterface.php +++ b/app/Helpers/Collector/GroupCollectorInterface.php @@ -85,6 +85,42 @@ interface GroupCollectorInterface */ public function setBill(Bill $bill): GroupCollectorInterface; + /** + * Limit the search to a specific set of bills. + * + * @param Collection $bills + * + * @return GroupCollectorInterface + */ + public function setBills(Collection $bills): GroupCollectorInterface; + + /** + * Get transactions with a specific amount. + * + * @param string $amount + * + * @return GroupCollectorInterface + */ + public function amountIs(string $amount): GroupCollectorInterface; + + /** + * Get transactions where the amount is less than. + * + * @param string $amount + * + * @return GroupCollectorInterface + */ + public function amountLess(string $amount): GroupCollectorInterface; + + /** + * Get transactions where the amount is more than. + * + * @param string $amount + * + * @return GroupCollectorInterface + */ + public function amountMore(string $amount): GroupCollectorInterface; + /** * Limit the search to a specific budget. * @@ -123,7 +159,7 @@ interface GroupCollectorInterface public function setCurrency(TransactionCurrency $currency): GroupCollectorInterface; /** - * Limit the result to a set of specific journals. + * Limit the result to a set of specific transaction journals. * * @param array $journalIds * @@ -131,6 +167,24 @@ interface GroupCollectorInterface */ public function setJournalIds(array $journalIds): GroupCollectorInterface; + /** + * Limit the result to a specific transaction group. + * + * @param TransactionGroup $transactionGroup + * + * @return GroupCollectorInterface + */ + public function setGroup(TransactionGroup $transactionGroup): GroupCollectorInterface; + + /** + * Search for words in descriptions. + * + * @param array $array + * + * @return GroupCollectorInterface + */ + public function setSearchWords(array $array): GroupCollectorInterface; + /** * Limit the number of returned entries. * @@ -241,6 +295,24 @@ interface GroupCollectorInterface */ public function setCategories(Collection $categories): GroupCollectorInterface; + /** + * Collect transactions before a specific date. + * + * @param Carbon $date + * + * @return GroupCollectorInterface + */ + public function setBefore(Carbon $date): GroupCollectorInterface; + + /** + * Collect transactions after a specific date. + * + * @param Carbon $date + * + * @return GroupCollectorInterface + */ + public function setAfter(Carbon $date): GroupCollectorInterface; + /** * Include bill name + ID. * diff --git a/app/Http/Controllers/Chart/ExpenseReportController.php b/app/Http/Controllers/Chart/ExpenseReportController.php index 8d335b545a..fd2110201e 100644 --- a/app/Http/Controllers/Chart/ExpenseReportController.php +++ b/app/Http/Controllers/Chart/ExpenseReportController.php @@ -71,8 +71,8 @@ class ExpenseReportController extends Controller * * @param Collection $accounts * @param Collection $expense - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return JsonResponse * @@ -89,7 +89,7 @@ class ExpenseReportController extends Controller $cache->addProperty($start); $cache->addProperty($end); if ($cache->has()) { - return response()->json($cache->get()); // @codeCoverageIgnore + // return response()->json($cache->get()); // @codeCoverageIgnore } $format = app('navigation')->preferredCarbonLocalizedFormat($start, $end); @@ -97,44 +97,43 @@ class ExpenseReportController extends Controller $chartData = []; $currentStart = clone $start; $combined = $this->combineAccounts($expense); - // make "all" set: $all = new Collection; - foreach ($combined as $name => $combi) { - $all = $all->merge($combi); + foreach ($combined as $name => $combination) { + $all = $all->merge($combination); } // prep chart data: /** - * @var string $name - * @var Collection $combi + * @var string $name + * @var Collection $combination */ - foreach ($combined as $name => $combi) { + foreach ($combined as $name => $combination) { // first is always expense account: /** @var Account $exp */ - $exp = $combi->first(); + $exp = $combination->first(); $chartData[$exp->id . '-in'] = [ - 'label' => $name . ' (' . strtolower((string)trans('firefly.income')) . ')', + 'label' => sprintf('%s (%s)', $name, strtolower((string)trans('firefly.income'))), 'type' => 'bar', 'yAxisID' => 'y-axis-0', 'entries' => [], ]; $chartData[$exp->id . '-out'] = [ - 'label' => $name . ' (' . strtolower((string)trans('firefly.expenses')) . ')', + 'label' => sprintf('%s (%s)', $name, strtolower((string)trans('firefly.expenses'))), 'type' => 'bar', 'yAxisID' => 'y-axis-0', 'entries' => [], ]; // total in, total out: $chartData[$exp->id . '-total-in'] = [ - 'label' => $name . ' (' . strtolower((string)trans('firefly.sum_of_income')) . ')', + 'label' => sprintf('%s (%s)', $name, strtolower((string)trans('firefly.sum_of_income'))), 'type' => 'line', 'fill' => false, 'yAxisID' => 'y-axis-1', 'entries' => [], ]; $chartData[$exp->id . '-total-out'] = [ - 'label' => $name . ' (' . strtolower((string)trans('firefly.sum_of_expenses')) . ')', + 'label' => sprintf('%s (%s)', $name, strtolower((string)trans('firefly.sum_of_expenses'))), 'type' => 'line', 'fill' => false, 'yAxisID' => 'y-axis-1', @@ -154,15 +153,15 @@ class ExpenseReportController extends Controller $income = $this->groupByName($this->getIncomeForOpposing($accounts, $all, $currentStart, $currentEnd)); $label = $currentStart->formatLocalized($format); - foreach ($combined as $name => $combi) { + foreach ($combined as $name => $combination) { // first is always expense account: /** @var Account $exp */ - $exp = $combi->first(); + $exp = $combination->first(); $labelIn = $exp->id . '-in'; $labelOut = $exp->id . '-out'; $labelSumIn = $exp->id . '-total-in'; $labelSumOut = $exp->id . '-total-out'; - $currentIncome = $income[$name] ?? '0'; + $currentIncome = bcmul($income[$name] ?? '0', '-1'); $currentExpense = $expenses[$name] ?? '0'; // add to sum: @@ -180,6 +179,7 @@ class ExpenseReportController extends Controller /** @var Carbon $currentStart */ $currentStart = clone $currentEnd; $currentStart->addDay(); + $currentStart->startOfDay(); } // remove all empty entries to prevent cluttering: $newSet = []; diff --git a/app/Http/Controllers/Import/IndexController.php b/app/Http/Controllers/Import/IndexController.php index 38ffcbf7bb..372c4c0a3f 100644 --- a/app/Http/Controllers/Import/IndexController.php +++ b/app/Http/Controllers/Import/IndexController.php @@ -32,6 +32,7 @@ use Illuminate\Http\Response as LaravelResponse; use Log; /** + * TODO make sure all import methods work. * * Class IndexController */ diff --git a/app/Http/Controllers/Json/ReconcileController.php b/app/Http/Controllers/Json/ReconcileController.php index f98d2c076a..00ed10edb2 100644 --- a/app/Http/Controllers/Json/ReconcileController.php +++ b/app/Http/Controllers/Json/ReconcileController.php @@ -96,6 +96,7 @@ class ReconcileController extends Controller */ public function overview(Request $request, Account $account, Carbon $start, Carbon $end): JsonResponse { + if (AccountType::ASSET !== $account->accountType->type) { throw new FireflyException(sprintf('Account %s is not an asset account.', $account->name)); } @@ -107,6 +108,7 @@ class ReconcileController extends Controller $clearedAmount = '0'; $route = route('accounts.reconcile.submit', [$account->id, $start->format('Ymd'), $end->format('Ymd')]); // get sum of transaction amounts: + // TODO these methods no longer exist: $transactions = $this->repository->getTransactionsById($transactionIds); $cleared = $this->repository->getTransactionsById($clearedIds); $countCleared = 0; diff --git a/app/Http/Controllers/Report/CategoryController.php b/app/Http/Controllers/Report/CategoryController.php index b7efe0e0e2..d41d859e76 100644 --- a/app/Http/Controllers/Report/CategoryController.php +++ b/app/Http/Controllers/Report/CategoryController.php @@ -70,7 +70,7 @@ class CategoryController extends Controller // @codeCoverageIgnoreStart } catch (Throwable $e) { Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); - $result = 'An error prevented Firefly III from rendering. Apologies.'; + $result = sprintf('An error prevented Firefly III from rendering: %s. Apologies.', $e->getMessage()); } // @codeCoverageIgnoreEnd @@ -112,7 +112,7 @@ class CategoryController extends Controller // @codeCoverageIgnoreStart } catch (Throwable $e) { Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); - $result = 'An error prevented Firefly III from rendering. Apologies.'; + $result = sprintf('An error prevented Firefly III from rendering: %s. Apologies.', $e->getMessage()); } // @codeCoverageIgnoreEnd $cache->store($result); @@ -167,7 +167,7 @@ class CategoryController extends Controller // @codeCoverageIgnoreStart } catch (Throwable $e) { Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); - $result = 'An error prevented Firefly III from rendering. Apologies.'; + $result = sprintf('An error prevented Firefly III from rendering: %s. Apologies.', $e->getMessage()); } // @codeCoverageIgnoreEnd diff --git a/app/Http/Controllers/Report/ExpenseController.php b/app/Http/Controllers/Report/ExpenseController.php index d54486624b..4a09328033 100644 --- a/app/Http/Controllers/Report/ExpenseController.php +++ b/app/Http/Controllers/Report/ExpenseController.php @@ -23,9 +23,8 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Report; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\CacheProperties; @@ -70,8 +69,8 @@ class ExpenseController extends Controller * * @param Collection $accounts * @param Collection $expense - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -110,7 +109,7 @@ class ExpenseController extends Controller // @codeCoverageIgnoreStart } catch (Throwable $e) { Log::error(sprintf('Could not render category::budget: %s', $e->getMessage())); - $result = 'An error prevented Firefly III from rendering. Apologies.'; + $result = sprintf('An error prevented Firefly III from rendering: %s. Apologies.', $e->getMessage()); } // @codeCoverageIgnoreEnd $cache->store($result); @@ -125,8 +124,8 @@ class ExpenseController extends Controller * * @param Collection $accounts * @param Collection $expense - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -175,7 +174,7 @@ class ExpenseController extends Controller // @codeCoverageIgnoreStart } catch (Throwable $e) { Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); - $result = 'An error prevented Firefly III from rendering. Apologies.'; + $result = sprintf('An error prevented Firefly III from rendering: %s. Apologies.', $e->getMessage()); } // @codeCoverageIgnoreEnd $cache->store($result); @@ -189,8 +188,8 @@ class ExpenseController extends Controller * * @param Collection $accounts * @param Collection $expense - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return array|mixed|string */ @@ -227,7 +226,7 @@ class ExpenseController extends Controller // @codeCoverageIgnoreStart } catch (Throwable $e) { Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); - $result = 'An error prevented Firefly III from rendering. Apologies.'; + $result = sprintf('An error prevented Firefly III from rendering: %s. Apologies.', $e->getMessage()); } // @codeCoverageIgnoreEnd $cache->store($result); @@ -242,8 +241,8 @@ class ExpenseController extends Controller * * @param Collection $accounts * @param Collection $expense - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return string */ @@ -265,22 +264,23 @@ class ExpenseController extends Controller $all = $all->merge($combi); } // get all expenses in period: - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($accounts); - $collector->setOpposingAccounts($all); - $set = $collector->getTransactions(); - $sorted = $set->sortBy( - function (Transaction $transaction) { - return (float)$transaction->transaction_amount; - } - ); + $collector->setAccounts($all); + $set = $collector->getExtractedJournals(); + + usort($set, function ($a, $b) { + return $a['amount'] <=> $b['amount']; + }); + try { $result = view('reports.partials.top-transactions', compact('sorted'))->render(); // @codeCoverageIgnoreStart } catch (Throwable $e) { Log::error(sprintf('Could not render category::topExpense: %s', $e->getMessage())); - $result = 'An error prevented Firefly III from rendering. Apologies.'; + $result = sprintf('An error prevented Firefly III from rendering: %s. Apologies.', $e->getMessage()); } // @codeCoverageIgnoreEnd $cache->store($result); @@ -293,8 +293,8 @@ class ExpenseController extends Controller * * @param Collection $accounts * @param Collection $expense - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return mixed|string */ @@ -316,22 +316,24 @@ class ExpenseController extends Controller $all = $all->merge($combi); } // get all expenses in period: - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($accounts); - $collector->setOpposingAccounts($all); - $set = $collector->getTransactions(); - $sorted = $set->sortByDesc( - function (Transaction $transaction) { - return (float)$transaction->transaction_amount; - } - ); + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + $total = $accounts->merge($all); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($total); + $journals = $collector->getExtractedJournals(); + + usort($journals, function ($a, $b) { + return $a['amount'] <=> $b['amount']; + }); + try { $result = view('reports.partials.top-transactions', compact('sorted'))->render(); // @codeCoverageIgnoreStart } catch (Throwable $e) { Log::error(sprintf('Could not render category::topIncome: %s', $e->getMessage())); - $result = 'An error prevented Firefly III from rendering. Apologies.'; + $result = sprintf('An error prevented Firefly III from rendering: %s. Apologies.', $e->getMessage()); } // @codeCoverageIgnoreEnd $cache->store($result); diff --git a/app/Http/Controllers/RuleGroupController.php b/app/Http/Controllers/RuleGroupController.php index 8a99cd2ab7..c57a93bd2f 100644 --- a/app/Http/Controllers/RuleGroupController.php +++ b/app/Http/Controllers/RuleGroupController.php @@ -165,11 +165,12 @@ class RuleGroupController extends Controller /** * Execute the given rulegroup on a set of existing transactions. * - * @param SelectTransactionsRequest $request + * @param SelectTransactionsRequest $request * @param AccountRepositoryInterface $repository - * @param RuleGroup $ruleGroup + * @param RuleGroup $ruleGroup * * @return RedirectResponse + * @throws \Exception */ public function execute(SelectTransactionsRequest $request, AccountRepositoryInterface $repository, RuleGroup $ruleGroup): RedirectResponse { diff --git a/app/Http/Controllers/Transaction/MassController.php b/app/Http/Controllers/Transaction/MassController.php index e422ab27fc..5c5fda5422 100644 --- a/app/Http/Controllers/Transaction/MassController.php +++ b/app/Http/Controllers/Transaction/MassController.php @@ -25,7 +25,7 @@ namespace FireflyIII\Http\Controllers\Transaction; use Carbon\Carbon; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Filter\TransactionViewFilter; use FireflyIII\Helpers\Filter\TransferFilter; use FireflyIII\Http\Controllers\Controller; @@ -126,10 +126,13 @@ class MassController extends Controller * @param Collection $journals * * @return IlluminateView + * + * TODO rebuild this feature. + * @throws FireflyException */ public function edit(Collection $journals): IlluminateView { - throw new FireflyException('Needs refactor'); + throw new FireflyException(sprintf('The mass-editor is not available in v%s of Firefly III. Sorry about that. It will be back soon.', config('firefly.version'))); /** @var User $user */ $user = auth()->user(); $subTitle = (string)trans('firefly.mass_edit_journals'); @@ -148,8 +151,8 @@ class MassController extends Controller $transformer = app(TransactionTransformer::class); $transformer->setParameters(new ParameterBag); - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); $collector->setUser($user); $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); $collector->setJournals($journals); @@ -175,7 +178,7 @@ class MassController extends Controller /** * Mass update of journals. * - * @param MassEditJournalRequest $request + * @param MassEditJournalRequest $request * @param JournalRepositoryInterface $repository * * @return mixed diff --git a/app/Http/Controllers/Transaction/SplitController.php b/app/Http/Controllers/Transaction/SplitController.php deleted file mode 100644 index 1296058911..0000000000 --- a/app/Http/Controllers/Transaction/SplitController.php +++ /dev/null @@ -1,177 +0,0 @@ -. - */ -declare(strict_types=1); - -namespace FireflyIII\Http\Controllers\Transaction; - -use FireflyIII\Events\UpdatedTransactionGroup; -use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; -use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Http\Requests\SplitJournalFormRequest; -use FireflyIII\Models\TransactionJournal; -use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; -use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; -use FireflyIII\Repositories\Journal\JournalRepositoryInterface; -use FireflyIII\Support\Http\Controllers\ModelInformation; -use FireflyIII\Support\Http\Controllers\RequestInformation; -use Illuminate\Http\Request; -use View; - -/** - * Class SplitController. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SplitController extends Controller -{ - use ModelInformation, RequestInformation; - - /** @var AttachmentHelperInterface Attachment helper */ - private $attachments; - - /** @var BudgetRepositoryInterface The budget repository */ - private $budgets; - - /** @var CurrencyRepositoryInterface The currency repository */ - private $currencies; - /** @var JournalRepositoryInterface Journals and transactions overview */ - private $repository; - - /** - * SplitController constructor. - */ - public function __construct() - { - throw new FireflyException('Do not use me.'); - parent::__construct(); - - // some useful repositories: - $this->middleware( - function ($request, $next) { - $this->budgets = app(BudgetRepositoryInterface::class); - $this->attachments = app(AttachmentHelperInterface::class); - $this->currencies = app(CurrencyRepositoryInterface::class); - $this->repository = app(JournalRepositoryInterface::class); - app('view')->share('mainTitleIcon', 'fa-share-alt'); - app('view')->share('title', (string)trans('firefly.split-transactions')); - - return $next($request); - } - ); - } - - /** - * Edit a split. - * - * @param Request $request - * @param TransactionJournal $journal - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View - * @throws FireflyException - */ - public function edit(Request $request, TransactionJournal $journal) - { - throw new FireflyException('Needs refactoring'); - if ($this->isOpeningBalance($journal)) { - return $this->redirectToAccount($journal); // @codeCoverageIgnore - } - // basic fields: - $uploadSize = min(app('steam')->phpBytes(ini_get('upload_max_filesize')), app('steam')->phpBytes(ini_get('post_max_size'))); - $subTitle = (string)trans('breadcrumbs.edit_journal', ['description' => $journal->description]); - $subTitleIcon = 'fa-pencil'; - - // lists and collections - $currencies = $this->currencies->get(); - $budgets = app('expandedform')->makeSelectListWithEmpty($this->budgets->getActiveBudgets()); - - // other fields - $optionalFields = app('preferences')->get('transaction_journal_optional_fields', [])->data; - $preFilled = $this->arrayFromJournal($request, $journal); - - // put previous url in session if not redirect from store (not "return_to_edit"). - if (true !== session('transactions.edit-split.fromUpdate')) { - $this->rememberPreviousUri('transactions.edit-split.uri'); - } - session()->forget('transactions.edit-split.fromUpdate'); - - return view( - 'transactions.split.edit', compact( - 'subTitleIcon', 'currencies', 'optionalFields', 'preFilled', 'subTitle', 'uploadSize', 'budgets', - 'journal' - ) - ); - } - - /** - * Store new split journal. - * - * @param SplitJournalFormRequest $request - * @param TransactionJournal $journal - * - * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - public function update(SplitJournalFormRequest $request, TransactionJournal $journal) - { - throw new FireflyException('Needs refactoring.'); - if ($this->isOpeningBalance($journal)) { - return $this->redirectToAccount($journal); // @codeCoverageIgnore - } - $data = $request->getAll(); - - // keep current bill: - $data['bill_id'] = $journal->bill_id; - $journal = $this->repository->update($journal, $data); - - /** @var array $files */ - $files = $request->hasFile('attachments') ? $request->file('attachments') : null; - // save attachments: - $this->attachments->saveAttachmentsForModel($journal, $files); - event(new UpdatedTransactionGroup($group)); - - // flash messages - // @codeCoverageIgnoreStart - if (count($this->attachments->getMessages()->get('attachments')) > 0) { - session()->flash('info', $this->attachments->getMessages()->get('attachments')); - } - // @codeCoverageIgnoreEnd - - $type = strtolower($this->repository->getTransactionType($journal)); - session()->flash('success', (string)trans('firefly.updated_' . $type, ['description' => $journal->description])); - app('preferences')->mark(); - - // @codeCoverageIgnoreStart - if (1 === (int)$request->get('return_to_edit')) { - // set value so edit routine will not overwrite URL: - session()->put('transactions.edit-split.fromUpdate', true); - - return redirect(route('transactions.split.edit', [$journal->id]))->withInput(['return_to_edit' => 1]); - } - // @codeCoverageIgnoreEnd - - // redirect to previous URL. - return redirect($this->getPreviousUri('transactions.edit-split.uri')); - } - -} diff --git a/app/Import/Storage/ImportArrayStorage.php b/app/Import/Storage/ImportArrayStorage.php index 310e619648..56dbf5be3e 100644 --- a/app/Import/Storage/ImportArrayStorage.php +++ b/app/Import/Storage/ImportArrayStorage.php @@ -26,20 +26,19 @@ namespace FireflyIII\Import\Storage; use Carbon\Carbon; use DB; +use Exception; use FireflyIII\Events\RequestedReportOnJournals; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; -use FireflyIII\Helpers\Filter\InternalTransferFilter; -use FireflyIII\Helpers\Filter\NegativeAmountFilter; -use FireflyIII\Helpers\Filter\PositiveAmountFilter; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\ImportJob; use FireflyIII\Models\Rule; -use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface; use FireflyIII\TransactionRules\Processor; use Illuminate\Database\QueryException; use Illuminate\Support\Collection; @@ -64,8 +63,10 @@ class ImportArrayStorage private $journalRepos; /** @var ImportJobRepositoryInterface Import job repository */ private $repository; - /** @var Collection The transfers the user already has. */ + /** @var array The transfers the user already has. */ private $transfers; + /** @var TransactionGroupRepositoryInterface */ + private $groupRepos; /** * Set job, count transfers in the array and create the repository. @@ -83,9 +84,59 @@ class ImportArrayStorage $this->journalRepos = app(JournalRepositoryInterface::class); $this->journalRepos->setUser($importJob->user); + $this->groupRepos = app(TransactionGroupRepositoryInterface::class); + $this->groupRepos->setUser($importJob->user); + Log::debug('Constructed ImportArrayStorage()'); } + /** + * Count the number of transfers in the array. If this is zero, don't bother checking for double transfers. + */ + private function countTransfers(): void + { + Log::debug('Now in countTransfers()'); + /** @var array $array */ + $array = $this->repository->getTransactions($this->importJob); + + + $count = 0; + foreach ($array as $index => $group) { + foreach ($group['transactions'] as $transaction) { + if (strtolower(TransactionType::TRANSFER) === strtolower($transaction['type'])) { + $count++; + Log::debug(sprintf('Row #%d is a transfer, increase count to %d', $index + 1, $count)); + } + } + } + Log::debug(sprintf('Count of transfers in import array is %d.', $count)); + if ($count > 0) { + $this->checkForTransfers = true; + Log::debug('Will check for duplicate transfers.'); + // get users transfers. Needed for comparison. + $this->getTransfers(); + } + } + + /** + * Get the users transfers, so they can be compared to whatever the user is trying to import. + */ + private function getTransfers(): void + { + Log::debug('Now in getTransfers()'); + app('preferences')->mark(); + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + $collector->setUser($this->importJob->user); + $collector + ->setTypes([TransactionType::TRANSFER])->setLimit(10000)->setPage(1) + ->withAccountInformation(); + $this->transfers = $collector->getExtractedJournals(); + Log::debug(sprintf('Count of getTransfers() is %d', count($this->transfers))); + } + /** * Actually does the storing. Does three things. * - Store journals @@ -99,7 +150,7 @@ class ImportArrayStorage { // store transactions $this->setStatus('storing_data'); - $collection = $this->storeArray(); + $collection = $this->storeGroupArray(); $this->setStatus('stored_data'); // link tag: @@ -124,87 +175,122 @@ class ImportArrayStorage } /** - * Applies the users rules to the created journals. - * - * @param Collection $collection + * Shorthand method to quickly set job status * + * @param string $status */ - private function applyRules(Collection $collection): void + private function setStatus(string $status): void { - $rules = $this->getRules(); - if ($rules->count() > 0) { - foreach ($collection as $journal) { - $rules->each( - function (Rule $rule) use ($journal) { - Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id)); - /** @var Processor $processor */ - $processor = app(Processor::class); - $processor->make($rule); - $processor->handleTransactionJournal($journal); - $journal->refresh(); - if ($rule->stop_processing) { - return false; - } - - return true; - } - ); - } - } + $this->repository->setStatus($this->importJob, $status); } /** - * Count the number of transfers in the array. If this is zero, don't bother checking for double transfers. + * Store array as journals. + * + * @return Collection + * @throws FireflyException + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - private function countTransfers(): void + private function storeGroupArray(): Collection { - Log::debug('Now in countTransfers()'); /** @var array $array */ $array = $this->repository->getTransactions($this->importJob); + $count = count($array); + Log::notice(sprintf('Will now store the groups. Count of groups is %d.', $count)); + Log::notice('Going to store...'); - $count = 0; - foreach ($array as $index => $transaction) { - if (strtolower(TransactionType::TRANSFER) === strtolower($transaction['type'])) { - $count++; - Log::debug(sprintf('Row #%d is a transfer, increase count to %d', $index + 1, $count)); + $collection = new Collection; + foreach ($array as $index => $group) { + Log::debug(sprintf('Now store #%d', ($index + 1))); + $result = $this->storeGroup($index, $group); + if (null !== $result) { + $collection->push($result); } } - Log::debug(sprintf('Count of transfers in import array is %d.', $count)); - if ($count > 0) { - $this->checkForTransfers = true; - Log::debug('Will check for duplicate transfers.'); - // get users transfers. Needed for comparison. - $this->getTransfers(); - } + Log::notice(sprintf('Done storing. Firefly III has stored %d transactions.', $collection->count())); + + return $collection; } /** - * @param int $index - * @param array $transaction - * - * @return bool - * @throws FireflyException + * @param int $index + * @param array $group + * @return TransactionGroup|null */ - private function duplicateDetected(int $index, array $transaction): bool + private function storeGroup(int $index, array $group): ?TransactionGroup { - $hash = $this->getHash($transaction); - $existingId = $this->hashExists($hash); - if (null !== $existingId) { - $message = sprintf('Row #%d ("%s") could not be imported. It already exists.', $index, $transaction['description']); - $this->logDuplicateObject($transaction, $existingId); - $this->repository->addErrorMessage($this->importJob, $message); - return true; + // do duplicate detection! + if ($this->duplicateDetected($index, $group)) { + Log::warning(sprintf('Row #%d seems to be a imported already and will be ignored.', $index)); + + return null; } - // do transfer detection: - if ($this->checkForTransfers && $this->transferExists($transaction)) { - $message = sprintf('Row #%d ("%s") could not be imported. Such a transfer already exists.', $index, $transaction['description']); - $this->logDuplicateTransfer($transaction); - $this->repository->addErrorMessage($this->importJob, $message); + Log::debug(sprintf('Going to store entry #%d', $index + 1)); - return true; + // do some basic error catching. + foreach ($group['transactions'] as $index => $transaction) { + $group['transactions'][$index]['date'] = Carbon::parse($transaction['date'], config('app.timezone')); + $group['transactions'][$index]['description'] = '' === $transaction['description'] ? '(empty description)' : $transaction['description']; + } + + // store the group + try { + $newGroup = $this->groupRepos->store($group); + } catch (FireflyException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + $this->repository->addErrorMessage($this->importJob, sprintf('Row #%d could not be imported. %s', $index, $e->getMessage())); + + return null; + } + Log::debug(sprintf('Stored as group #%d', $newGroup->id)); + + // add to collection of transfers, if necessary: + if ('transfer' === strtolower($group['transactions'][0]['type'])) { + $journals = $this->getTransactionFromJournal($newGroup); + Log::debug('We just stored a transfer, so add the journal to the list of transfers.'); + foreach ($journals as $newJournal) { + $this->transfers[] = $newJournal; + } + Log::debug(sprintf('List length is now %d', count($this->transfers))); + } + + return $newGroup; + } + + /** + * @param int $index + * @param array $group + * + * @return bool + */ + private function duplicateDetected(int $index, array $group): bool + { + $transactions = $group['transactions'] ?? []; + foreach ($transactions as $transaction) { + $hash = $this->getHash($transaction); + $existingId = $this->hashExists($hash); + if (null !== $existingId) { + $message = sprintf('Row #%d ("%s") could not be imported. It already exists.', $index, $transaction['description']); + $this->logDuplicateObject($transaction, $existingId); + $this->repository->addErrorMessage($this->importJob, $message); + + return true; + } + + // do transfer detection: + if ($this->checkForTransfers && $this->transferExists($transaction)) { + $message = sprintf('Row #%d ("%s") could not be imported. Such a transfer already exists.', $index, $transaction['description']); + $this->logDuplicateTransfer($transaction); + $this->repository->addErrorMessage($this->importJob, $message); + + return true; + } } return false; @@ -215,7 +301,6 @@ class ImportArrayStorage * * @param array $transaction * - * @throws FireflyException * @return string */ private function getHash(array $transaction): string @@ -226,7 +311,12 @@ class ImportArrayStorage // @codeCoverageIgnoreStart /** @noinspection ForgottenDebugOutputInspection */ Log::error('Could not encode import array.', $transaction); - throw new FireflyException('Could not encode import array. Please see the logs.'); + try { + $json = random_int(1, 10000); + } catch (Exception $e) { + // seriously? + Log::error(sprintf('random_int() just failed. I want a medal: %s', $e->getMessage())); + } // @codeCoverageIgnoreEnd } $hash = hash('sha256', $json); @@ -235,72 +325,6 @@ class ImportArrayStorage return $hash; } - /** - * Gets the users rules. - * - * @return Collection - */ - private function getRules(): Collection - { - /** @var RuleRepositoryInterface $repository */ - $repository = app(RuleRepositoryInterface::class); - $repository->setUser($this->importJob->user); - $set = $repository->getForImport(); - - Log::debug(sprintf('Found %d user rules.', $set->count())); - - return $set; - } - - /** - * @param $journal - * - * @return Transaction - */ - private function getTransactionFromJournal($journal): Transaction - { - // collect transactions using the journal collector - $collector = app(TransactionCollectorInterface::class); - $collector->setUser($this->importJob->user); - $collector->withOpposingAccount(); - // filter on specific journals. - $collector->setJournals(new Collection([$journal])); - - // add filter to remove transactions: - $transactionType = $journal->transactionType->type; - if ($transactionType === TransactionType::WITHDRAWAL) { - $collector->addFilter(PositiveAmountFilter::class); - } - if (!($transactionType === TransactionType::WITHDRAWAL)) { - $collector->addFilter(NegativeAmountFilter::class); - } - /** @var Transaction $result */ - $result = $collector->getTransactions()->first(); - Log::debug(sprintf('Return transaction #%d with journal id #%d based on ID #%d', $result->id, $result->journal_id, $journal->id)); - - return $result; - } - - /** - * Get the users transfers, so they can be compared to whatever the user is trying to import. - */ - private function getTransfers(): void - { - Log::debug('Now in getTransfers()'); - app('preferences')->mark(); - - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setUser($this->importJob->user); - $collector->setAllAssetAccounts() - ->ignoreCache() - ->setTypes([TransactionType::TRANSFER]) - ->withOpposingAccount(); - $collector->removeFilter(InternalTransferFilter::class); - $this->transfers = $collector->getTransactions(); - Log::debug(sprintf('Count of getTransfers() is %d', $this->transfers->count())); - } - /** * Check if the hash exists for the array the user wants to import. * @@ -321,6 +345,188 @@ class ImportArrayStorage return (int)$entry->transaction_journal_id; } + /** + * Log about a duplicate object (double hash). + * + * @param array $transaction + * @param int $existingId + */ + private function logDuplicateObject(array $transaction, int $existingId): void + { + Log::info( + 'Transaction is a duplicate, and will not be imported (the hash exists).', + [ + 'existing' => $existingId, + 'description' => $transaction['description'] ?? '', + 'amount' => $transaction['transactions'][0]['amount'] ?? 0, + 'date' => $transaction['date'] ?? '', + ] + ); + + } + + /** + * Check if a transfer exists. + * + * @param array $transaction + * + * @return bool + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function transferExists(array $transaction): bool + { + Log::debug('Check if transaction is a double transfer.'); + + // how many hits do we need? + Log::debug(sprintf('System has %d existing transfers', count($this->transfers))); + // loop over each split: + + // check if is a transfer + if (strtolower(TransactionType::TRANSFER) !== strtolower($transaction['type'])) { + Log::debug(sprintf('Is a %s, not a transfer so no.', $transaction['type'])); + + return false; + } + + + Log::debug(sprintf('Required hits for transfer comparison is %d', self::REQUIRED_HITS)); + + // get the amount: + /** @noinspection UnnecessaryCastingInspection */ + $amount = (string)($transaction['amount'] ?? '0'); + if (bccomp($amount, '0') === -1) { + $amount = bcmul($amount, '-1'); // @codeCoverageIgnore + } + + // get the description: + $description = '' === (string)$transaction['description'] ? $transaction['description'] : $transaction['description']; + + // get the source and destination ID's: + $transactionSourceIDs = [(int)$transaction['source_id'], (int)$transaction['destination_id']]; + sort($transactionSourceIDs); + + // get the source and destination names: + $transactionSourceNames = [(string)$transaction['source_name'], (string)$transaction['destination_name']]; + sort($transactionSourceNames); + + // then loop all transfers: + /** @var array $transfer */ + foreach ($this->transfers as $transfer) { + // number of hits for this split-transfer combination: + $hits = 0; + Log::debug(sprintf('Now looking at transaction journal #%d', $transfer['transaction_journal_id'])); + // compare amount: + $originalAmount = app('steam')->positive($transfer['amount']); + Log::debug(sprintf('Amount %s compared to %s', $amount, $originalAmount)); + if (0 !== bccomp($amount, $originalAmount)) { + Log::debug('Amount is not a match, continue with next transfer.'); + continue; + } + ++$hits; + Log::debug(sprintf('Comparison is a hit! (%s)', $hits)); + + // compare description: + $comparison = '(empty description)' === $transfer['description'] ? '' : $transfer['description']; + Log::debug(sprintf('Comparing "%s" to "%s" (original: "%s")', $description, $transfer['description'], $comparison)); + if ($description !== $comparison) { + Log::debug('Description is not a match, continue with next transfer.'); + continue; // @codeCoverageIgnore + } + ++$hits; + Log::debug(sprintf('Comparison is a hit! (%s)', $hits)); + + // compare date: + $transferDate = $transfer['date']->format('Y-m-d H:i:s'); + Log::debug(sprintf('Comparing dates "%s" to "%s"', $transaction['date'], $transferDate)); + if ($transaction['date'] !== $transferDate) { + Log::debug('Date is not a match, continue with next transfer.'); + continue; // @codeCoverageIgnore + } + ++$hits; + Log::debug(sprintf('Comparison is a hit! (%s)', $hits)); + + // compare source and destination id's + $transferSourceIDs = [(int)$transfer['source_account_id'], (int)$transfer['destination_account_id']]; + sort($transferSourceIDs); + /** @noinspection DisconnectedForeachInstructionInspection */ + Log::debug('Comparing current transaction source+dest IDs', $transactionSourceIDs); + Log::debug('.. with current transfer source+dest IDs', $transferSourceIDs); + if ($transactionSourceIDs === $transferSourceIDs) { + ++$hits; + Log::debug(sprintf('Source IDs are the same! (%d)', $hits)); + } + if ($transactionSourceIDs !== $transactionSourceIDs) { + Log::debug('Source IDs are not the same.'); + } + unset($transferSourceIDs); + + // compare source and destination names + $transferSource = [(string)($transfer['source_account_name'] ?? ''), (string)($transfer['destination_account_name'] ?? '')]; + sort($transferSource); + /** @noinspection DisconnectedForeachInstructionInspection */ + Log::debug('Comparing current transaction source+dest names', $transactionSourceNames); + Log::debug('.. with current transfer source+dest names', $transferSource); + if ($transactionSourceNames === $transferSource) { + // @codeCoverageIgnoreStart + ++$hits; + Log::debug(sprintf('Source names are the same! (%d)', $hits)); + // @codeCoverageIgnoreEnd + } + if ($transactionSourceNames !== $transferSource) { + Log::debug('Source names are not the same.'); + } + + Log::debug(sprintf('Number of hits is %d', $hits)); + if ($hits >= self::REQUIRED_HITS) { + Log::debug(sprintf('Is more than %d, return true.', self::REQUIRED_HITS)); + + return true; + } + } + Log::debug('Is not an existing transfer, return false.'); + + return false; + } + + /** + * Log about a duplicate transfer. + * + * @param array $transaction + */ + private function logDuplicateTransfer(array $transaction): void + { + Log::info( + 'Transaction is a duplicate transfer, and will not be imported (such a transfer exists already).', + [ + 'description' => $transaction['description'] ?? '', + 'amount' => $transaction['transactions'][0]['amount'] ?? 0, + 'date' => $transaction['date'] ?? '', + ] + ); + } + + /** + * @param TransactionGroup $transactionGroup + * + * @return array + */ + private function getTransactionFromJournal(TransactionGroup $transactionGroup): array + { + // collect transactions using the journal collector + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + $collector->setUser($this->importJob->user); + $collector->setGroup($transactionGroup); + + $result = $collector->getExtractedJournals(); + + return $result; + } + /** * Link all imported journals to a tag. * @@ -365,248 +571,50 @@ class ImportArrayStorage } /** - * Log about a duplicate object (double hash). + * Applies the users rules to the created journals. + * + * @param Collection $collection * - * @param array $transaction - * @param int $existingId */ - private function logDuplicateObject(array $transaction, int $existingId): void + private function applyRules(Collection $collection): void { - Log::info( - 'Transaction is a duplicate, and will not be imported (the hash exists).', - [ - 'existing' => $existingId, - 'description' => $transaction['description'] ?? '', - 'amount' => $transaction['transactions'][0]['amount'] ?? 0, - 'date' => $transaction['date'] ?? '', - ] - ); + $rules = $this->getRules(); + if ($rules->count() > 0) { + foreach ($collection as $journal) { + $rules->each( + function (Rule $rule) use ($journal) { + Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id)); + /** @var Processor $processor */ + $processor = app(Processor::class); + $processor->make($rule); + $processor->handleTransactionJournal($journal); + $journal->refresh(); + if ($rule->stop_processing) { + return false; + } + return true; + } + ); + } + } } /** - * Log about a duplicate transfer. - * - * @param array $transaction - */ - private function logDuplicateTransfer(array $transaction): void - { - Log::info( - 'Transaction is a duplicate transfer, and will not be imported (such a transfer exists already).', - [ - 'description' => $transaction['description'] ?? '', - 'amount' => $transaction['transactions'][0]['amount'] ?? 0, - 'date' => $transaction['date'] ?? '', - ] - ); - } - - /** - * Shorthand method to quickly set job status - * - * @param string $status - */ - private function setStatus(string $status): void - { - $this->repository->setStatus($this->importJob, $status); - } - - /** - * Store array as journals. + * Gets the users rules. * * @return Collection - * @throws FireflyException - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - private function storeArray(): Collection + private function getRules(): Collection { - /** @var array $array */ - $array = $this->repository->getTransactions($this->importJob); - $count = count($array); - $toStore = []; + /** @var RuleRepositoryInterface $repository */ + $repository = app(RuleRepositoryInterface::class); + $repository->setUser($this->importJob->user); + $set = $repository->getForImport(); - Log::notice(sprintf('Will now store the transactions. Count of items is %d.', $count)); + Log::debug(sprintf('Found %d user rules.', $set->count())); - /* - * Detect duplicates in initial array: - */ - foreach ($array as $index => $transaction) { - Log::debug(sprintf('Now at item %d out of %d', $index + 1, $count)); - if ($this->duplicateDetected($index, $transaction)) { - Log::warning(sprintf('Row #%d seems to be a duplicate entry and will be ignored.', $index)); - continue; - } - $transaction['import_hash_v2'] = $this->getHash($transaction); - $toStore[] = $transaction; - } - $count = count($toStore); - if (0 === $count) { - Log::info('No transactions to store left!'); - - return new Collection; - } - Log::notice(sprintf('After a first check for duplicates, the count of items is %d.', $count)); - Log::notice('Going to store...'); - // now actually store them: - $collection = new Collection; - foreach ($toStore as $index => $store) { - // do duplicate detection again! - if ($this->duplicateDetected($index, $store)) { - Log::warning(sprintf('Row #%d seems to be a imported already and will be ignored.', $index), $store); - continue; - } - - Log::debug(sprintf('Going to store entry %d of %d', $index + 1, $count)); - // convert the date to an object: - $store['date'] = Carbon::parse($store['date'], config('app.timezone')); - $store['description'] = '' === $store['description'] ? '(empty description)' : $store['description']; - // store the journal. - try { - $journal = $this->journalRepos->store($store); - } catch (FireflyException $e) { - Log::error($e->getMessage()); - Log::error($e->getTraceAsString()); - $this->repository->addErrorMessage($this->importJob, sprintf('Row #%d could not be imported. %s', $index, $e->getMessage())); - continue; - } - - Log::info(sprintf('Stored #%d: "%s" (ID #%d)', $index, $journal->description, $journal->id)); - Log::debug(sprintf('Stored as journal #%d', $journal->id)); - $collection->push($journal); - - // add to collection of transfers, if necessary: - if ('transfer' === strtolower($store['type'])) { - $transaction = $this->getTransactionFromJournal($journal); - Log::debug('We just stored a transfer, so add the journal to the list of transfers.'); - $this->transfers->push($transaction); - Log::debug(sprintf('List length is now %d', $this->transfers->count())); - } - } - Log::notice(sprintf('Done storing. Firefly III has stored %d transactions.', $collection->count())); - - return $collection; - } - - /** - * Check if a transfer exists. - * - * @param $transaction - * - * @return bool - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - private function transferExists(array $transaction): bool - { - Log::debug('Check if array is a double transfer.'); - if (strtolower(TransactionType::TRANSFER) !== strtolower($transaction['type'])) { - Log::debug(sprintf('Is a %s, not a transfer so no.', $transaction['type'])); - - return false; - } - // how many hits do we need? - Log::debug(sprintf('Array has %d transactions.', count($transaction['transactions']))); - Log::debug(sprintf('System has %d existing transfers', count($this->transfers))); - // loop over each split: - Log::debug(sprintf('This transfer has %d split(s)', count($transaction['transactions']))); - foreach ($transaction['transactions'] as $index => $current) { - Log::debug(sprintf('Required hits for transfer comparison is %d', self::REQUIRED_HITS)); - Log::debug(sprintf('Now at transfer split %d of %d', $index + 1, count($transaction['transactions']))); - - // get the amount: - /** @noinspection UnnecessaryCastingInspection */ - $amount = (string)($current['amount'] ?? '0'); - if (bccomp($amount, '0') === -1) { - $amount = bcmul($amount, '-1'); // @codeCoverageIgnore - } - - // get the description: - $description = '' === (string)$current['description'] ? $transaction['description'] : $current['description']; - - // get the source and destination ID's: - $currentSourceIDs = [(int)$current['source_id'], (int)$current['destination_id']]; - sort($currentSourceIDs); - - // get the source and destination names: - $currentSourceNames = [(string)$current['source_name'], (string)$current['destination_name']]; - sort($currentSourceNames); - - // then loop all transfers: - /** @var Transaction $transfer */ - foreach ($this->transfers as $transfer) { - // number of hits for this split-transfer combination: - $hits = 0; - Log::debug(sprintf('Now looking at transaction journal #%d', $transfer->journal_id)); - // compare amount: - Log::debug(sprintf('Amount %s compared to %s', $amount, $transfer->transaction_amount)); - if (0 !== bccomp($amount, $transfer->transaction_amount)) { - Log::debug('Amount is not a match, continue with next transfer.'); - continue; - } - ++$hits; - Log::debug(sprintf('Comparison is a hit! (%s)', $hits)); - - // compare description: - $comparison = '(empty description)' === $transfer->description ? '' : $transfer->description; - Log::debug(sprintf('Comparing "%s" to "%s" (original: "%s")', $description, $transfer->description, $comparison)); - if ($description !== $comparison) { - Log::debug('Description is not a match, continue with next transfer.'); - continue; // @codeCoverageIgnore - } - ++$hits; - Log::debug(sprintf('Comparison is a hit! (%s)', $hits)); - - // compare date: - $transferDate = $transfer->date->format('Y-m-d H:i:s'); - Log::debug(sprintf('Comparing dates "%s" to "%s"', $transaction['date'], $transferDate)); - if ($transaction['date'] !== $transferDate) { - Log::debug('Date is not a match, continue with next transfer.'); - continue; // @codeCoverageIgnore - } - ++$hits; - Log::debug(sprintf('Comparison is a hit! (%s)', $hits)); - - // compare source and destination id's - $transferSourceIDs = [(int)$transfer->account_id, (int)$transfer->opposing_account_id]; - sort($transferSourceIDs); - /** @noinspection DisconnectedForeachInstructionInspection */ - Log::debug('Comparing current transaction source+dest IDs', $currentSourceIDs); - Log::debug('.. with current transfer source+dest IDs', $transferSourceIDs); - if ($currentSourceIDs === $transferSourceIDs) { - ++$hits; - Log::debug(sprintf('Source IDs are the same! (%d)', $hits)); - } - Log::debug('Source IDs are not the same.'); - unset($transferSourceIDs); - - // compare source and destination names - $transferSource = [(string)$transfer->account_name, (string)$transfer->opposing_account_name]; - sort($transferSource); - /** @noinspection DisconnectedForeachInstructionInspection */ - Log::debug('Comparing current transaction source+dest names', $currentSourceNames); - Log::debug('.. with current transfer source+dest names', $transferSource); - if ($currentSourceNames === $transferSource) { - // @codeCoverageIgnoreStart - ++$hits; - Log::debug(sprintf('Source names are the same! (%d)', $hits)); - // @codeCoverageIgnoreEnd - } - Log::debug('Source names are not the same.'); - Log::debug(sprintf('Number of hits is %d', $hits)); - if ($hits >= self::REQUIRED_HITS) { - Log::debug(sprintf('Is more than %d, return true.', self::REQUIRED_HITS)); - - return true; - } - } - } - Log::debug('Is not an existing transfer, return false.'); - - return false; + return $set; } } diff --git a/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php b/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php index ec1931df95..1144aeb786 100644 --- a/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php +++ b/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Jobs; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\RuleGroup; use FireflyIII\TransactionRules\Processor; use FireflyIII\User; @@ -34,6 +34,7 @@ use Illuminate\Support\Collection; /** * Class ExecuteRuleGroupOnExistingTransactions. + * TODO make sure this job honors the "stop_processing" rules. */ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue { @@ -148,19 +149,20 @@ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue public function handle() { // Lookup all journals that match the parameters specified - $transactions = $this->collectJournals(); + $journals = $this->collectJournals(); // Find processors for each rule within the current rule group $processors = $this->collectProcessors(); // Execute the rules for each transaction foreach ($processors as $processor) { - foreach ($transactions as $transaction) { + /** @var array $journal */ + foreach ($journals as $journal) { /** @var Processor $processor */ - $processor->handleTransaction($transaction); - + $processor->handleJournalArray($journal); } // Stop processing this group if the rule specifies 'stop_processing' + // TODO Fix this. if ($processor->getRule()->stop_processing) { break; } @@ -170,16 +172,16 @@ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue /** * Collect all journals that should be processed. * - * @return Collection + * @return array */ - protected function collectJournals(): Collection + protected function collectJournals(): array { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user); $collector->setAccounts($this->accounts)->setRange($this->startDate, $this->endDate); - return $collector->getTransactions(); + return $collector->getExtractedJournals(); } /** diff --git a/app/Jobs/ExecuteRuleOnExistingTransactions.php b/app/Jobs/ExecuteRuleOnExistingTransactions.php index a9476a36aa..67667e65ef 100644 --- a/app/Jobs/ExecuteRuleOnExistingTransactions.php +++ b/app/Jobs/ExecuteRuleOnExistingTransactions.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Jobs; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Rule; use FireflyIII\TransactionRules\Processor; use FireflyIII\User; @@ -161,7 +161,7 @@ class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue public function handle() { // Lookup all journals that match the parameters specified - $transactions = $this->collectJournals(); + $journals = $this->collectJournals(); /** @var Processor $processor */ $processor = app(Processor::class); $processor->make($this->rule, true); @@ -169,9 +169,10 @@ class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue $misses = 0; $total = 0; // Execute the rules for each transaction - foreach ($transactions as $transaction) { + /** @var array $journal */ + foreach ($journals as $journal) { ++$total; - $result = $processor->handleTransaction($transaction); + $result = $processor->handleJournalArray($journal); if ($result) { ++$hits; } @@ -186,15 +187,16 @@ class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue /** * Collect all journals that should be processed. * - * @return Collection + * @return array */ - protected function collectJournals(): Collection + protected function collectJournals(): array { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user); $collector->setAccounts($this->accounts)->setRange($this->startDate, $this->endDate); - return $collector->getTransactions(); + return $collector->getExtractedJournals(); } } diff --git a/app/Repositories/Account/AccountTasker.php b/app/Repositories/Account/AccountTasker.php index de43d5b0aa..9432277957 100644 --- a/app/Repositories/Account/AccountTasker.php +++ b/app/Repositories/Account/AccountTasker.php @@ -24,9 +24,7 @@ namespace FireflyIII\Repositories\Account; use Carbon\Carbon; use FireflyIII\Helpers\Collector\GroupCollectorInterface; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Account; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\User; @@ -230,7 +228,7 @@ class AccountTasker implements AccountTaskerInterface $collector->setAccounts($accounts)->setRange($start, $end); $collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) ->withAccountInformation(); - $income = $this->groupByDestination($collector->getExtractedJournals()); + $income = $this->groupByDestination($collector->getExtractedJournals()); // sort the result // Obtain a list of columns diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index c3dc206fe7..922635aeed 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -27,14 +27,12 @@ use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TransactionCurrencyFactory; use FireflyIII\Helpers\Collector\GroupCollectorInterface; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\AccountType; use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleTrigger; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; @@ -164,6 +162,7 @@ class BudgetRepository implements BudgetRepositoryInterface if ($accounts->count() > 0) { $collector->setAccounts($accounts); } + return $collector->getSum(); } @@ -556,18 +555,19 @@ class BudgetRepository implements BudgetRepositoryInterface } // get all transactions: - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); $collector->setBudgets($budgets); - $transactions = $collector->getTransactions(); + $journals = $collector->getExtractedJournals(); // loop transactions: - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - $budgetId = max((int)$transaction->transaction_journal_budget_id, (int)$transaction->transaction_budget_id); - $date = $transaction->date->format($carbonFormat); - $data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date] ?? '0', $transaction->transaction_amount); + /** @var array $journal */ + foreach ($journals as $journal) { + $budgetId = (int)$journal['budget_id']; + $date = $journal['date']->format($carbonFormat); + $data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date] ?? '0', $journal['amount']); } return $data; @@ -623,25 +623,28 @@ class BudgetRepository implements BudgetRepositoryInterface public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array { $carbonFormat = Navigation::preferredCarbonFormat($start, $end); - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); $collector->setTypes([TransactionType::WITHDRAWAL]); $collector->withoutBudget(); - $transactions = $collector->getTransactions(); - $result = [ + $journals = $collector->getExtractedJournals(); + $result = [ 'entries' => [], 'name' => (string)trans('firefly.no_budget'), 'sum' => '0', ]; - foreach ($transactions as $transaction) { - $date = $transaction->date->format($carbonFormat); + /** @var array $journal */ + foreach ($journals as $journal) { + $date = $journal['date']->format($carbonFormat); if (!isset($result['entries'][$date])) { $result['entries'][$date] = '0'; } - $result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount); + $result['entries'][$date] = bcadd($result['entries'][$date], $journal['amount']); } return $result; @@ -794,39 +797,41 @@ class BudgetRepository implements BudgetRepositoryInterface */ public function spentInPeriodWoBudgetMc(Collection $accounts, Carbon $start, Carbon $end): array { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutBudget(); if ($accounts->count() > 0) { $collector->setAccounts($accounts); } - if (0 === $accounts->count()) { - $collector->setAllAssetAccounts(); - } - - $set = $collector->getTransactions(); + $journals = $collector->getExtractedJournals(); $return = []; $total = []; $currencies = []; - /** @var Transaction $transaction */ - foreach ($set as $transaction) { - $code = $transaction->transaction_currency_code; + /** @var array $journal */ + foreach ($journals as $journal) { + $code = $journal['currency_code']; if (!isset($currencies[$code])) { - $currencies[$code] = $transaction->transactionCurrency; + $currencies[$code] = [ + 'id' => $journal['currency_id'], + 'name' => $journal['currency_name'], + 'symbol' => $journal['currency_symbol'], + 'decimal_places' => $journal['currency_decimal_places'], + ]; } - $total[$code] = isset($total[$code]) ? bcadd($total[$code], $transaction->transaction_amount) : $transaction->transaction_amount; + $total[$code] = isset($total[$code]) ? bcadd($total[$code], $journal['amount']) : $journal['amount']; } foreach ($total as $code => $spent) { /** @var TransactionCurrency $currency */ $currency = $currencies[$code]; $return[] = [ - 'currency_id' => $currency->id, + 'currency_id' => $currency['id'], 'currency_code' => $code, - 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, - 'amount' => round($spent, $currency->decimal_places), + 'currency_symbol' => $currency['symbol'], + 'currency_decimal_places' => $currency['decimal_places'], + 'amount' => round($spent, $currency['decimal_places']), ]; } diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index 869ff1b61a..acb0a7d45d 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -25,9 +25,7 @@ namespace FireflyIII\Repositories\Category; use Carbon\Carbon; use FireflyIII\Factory\CategoryFactory; use FireflyIII\Helpers\Collector\GroupCollectorInterface; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Category; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionType; use FireflyIII\Services\Internal\Destroy\CategoryDestroyService; use FireflyIII\Services\Internal\Update\CategoryUpdateService; @@ -142,43 +140,30 @@ class CategoryRepository implements CategoryRepositoryInterface */ public function earnedInPeriodPcWoCategory(Collection $accounts, Carbon $start, Carbon $end): array { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->withoutCategory(); if ($accounts->count() > 0) { $collector->setAccounts($accounts); } - if (0 === $accounts->count()) { - $collector->setAllAssetAccounts(); - } + $journals = $collector->getExtractedJournals(); + $return = []; - $set = $collector->getTransactions(); - $set = $set->filter( - function (Transaction $transaction) { - if (bccomp($transaction->transaction_amount, '0') === 1) { - return $transaction; - } - - return null; - } - ); - - $return = []; - /** @var Transaction $transaction */ - foreach ($set as $transaction) { - $currencyId = $transaction->transaction_currency_id; + foreach ($journals as $journal) { + $currencyId = (int)$journal['currency_id']; if (!isset($return[$currencyId])) { $return[$currencyId] = [ 'spent' => '0', 'currency_id' => $currencyId, - 'currency_symbol' => $transaction->transaction_currency_symbol, - 'currency_code' => $transaction->transaction_currency_code, - 'currency_decimal_places' => $transaction->transaction_currency_dp, + 'currency_symbol' => $journal['currency_symbol'], + 'currency_code' => $journal['currency_code'], + 'currency_decimal_places' => $journal['currency_decimal_places'], ]; } - $return[$currencyId]['spent'] = bcadd($return[$currencyId]['spent'], $transaction->transaction_amount); + $return[$currencyId]['spent'] = bcadd($return[$currencyId]['spent'], $journal['amount']); } return $return; @@ -194,8 +179,9 @@ class CategoryRepository implements CategoryRepositoryInterface */ public function earnedInPeriodPerCurrency(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT]); @@ -209,20 +195,12 @@ class CategoryRepository implements CategoryRepositoryInterface if ($accounts->count() > 0) { $collector->setAccounts($accounts); } - if (0 === $accounts->count()) { - $collector->setAllAssetAccounts(); - } - - $set = $collector->getTransactions(); - $return = []; - /** @var Transaction $transaction */ - foreach ($set as $transaction) { - $jrnlCatId = (int)$transaction->transaction_journal_category_id; - $transCatId = (int)$transaction->transaction_category_id; - $categoryId = max($jrnlCatId, $transCatId); - $currencyId = (int)$transaction->transaction_currency_id; - $name = $transaction->transaction_category_name; - $name = '' === (string)$name ? $transaction->transaction_journal_category_name : $name; + $journals = $collector->getExtractedJournals(); + $return = []; + foreach ($journals as $journal) { + $categoryId = (int)$journal['category_id']; + $currencyId = (int)$journal['currency_id']; + $name = $journal['category_name']; // make array for category: if (!isset($return[$categoryId])) { $return[$categoryId] = [ @@ -234,13 +212,13 @@ class CategoryRepository implements CategoryRepositoryInterface $return[$categoryId]['earned'][$currencyId] = [ 'earned' => '0', 'currency_id' => $currencyId, - 'currency_symbol' => $transaction->transaction_currency_symbol, - 'currency_code' => $transaction->transaction_currency_code, - 'currency_decimal_places' => $transaction->transaction_currency_dp, + 'currency_symbol' => $journal['currency_symbol'], + 'currency_code' => $journal['currency_code'], + 'currency_decimal_places' => $journal['currency_decimal_places'], ]; } $return[$categoryId]['earned'][$currencyId]['earned'] - = bcadd($return[$categoryId]['earned'][$currencyId]['earned'], $transaction->transaction_amount); + = bcadd($return[$categoryId]['earned'][$currencyId]['earned'], $journal['amount']); } return $return; @@ -521,23 +499,20 @@ class CategoryRepository implements CategoryRepositoryInterface } // get all transactions: - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); $collector->setCategories($categories)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) - ->withOpposingAccount(); - $transactions = $collector->getTransactions(); + ->withAccountInformation()->withCategoryInformation(); + $journals = $collector->getExtractedJournals(); // loop transactions: - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - // if positive, skip: - if (1 === bccomp($transaction->transaction_amount, '0')) { - continue; - } - $categoryId = max((int)$transaction->transaction_journal_category_id, (int)$transaction->transaction_category_id); - $date = $transaction->date->format($carbonFormat); - $data[$categoryId]['entries'][$date] = bcadd($data[$categoryId]['entries'][$date] ?? '0', $transaction->transaction_amount); + + foreach ($journals as $journal) { + $categoryId = (int)$journal['category_id']; + $date = $journal['date']->format($carbonFormat); + $data[$categoryId]['entries'][$date] = bcadd($data[$categoryId]['entries'][$date] ?? '0', $journal['amount']); } return $data; @@ -553,29 +528,28 @@ class CategoryRepository implements CategoryRepositoryInterface public function periodExpensesNoCategory(Collection $accounts, Carbon $start, Carbon $end): array { $carbonFormat = Navigation::preferredCarbonFormat($start, $end); - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setAccounts($accounts)->setRange($start, $end)->withOpposingAccount(); + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + $collector->setAccounts($accounts)->setRange($start, $end)->withAccountInformation(); $collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]); $collector->withoutCategory(); - $transactions = $collector->getTransactions(); - $result = [ + $journals = $collector->getExtractedJournals(); + $result = [ 'entries' => [], 'name' => (string)trans('firefly.no_category'), 'sum' => '0', ]; - foreach ($transactions as $transaction) { - // if positive, skip: - if (1 === bccomp($transaction->transaction_amount, '0')) { - continue; - } - $date = $transaction->date->format($carbonFormat); + /** @var array $journal */ + foreach ($journals as $journal) { + $date = $journal['date']->format($carbonFormat); if (!isset($result['entries'][$date])) { $result['entries'][$date] = '0'; } - $result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount); + $result['entries'][$date] = bcadd($result['entries'][$date], $journal['amount']); } return $result; @@ -604,23 +578,20 @@ class CategoryRepository implements CategoryRepositoryInterface } // get all transactions: - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); $collector->setCategories($categories)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) - ->withOpposingAccount(); - $transactions = $collector->getTransactions(); + ->withAccountInformation(); + $journals = $collector->getExtractedJournals(); // loop transactions: - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - // if negative, skip: - if (bccomp($transaction->transaction_amount, '0') === -1) { - continue; - } - $categoryId = max((int)$transaction->transaction_journal_category_id, (int)$transaction->transaction_category_id); - $date = $transaction->date->format($carbonFormat); - $data[$categoryId]['entries'][$date] = bcadd($data[$categoryId]['entries'][$date] ?? '0', $transaction->transaction_amount); + /** @var array $journal */ + foreach ($journals as $journal) { + $categoryId = (int)$journal['category_id']; + $date = $journal['date']->format($carbonFormat); + $data[$categoryId]['entries'][$date] = bcadd($data[$categoryId]['entries'][$date] ?? '0', $journal['amount']); } return $data; @@ -637,29 +608,28 @@ class CategoryRepository implements CategoryRepositoryInterface { Log::debug('Now in periodIncomeNoCategory()'); $carbonFormat = Navigation::preferredCarbonFormat($start, $end); - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setAccounts($accounts)->setRange($start, $end)->withOpposingAccount(); + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + $collector->setAccounts($accounts)->setRange($start, $end)->withAccountInformation(); $collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]); $collector->withoutCategory(); - $transactions = $collector->getTransactions(); - $result = [ + $journals = $collector->getExtractedJournals(); + $result = [ 'entries' => [], 'name' => (string)trans('firefly.no_category'), 'sum' => '0', ]; Log::debug('Looping transactions..'); - foreach ($transactions as $transaction) { - // if negative, skip: - if (bccomp($transaction->transaction_amount, '0') === -1) { - continue; - } - $date = $transaction->date->format($carbonFormat); + + foreach ($journals as $journal) { + $date = $journal['date']->format($carbonFormat); if (!isset($result['entries'][$date])) { $result['entries'][$date] = '0'; } - $result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount); + $result['entries'][$date] = bcadd($result['entries'][$date], $journal['amount']); } Log::debug('Done looping transactions..'); Log::debug('Finished periodIncomeNoCategory()'); @@ -714,6 +684,8 @@ class CategoryRepository implements CategoryRepositoryInterface { /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); + + $collector->setUser($this->user); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setCategories($categories); @@ -750,7 +722,7 @@ class CategoryRepository implements CategoryRepositoryInterface $set = $collector->getExtractedJournals(); $return = []; /** @var array $journal */ - foreach($set as $journal) { + foreach ($set as $journal) { $currencyId = (int)$journal['currency_id']; if (!isset($return[$currencyId])) { $return[$currencyId] = [ @@ -833,30 +805,17 @@ class CategoryRepository implements CategoryRepositoryInterface */ public function spentInPeriodWithoutCategory(Collection $accounts, Carbon $start, Carbon $end): string { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutCategory(); if ($accounts->count() > 0) { $collector->setAccounts($accounts); } - if (0 === $accounts->count()) { - $collector->setAllAssetAccounts(); - } - $set = $collector->getTransactions(); - $set = $set->filter( - function (Transaction $transaction) { - if (bccomp($transaction->transaction_amount, '0') === -1) { - return $transaction; - } - - return null; - } - ); - - return (string)$set->sum('transaction_amount'); + return $collector->getSum(); } /** diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 2be8a0ab26..a91ef3a505 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -25,11 +25,6 @@ namespace FireflyIII\Repositories\Journal; use Carbon\Carbon; use DB; use Exception; -use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Factory\TransactionGroupFactory; -use FireflyIII\Factory\TransactionJournalFactory; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; -use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Note; @@ -74,9 +69,9 @@ class JournalRepository implements JournalRepositoryInterface /** @noinspection MoreThanThreeArgumentsInspection */ /** * @param TransactionJournal $journal - * @param TransactionType $type - * @param Account $source - * @param Account $destination + * @param TransactionType $type + * @param Account $source + * @param Account $destination * * @return MessageBag * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -354,7 +349,7 @@ class JournalRepository implements JournalRepositoryInterface * otherwise look for meta field and return that one. * * @param TransactionJournal $journal - * @param null|string $field + * @param null|string $field * * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -386,11 +381,40 @@ class JournalRepository implements JournalRepositoryInterface return ''; } + /** + * Return Carbon value of a meta field (or NULL). + * + * @param TransactionJournal $journal + * @param string $field + * + * @return null|Carbon + */ + public function getMetaDate(TransactionJournal $journal, string $field): ?Carbon + { + $cache = new CacheProperties; + $cache->addProperty('journal-meta-updated'); + $cache->addProperty($journal->id); + $cache->addProperty($field); + + if ($cache->has()) { + return new Carbon($cache->get()); // @codeCoverageIgnore + } + + $entry = $journal->transactionJournalMeta()->where('name', $field)->first(); + if (null === $entry) { + return null; + } + $value = new Carbon($entry->data); + $cache->store($entry->data); + + return $value; + } + /** * Return a list of all destination accounts related to journal. * * @param TransactionJournal $journal - * @param bool $useCache + * @param bool $useCache * * @return Collection */ @@ -418,7 +442,7 @@ class JournalRepository implements JournalRepositoryInterface * Return a list of all source accounts related to journal. * * @param TransactionJournal $journal - * @param bool $useCache + * @param bool $useCache * * @return Collection */ @@ -493,40 +517,11 @@ class JournalRepository implements JournalRepositoryInterface return ''; } - /** - * Return Carbon value of a meta field (or NULL). - * - * @param TransactionJournal $journal - * @param string $field - * - * @return null|Carbon - */ - public function getMetaDate(TransactionJournal $journal, string $field): ?Carbon - { - $cache = new CacheProperties; - $cache->addProperty('journal-meta-updated'); - $cache->addProperty($journal->id); - $cache->addProperty($field); - - if ($cache->has()) { - return new Carbon($cache->get()); // @codeCoverageIgnore - } - - $entry = $journal->transactionJournalMeta()->where('name', $field)->first(); - if (null === $entry) { - return null; - } - $value = new Carbon($entry->data); - $cache->store($entry->data); - - return $value; - } - /** * Return string value of a meta date (or NULL). * * @param TransactionJournal $journal - * @param string $field + * @param string $field * * @return null|string */ @@ -544,7 +539,7 @@ class JournalRepository implements JournalRepositoryInterface * Return value of a meta field (or NULL) as a string. * * @param TransactionJournal $journal - * @param string $field + * @param string $field * * @return null|string * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -666,33 +661,6 @@ class JournalRepository implements JournalRepositoryInterface return $journal->transactionType->type; } - /** - * @param array $transactionIds - * - * @return Collection - */ - public function getTransactionsById(array $transactionIds): Collection - { - $journalIds = Transaction::whereIn('id', $transactionIds)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray(); - $journals = new Collection; - foreach ($journalIds as $journalId) { - $result = $this->findNull((int)$journalId); - if (null !== $result) { - $journals->push($result); - } - } - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setUser($this->user); - $collector->setAllAssetAccounts(); - $collector->removeFilter(InternalTransferFilter::class); - //$collector->addFilter(TransferFilter::class); - - $collector->setJournals($journals)->withOpposingAccount(); - - return $collector->getTransactions(); - } - /** * Will tell you if journal is reconciled or not. * @@ -711,6 +679,22 @@ class JournalRepository implements JournalRepositoryInterface return false; } + /** + * @param int $transactionId + * + * @return bool + */ + public function reconcileById(int $transactionId): bool + { + /** @var Transaction $transaction */ + $transaction = $this->user->transactions()->find($transactionId); + if (null !== $transaction) { + return $this->reconcile($transaction); + } + + return false; + } + /** * @param Transaction $transaction * @@ -736,25 +720,9 @@ class JournalRepository implements JournalRepositoryInterface return true; } - /** - * @param int $transactionId - * - * @return bool - */ - public function reconcileById(int $transactionId): bool - { - /** @var Transaction $transaction */ - $transaction = $this->user->transactions()->find($transactionId); - if (null !== $transaction) { - return $this->reconcile($transaction); - } - - return false; - } - /** * @param TransactionJournal $journal - * @param int $order + * @param int $order * * @return bool */ @@ -778,7 +746,7 @@ class JournalRepository implements JournalRepositoryInterface * Update budget for a journal. * * @param TransactionJournal $journal - * @param int $budgetId + * @param int $budgetId * * @return TransactionJournal */ @@ -794,7 +762,7 @@ class JournalRepository implements JournalRepositoryInterface * Update category for a journal. * * @param TransactionJournal $journal - * @param string $category + * @param string $category * * @return TransactionJournal */ @@ -810,7 +778,7 @@ class JournalRepository implements JournalRepositoryInterface * Update tag(s) for a journal. * * @param TransactionJournal $journal - * @param array $tags + * @param array $tags * * @return TransactionJournal */ diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index d643783927..6d4825fd2b 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -276,13 +276,6 @@ interface JournalRepositoryInterface */ public function getTransactionType(TransactionJournal $journal): string; - /** - * @param array $transactionIds - * - * @return Collection - */ - public function getTransactionsById(array $transactionIds): Collection; - /** * Will tell you if journal is reconciled or not. * @@ -319,8 +312,6 @@ interface JournalRepositoryInterface */ public function setUser(User $user); - - /** * Update budget for a journal. * diff --git a/app/Repositories/Recurring/RecurringRepository.php b/app/Repositories/Recurring/RecurringRepository.php index 82b8b086fd..dcefc99d87 100644 --- a/app/Repositories/Recurring/RecurringRepository.php +++ b/app/Repositories/Recurring/RecurringRepository.php @@ -26,8 +26,7 @@ namespace FireflyIII\Repositories\Recurring; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\RecurrenceFactory; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; -use FireflyIII\Helpers\Filter\InternalTransferFilter; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Note; use FireflyIII\Models\Preference; use FireflyIII\Models\Recurrence; @@ -151,7 +150,7 @@ class RecurringRepository implements RecurringRepositoryInterface /** * Returns the journals created for this recurrence, possibly limited by time. * - * @param Recurrence $recurrence + * @param Recurrence $recurrence * @param Carbon|null $start * @param Carbon|null $end * @@ -213,8 +212,8 @@ class RecurringRepository implements RecurringRepositoryInterface * Generate events in the date range. * * @param RecurrenceRepetition $repetition - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * * @return array @@ -274,8 +273,8 @@ class RecurringRepository implements RecurringRepositoryInterface /** * @param Recurrence $recurrence - * @param int $page - * @param int $pageSize + * @param int $page + * @param int $pageSize * * @return LengthAwarePaginator */ @@ -290,25 +289,27 @@ class RecurringRepository implements RecurringRepositoryInterface ->get()->pluck('transaction_journal_id')->toArray(); $search = []; foreach ($journalMeta as $journalId) { - $search[] = ['id' => (int)$journalId]; + $search[] = (int)$journalId; } - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setUser($recurrence->user); - $collector->withOpposingAccount()->setAllAssetAccounts()->withCategoryInformation()->withBudgetInformation()->setLimit($pageSize)->setPage($page); - // filter on specific journals. - $collector->removeFilter(InternalTransferFilter::class); - $collector->setJournals(new Collection($search)); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); - return $collector->getPaginatedTransactions(); + $collector->setUser($recurrence->user); + $collector->withCategoryInformation()->withBudgetInformation()->setLimit($pageSize)->setPage($page) + ->withAccountInformation(); + $collector->setJournalIds($search); + + return $collector->getPaginatedGroups(); } /** + * TODO check usage and verify it still works. + * * @param Recurrence $recurrence * * @return Collection */ - public function getTransactions(Recurrence $recurrence): Collection + public function getTransactions(Recurrence $recurrence): array { $journalMeta = TransactionJournalMeta ::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') @@ -319,25 +320,25 @@ class RecurringRepository implements RecurringRepositoryInterface ->get()->pluck('transaction_journal_id')->toArray(); $search = []; foreach ($journalMeta as $journalId) { - $search[] = ['id' => (int)$journalId]; + $search[] = (int)$journalId; } - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setUser($recurrence->user); - $collector->withOpposingAccount()->setAllAssetAccounts()->withCategoryInformation()->withBudgetInformation(); - // filter on specific journals. - $collector->removeFilter(InternalTransferFilter::class); - $collector->setJournals(new Collection($search)); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); - return $collector->getTransactions(); + $collector->setUser($recurrence->user); + $collector->withCategoryInformation()->withBudgetInformation()->withAccountInformation(); + // filter on specific journals. + $collector->setJournalIds($search); + + return $collector->getExtractedJournals(); } /** * Calculate the next X iterations starting on the date given in $date. * * @param RecurrenceRepetition $repetition - * @param Carbon $date - * @param int $count + * @param Carbon $date + * @param int $count * * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -441,7 +442,7 @@ class RecurringRepository implements RecurringRepositoryInterface * Update a recurring transaction. * * @param Recurrence $recurrence - * @param array $data + * @param array $data * * @return Recurrence * @throws FireflyException diff --git a/app/Repositories/Recurring/RecurringRepositoryInterface.php b/app/Repositories/Recurring/RecurringRepositoryInterface.php index fb28828be9..be8648f43b 100644 --- a/app/Repositories/Recurring/RecurringRepositoryInterface.php +++ b/app/Repositories/Recurring/RecurringRepositoryInterface.php @@ -141,9 +141,9 @@ interface RecurringRepositoryInterface /** * @param Recurrence $recurrence * - * @return Collection + * @return array */ - public function getTransactions(Recurrence $recurrence): Collection; + public function getTransactions(Recurrence $recurrence): array; /** * Calculate the next X iterations starting on the date given in $date. diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 3ed62a9b64..c6ae561452 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -26,7 +26,6 @@ use Carbon\Carbon; use DB; use FireflyIII\Factory\TagFactory; use FireflyIII\Helpers\Collector\GroupCollectorInterface; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionType; use FireflyIII\User; @@ -84,13 +83,13 @@ class TagRepository implements TagRepositoryInterface */ public function earnedInPeriod(Tag $tag, Carbon $start, Carbon $end): string { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setUser($this->user); - $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAllAssetAccounts()->setTag($tag); - $set = $collector->getTransactions(); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); - return (string)$set->sum('transaction_amount'); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setTag($tag); + + return $collector->getSum(); } /** @@ -167,7 +166,7 @@ class TagRepository implements TagRepositoryInterface public function incomeInPeriod(Tag $tag, Carbon $start, Carbon $end): array { /** @var GroupCollectorInterface $collector */ - $collector =app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setTag($tag); @@ -244,13 +243,13 @@ class TagRepository implements TagRepositoryInterface */ public function spentInPeriod(Tag $tag, Carbon $start, Carbon $end): string { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setUser($this->user); - $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAllAssetAccounts()->setTag($tag); - $set = $collector->getTransactions(); + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); - return (string)$set->sum('transaction_amount'); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setTag($tag); + + return $collector->getSum(); } /** diff --git a/app/Support/Http/Controllers/AugumentData.php b/app/Support/Http/Controllers/AugumentData.php index 80009e8376..53bb5d1025 100644 --- a/app/Support/Http/Controllers/AugumentData.php +++ b/app/Support/Http/Controllers/AugumentData.php @@ -25,12 +25,10 @@ namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; use FireflyIII\Helpers\Collector\GroupCollectorInterface; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; @@ -86,22 +84,19 @@ trait AugumentData */ protected function earnedByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); - $collector->setOpposingAccounts($opposing)->withCategoryInformation(); - $set = $collector->getTransactions(); - $sum = []; + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + $total = $assets->merge($opposing); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($total); + $collector->withCategoryInformation(); + $journals = $collector->getExtractedJournals(); + $sum = []; // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = $transaction->transaction_currency_id; - $categoryName = $transaction->transaction_category_name; - $categoryId = (int)$transaction->transaction_category_id; - // if null, grab from journal: - if (0 === $categoryId) { - $categoryName = $transaction->transaction_journal_category_name; - $categoryId = (int)$transaction->transaction_journal_category_id; - } + foreach ($journals as $journal) { + $currencyId = $journal['currency_id']; + $categoryName = $journal['category_name']; + $categoryId = (int)$journal['category_id']; // if not set, set to zero: if (!isset($sum[$categoryId][$currencyId])) { @@ -116,8 +111,8 @@ trait AugumentData 'name' => $categoryName, ], 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, + 'symbol' => $journal['currency_symbol'], + 'dp' => $journal['currency_decimal_places'], ], ], ], @@ -126,9 +121,9 @@ trait AugumentData // add amount $sum[$categoryId]['per_currency'][$currencyId]['sum'] = bcadd( - $sum[$categoryId]['per_currency'][$currencyId]['sum'], $transaction->transaction_amount + $sum[$categoryId]['per_currency'][$currencyId]['sum'], $journal['amount'] ); - $sum[$categoryId]['grand_total'] = bcadd($sum[$categoryId]['grand_total'], $transaction->transaction_amount); + $sum[$categoryId]['grand_total'] = bcadd($sum[$categoryId]['grand_total'], $journal['amount']); } return $sum; @@ -146,33 +141,34 @@ trait AugumentData */ protected function earnedInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); - $collector->setOpposingAccounts($opposing); - $set = $collector->getTransactions(); - $sum = [ + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + $total = $assets->merge($opposing); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($total); + $journals = $collector->getExtractedJournals(); + $sum = [ 'grand_sum' => '0', 'per_currency' => [], ]; // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = $transaction->transaction_currency_id; + foreach ($journals as $journal) { + $currencyId = (int)$journal['currency_id']; // if not set, set to zero: if (!isset($sum['per_currency'][$currencyId])) { $sum['per_currency'][$currencyId] = [ 'sum' => '0', 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, + 'symbol' => $journal['currency_symbol'], + 'decimal_places' => $journal['currency_decimal_places'], ], ]; } // add amount - $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $transaction->transaction_amount); - $sum['grand_sum'] = bcadd($sum['grand_sum'], $transaction->transaction_amount); + $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $journal['amount']); + $sum['grand_sum'] = bcadd($sum['grand_sum'], $journal['amount']); } return $sum; @@ -525,19 +521,27 @@ trait AugumentData /** * Group set of transactions by name of opposing account. * - * @param Collection $set + * @param array $array * * @return array */ - protected function groupByName(Collection $set): array // filter + group data + protected function groupByName(array $array): array // filter + group data { + // group by opposing account name. $grouped = []; - /** @var Transaction $transaction */ - foreach ($set as $transaction) { - $name = $transaction->opposing_account_name; + /** @var array $journal */ + foreach ($array as $journal) { + $name = '(no name)'; + if (TransactionType::WITHDRAWAL === $journal['transaction_type_type']) { + $name = $journal['destination_account_name']; + } + if (TransactionType::WITHDRAWAL !== $journal['transaction_type_type']) { + $name = $journal['source_account_name']; + } + $grouped[$name] = $grouped[$name] ?? '0'; - $grouped[$name] = bcadd($transaction->transaction_amount, $grouped[$name]); + $grouped[$name] = bcadd($journal['amount'], $grouped[$name]); } return $grouped; @@ -587,22 +591,18 @@ trait AugumentData */ protected function spentByBudget(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); - $collector->setOpposingAccounts($opposing)->withBudgetInformation(); - $set = $collector->getTransactions(); - $sum = []; + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $total = $assets->merge($opposing); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($total); + $collector->withBudgetInformation(); + $journals = $collector->getExtractedJournals(); + $sum = []; // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = $transaction->transaction_currency_id; - $budgetName = $transaction->transaction_budget_name; - $budgetId = (int)$transaction->transaction_budget_id; - // if null, grab from journal: - if (0 === $budgetId) { - $budgetName = $transaction->transaction_journal_budget_name; - $budgetId = (int)$transaction->transaction_journal_budget_id; - } + foreach ($journals as $journal) { + $currencyId = $journal['currency_id']; + $budgetName = $journal['budget_name']; + $budgetId = (int)$journal['budget_id']; // if not set, set to zero: if (!isset($sum[$budgetId][$currencyId])) { @@ -617,8 +617,8 @@ trait AugumentData 'name' => $budgetName, ], 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, + 'symbol' => $journal['currency_symbol'], + 'dp' => $journal['currency_decimal_places'], ], ], ], @@ -627,9 +627,9 @@ trait AugumentData // add amount $sum[$budgetId]['per_currency'][$currencyId]['sum'] = bcadd( - $sum[$budgetId]['per_currency'][$currencyId]['sum'], $transaction->transaction_amount + $sum[$budgetId]['per_currency'][$currencyId]['sum'], $journal['amount'] ); - $sum[$budgetId]['grand_total'] = bcadd($sum[$budgetId]['grand_total'], $transaction->transaction_amount); + $sum[$budgetId]['grand_total'] = bcadd($sum[$budgetId]['grand_total'], $journal['amount']); } return $sum; @@ -652,22 +652,18 @@ trait AugumentData */ protected function spentByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); - $collector->setOpposingAccounts($opposing)->withCategoryInformation(); - $set = $collector->getTransactions(); - $sum = []; + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $total = $assets->merge($opposing); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($total); + $collector->withCategoryInformation(); + $journals = $collector->getExtractedJournals(); + $sum = []; // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = $transaction->transaction_currency_id; - $categoryName = $transaction->transaction_category_name; - $categoryId = (int)$transaction->transaction_category_id; - // if null, grab from journal: - if (0 === $categoryId) { - $categoryName = $transaction->transaction_journal_category_name; - $categoryId = (int)$transaction->transaction_journal_category_id; - } + foreach ($journals as $journal) { + $currencyId = (int)$journal['currency_id']; + $categoryName = $journal['category_name']; + $categoryId = (int)$journal['category_id']; // if not set, set to zero: if (!isset($sum[$categoryId][$currencyId])) { @@ -682,8 +678,8 @@ trait AugumentData 'name' => $categoryName, ], 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, + 'symbol' => $journal['currency_symbol'], + 'dp' => $journal['currency_decimal_places'], ], ], ], @@ -692,9 +688,9 @@ trait AugumentData // add amount $sum[$categoryId]['per_currency'][$currencyId]['sum'] = bcadd( - $sum[$categoryId]['per_currency'][$currencyId]['sum'], $transaction->transaction_amount + $sum[$categoryId]['per_currency'][$currencyId]['sum'], $journal['amount'] ); - $sum[$categoryId]['grand_total'] = bcadd($sum[$categoryId]['grand_total'], $transaction->transaction_amount); + $sum[$categoryId]['grand_total'] = bcadd($sum[$categoryId]['grand_total'], $journal['amount']); } return $sum; @@ -714,33 +710,35 @@ trait AugumentData */ protected function spentInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); - $collector->setOpposingAccounts($opposing); - $set = $collector->getTransactions(); - $sum = [ + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + $total = $assets->merge($opposing); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($total); + $journals = $collector->getExtractedJournals(); + $sum = [ 'grand_sum' => '0', 'per_currency' => [], ]; // loop to support multi currency - foreach ($set as $transaction) { - $currencyId = (int)$transaction->transaction_currency_id; + foreach ($journals as $journal) { + $currencyId = (int)$journal['currency_id']; // if not set, set to zero: if (!isset($sum['per_currency'][$currencyId])) { $sum['per_currency'][$currencyId] = [ 'sum' => '0', 'currency' => [ - 'symbol' => $transaction->transaction_currency_symbol, - 'dp' => $transaction->transaction_currency_dp, + 'name' => $journal['currency_name'], + 'symbol' => $journal['currency_symbol'], + 'decimal_places' => $journal['currency_decimal_places'], ], ]; } // add amount - $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $transaction->transaction_amount); - $sum['grand_sum'] = bcadd($sum['grand_sum'], $transaction->transaction_amount); + $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $journal['amount']); + $sum['grand_sum'] = bcadd($sum['grand_sum'], $journal['amount']); } return $sum; @@ -767,6 +765,7 @@ trait AugumentData $collector = app(GroupCollectorInterface::class); $types = [TransactionType::WITHDRAWAL]; $collector->setTypes($types)->setRange($start, $end)->withoutBudget(); + return $collector->getSum(); } } diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 5b0f6fe5da..fec89a7edb 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -24,11 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupSumCollectorInterface; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; -use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Models\Account; use FireflyIII\Models\Category; use FireflyIII\Models\Tag; diff --git a/app/Support/Http/Controllers/RequestInformation.php b/app/Support/Http/Controllers/RequestInformation.php index 76f6c864cd..bfff5dddbc 100644 --- a/app/Support/Http/Controllers/RequestInformation.php +++ b/app/Support/Http/Controllers/RequestInformation.php @@ -24,29 +24,18 @@ declare(strict_types=1); namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\ValidationException; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Help\HelpInterface; -use FireflyIII\Http\Requests\SplitJournalFormRequest; use FireflyIII\Http\Requests\TestRuleFormRequest; -use FireflyIII\Models\Transaction; -use FireflyIII\Models\TransactionJournal; -use FireflyIII\Models\TransactionType; -use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Support\Binder\AccountList; -use FireflyIII\Transformers\TransactionTransformer; use FireflyIII\User; use Hash; use Illuminate\Contracts\Validation\Validator as ValidatorContract; -use Illuminate\Http\Request; use Illuminate\Routing\Route; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Validator; use InvalidArgumentException; use Log; use Route as RouteFacade; -use Symfony\Component\HttpFoundation\ParameterBag; /** * Trait RequestInformation @@ -54,138 +43,7 @@ use Symfony\Component\HttpFoundation\ParameterBag; */ trait RequestInformation { - /** - * Create data-array from a journal. - * - * @param SplitJournalFormRequest|Request $request - * @param TransactionJournal $journal - * - * @return array - * @throws FireflyException - */ - protected function arrayFromJournal(Request $request, TransactionJournal $journal): array // convert user input. - { - /** @var JournalRepositoryInterface $repository */ - $repository = app(JournalRepositoryInterface::class); - $sourceAccounts = $repository->getJournalSourceAccounts($journal); - $destinationAccounts = $repository->getJournalDestinationAccounts($journal); - $array = [ - 'journal_description' => $request->old('journal_description', $journal->description), - 'journal_amount' => '0', - 'journal_foreign_amount' => '0', - 'sourceAccounts' => $sourceAccounts, - 'journal_source_id' => $request->old('journal_source_id', $sourceAccounts->first()->id), - 'journal_source_name' => $request->old('journal_source_name', $sourceAccounts->first()->name), - 'journal_destination_id' => $request->old('journal_destination_id', $destinationAccounts->first()->id), - 'destinationAccounts' => $destinationAccounts, - 'what' => strtolower($this->repository->getTransactionType($journal)), - 'date' => $request->old('date', $this->repository->getJournalDate($journal, null)), - 'tags' => implode(',', $journal->tags->pluck('tag')->toArray()), - // all custom fields: - 'interest_date' => $request->old('interest_date', $repository->getMetaField($journal, 'interest_date')), - 'book_date' => $request->old('book_date', $repository->getMetaField($journal, 'book_date')), - 'process_date' => $request->old('process_date', $repository->getMetaField($journal, 'process_date')), - 'due_date' => $request->old('due_date', $repository->getMetaField($journal, 'due_date')), - 'payment_date' => $request->old('payment_date', $repository->getMetaField($journal, 'payment_date')), - 'invoice_date' => $request->old('invoice_date', $repository->getMetaField($journal, 'invoice_date')), - 'internal_reference' => $request->old('internal_reference', $repository->getMetaField($journal, 'internal_reference')), - 'notes' => $request->old('notes', $repository->getNoteText($journal)), - - // transactions. - 'transactions' => $this->getTransactionDataFromJournal($journal), - ]; - // update transactions array with old request data. - $array['transactions'] = $this->updateWithPrevious($array['transactions'], $request->old()); - - // update journal amount and foreign amount: - $array['journal_amount'] = array_sum(array_column($array['transactions'], 'amount')); - $array['journal_foreign_amount'] = array_sum(array_column($array['transactions'], 'foreign_amount')); - - return $array; - } - - /** - * Get transaction overview from journal. - * - * @param TransactionJournal $journal - * - * @return array - * @throws FireflyException - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function getTransactionDataFromJournal(TransactionJournal $journal): array // convert object - { - // use collector to collect transactions. - $collector = app(TransactionCollectorInterface::class); - $collector->setUser(auth()->user()); - $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); - // filter on specific journals. - $collector->setJournals(new Collection([$journal])); - $set = $collector->getTransactions(); - $transactions = []; - - /** @var TransactionTransformer $transformer */ - $transformer = app(TransactionTransformer::class); - $transformer->setParameters(new ParameterBag()); - /** @var Transaction $transaction */ - foreach ($set as $transaction) { - $res = []; - if ((float)$transaction->transaction_amount > 0 && $journal->transactionType->type === TransactionType::DEPOSIT) { - $res = $transformer->transform($transaction); - } - if ((float)$transaction->transaction_amount < 0 && $journal->transactionType->type !== TransactionType::DEPOSIT) { - $res = $transformer->transform($transaction); - } - - if (count($res) > 0) { - $res['amount'] = app('steam')->positive((string)$res['amount']); - $res['foreign_amount'] = app('steam')->positive((string)$res['foreign_amount']); - $transactions[] = $res; - } - } - - return $transactions; - } - - /** - * Get info from old input. - * - * @param $array - * @param $old - * - * @return array - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function updateWithPrevious($array, $old): array // update object with new info - { - if (0 === count($old) || !isset($old['transactions'])) { - return $array; - } - $old = $old['transactions']; - - foreach ($old as $index => $row) { - if (isset($array[$index])) { - /** @noinspection SlowArrayOperationsInLoopInspection */ - $array[$index] = array_merge($array[$index], $row); - continue; - } - // take some info from first transaction, that should at least exist. - $array[$index] = $row; - $array[$index]['currency_id'] = $array[0]['currency_id']; - $array[$index]['currency_code'] = $array[0]['currency_code'] ?? ''; - $array[$index]['currency_symbol'] = $array[0]['currency_symbol'] ?? ''; - $array[$index]['foreign_amount'] = round($array[0]['foreign_destination_amount'] ?? '0', 12); - $array[$index]['foreign_currency_id'] = $array[0]['foreign_currency_id']; - $array[$index]['foreign_currency_code'] = $array[0]['foreign_currency_code']; - $array[$index]['foreign_currency_symbol'] = $array[0]['foreign_currency_symbol']; - } - - return $array; - } /** * Get the domain of FF system. diff --git a/app/Support/Http/Controllers/TransactionCalculation.php b/app/Support/Http/Controllers/TransactionCalculation.php index f2db26bfdf..8fa5949477 100644 --- a/app/Support/Http/Controllers/TransactionCalculation.php +++ b/app/Support/Http/Controllers/TransactionCalculation.php @@ -46,14 +46,18 @@ trait TransactionCalculation */ protected function getExpensesForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): array { + $total = $accounts->merge($opposing); + /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); - $collector->setAccounts($accounts) + $collector->setAccounts($total) ->setRange($start, $end) - ->setTypes([TransactionType::WITHDRAWAL]) - ->setAccounts($opposing); + ->withAccountInformation() + ->setTypes([TransactionType::WITHDRAWAL]); - return $collector->getExtractedJournals(); + $result = $collector->getExtractedJournals(); + + return $result; } /** @@ -156,10 +160,10 @@ trait TransactionCalculation */ protected function getIncomeForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): array { + $total =$accounts->merge($opposing); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); - $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT]) - ->setAccounts($opposing); + $collector->setAccounts($total)->setRange($start, $end)->withAccountInformation()->setTypes([TransactionType::DEPOSIT]); return $collector->getExtractedJournals(); } diff --git a/app/Support/Import/Placeholder/ImportTransaction.php b/app/Support/Import/Placeholder/ImportTransaction.php index 15a24b5103..7395d67835 100644 --- a/app/Support/Import/Placeholder/ImportTransaction.php +++ b/app/Support/Import/Placeholder/ImportTransaction.php @@ -160,6 +160,12 @@ class ImportTransaction 'opposing-bic' => 'opposingBic', 'opposing-number' => 'opposingNumber', ]; + + // overrule some old role values. + if ('original-source' === $role) { + $role = 'original_source'; + } + if (isset($basics[$role])) { $field = $basics[$role]; $this->$field = $columnValue->getValue(); @@ -229,6 +235,18 @@ class ImportTransaction } } + /** + * Returns the mapped value if it exists in the ColumnValue object. + * + * @param ColumnValue $columnValue + * + * @return int + */ + private function getMappedValue(ColumnValue $columnValue): int + { + return $columnValue->getMappedValue() > 0 ? $columnValue->getMappedValue() : (int)$columnValue->getValue(); + } + /** * Calculate the amount of this transaction. * @@ -276,6 +294,40 @@ class ImportTransaction return $result; } + /** + * This methods decides which input value to use for the amount calculation. + * + * @return array + */ + private function selectAmountInput(): array + { + $info = []; + $converterClass = ''; + if (null !== $this->amount) { + Log::debug('Amount value is not NULL, assume this is the correct value.'); + $converterClass = Amount::class; + $info['amount'] = $this->amount; + } + if (null !== $this->amountDebit) { + Log::debug('Amount DEBIT value is not NULL, assume this is the correct value (overrules Amount).'); + $converterClass = AmountDebit::class; + $info['amount'] = $this->amountDebit; + } + if (null !== $this->amountCredit) { + Log::debug('Amount CREDIT value is not NULL, assume this is the correct value (overrules Amount and AmountDebit).'); + $converterClass = AmountCredit::class; + $info['amount'] = $this->amountCredit; + } + if (null !== $this->amountNegated) { + Log::debug('Amount NEGATED value is not NULL, assume this is the correct value (overrules Amount and AmountDebit and AmountCredit).'); + $converterClass = AmountNegated::class; + $info['amount'] = $this->amountNegated; + } + $info['class'] = $converterClass; + + return $info; + } + /** * The method that calculates the foreign amount isn't nearly as complex,\ * because Firefly III only supports one foreign amount field. So the foreign amount is there @@ -372,50 +424,4 @@ class ImportTransaction ]; } - /** - * Returns the mapped value if it exists in the ColumnValue object. - * - * @param ColumnValue $columnValue - * - * @return int - */ - private function getMappedValue(ColumnValue $columnValue): int - { - return $columnValue->getMappedValue() > 0 ? $columnValue->getMappedValue() : (int)$columnValue->getValue(); - } - - /** - * This methods decides which input value to use for the amount calculation. - * - * @return array - */ - private function selectAmountInput(): array - { - $info = []; - $converterClass = ''; - if (null !== $this->amount) { - Log::debug('Amount value is not NULL, assume this is the correct value.'); - $converterClass = Amount::class; - $info['amount'] = $this->amount; - } - if (null !== $this->amountDebit) { - Log::debug('Amount DEBIT value is not NULL, assume this is the correct value (overrules Amount).'); - $converterClass = AmountDebit::class; - $info['amount'] = $this->amountDebit; - } - if (null !== $this->amountCredit) { - Log::debug('Amount CREDIT value is not NULL, assume this is the correct value (overrules Amount and AmountDebit).'); - $converterClass = AmountCredit::class; - $info['amount'] = $this->amountCredit; - } - if (null !== $this->amountNegated) { - Log::debug('Amount NEGATED value is not NULL, assume this is the correct value (overrules Amount and AmountDebit and AmountCredit).'); - $converterClass = AmountNegated::class; - $info['amount'] = $this->amountNegated; - } - $info['class'] = $converterClass; - - return $info; - } - } diff --git a/app/Support/Import/Routine/File/ImportableConverter.php b/app/Support/Import/Routine/File/ImportableConverter.php index 8059345d34..002f2131f7 100644 --- a/app/Support/Import/Routine/File/ImportableConverter.php +++ b/app/Support/Import/Routine/File/ImportableConverter.php @@ -89,79 +89,11 @@ class ImportableConverter return $result; } - /** - * @param ImportJob $importJob - */ - public function setImportJob(ImportJob $importJob): void - { - $this->importJob = $importJob; - $this->config = $importJob->configuration; - - // repository is used for error messages - $this->repository = app(ImportJobRepositoryInterface::class); - $this->repository->setUser($importJob->user); - - // asset account mapper can map asset accounts (makes sense right?) - $this->assetMapper = app(AssetAccountMapper::class); - $this->assetMapper->setUser($importJob->user); - $this->assetMapper->setDefaultAccount($this->config['import-account'] ?? 0); - - // asset account repository is used for currency information - $this->accountRepository = app(AccountRepositoryInterface::class); - $this->accountRepository->setUser($importJob->user); - - // opposing account mapper: - $this->opposingMapper = app(OpposingAccountMapper::class); - $this->opposingMapper->setUser($importJob->user); - - // currency mapper: - $this->currencyMapper = app(CurrencyMapper::class); - $this->currencyMapper->setUser($importJob->user); - $this->defaultCurrency = app('amount')->getDefaultCurrencyByUser($importJob->user); - } - - /** - * @codeCoverageIgnore - * - * @param array $mappedValues - */ - public function setMappedValues(array $mappedValues): void - { - $this->mappedValues = $mappedValues; - } - - /** - * @param string|null $date - * - * @return string|null - */ - private function convertDateValue(string $date = null): ?string - { - $result = null; - if (null !== $date) { - try { - // add exclamation mark for better parsing. http://php.net/manual/en/datetime.createfromformat.php - $dateFormat = $this->config['date-format'] ?? 'Ymd'; - if ('!' !== $dateFormat{0}) { - $dateFormat = '!' . $dateFormat; - } - $object = Carbon::createFromFormat($dateFormat, $date); - $result = $object->format('Y-m-d H:i:s'); - Log::debug(sprintf('createFromFormat: Turning "%s" into "%s" using "%s"', $date, $result, $this->config['date-format'] ?? 'Ymd')); - } catch (InvalidDateException|InvalidArgumentException $e) { - Log::error($e->getMessage()); - Log::error($e->getTraceAsString()); - } - } - - return $result; - } - /** * @param ImportTransaction $importable * - * @throws FireflyException * @return array + * @throws FireflyException */ private function convertSingle(ImportTransaction $importable): array { @@ -218,60 +150,98 @@ class ImportableConverter } return [ - 'type' => $transactionType, - 'date' => $this->convertDateValue($importable->date) ?? Carbon::now()->format('Y-m-d H:i:s'), - 'tags' => $importable->tags, - 'user' => $this->importJob->user_id, - 'notes' => $importable->note, - // all custom fields: - 'internal_reference' => $importable->meta['internal-reference'] ?? null, - 'sepa_cc' => $importable->meta['sepa_cc'] ?? null, - 'sepa_ct_op' => $importable->meta['sepa_ct_op'] ?? null, - 'sepa_ct_id' => $importable->meta['sepa_ct_id'] ?? null, - 'sepa_db' => $importable->meta['sepa_db'] ?? null, - 'sepa_country' => $importable->meta['sepa_country'] ?? null, - 'sepa_ep' => $importable->meta['sepa_ep'] ?? null, - 'sepa_ci' => $importable->meta['sepa_ci'] ?? null, - 'sepa_batch_id' => $importable->meta['sepa_batch_id'] ?? null, - 'interest_date' => $this->convertDateValue($importable->meta['date-interest'] ?? null), - 'book_date' => $this->convertDateValue($importable->meta['date-book'] ?? null), - 'process_date' => $this->convertDateValue($importable->meta['date-process'] ?? null), - 'due_date' => $this->convertDateValue($importable->meta['date-due'] ?? null), - 'payment_date' => $this->convertDateValue($importable->meta['date-payment'] ?? null), - 'invoice_date' => $this->convertDateValue($importable->meta['date-invoice'] ?? null), - 'external_id' => $importable->externalId, - 'original-source' => $importable->meta['original-source'] ?? null, - // journal data: - 'description' => $importable->description, - 'piggy_bank_id' => null, - 'piggy_bank_name' => null, - 'bill_id' => $importable->billId, - 'bill_name' => $importable->billName, - - // transaction data: - 'transactions' => [ + 'user' => $this->importJob->user_id, + 'group_title' => null, + 'transactions' => [ [ - 'currency_id' => $currency->id, - 'currency_code' => null, - 'description' => null, - 'amount' => $amount, - 'budget_id' => $importable->budgetId, - 'budget_name' => $importable->budgetName, - 'category_id' => $importable->categoryId, - 'category_name' => $importable->categoryName, - 'source_id' => $source->id, - 'source_name' => null, - 'destination_id' => $destination->id, - 'destination_name' => null, + 'user' => $this->importJob->user_id, + 'type' => $transactionType, + 'date' => $this->convertDateValue($importable->date) ?? Carbon::now()->format('Y-m-d H:i:s'), + 'order' => 0, + + 'currency_id' => $currency->id, + 'currency_code' => null, + 'foreign_currency_id' => $importable->foreignCurrencyId, 'foreign_currency_code' => null === $foreignCurrency ? null : $foreignCurrency->code, - 'foreign_amount' => $foreignAmount, - 'reconciled' => false, - 'identifier' => 0, + + 'amount' => $amount, + 'foreign_amount' => $foreignAmount, + + 'description' => $importable->description, + + 'source_id' => $source->id, + 'source_name' => null, + 'destination_id' => $destination->id, + 'destination_name' => null, + + 'budget_id' => $importable->budgetId, + 'budget_name' => $importable->budgetName, + + 'category_id' => $importable->categoryId, + 'category_name' => $importable->categoryName, + + 'bill_id' => $importable->billId, + 'bill_name' => $importable->billName, + + 'piggy_bank_id' => null, + 'piggy_bank_name' => null, + + 'reconciled' => false, + + 'notes' => $importable->note, + 'tags' => $importable->tags, + + 'internal_reference' => $importable->meta['internal-reference'] ?? null, + 'external_id' => $importable->externalId, + 'original_source' => $importable->meta['original-source'] ?? null, + + 'sepa_cc' => $importable->meta['sepa_cc'] ?? null, + 'sepa_ct_op' => $importable->meta['sepa_ct_op'] ?? null, + 'sepa_ct_id' => $importable->meta['sepa_ct_id'] ?? null, + 'sepa_db' => $importable->meta['sepa_db'] ?? null, + 'sepa_country' => $importable->meta['sepa_country'] ?? null, + 'sepa_ep' => $importable->meta['sepa_ep'] ?? null, + 'sepa_ci' => $importable->meta['sepa_ci'] ?? null, + 'sepa_batch_id' => $importable->meta['sepa_batch_id'] ?? null, + + 'interest_date' => $this->convertDateValue($importable->meta['date-interest'] ?? null), + 'book_date' => $this->convertDateValue($importable->meta['date-book'] ?? null), + 'process_date' => $this->convertDateValue($importable->meta['date-process'] ?? null), + 'due_date' => $this->convertDateValue($importable->meta['date-due'] ?? null), + 'payment_date' => $this->convertDateValue($importable->meta['date-payment'] ?? null), + 'invoice_date' => $this->convertDateValue($importable->meta['date-invoice'] ?? null), ], ], ]; + + } + + /** + * @param string $source + * @param string $destination + * + * @return string + */ + private function getTransactionType(string $source, string $destination): string + { + $type = 'unknown'; + + if ($source === AccountType::ASSET && $destination === AccountType::ASSET) { + Log::debug('Source and destination are asset accounts. This is a transfer.'); + $type = 'transfer'; + } + if ($source === AccountType::REVENUE) { + Log::debug('Source is a revenue account. This is a deposit.'); + $type = 'deposit'; + } + if ($destination === AccountType::EXPENSE) { + Log::debug('Destination is an expense account. This is a withdrawal.'); + $type = 'withdrawal'; + } + + return $type; } /** @@ -305,28 +275,70 @@ class ImportableConverter } /** - * @param string $source - * @param string $destination + * @param string|null $date * - * @return string + * @return string|null */ - private function getTransactionType(string $source, string $destination): string + private function convertDateValue(string $date = null): ?string { - $type = 'unknown'; - - if ($source === AccountType::ASSET && $destination === AccountType::ASSET) { - Log::debug('Source and destination are asset accounts. This is a transfer.'); - $type = 'transfer'; - } - if ($source === AccountType::REVENUE) { - Log::debug('Source is a revenue account. This is a deposit.'); - $type = 'deposit'; - } - if ($destination === AccountType::EXPENSE) { - Log::debug('Destination is an expense account. This is a withdrawal.'); - $type = 'withdrawal'; + $result = null; + if (null !== $date) { + try { + // add exclamation mark for better parsing. http://php.net/manual/en/datetime.createfromformat.php + $dateFormat = $this->config['date-format'] ?? 'Ymd'; + if ('!' !== $dateFormat{0}) { + $dateFormat = '!' . $dateFormat; + } + $object = Carbon::createFromFormat($dateFormat, $date); + $result = $object->format('Y-m-d H:i:s'); + Log::debug(sprintf('createFromFormat: Turning "%s" into "%s" using "%s"', $date, $result, $this->config['date-format'] ?? 'Ymd')); + } catch (InvalidDateException|InvalidArgumentException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + } } - return $type; + return $result; + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->config = $importJob->configuration; + + // repository is used for error messages + $this->repository = app(ImportJobRepositoryInterface::class); + $this->repository->setUser($importJob->user); + + // asset account mapper can map asset accounts (makes sense right?) + $this->assetMapper = app(AssetAccountMapper::class); + $this->assetMapper->setUser($importJob->user); + $this->assetMapper->setDefaultAccount($this->config['import-account'] ?? 0); + + // asset account repository is used for currency information + $this->accountRepository = app(AccountRepositoryInterface::class); + $this->accountRepository->setUser($importJob->user); + + // opposing account mapper: + $this->opposingMapper = app(OpposingAccountMapper::class); + $this->opposingMapper->setUser($importJob->user); + + // currency mapper: + $this->currencyMapper = app(CurrencyMapper::class); + $this->currencyMapper->setUser($importJob->user); + $this->defaultCurrency = app('amount')->getDefaultCurrencyByUser($importJob->user); + } + + /** + * @codeCoverageIgnore + * + * @param array $mappedValues + */ + public function setMappedValues(array $mappedValues): void + { + $this->mappedValues = $mappedValues; } } diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index ea3c06d324..03eb3233ca 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -23,9 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Support\Search; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; -use FireflyIII\Helpers\Filter\DoubleTransactionFilter; -use FireflyIII\Helpers\Filter\InternalTransferFilter; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; @@ -132,6 +130,22 @@ class Search implements SearchInterface } } + /** + * @param string $string + */ + private function extractModifier(string $string): void + { + $parts = explode(':', $string); + if (2 === count($parts) && '' !== trim((string)$parts[1]) && '' !== trim((string)$parts[0])) { + $type = trim((string)$parts[0]); + $value = trim((string)$parts[1]); + if (\in_array($type, $this->validModifiers, true)) { + // filter for valid type + $this->modifiers->push(['type' => $type, 'value' => $value]); + } + } + } + /** * @return float */ @@ -149,16 +163,13 @@ class Search implements SearchInterface $pageSize = 50; $page = 1; - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->withOpposingAccount(); - if ($this->hasModifiers()) { - $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); - } + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + $collector->setLimit($pageSize)->setPage($page)->withAccountInformation(); + $collector->withCategoryInformation()->withBudgetInformation(); $collector->setSearchWords($this->words); - $collector->removeFilter(InternalTransferFilter::class); - $collector->addFilter(DoubleTransactionFilter::class); // Most modifiers can be applied to the collector directly. $collector = $this->applyModifiers($collector); @@ -168,37 +179,18 @@ class Search implements SearchInterface } /** - * @param int $limit - */ - public function setLimit(int $limit): void - { - $this->limit = $limit; - } - - /** - * @param User $user - */ - public function setUser(User $user): void - { - $this->user = $user; - $this->accountRepository->setUser($user); - $this->billRepository->setUser($user); - $this->categoryRepository->setUser($user); - $this->budgetRepository->setUser($user); - } - - /** - * @param TransactionCollectorInterface $collector + * @param GroupCollectorInterface $collector * - * @return TransactionCollectorInterface + * @return GroupCollectorInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - private function applyModifiers(TransactionCollectorInterface $collector): TransactionCollectorInterface + private function applyModifiers(GroupCollectorInterface $collector): GroupCollectorInterface { /* * TODO: * 'bill', */ + $totalAccounts = new Collection; foreach ($this->modifiers as $modifier) { switch ($modifier['type']) { @@ -209,7 +201,7 @@ class Search implements SearchInterface $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE]; $accounts = $this->accountRepository->searchAccount($modifier['value'], $searchTypes); if ($accounts->count() > 0) { - $collector->setAccounts($accounts); + $totalAccounts = $accounts->merge($totalAccounts); } break; case 'destination': @@ -217,7 +209,7 @@ class Search implements SearchInterface $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE]; $accounts = $this->accountRepository->searchAccount($modifier['value'], $searchTypes); if ($accounts->count() > 0) { - $collector->setOpposingAccounts($accounts); + $totalAccounts = $accounts->merge($totalAccounts); } break; case 'category': @@ -280,23 +272,28 @@ class Search implements SearchInterface break; } } + $collector->setAccounts($totalAccounts); return $collector; } /** - * @param string $string + * @param int $limit */ - private function extractModifier(string $string): void + public function setLimit(int $limit): void { - $parts = explode(':', $string); - if (2 === count($parts) && '' !== trim((string)$parts[1]) && '' !== trim((string)$parts[0])) { - $type = trim((string)$parts[0]); - $value = trim((string)$parts[1]); - if (\in_array($type, $this->validModifiers, true)) { - // filter for valid type - $this->modifiers->push(['type' => $type, 'value' => $value]); - } - } + $this->limit = $limit; + } + + /** + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + $this->accountRepository->setUser($user); + $this->billRepository->setUser($user); + $this->categoryRepository->setUser($user); + $this->budgetRepository->setUser($user); } } diff --git a/app/TransactionRules/TransactionMatcher.php b/app/TransactionRules/TransactionMatcher.php index b589214b0f..ac01df0d57 100644 --- a/app/TransactionRules/TransactionMatcher.php +++ b/app/TransactionRules/TransactionMatcher.php @@ -23,12 +23,11 @@ declare(strict_types=1); namespace FireflyIII\TransactionRules; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; -use FireflyIII\Helpers\Filter\InternalTransferFilter; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Rule; use FireflyIII\Models\RuleTrigger; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionType; +use FireflyIII\User; use Illuminate\Support\Collection; use Log; @@ -78,16 +77,16 @@ class TransactionMatcher * transaction journals matching the given rule. This is accomplished by trying to fire these * triggers onto each transaction journal until enough matches are found ($limit). * - * @return Collection + * @return array * @throws \FireflyIII\Exceptions\FireflyException */ - public function findTransactionsByRule(): Collection + public function findTransactionsByRule(): array { Log::debug('Now in findTransactionsByRule()'); if (0 === count($this->rule->ruleTriggers)) { Log::error('Rule has no triggers!'); - return new Collection; + return []; } // Variables used within the loop. @@ -98,7 +97,7 @@ class TransactionMatcher // If the list of matchingTransactions is larger than the maximum number of results // (e.g. if a large percentage of the transactions match), truncate the list - $result = $result->slice(0, $this->searchLimit); + $result = array_slice($result, 0, $this->searchLimit); return $result; } @@ -254,7 +253,7 @@ class TransactionMatcher * * @return Collection */ - private function runProcessor(Processor $processor): Collection + private function runProcessor(Processor $processor): array { Log::debug('Now in runprocessor()'); // since we have a rule in $this->rule, we can add some of the triggers @@ -269,22 +268,23 @@ class TransactionMatcher $pageSize = min($this->searchLimit, min($this->triggeredLimit, 50)); $processed = 0; $page = 1; - $result = new Collection; + $result = []; Log::debug(sprintf('Search limit is %d, triggered limit is %d, so page size is %d', $this->searchLimit, $this->triggeredLimit, $pageSize)); do { Log::debug('Start of do-loop'); // Fetch a batch of transactions from the database - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setUser(auth()->user()); - $collector->withOpposingAccount(); + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + + /** @var User $user */ + $user = auth()->user(); + + $collector->setUser($user); // limit asset accounts: - if (0 === $this->accounts->count()) { - $collector->setAllAssetAccounts(); - } if ($this->accounts->count() > 0) { $collector->setAccounts($this->accounts); } @@ -305,36 +305,35 @@ class TransactionMatcher Log::debug(sprintf('Amount must be exactly %s', $this->exactAmount)); $collector->amountIs($this->exactAmount); } - $collector->removeFilter(InternalTransferFilter::class); - $set = $collector->getPaginatedTransactions(); - Log::debug(sprintf('Found %d journals to check. ', $set->count())); + $journals = $collector->getExtractedJournals(); + Log::debug(sprintf('Found %d transaction journals to check. ', count($journals))); // Filter transactions that match the given triggers. - $filtered = $set->filter( - function (Transaction $transaction) use ($processor) { - Log::debug(sprintf('Test the triggers on journal #%d (transaction #%d)', $transaction->transaction_journal_id, $transaction->id)); - - return $processor->handleTransaction($transaction); + $filtered = []; + /** @var array $journal */ + foreach ($journals as $journal) { + $result = $processor->handleJournalArray($journal); + if ($result) { + $filtered[] = $journal; } - ); + } - Log::debug(sprintf('Found %d journals that match.', $filtered->count())); + Log::debug(sprintf('Found %d journals that match.', count($filtered))); // merge: - /** @var Collection $result */ - $result = $result->merge($filtered); - Log::debug(sprintf('Total count is now %d', $result->count())); + $result = $result + $filtered; + Log::debug(sprintf('Total count is now %d', count($result))); // Update counters ++$page; - $processed += count($set); + $processed += count($journals); Log::debug(sprintf('Page is now %d, processed is %d', $page, $processed)); // Check for conditions to finish the loop - $reachedEndOfList = $set->count() < 1; - $foundEnough = $result->count() >= $this->triggeredLimit; + $reachedEndOfList = count($journals) < 1; + $foundEnough = count($result) >= $this->triggeredLimit; $searchedEnough = ($processed >= $this->searchLimit); Log::debug(sprintf('reachedEndOfList: %s', var_export($reachedEndOfList, true))); diff --git a/resources/views/v2/emails/report-new-journals-html.twig b/resources/views/v2/emails/report-new-journals-html.twig index a3022d0775..648a72baf4 100644 --- a/resources/views/v2/emails/report-new-journals-html.twig +++ b/resources/views/v2/emails/report-new-journals-html.twig @@ -13,7 +13,7 @@
You can find it in your Firefly III installation:
{% for journal in journals %}
- {{ journal.description }} ({{ journal|journalTotalAmount }})
+ {{ journal.description }} (TODO)
{% endfor %}