Migrated all code to group collector.

This commit is contained in:
James Cole 2019-05-31 13:35:33 +02:00
parent eb6329e556
commit e15c35de64
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
31 changed files with 1540 additions and 1671 deletions

View File

@ -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. * 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 public function setBill(Bill $bill): GroupCollectorInterface
{ {
$this->withBudgetInformation(); $this->withBillInformation();
$this->query->where('transaction_journals.bill_id', '=', $bill->id); $this->query->where('transaction_journals.bill_id', '=', $bill->id);
return $this; 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. * Will include budget ID + name, if any.
* *
@ -382,21 +256,6 @@ class GroupCollector implements GroupCollectorInterface
return $this; 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. * Limit the search to a specific set of budgets.
* *
@ -554,26 +413,6 @@ class GroupCollector implements GroupCollectorInterface
return $this; 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. * Limit the search to one specific transaction group.
* *
@ -617,44 +456,6 @@ class GroupCollector implements GroupCollectorInterface
return $this; 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. * Automatically include all stuff required to make API calls work.
* *
@ -708,25 +509,6 @@ class GroupCollector implements GroupCollectorInterface
return $this; 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. * Limit the search to a specific bunch of categories.
* *
@ -793,6 +575,18 @@ class GroupCollector implements GroupCollectorInterface
return $this; return $this;
} }
/**
*
*/
public function dumpQuery(): void
{
echo $this->query->toSql();
echo '<pre>';
print_r($this->query->getBindings());
echo '</pre>';
}
/** /**
* Return the sum of all journals. * Return the sum of all journals.
* *
@ -830,4 +624,370 @@ class GroupCollector implements GroupCollectorInterface
return $return; 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');
}
} }

View File

@ -85,6 +85,42 @@ interface GroupCollectorInterface
*/ */
public function setBill(Bill $bill): 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. * Limit the search to a specific budget.
* *
@ -123,7 +159,7 @@ interface GroupCollectorInterface
public function setCurrency(TransactionCurrency $currency): 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 * @param array $journalIds
* *
@ -131,6 +167,24 @@ interface GroupCollectorInterface
*/ */
public function setJournalIds(array $journalIds): 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. * Limit the number of returned entries.
* *
@ -241,6 +295,24 @@ interface GroupCollectorInterface
*/ */
public function setCategories(Collection $categories): 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. * Include bill name + ID.
* *

View File

@ -89,7 +89,7 @@ class ExpenseReportController extends Controller
$cache->addProperty($start); $cache->addProperty($start);
$cache->addProperty($end); $cache->addProperty($end);
if ($cache->has()) { if ($cache->has()) {
return response()->json($cache->get()); // @codeCoverageIgnore // return response()->json($cache->get()); // @codeCoverageIgnore
} }
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end); $format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
@ -97,44 +97,43 @@ class ExpenseReportController extends Controller
$chartData = []; $chartData = [];
$currentStart = clone $start; $currentStart = clone $start;
$combined = $this->combineAccounts($expense); $combined = $this->combineAccounts($expense);
// make "all" set: // make "all" set:
$all = new Collection; $all = new Collection;
foreach ($combined as $name => $combi) { foreach ($combined as $name => $combination) {
$all = $all->merge($combi); $all = $all->merge($combination);
} }
// prep chart data: // prep chart data:
/** /**
* @var string $name * @var string $name
* @var Collection $combi * @var Collection $combination
*/ */
foreach ($combined as $name => $combi) { foreach ($combined as $name => $combination) {
// first is always expense account: // first is always expense account:
/** @var Account $exp */ /** @var Account $exp */
$exp = $combi->first(); $exp = $combination->first();
$chartData[$exp->id . '-in'] = [ $chartData[$exp->id . '-in'] = [
'label' => $name . ' (' . strtolower((string)trans('firefly.income')) . ')', 'label' => sprintf('%s (%s)', $name, strtolower((string)trans('firefly.income'))),
'type' => 'bar', 'type' => 'bar',
'yAxisID' => 'y-axis-0', 'yAxisID' => 'y-axis-0',
'entries' => [], 'entries' => [],
]; ];
$chartData[$exp->id . '-out'] = [ $chartData[$exp->id . '-out'] = [
'label' => $name . ' (' . strtolower((string)trans('firefly.expenses')) . ')', 'label' => sprintf('%s (%s)', $name, strtolower((string)trans('firefly.expenses'))),
'type' => 'bar', 'type' => 'bar',
'yAxisID' => 'y-axis-0', 'yAxisID' => 'y-axis-0',
'entries' => [], 'entries' => [],
]; ];
// total in, total out: // total in, total out:
$chartData[$exp->id . '-total-in'] = [ $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', 'type' => 'line',
'fill' => false, 'fill' => false,
'yAxisID' => 'y-axis-1', 'yAxisID' => 'y-axis-1',
'entries' => [], 'entries' => [],
]; ];
$chartData[$exp->id . '-total-out'] = [ $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', 'type' => 'line',
'fill' => false, 'fill' => false,
'yAxisID' => 'y-axis-1', 'yAxisID' => 'y-axis-1',
@ -154,15 +153,15 @@ class ExpenseReportController extends Controller
$income = $this->groupByName($this->getIncomeForOpposing($accounts, $all, $currentStart, $currentEnd)); $income = $this->groupByName($this->getIncomeForOpposing($accounts, $all, $currentStart, $currentEnd));
$label = $currentStart->formatLocalized($format); $label = $currentStart->formatLocalized($format);
foreach ($combined as $name => $combi) { foreach ($combined as $name => $combination) {
// first is always expense account: // first is always expense account:
/** @var Account $exp */ /** @var Account $exp */
$exp = $combi->first(); $exp = $combination->first();
$labelIn = $exp->id . '-in'; $labelIn = $exp->id . '-in';
$labelOut = $exp->id . '-out'; $labelOut = $exp->id . '-out';
$labelSumIn = $exp->id . '-total-in'; $labelSumIn = $exp->id . '-total-in';
$labelSumOut = $exp->id . '-total-out'; $labelSumOut = $exp->id . '-total-out';
$currentIncome = $income[$name] ?? '0'; $currentIncome = bcmul($income[$name] ?? '0', '-1');
$currentExpense = $expenses[$name] ?? '0'; $currentExpense = $expenses[$name] ?? '0';
// add to sum: // add to sum:
@ -180,6 +179,7 @@ class ExpenseReportController extends Controller
/** @var Carbon $currentStart */ /** @var Carbon $currentStart */
$currentStart = clone $currentEnd; $currentStart = clone $currentEnd;
$currentStart->addDay(); $currentStart->addDay();
$currentStart->startOfDay();
} }
// remove all empty entries to prevent cluttering: // remove all empty entries to prevent cluttering:
$newSet = []; $newSet = [];

View File

@ -32,6 +32,7 @@ use Illuminate\Http\Response as LaravelResponse;
use Log; use Log;
/** /**
* TODO make sure all import methods work.
* *
* Class IndexController * Class IndexController
*/ */

View File

@ -96,6 +96,7 @@ class ReconcileController extends Controller
*/ */
public function overview(Request $request, Account $account, Carbon $start, Carbon $end): JsonResponse public function overview(Request $request, Account $account, Carbon $start, Carbon $end): JsonResponse
{ {
if (AccountType::ASSET !== $account->accountType->type) { if (AccountType::ASSET !== $account->accountType->type) {
throw new FireflyException(sprintf('Account %s is not an asset account.', $account->name)); throw new FireflyException(sprintf('Account %s is not an asset account.', $account->name));
} }
@ -107,6 +108,7 @@ class ReconcileController extends Controller
$clearedAmount = '0'; $clearedAmount = '0';
$route = route('accounts.reconcile.submit', [$account->id, $start->format('Ymd'), $end->format('Ymd')]); $route = route('accounts.reconcile.submit', [$account->id, $start->format('Ymd'), $end->format('Ymd')]);
// get sum of transaction amounts: // get sum of transaction amounts:
// TODO these methods no longer exist:
$transactions = $this->repository->getTransactionsById($transactionIds); $transactions = $this->repository->getTransactionsById($transactionIds);
$cleared = $this->repository->getTransactionsById($clearedIds); $cleared = $this->repository->getTransactionsById($clearedIds);
$countCleared = 0; $countCleared = 0;

View File

@ -70,7 +70,7 @@ class CategoryController extends Controller
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); 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 // @codeCoverageIgnoreEnd
@ -112,7 +112,7 @@ class CategoryController extends Controller
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); 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 // @codeCoverageIgnoreEnd
$cache->store($result); $cache->store($result);
@ -167,7 +167,7 @@ class CategoryController extends Controller
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); 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 // @codeCoverageIgnoreEnd

View File

@ -23,9 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Report; namespace FireflyIII\Http\Controllers\Report;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\CacheProperties; use FireflyIII\Support\CacheProperties;
@ -110,7 +109,7 @@ class ExpenseController extends Controller
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error(sprintf('Could not render category::budget: %s', $e->getMessage())); 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 // @codeCoverageIgnoreEnd
$cache->store($result); $cache->store($result);
@ -175,7 +174,7 @@ class ExpenseController extends Controller
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); 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 // @codeCoverageIgnoreEnd
$cache->store($result); $cache->store($result);
@ -227,7 +226,7 @@ class ExpenseController extends Controller
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); 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 // @codeCoverageIgnoreEnd
$cache->store($result); $cache->store($result);
@ -265,22 +264,23 @@ class ExpenseController extends Controller
$all = $all->merge($combi); $all = $all->merge($combi);
} }
// get all expenses in period: // get all expenses in period:
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($accounts); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($accounts);
$collector->setOpposingAccounts($all); $collector->setAccounts($all);
$set = $collector->getTransactions(); $set = $collector->getExtractedJournals();
$sorted = $set->sortBy(
function (Transaction $transaction) { usort($set, function ($a, $b) {
return (float)$transaction->transaction_amount; return $a['amount'] <=> $b['amount'];
} });
);
try { try {
$result = view('reports.partials.top-transactions', compact('sorted'))->render(); $result = view('reports.partials.top-transactions', compact('sorted'))->render();
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error(sprintf('Could not render category::topExpense: %s', $e->getMessage())); 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 // @codeCoverageIgnoreEnd
$cache->store($result); $cache->store($result);
@ -316,22 +316,24 @@ class ExpenseController extends Controller
$all = $all->merge($combi); $all = $all->merge($combi);
} }
// get all expenses in period: // get all expenses in period:
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); /** @var GroupCollectorInterface $collector */
$collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($accounts); $collector = app(GroupCollectorInterface::class);
$collector->setOpposingAccounts($all);
$set = $collector->getTransactions(); $total = $accounts->merge($all);
$sorted = $set->sortByDesc( $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($total);
function (Transaction $transaction) { $journals = $collector->getExtractedJournals();
return (float)$transaction->transaction_amount;
} usort($journals, function ($a, $b) {
); return $a['amount'] <=> $b['amount'];
});
try { try {
$result = view('reports.partials.top-transactions', compact('sorted'))->render(); $result = view('reports.partials.top-transactions', compact('sorted'))->render();
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error(sprintf('Could not render category::topIncome: %s', $e->getMessage())); 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 // @codeCoverageIgnoreEnd
$cache->store($result); $cache->store($result);

View File

@ -170,6 +170,7 @@ class RuleGroupController extends Controller
* @param RuleGroup $ruleGroup * @param RuleGroup $ruleGroup
* *
* @return RedirectResponse * @return RedirectResponse
* @throws \Exception
*/ */
public function execute(SelectTransactionsRequest $request, AccountRepositoryInterface $repository, RuleGroup $ruleGroup): RedirectResponse public function execute(SelectTransactionsRequest $request, AccountRepositoryInterface $repository, RuleGroup $ruleGroup): RedirectResponse
{ {

View File

@ -25,7 +25,7 @@ namespace FireflyIII\Http\Controllers\Transaction;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Filter\TransactionViewFilter; use FireflyIII\Helpers\Filter\TransactionViewFilter;
use FireflyIII\Helpers\Filter\TransferFilter; use FireflyIII\Helpers\Filter\TransferFilter;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
@ -126,10 +126,13 @@ class MassController extends Controller
* @param Collection $journals * @param Collection $journals
* *
* @return IlluminateView * @return IlluminateView
*
* TODO rebuild this feature.
* @throws FireflyException
*/ */
public function edit(Collection $journals): IlluminateView 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 */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
$subTitle = (string)trans('firefly.mass_edit_journals'); $subTitle = (string)trans('firefly.mass_edit_journals');
@ -148,8 +151,8 @@ class MassController extends Controller
$transformer = app(TransactionTransformer::class); $transformer = app(TransactionTransformer::class);
$transformer->setParameters(new ParameterBag); $transformer->setParameters(new ParameterBag);
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($user); $collector->setUser($user);
$collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation();
$collector->setJournals($journals); $collector->setJournals($journals);

View File

@ -1,177 +0,0 @@
<?php
/**
* SplitController.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
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'));
}
}

View File

@ -26,20 +26,19 @@ namespace FireflyIII\Import\Storage;
use Carbon\Carbon; use Carbon\Carbon;
use DB; use DB;
use Exception;
use FireflyIII\Events\RequestedReportOnJournals; use FireflyIII\Events\RequestedReportOnJournals;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
use FireflyIII\Models\ImportJob; use FireflyIII\Models\ImportJob;
use FireflyIII\Models\Rule; use FireflyIII\Models\Rule;
use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\TransactionRules\Processor; use FireflyIII\TransactionRules\Processor;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -64,8 +63,10 @@ class ImportArrayStorage
private $journalRepos; private $journalRepos;
/** @var ImportJobRepositoryInterface Import job repository */ /** @var ImportJobRepositoryInterface Import job repository */
private $repository; private $repository;
/** @var Collection The transfers the user already has. */ /** @var array The transfers the user already has. */
private $transfers; private $transfers;
/** @var TransactionGroupRepositoryInterface */
private $groupRepos;
/** /**
* Set job, count transfers in the array and create the repository. * Set job, count transfers in the array and create the repository.
@ -83,9 +84,59 @@ class ImportArrayStorage
$this->journalRepos = app(JournalRepositoryInterface::class); $this->journalRepos = app(JournalRepositoryInterface::class);
$this->journalRepos->setUser($importJob->user); $this->journalRepos->setUser($importJob->user);
$this->groupRepos = app(TransactionGroupRepositoryInterface::class);
$this->groupRepos->setUser($importJob->user);
Log::debug('Constructed ImportArrayStorage()'); 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. * Actually does the storing. Does three things.
* - Store journals * - Store journals
@ -99,7 +150,7 @@ class ImportArrayStorage
{ {
// store transactions // store transactions
$this->setStatus('storing_data'); $this->setStatus('storing_data');
$collection = $this->storeArray(); $collection = $this->storeGroupArray();
$this->setStatus('stored_data'); $this->setStatus('stored_data');
// link tag: // link tag:
@ -124,70 +175,104 @@ class ImportArrayStorage
} }
/** /**
* Applies the users rules to the created journals. * Shorthand method to quickly set job status
*
* @param Collection $collection
* *
* @param string $status
*/ */
private function applyRules(Collection $collection): void private function setStatus(string $status): void
{ {
$rules = $this->getRules(); $this->repository->setStatus($this->importJob, $status);
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;
}
);
}
}
} }
/** /**
* 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 */ /** @var array $array */
$array = $this->repository->getTransactions($this->importJob); $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; $collection = new Collection;
foreach ($array as $index => $transaction) { foreach ($array as $index => $group) {
if (strtolower(TransactionType::TRANSFER) === strtolower($transaction['type'])) { Log::debug(sprintf('Now store #%d', ($index + 1)));
$count++; $result = $this->storeGroup($index, $group);
Log::debug(sprintf('Row #%d is a transfer, increase count to %d', $index + 1, $count)); if (null !== $result) {
$collection->push($result);
} }
} }
Log::debug(sprintf('Count of transfers in import array is %d.', $count)); Log::notice(sprintf('Done storing. Firefly III has stored %d transactions.', $collection->count()));
if ($count > 0) {
$this->checkForTransfers = true; return $collection;
Log::debug('Will check for duplicate transfers.');
// get users transfers. Needed for comparison.
$this->getTransfers();
}
} }
/** /**
* @param int $index * @param int $index
* @param array $transaction * @param array $group
* @return TransactionGroup|null
*/
private function storeGroup(int $index, array $group): ?TransactionGroup
{
// 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;
}
Log::debug(sprintf('Going to store entry #%d', $index + 1));
// 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 * @return bool
* @throws FireflyException
*/ */
private function duplicateDetected(int $index, array $transaction): bool private function duplicateDetected(int $index, array $group): bool
{ {
$transactions = $group['transactions'] ?? [];
foreach ($transactions as $transaction) {
$hash = $this->getHash($transaction); $hash = $this->getHash($transaction);
$existingId = $this->hashExists($hash); $existingId = $this->hashExists($hash);
if (null !== $existingId) { if (null !== $existingId) {
@ -206,6 +291,7 @@ class ImportArrayStorage
return true; return true;
} }
}
return false; return false;
} }
@ -215,7 +301,6 @@ class ImportArrayStorage
* *
* @param array $transaction * @param array $transaction
* *
* @throws FireflyException
* @return string * @return string
*/ */
private function getHash(array $transaction): string private function getHash(array $transaction): string
@ -226,7 +311,12 @@ class ImportArrayStorage
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
/** @noinspection ForgottenDebugOutputInspection */ /** @noinspection ForgottenDebugOutputInspection */
Log::error('Could not encode import array.', $transaction); 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 // @codeCoverageIgnoreEnd
} }
$hash = hash('sha256', $json); $hash = hash('sha256', $json);
@ -235,72 +325,6 @@ class ImportArrayStorage
return $hash; 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. * 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; 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. * 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( $rules = $this->getRules();
'Transaction is a duplicate, and will not be imported (the hash exists).', if ($rules->count() > 0) {
[ foreach ($collection as $journal) {
'existing' => $existingId, $rules->each(
'description' => $transaction['description'] ?? '', function (Rule $rule) use ($journal) {
'amount' => $transaction['transactions'][0]['amount'] ?? 0, Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id));
'date' => $transaction['date'] ?? '', /** @var Processor $processor */
] $processor = app(Processor::class);
); $processor->make($rule);
$processor->handleTransactionJournal($journal);
} $journal->refresh();
if ($rule->stop_processing) {
/**
* 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.
*
* @return Collection
* @throws FireflyException
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
private function storeArray(): Collection
{
/** @var array $array */
$array = $this->repository->getTransactions($this->importJob);
$count = count($array);
$toStore = [];
Log::notice(sprintf('Will now store the transactions. Count of items is %d.', $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; 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; return true;
} }
);
}
} }
} }
Log::debug('Is not an existing transfer, return false.');
return false; /**
* 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;
} }
} }

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Jobs; namespace FireflyIII\Jobs;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\RuleGroup; use FireflyIII\Models\RuleGroup;
use FireflyIII\TransactionRules\Processor; use FireflyIII\TransactionRules\Processor;
use FireflyIII\User; use FireflyIII\User;
@ -34,6 +34,7 @@ use Illuminate\Support\Collection;
/** /**
* Class ExecuteRuleGroupOnExistingTransactions. * Class ExecuteRuleGroupOnExistingTransactions.
* TODO make sure this job honors the "stop_processing" rules.
*/ */
class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue
{ {
@ -148,19 +149,20 @@ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue
public function handle() public function handle()
{ {
// Lookup all journals that match the parameters specified // Lookup all journals that match the parameters specified
$transactions = $this->collectJournals(); $journals = $this->collectJournals();
// Find processors for each rule within the current rule group // Find processors for each rule within the current rule group
$processors = $this->collectProcessors(); $processors = $this->collectProcessors();
// Execute the rules for each transaction // Execute the rules for each transaction
foreach ($processors as $processor) { foreach ($processors as $processor) {
foreach ($transactions as $transaction) { /** @var array $journal */
foreach ($journals as $journal) {
/** @var Processor $processor */ /** @var Processor $processor */
$processor->handleTransaction($transaction); $processor->handleJournalArray($journal);
} }
// Stop processing this group if the rule specifies 'stop_processing' // Stop processing this group if the rule specifies 'stop_processing'
// TODO Fix this.
if ($processor->getRule()->stop_processing) { if ($processor->getRule()->stop_processing) {
break; break;
} }
@ -170,16 +172,16 @@ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue
/** /**
* Collect all journals that should be processed. * Collect all journals that should be processed.
* *
* @return Collection * @return array
*/ */
protected function collectJournals(): Collection protected function collectJournals(): array
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user); $collector->setUser($this->user);
$collector->setAccounts($this->accounts)->setRange($this->startDate, $this->endDate); $collector->setAccounts($this->accounts)->setRange($this->startDate, $this->endDate);
return $collector->getTransactions(); return $collector->getExtractedJournals();
} }
/** /**

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Jobs; namespace FireflyIII\Jobs;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Rule; use FireflyIII\Models\Rule;
use FireflyIII\TransactionRules\Processor; use FireflyIII\TransactionRules\Processor;
use FireflyIII\User; use FireflyIII\User;
@ -161,7 +161,7 @@ class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue
public function handle() public function handle()
{ {
// Lookup all journals that match the parameters specified // Lookup all journals that match the parameters specified
$transactions = $this->collectJournals(); $journals = $this->collectJournals();
/** @var Processor $processor */ /** @var Processor $processor */
$processor = app(Processor::class); $processor = app(Processor::class);
$processor->make($this->rule, true); $processor->make($this->rule, true);
@ -169,9 +169,10 @@ class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue
$misses = 0; $misses = 0;
$total = 0; $total = 0;
// Execute the rules for each transaction // Execute the rules for each transaction
foreach ($transactions as $transaction) { /** @var array $journal */
foreach ($journals as $journal) {
++$total; ++$total;
$result = $processor->handleTransaction($transaction); $result = $processor->handleJournalArray($journal);
if ($result) { if ($result) {
++$hits; ++$hits;
} }
@ -186,15 +187,16 @@ class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue
/** /**
* Collect all journals that should be processed. * Collect all journals that should be processed.
* *
* @return Collection * @return array
*/ */
protected function collectJournals(): Collection protected function collectJournals(): array
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user); $collector->setUser($this->user);
$collector->setAccounts($this->accounts)->setRange($this->startDate, $this->endDate); $collector->setAccounts($this->accounts)->setRange($this->startDate, $this->endDate);
return $collector->getTransactions(); return $collector->getExtractedJournals();
} }
} }

View File

@ -24,9 +24,7 @@ namespace FireflyIII\Repositories\Account;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\User; use FireflyIII\User;

View File

@ -27,14 +27,12 @@ use Exception;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\TransactionCurrencyFactory; use FireflyIII\Factory\TransactionCurrencyFactory;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\AvailableBudget;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleAction;
use FireflyIII\Models\RuleTrigger; use FireflyIII\Models\RuleTrigger;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
@ -164,6 +162,7 @@ class BudgetRepository implements BudgetRepositoryInterface
if ($accounts->count() > 0) { if ($accounts->count() > 0) {
$collector->setAccounts($accounts); $collector->setAccounts($accounts);
} }
return $collector->getSum(); return $collector->getSum();
} }
@ -556,18 +555,19 @@ class BudgetRepository implements BudgetRepositoryInterface
} }
// get all transactions: // get all transactions:
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end); $collector->setAccounts($accounts)->setRange($start, $end);
$collector->setBudgets($budgets); $collector->setBudgets($budgets);
$transactions = $collector->getTransactions(); $journals = $collector->getExtractedJournals();
// loop transactions: // loop transactions:
/** @var Transaction $transaction */ /** @var array $journal */
foreach ($transactions as $transaction) { foreach ($journals as $journal) {
$budgetId = max((int)$transaction->transaction_journal_budget_id, (int)$transaction->transaction_budget_id); $budgetId = (int)$journal['budget_id'];
$date = $transaction->date->format($carbonFormat); $date = $journal['date']->format($carbonFormat);
$data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date] ?? '0', $transaction->transaction_amount); $data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date] ?? '0', $journal['amount']);
} }
return $data; return $data;
@ -623,25 +623,28 @@ class BudgetRepository implements BudgetRepositoryInterface
public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array
{ {
$carbonFormat = Navigation::preferredCarbonFormat($start, $end); $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->setAccounts($accounts)->setRange($start, $end);
$collector->setTypes([TransactionType::WITHDRAWAL]); $collector->setTypes([TransactionType::WITHDRAWAL]);
$collector->withoutBudget(); $collector->withoutBudget();
$transactions = $collector->getTransactions(); $journals = $collector->getExtractedJournals();
$result = [ $result = [
'entries' => [], 'entries' => [],
'name' => (string)trans('firefly.no_budget'), 'name' => (string)trans('firefly.no_budget'),
'sum' => '0', 'sum' => '0',
]; ];
foreach ($transactions as $transaction) { /** @var array $journal */
$date = $transaction->date->format($carbonFormat); foreach ($journals as $journal) {
$date = $journal['date']->format($carbonFormat);
if (!isset($result['entries'][$date])) { if (!isset($result['entries'][$date])) {
$result['entries'][$date] = '0'; $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; return $result;
@ -794,39 +797,41 @@ class BudgetRepository implements BudgetRepositoryInterface
*/ */
public function spentInPeriodWoBudgetMc(Collection $accounts, Carbon $start, Carbon $end): array public function spentInPeriodWoBudgetMc(Collection $accounts, Carbon $start, Carbon $end): array
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user); $collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutBudget(); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutBudget();
if ($accounts->count() > 0) { if ($accounts->count() > 0) {
$collector->setAccounts($accounts); $collector->setAccounts($accounts);
} }
if (0 === $accounts->count()) { $journals = $collector->getExtractedJournals();
$collector->setAllAssetAccounts();
}
$set = $collector->getTransactions();
$return = []; $return = [];
$total = []; $total = [];
$currencies = []; $currencies = [];
/** @var Transaction $transaction */ /** @var array $journal */
foreach ($set as $transaction) { foreach ($journals as $journal) {
$code = $transaction->transaction_currency_code; $code = $journal['currency_code'];
if (!isset($currencies[$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) { foreach ($total as $code => $spent) {
/** @var TransactionCurrency $currency */ /** @var TransactionCurrency $currency */
$currency = $currencies[$code]; $currency = $currencies[$code];
$return[] = [ $return[] = [
'currency_id' => $currency->id, 'currency_id' => $currency['id'],
'currency_code' => $code, 'currency_code' => $code,
'currency_symbol' => $currency->symbol, 'currency_symbol' => $currency['symbol'],
'currency_decimal_places' => $currency->decimal_places, 'currency_decimal_places' => $currency['decimal_places'],
'amount' => round($spent, $currency->decimal_places), 'amount' => round($spent, $currency['decimal_places']),
]; ];
} }

View File

@ -25,9 +25,7 @@ namespace FireflyIII\Repositories\Category;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Factory\CategoryFactory; use FireflyIII\Factory\CategoryFactory;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Models\Category; use FireflyIII\Models\Category;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\Services\Internal\Destroy\CategoryDestroyService; use FireflyIII\Services\Internal\Destroy\CategoryDestroyService;
use FireflyIII\Services\Internal\Update\CategoryUpdateService; use FireflyIII\Services\Internal\Update\CategoryUpdateService;
@ -142,43 +140,30 @@ class CategoryRepository implements CategoryRepositoryInterface
*/ */
public function earnedInPeriodPcWoCategory(Collection $accounts, Carbon $start, Carbon $end): array public function earnedInPeriodPcWoCategory(Collection $accounts, Carbon $start, Carbon $end): array
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user); $collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->withoutCategory(); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->withoutCategory();
if ($accounts->count() > 0) { if ($accounts->count() > 0) {
$collector->setAccounts($accounts); $collector->setAccounts($accounts);
} }
if (0 === $accounts->count()) { $journals = $collector->getExtractedJournals();
$collector->setAllAssetAccounts();
}
$set = $collector->getTransactions();
$set = $set->filter(
function (Transaction $transaction) {
if (bccomp($transaction->transaction_amount, '0') === 1) {
return $transaction;
}
return null;
}
);
$return = []; $return = [];
/** @var Transaction $transaction */
foreach ($set as $transaction) { foreach ($journals as $journal) {
$currencyId = $transaction->transaction_currency_id; $currencyId = (int)$journal['currency_id'];
if (!isset($return[$currencyId])) { if (!isset($return[$currencyId])) {
$return[$currencyId] = [ $return[$currencyId] = [
'spent' => '0', 'spent' => '0',
'currency_id' => $currencyId, 'currency_id' => $currencyId,
'currency_symbol' => $transaction->transaction_currency_symbol, 'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $transaction->transaction_currency_code, 'currency_code' => $journal['currency_code'],
'currency_decimal_places' => $transaction->transaction_currency_dp, '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; return $return;
@ -194,8 +179,9 @@ class CategoryRepository implements CategoryRepositoryInterface
*/ */
public function earnedInPeriodPerCurrency(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array public function earnedInPeriodPerCurrency(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user); $collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT]); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT]);
@ -209,20 +195,12 @@ class CategoryRepository implements CategoryRepositoryInterface
if ($accounts->count() > 0) { if ($accounts->count() > 0) {
$collector->setAccounts($accounts); $collector->setAccounts($accounts);
} }
if (0 === $accounts->count()) { $journals = $collector->getExtractedJournals();
$collector->setAllAssetAccounts();
}
$set = $collector->getTransactions();
$return = []; $return = [];
/** @var Transaction $transaction */ foreach ($journals as $journal) {
foreach ($set as $transaction) { $categoryId = (int)$journal['category_id'];
$jrnlCatId = (int)$transaction->transaction_journal_category_id; $currencyId = (int)$journal['currency_id'];
$transCatId = (int)$transaction->transaction_category_id; $name = $journal['category_name'];
$categoryId = max($jrnlCatId, $transCatId);
$currencyId = (int)$transaction->transaction_currency_id;
$name = $transaction->transaction_category_name;
$name = '' === (string)$name ? $transaction->transaction_journal_category_name : $name;
// make array for category: // make array for category:
if (!isset($return[$categoryId])) { if (!isset($return[$categoryId])) {
$return[$categoryId] = [ $return[$categoryId] = [
@ -234,13 +212,13 @@ class CategoryRepository implements CategoryRepositoryInterface
$return[$categoryId]['earned'][$currencyId] = [ $return[$categoryId]['earned'][$currencyId] = [
'earned' => '0', 'earned' => '0',
'currency_id' => $currencyId, 'currency_id' => $currencyId,
'currency_symbol' => $transaction->transaction_currency_symbol, 'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $transaction->transaction_currency_code, 'currency_code' => $journal['currency_code'],
'currency_decimal_places' => $transaction->transaction_currency_dp, 'currency_decimal_places' => $journal['currency_decimal_places'],
]; ];
} }
$return[$categoryId]['earned'][$currencyId]['earned'] $return[$categoryId]['earned'][$currencyId]['earned']
= bcadd($return[$categoryId]['earned'][$currencyId]['earned'], $transaction->transaction_amount); = bcadd($return[$categoryId]['earned'][$currencyId]['earned'], $journal['amount']);
} }
return $return; return $return;
@ -521,23 +499,20 @@ class CategoryRepository implements CategoryRepositoryInterface
} }
// get all transactions: // get all transactions:
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end); $collector->setAccounts($accounts)->setRange($start, $end);
$collector->setCategories($categories)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) $collector->setCategories($categories)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
->withOpposingAccount(); ->withAccountInformation()->withCategoryInformation();
$transactions = $collector->getTransactions(); $journals = $collector->getExtractedJournals();
// loop transactions: // loop transactions:
/** @var Transaction $transaction */
foreach ($transactions as $transaction) { foreach ($journals as $journal) {
// if positive, skip: $categoryId = (int)$journal['category_id'];
if (1 === bccomp($transaction->transaction_amount, '0')) { $date = $journal['date']->format($carbonFormat);
continue; $data[$categoryId]['entries'][$date] = bcadd($data[$categoryId]['entries'][$date] ?? '0', $journal['amount']);
}
$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);
} }
return $data; return $data;
@ -553,29 +528,28 @@ class CategoryRepository implements CategoryRepositoryInterface
public function periodExpensesNoCategory(Collection $accounts, Carbon $start, Carbon $end): array public function periodExpensesNoCategory(Collection $accounts, Carbon $start, Carbon $end): array
{ {
$carbonFormat = Navigation::preferredCarbonFormat($start, $end); $carbonFormat = Navigation::preferredCarbonFormat($start, $end);
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); /** @var GroupCollectorInterface $collector */
$collector->setAccounts($accounts)->setRange($start, $end)->withOpposingAccount(); $collector = app(GroupCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end)->withAccountInformation();
$collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]); $collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]);
$collector->withoutCategory(); $collector->withoutCategory();
$transactions = $collector->getTransactions(); $journals = $collector->getExtractedJournals();
$result = [ $result = [
'entries' => [], 'entries' => [],
'name' => (string)trans('firefly.no_category'), 'name' => (string)trans('firefly.no_category'),
'sum' => '0', 'sum' => '0',
]; ];
foreach ($transactions as $transaction) { /** @var array $journal */
// if positive, skip: foreach ($journals as $journal) {
if (1 === bccomp($transaction->transaction_amount, '0')) { $date = $journal['date']->format($carbonFormat);
continue;
}
$date = $transaction->date->format($carbonFormat);
if (!isset($result['entries'][$date])) { if (!isset($result['entries'][$date])) {
$result['entries'][$date] = '0'; $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; return $result;
@ -604,23 +578,20 @@ class CategoryRepository implements CategoryRepositoryInterface
} }
// get all transactions: // get all transactions:
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end); $collector->setAccounts($accounts)->setRange($start, $end);
$collector->setCategories($categories)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) $collector->setCategories($categories)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
->withOpposingAccount(); ->withAccountInformation();
$transactions = $collector->getTransactions(); $journals = $collector->getExtractedJournals();
// loop transactions: // loop transactions:
/** @var Transaction $transaction */ /** @var array $journal */
foreach ($transactions as $transaction) { foreach ($journals as $journal) {
// if negative, skip: $categoryId = (int)$journal['category_id'];
if (bccomp($transaction->transaction_amount, '0') === -1) { $date = $journal['date']->format($carbonFormat);
continue; $data[$categoryId]['entries'][$date] = bcadd($data[$categoryId]['entries'][$date] ?? '0', $journal['amount']);
}
$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);
} }
return $data; return $data;
@ -637,29 +608,28 @@ class CategoryRepository implements CategoryRepositoryInterface
{ {
Log::debug('Now in periodIncomeNoCategory()'); Log::debug('Now in periodIncomeNoCategory()');
$carbonFormat = Navigation::preferredCarbonFormat($start, $end); $carbonFormat = Navigation::preferredCarbonFormat($start, $end);
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); /** @var GroupCollectorInterface $collector */
$collector->setAccounts($accounts)->setRange($start, $end)->withOpposingAccount(); $collector = app(GroupCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end)->withAccountInformation();
$collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]); $collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]);
$collector->withoutCategory(); $collector->withoutCategory();
$transactions = $collector->getTransactions(); $journals = $collector->getExtractedJournals();
$result = [ $result = [
'entries' => [], 'entries' => [],
'name' => (string)trans('firefly.no_category'), 'name' => (string)trans('firefly.no_category'),
'sum' => '0', 'sum' => '0',
]; ];
Log::debug('Looping transactions..'); Log::debug('Looping transactions..');
foreach ($transactions as $transaction) {
// if negative, skip: foreach ($journals as $journal) {
if (bccomp($transaction->transaction_amount, '0') === -1) { $date = $journal['date']->format($carbonFormat);
continue;
}
$date = $transaction->date->format($carbonFormat);
if (!isset($result['entries'][$date])) { if (!isset($result['entries'][$date])) {
$result['entries'][$date] = '0'; $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('Done looping transactions..');
Log::debug('Finished periodIncomeNoCategory()'); Log::debug('Finished periodIncomeNoCategory()');
@ -714,6 +684,8 @@ class CategoryRepository implements CategoryRepositoryInterface
{ {
/** @var GroupCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user); $collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setCategories($categories); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setCategories($categories);
@ -833,30 +805,17 @@ class CategoryRepository implements CategoryRepositoryInterface
*/ */
public function spentInPeriodWithoutCategory(Collection $accounts, Carbon $start, Carbon $end): string public function spentInPeriodWithoutCategory(Collection $accounts, Carbon $start, Carbon $end): string
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user); $collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutCategory(); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutCategory();
if ($accounts->count() > 0) { if ($accounts->count() > 0) {
$collector->setAccounts($accounts); $collector->setAccounts($accounts);
} }
if (0 === $accounts->count()) {
$collector->setAllAssetAccounts();
}
$set = $collector->getTransactions(); return $collector->getSum();
$set = $set->filter(
function (Transaction $transaction) {
if (bccomp($transaction->transaction_amount, '0') === -1) {
return $transaction;
}
return null;
}
);
return (string)$set->sum('transaction_amount');
} }
/** /**

View File

@ -25,11 +25,6 @@ namespace FireflyIII\Repositories\Journal;
use Carbon\Carbon; use Carbon\Carbon;
use DB; use DB;
use Exception; 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\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\Note; use FireflyIII\Models\Note;
@ -386,6 +381,35 @@ class JournalRepository implements JournalRepositoryInterface
return ''; 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. * Return a list of all destination accounts related to journal.
* *
@ -493,35 +517,6 @@ class JournalRepository implements JournalRepositoryInterface
return ''; 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). * Return string value of a meta date (or NULL).
* *
@ -666,33 +661,6 @@ class JournalRepository implements JournalRepositoryInterface
return $journal->transactionType->type; 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. * Will tell you if journal is reconciled or not.
* *
@ -711,6 +679,22 @@ class JournalRepository implements JournalRepositoryInterface
return false; 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 * @param Transaction $transaction
* *
@ -736,22 +720,6 @@ class JournalRepository implements JournalRepositoryInterface
return true; 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 TransactionJournal $journal
* @param int $order * @param int $order

View File

@ -276,13 +276,6 @@ interface JournalRepositoryInterface
*/ */
public function getTransactionType(TransactionJournal $journal): string; 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. * Will tell you if journal is reconciled or not.
* *
@ -319,8 +312,6 @@ interface JournalRepositoryInterface
*/ */
public function setUser(User $user); public function setUser(User $user);
/** /**
* Update budget for a journal. * Update budget for a journal.
* *

View File

@ -26,8 +26,7 @@ namespace FireflyIII\Repositories\Recurring;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\RecurrenceFactory; use FireflyIII\Factory\RecurrenceFactory;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Models\Note; use FireflyIII\Models\Note;
use FireflyIII\Models\Preference; use FireflyIII\Models\Preference;
use FireflyIII\Models\Recurrence; use FireflyIII\Models\Recurrence;
@ -290,25 +289,27 @@ class RecurringRepository implements RecurringRepositoryInterface
->get()->pluck('transaction_journal_id')->toArray(); ->get()->pluck('transaction_journal_id')->toArray();
$search = []; $search = [];
foreach ($journalMeta as $journalId) { foreach ($journalMeta as $journalId) {
$search[] = ['id' => (int)$journalId]; $search[] = (int)$journalId;
} }
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::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));
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 * @param Recurrence $recurrence
* *
* @return Collection * @return Collection
*/ */
public function getTransactions(Recurrence $recurrence): Collection public function getTransactions(Recurrence $recurrence): array
{ {
$journalMeta = TransactionJournalMeta $journalMeta = TransactionJournalMeta
::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') ::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
@ -319,17 +320,17 @@ class RecurringRepository implements RecurringRepositoryInterface
->get()->pluck('transaction_journal_id')->toArray(); ->get()->pluck('transaction_journal_id')->toArray();
$search = []; $search = [];
foreach ($journalMeta as $journalId) { foreach ($journalMeta as $journalId) {
$search[] = ['id' => (int)$journalId]; $search[] = (int)$journalId;
} }
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($recurrence->user);
$collector->withOpposingAccount()->setAllAssetAccounts()->withCategoryInformation()->withBudgetInformation();
// filter on specific journals.
$collector->removeFilter(InternalTransferFilter::class);
$collector->setJournals(new Collection($search));
return $collector->getTransactions(); $collector->setUser($recurrence->user);
$collector->withCategoryInformation()->withBudgetInformation()->withAccountInformation();
// filter on specific journals.
$collector->setJournalIds($search);
return $collector->getExtractedJournals();
} }
/** /**

View File

@ -141,9 +141,9 @@ interface RecurringRepositoryInterface
/** /**
* @param Recurrence $recurrence * @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. * Calculate the next X iterations starting on the date given in $date.

View File

@ -26,7 +26,6 @@ use Carbon\Carbon;
use DB; use DB;
use FireflyIII\Factory\TagFactory; use FireflyIII\Factory\TagFactory;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Models\Tag; use FireflyIII\Models\Tag;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\User; use FireflyIII\User;
@ -84,13 +83,13 @@ class TagRepository implements TagRepositoryInterface
*/ */
public function earnedInPeriod(Tag $tag, Carbon $start, Carbon $end): string public function earnedInPeriod(Tag $tag, Carbon $start, Carbon $end): string
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAllAssetAccounts()->setTag($tag);
$set = $collector->getTransactions();
return (string)$set->sum('transaction_amount'); $collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setTag($tag);
return $collector->getSum();
} }
/** /**
@ -244,13 +243,13 @@ class TagRepository implements TagRepositoryInterface
*/ */
public function spentInPeriod(Tag $tag, Carbon $start, Carbon $end): string public function spentInPeriod(Tag $tag, Carbon $start, Carbon $end): string
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAllAssetAccounts()->setTag($tag);
$set = $collector->getTransactions();
return (string)$set->sum('transaction_amount'); $collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setTag($tag);
return $collector->getSum();
} }
/** /**

View File

@ -25,12 +25,10 @@ namespace FireflyIII\Support\Http\Controllers;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; 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 protected function earnedByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets);
$collector->setOpposingAccounts($opposing)->withCategoryInformation(); $total = $assets->merge($opposing);
$set = $collector->getTransactions(); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($total);
$collector->withCategoryInformation();
$journals = $collector->getExtractedJournals();
$sum = []; $sum = [];
// loop to support multi currency // loop to support multi currency
foreach ($set as $transaction) { foreach ($journals as $journal) {
$currencyId = $transaction->transaction_currency_id; $currencyId = $journal['currency_id'];
$categoryName = $transaction->transaction_category_name; $categoryName = $journal['category_name'];
$categoryId = (int)$transaction->transaction_category_id; $categoryId = (int)$journal['category_id'];
// if null, grab from journal:
if (0 === $categoryId) {
$categoryName = $transaction->transaction_journal_category_name;
$categoryId = (int)$transaction->transaction_journal_category_id;
}
// if not set, set to zero: // if not set, set to zero:
if (!isset($sum[$categoryId][$currencyId])) { if (!isset($sum[$categoryId][$currencyId])) {
@ -116,8 +111,8 @@ trait AugumentData
'name' => $categoryName, 'name' => $categoryName,
], ],
'currency' => [ 'currency' => [
'symbol' => $transaction->transaction_currency_symbol, 'symbol' => $journal['currency_symbol'],
'dp' => $transaction->transaction_currency_dp, 'dp' => $journal['currency_decimal_places'],
], ],
], ],
], ],
@ -126,9 +121,9 @@ trait AugumentData
// add amount // add amount
$sum[$categoryId]['per_currency'][$currencyId]['sum'] = bcadd( $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; 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 protected function earnedInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets);
$collector->setOpposingAccounts($opposing); $total = $assets->merge($opposing);
$set = $collector->getTransactions(); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($total);
$journals = $collector->getExtractedJournals();
$sum = [ $sum = [
'grand_sum' => '0', 'grand_sum' => '0',
'per_currency' => [], 'per_currency' => [],
]; ];
// loop to support multi currency // loop to support multi currency
foreach ($set as $transaction) { foreach ($journals as $journal) {
$currencyId = $transaction->transaction_currency_id; $currencyId = (int)$journal['currency_id'];
// if not set, set to zero: // if not set, set to zero:
if (!isset($sum['per_currency'][$currencyId])) { if (!isset($sum['per_currency'][$currencyId])) {
$sum['per_currency'][$currencyId] = [ $sum['per_currency'][$currencyId] = [
'sum' => '0', 'sum' => '0',
'currency' => [ 'currency' => [
'symbol' => $transaction->transaction_currency_symbol, 'symbol' => $journal['currency_symbol'],
'dp' => $transaction->transaction_currency_dp, 'decimal_places' => $journal['currency_decimal_places'],
], ],
]; ];
} }
// add amount // add amount
$sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $transaction->transaction_amount); $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $journal['amount']);
$sum['grand_sum'] = bcadd($sum['grand_sum'], $transaction->transaction_amount); $sum['grand_sum'] = bcadd($sum['grand_sum'], $journal['amount']);
} }
return $sum; return $sum;
@ -525,19 +521,27 @@ trait AugumentData
/** /**
* Group set of transactions by name of opposing account. * Group set of transactions by name of opposing account.
* *
* @param Collection $set * @param array $array
* *
* @return 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. // group by opposing account name.
$grouped = []; $grouped = [];
/** @var Transaction $transaction */ /** @var array $journal */
foreach ($set as $transaction) { foreach ($array as $journal) {
$name = $transaction->opposing_account_name; $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] = $grouped[$name] ?? '0';
$grouped[$name] = bcadd($transaction->transaction_amount, $grouped[$name]); $grouped[$name] = bcadd($journal['amount'], $grouped[$name]);
} }
return $grouped; 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 protected function spentByBudget(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); $total = $assets->merge($opposing);
$collector->setOpposingAccounts($opposing)->withBudgetInformation(); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($total);
$set = $collector->getTransactions(); $collector->withBudgetInformation();
$journals = $collector->getExtractedJournals();
$sum = []; $sum = [];
// loop to support multi currency // loop to support multi currency
foreach ($set as $transaction) { foreach ($journals as $journal) {
$currencyId = $transaction->transaction_currency_id; $currencyId = $journal['currency_id'];
$budgetName = $transaction->transaction_budget_name; $budgetName = $journal['budget_name'];
$budgetId = (int)$transaction->transaction_budget_id; $budgetId = (int)$journal['budget_id'];
// if null, grab from journal:
if (0 === $budgetId) {
$budgetName = $transaction->transaction_journal_budget_name;
$budgetId = (int)$transaction->transaction_journal_budget_id;
}
// if not set, set to zero: // if not set, set to zero:
if (!isset($sum[$budgetId][$currencyId])) { if (!isset($sum[$budgetId][$currencyId])) {
@ -617,8 +617,8 @@ trait AugumentData
'name' => $budgetName, 'name' => $budgetName,
], ],
'currency' => [ 'currency' => [
'symbol' => $transaction->transaction_currency_symbol, 'symbol' => $journal['currency_symbol'],
'dp' => $transaction->transaction_currency_dp, 'dp' => $journal['currency_decimal_places'],
], ],
], ],
], ],
@ -627,9 +627,9 @@ trait AugumentData
// add amount // add amount
$sum[$budgetId]['per_currency'][$currencyId]['sum'] = bcadd( $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; 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 protected function spentByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); $total = $assets->merge($opposing);
$collector->setOpposingAccounts($opposing)->withCategoryInformation(); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($total);
$set = $collector->getTransactions(); $collector->withCategoryInformation();
$journals = $collector->getExtractedJournals();
$sum = []; $sum = [];
// loop to support multi currency // loop to support multi currency
foreach ($set as $transaction) { foreach ($journals as $journal) {
$currencyId = $transaction->transaction_currency_id; $currencyId = (int)$journal['currency_id'];
$categoryName = $transaction->transaction_category_name; $categoryName = $journal['category_name'];
$categoryId = (int)$transaction->transaction_category_id; $categoryId = (int)$journal['category_id'];
// if null, grab from journal:
if (0 === $categoryId) {
$categoryName = $transaction->transaction_journal_category_name;
$categoryId = (int)$transaction->transaction_journal_category_id;
}
// if not set, set to zero: // if not set, set to zero:
if (!isset($sum[$categoryId][$currencyId])) { if (!isset($sum[$categoryId][$currencyId])) {
@ -682,8 +678,8 @@ trait AugumentData
'name' => $categoryName, 'name' => $categoryName,
], ],
'currency' => [ 'currency' => [
'symbol' => $transaction->transaction_currency_symbol, 'symbol' => $journal['currency_symbol'],
'dp' => $transaction->transaction_currency_dp, 'dp' => $journal['currency_decimal_places'],
], ],
], ],
], ],
@ -692,9 +688,9 @@ trait AugumentData
// add amount // add amount
$sum[$categoryId]['per_currency'][$currencyId]['sum'] = bcadd( $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; 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 protected function spentInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info
{ {
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets);
$collector->setOpposingAccounts($opposing); $total = $assets->merge($opposing);
$set = $collector->getTransactions(); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($total);
$journals = $collector->getExtractedJournals();
$sum = [ $sum = [
'grand_sum' => '0', 'grand_sum' => '0',
'per_currency' => [], 'per_currency' => [],
]; ];
// loop to support multi currency // loop to support multi currency
foreach ($set as $transaction) { foreach ($journals as $journal) {
$currencyId = (int)$transaction->transaction_currency_id; $currencyId = (int)$journal['currency_id'];
// if not set, set to zero: // if not set, set to zero:
if (!isset($sum['per_currency'][$currencyId])) { if (!isset($sum['per_currency'][$currencyId])) {
$sum['per_currency'][$currencyId] = [ $sum['per_currency'][$currencyId] = [
'sum' => '0', 'sum' => '0',
'currency' => [ 'currency' => [
'symbol' => $transaction->transaction_currency_symbol, 'name' => $journal['currency_name'],
'dp' => $transaction->transaction_currency_dp, 'symbol' => $journal['currency_symbol'],
'decimal_places' => $journal['currency_decimal_places'],
], ],
]; ];
} }
// add amount // add amount
$sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $transaction->transaction_amount); $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], $journal['amount']);
$sum['grand_sum'] = bcadd($sum['grand_sum'], $transaction->transaction_amount); $sum['grand_sum'] = bcadd($sum['grand_sum'], $journal['amount']);
} }
return $sum; return $sum;
@ -767,6 +765,7 @@ trait AugumentData
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$types = [TransactionType::WITHDRAWAL]; $types = [TransactionType::WITHDRAWAL];
$collector->setTypes($types)->setRange($start, $end)->withoutBudget(); $collector->setTypes($types)->setRange($start, $end)->withoutBudget();
return $collector->getSum(); return $collector->getSum();
} }
} }

View File

@ -24,11 +24,8 @@ declare(strict_types=1);
namespace FireflyIII\Support\Http\Controllers; namespace FireflyIII\Support\Http\Controllers;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Collector\GroupSumCollectorInterface; use FireflyIII\Helpers\Collector\GroupSumCollectorInterface;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\Category; use FireflyIII\Models\Category;
use FireflyIII\Models\Tag; use FireflyIII\Models\Tag;

View File

@ -24,29 +24,18 @@ declare(strict_types=1);
namespace FireflyIII\Support\Http\Controllers; namespace FireflyIII\Support\Http\Controllers;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Exceptions\ValidationException; use FireflyIII\Exceptions\ValidationException;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Help\HelpInterface; use FireflyIII\Helpers\Help\HelpInterface;
use FireflyIII\Http\Requests\SplitJournalFormRequest;
use FireflyIII\Http\Requests\TestRuleFormRequest; 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\Support\Binder\AccountList;
use FireflyIII\Transformers\TransactionTransformer;
use FireflyIII\User; use FireflyIII\User;
use Hash; use Hash;
use Illuminate\Contracts\Validation\Validator as ValidatorContract; use Illuminate\Contracts\Validation\Validator as ValidatorContract;
use Illuminate\Http\Request;
use Illuminate\Routing\Route; use Illuminate\Routing\Route;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use InvalidArgumentException; use InvalidArgumentException;
use Log; use Log;
use Route as RouteFacade; use Route as RouteFacade;
use Symfony\Component\HttpFoundation\ParameterBag;
/** /**
* Trait RequestInformation * Trait RequestInformation
@ -54,138 +43,7 @@ use Symfony\Component\HttpFoundation\ParameterBag;
*/ */
trait RequestInformation 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. * Get the domain of FF system.

View File

@ -46,14 +46,18 @@ trait TransactionCalculation
*/ */
protected function getExpensesForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): array protected function getExpensesForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): array
{ {
$total = $accounts->merge($opposing);
/** @var GroupCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setAccounts($accounts) $collector->setAccounts($total)
->setRange($start, $end) ->setRange($start, $end)
->setTypes([TransactionType::WITHDRAWAL]) ->withAccountInformation()
->setAccounts($opposing); ->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 protected function getIncomeForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): array
{ {
$total =$accounts->merge($opposing);
/** @var GroupCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT]) $collector->setAccounts($total)->setRange($start, $end)->withAccountInformation()->setTypes([TransactionType::DEPOSIT]);
->setAccounts($opposing);
return $collector->getExtractedJournals(); return $collector->getExtractedJournals();
} }

View File

@ -160,6 +160,12 @@ class ImportTransaction
'opposing-bic' => 'opposingBic', 'opposing-bic' => 'opposingBic',
'opposing-number' => 'opposingNumber', 'opposing-number' => 'opposingNumber',
]; ];
// overrule some old role values.
if ('original-source' === $role) {
$role = 'original_source';
}
if (isset($basics[$role])) { if (isset($basics[$role])) {
$field = $basics[$role]; $field = $basics[$role];
$this->$field = $columnValue->getValue(); $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. * Calculate the amount of this transaction.
* *
@ -276,6 +294,40 @@ class ImportTransaction
return $result; 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,\ * 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 * 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;
}
} }

View File

@ -89,79 +89,11 @@ class ImportableConverter
return $result; 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 * @param ImportTransaction $importable
* *
* @throws FireflyException
* @return array * @return array
* @throws FireflyException
*/ */
private function convertSingle(ImportTransaction $importable): array private function convertSingle(ImportTransaction $importable): array
{ {
@ -218,14 +150,53 @@ class ImportableConverter
} }
return [ return [
'user' => $this->importJob->user_id,
'group_title' => null,
'transactions' => [
[
'user' => $this->importJob->user_id,
'type' => $transactionType, 'type' => $transactionType,
'date' => $this->convertDateValue($importable->date) ?? Carbon::now()->format('Y-m-d H:i:s'), 'date' => $this->convertDateValue($importable->date) ?? Carbon::now()->format('Y-m-d H:i:s'),
'tags' => $importable->tags, 'order' => 0,
'user' => $this->importJob->user_id,
'notes' => $importable->note, 'currency_id' => $currency->id,
'currency_code' => null,
'foreign_currency_id' => $importable->foreignCurrencyId,
'foreign_currency_code' => null === $foreignCurrency ? null : $foreignCurrency->code,
'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,
// all custom fields:
'internal_reference' => $importable->meta['internal-reference'] ?? null, '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_cc' => $importable->meta['sepa_cc'] ?? null,
'sepa_ct_op' => $importable->meta['sepa_ct_op'] ?? null, 'sepa_ct_op' => $importable->meta['sepa_ct_op'] ?? null,
'sepa_ct_id' => $importable->meta['sepa_ct_id'] ?? null, 'sepa_ct_id' => $importable->meta['sepa_ct_id'] ?? null,
@ -234,44 +205,43 @@ class ImportableConverter
'sepa_ep' => $importable->meta['sepa_ep'] ?? null, 'sepa_ep' => $importable->meta['sepa_ep'] ?? null,
'sepa_ci' => $importable->meta['sepa_ci'] ?? null, 'sepa_ci' => $importable->meta['sepa_ci'] ?? null,
'sepa_batch_id' => $importable->meta['sepa_batch_id'] ?? null, 'sepa_batch_id' => $importable->meta['sepa_batch_id'] ?? null,
'interest_date' => $this->convertDateValue($importable->meta['date-interest'] ?? null), 'interest_date' => $this->convertDateValue($importable->meta['date-interest'] ?? null),
'book_date' => $this->convertDateValue($importable->meta['date-book'] ?? null), 'book_date' => $this->convertDateValue($importable->meta['date-book'] ?? null),
'process_date' => $this->convertDateValue($importable->meta['date-process'] ?? null), 'process_date' => $this->convertDateValue($importable->meta['date-process'] ?? null),
'due_date' => $this->convertDateValue($importable->meta['date-due'] ?? null), 'due_date' => $this->convertDateValue($importable->meta['date-due'] ?? null),
'payment_date' => $this->convertDateValue($importable->meta['date-payment'] ?? null), 'payment_date' => $this->convertDateValue($importable->meta['date-payment'] ?? null),
'invoice_date' => $this->convertDateValue($importable->meta['date-invoice'] ?? 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' => [
[
'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,
'foreign_currency_id' => $importable->foreignCurrencyId,
'foreign_currency_code' => null === $foreignCurrency ? null : $foreignCurrency->code,
'foreign_amount' => $foreignAmount,
'reconciled' => false,
'identifier' => 0,
], ],
], ],
]; ];
}
/**
* @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|null $date
* @param string $destination
* *
* @return string * @return string|null
*/ */
private function getTransactionType(string $source, string $destination): string private function convertDateValue(string $date = null): ?string
{ {
$type = 'unknown'; $result = null;
if (null !== $date) {
if ($source === AccountType::ASSET && $destination === AccountType::ASSET) { try {
Log::debug('Source and destination are asset accounts. This is a transfer.'); // add exclamation mark for better parsing. http://php.net/manual/en/datetime.createfromformat.php
$type = 'transfer'; $dateFormat = $this->config['date-format'] ?? 'Ymd';
if ('!' !== $dateFormat{0}) {
$dateFormat = '!' . $dateFormat;
} }
if ($source === AccountType::REVENUE) { $object = Carbon::createFromFormat($dateFormat, $date);
Log::debug('Source is a revenue account. This is a deposit.'); $result = $object->format('Y-m-d H:i:s');
$type = 'deposit'; 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());
} }
if ($destination === AccountType::EXPENSE) {
Log::debug('Destination is an expense account. This is a withdrawal.');
$type = 'withdrawal';
} }
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;
} }
} }

View File

@ -23,9 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Support\Search; namespace FireflyIII\Support\Search;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Filter\DoubleTransactionFilter;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface; 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 * @return float
*/ */
@ -149,16 +163,13 @@ class Search implements SearchInterface
$pageSize = 50; $pageSize = 50;
$page = 1; $page = 1;
/** @var TransactionCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->withOpposingAccount();
if ($this->hasModifiers()) { $collector->setLimit($pageSize)->setPage($page)->withAccountInformation();
$collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); $collector->withCategoryInformation()->withBudgetInformation();
}
$collector->setSearchWords($this->words); $collector->setSearchWords($this->words);
$collector->removeFilter(InternalTransferFilter::class);
$collector->addFilter(DoubleTransactionFilter::class);
// Most modifiers can be applied to the collector directly. // Most modifiers can be applied to the collector directly.
$collector = $this->applyModifiers($collector); $collector = $this->applyModifiers($collector);
@ -168,37 +179,18 @@ class Search implements SearchInterface
} }
/** /**
* @param int $limit * @param GroupCollectorInterface $collector
*/
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
* *
* @return TransactionCollectorInterface * @return GroupCollectorInterface
* @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/ */
private function applyModifiers(TransactionCollectorInterface $collector): TransactionCollectorInterface private function applyModifiers(GroupCollectorInterface $collector): GroupCollectorInterface
{ {
/* /*
* TODO: * TODO:
* 'bill', * 'bill',
*/ */
$totalAccounts = new Collection;
foreach ($this->modifiers as $modifier) { foreach ($this->modifiers as $modifier) {
switch ($modifier['type']) { switch ($modifier['type']) {
@ -209,7 +201,7 @@ class Search implements SearchInterface
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE]; $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE];
$accounts = $this->accountRepository->searchAccount($modifier['value'], $searchTypes); $accounts = $this->accountRepository->searchAccount($modifier['value'], $searchTypes);
if ($accounts->count() > 0) { if ($accounts->count() > 0) {
$collector->setAccounts($accounts); $totalAccounts = $accounts->merge($totalAccounts);
} }
break; break;
case 'destination': case 'destination':
@ -217,7 +209,7 @@ class Search implements SearchInterface
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE]; $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE];
$accounts = $this->accountRepository->searchAccount($modifier['value'], $searchTypes); $accounts = $this->accountRepository->searchAccount($modifier['value'], $searchTypes);
if ($accounts->count() > 0) { if ($accounts->count() > 0) {
$collector->setOpposingAccounts($accounts); $totalAccounts = $accounts->merge($totalAccounts);
} }
break; break;
case 'category': case 'category':
@ -280,23 +272,28 @@ class Search implements SearchInterface
break; break;
} }
} }
$collector->setAccounts($totalAccounts);
return $collector; return $collector;
} }
/** /**
* @param string $string * @param int $limit
*/ */
private function extractModifier(string $string): void public function setLimit(int $limit): void
{ {
$parts = explode(':', $string); $this->limit = $limit;
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)) { * @param User $user
// filter for valid type */
$this->modifiers->push(['type' => $type, 'value' => $value]); 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);
} }
} }

View File

@ -23,12 +23,11 @@ declare(strict_types=1);
namespace FireflyIII\TransactionRules; namespace FireflyIII\TransactionRules;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Models\Rule; use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleTrigger; use FireflyIII\Models\RuleTrigger;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\User;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Log; use Log;
@ -78,16 +77,16 @@ class TransactionMatcher
* transaction journals matching the given rule. This is accomplished by trying to fire these * 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). * triggers onto each transaction journal until enough matches are found ($limit).
* *
* @return Collection * @return array
* @throws \FireflyIII\Exceptions\FireflyException * @throws \FireflyIII\Exceptions\FireflyException
*/ */
public function findTransactionsByRule(): Collection public function findTransactionsByRule(): array
{ {
Log::debug('Now in findTransactionsByRule()'); Log::debug('Now in findTransactionsByRule()');
if (0 === count($this->rule->ruleTriggers)) { if (0 === count($this->rule->ruleTriggers)) {
Log::error('Rule has no triggers!'); Log::error('Rule has no triggers!');
return new Collection; return [];
} }
// Variables used within the loop. // Variables used within the loop.
@ -98,7 +97,7 @@ class TransactionMatcher
// If the list of matchingTransactions is larger than the maximum number of results // 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 // (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; return $result;
} }
@ -254,7 +253,7 @@ class TransactionMatcher
* *
* @return Collection * @return Collection
*/ */
private function runProcessor(Processor $processor): Collection private function runProcessor(Processor $processor): array
{ {
Log::debug('Now in runprocessor()'); Log::debug('Now in runprocessor()');
// since we have a rule in $this->rule, we can add some of the triggers // 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)); $pageSize = min($this->searchLimit, min($this->triggeredLimit, 50));
$processed = 0; $processed = 0;
$page = 1; $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)); Log::debug(sprintf('Search limit is %d, triggered limit is %d, so page size is %d', $this->searchLimit, $this->triggeredLimit, $pageSize));
do { do {
Log::debug('Start of do-loop'); Log::debug('Start of do-loop');
// Fetch a batch of transactions from the database // Fetch a batch of transactions from the database
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); /** @var GroupCollectorInterface $collector */
$collector->setUser(auth()->user()); $collector = app(GroupCollectorInterface::class);
$collector->withOpposingAccount();
/** @var User $user */
$user = auth()->user();
$collector->setUser($user);
// limit asset accounts: // limit asset accounts:
if (0 === $this->accounts->count()) {
$collector->setAllAssetAccounts();
}
if ($this->accounts->count() > 0) { if ($this->accounts->count() > 0) {
$collector->setAccounts($this->accounts); $collector->setAccounts($this->accounts);
} }
@ -305,36 +305,35 @@ class TransactionMatcher
Log::debug(sprintf('Amount must be exactly %s', $this->exactAmount)); Log::debug(sprintf('Amount must be exactly %s', $this->exactAmount));
$collector->amountIs($this->exactAmount); $collector->amountIs($this->exactAmount);
} }
$collector->removeFilter(InternalTransferFilter::class);
$set = $collector->getPaginatedTransactions(); $journals = $collector->getExtractedJournals();
Log::debug(sprintf('Found %d journals to check. ', $set->count())); Log::debug(sprintf('Found %d transaction journals to check. ', count($journals)));
// Filter transactions that match the given triggers. // Filter transactions that match the given triggers.
$filtered = $set->filter( $filtered = [];
function (Transaction $transaction) use ($processor) { /** @var array $journal */
Log::debug(sprintf('Test the triggers on journal #%d (transaction #%d)', $transaction->transaction_journal_id, $transaction->id)); foreach ($journals as $journal) {
$result = $processor->handleJournalArray($journal);
return $processor->handleTransaction($transaction); 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: // merge:
/** @var Collection $result */ $result = $result + $filtered;
$result = $result->merge($filtered); Log::debug(sprintf('Total count is now %d', count($result)));
Log::debug(sprintf('Total count is now %d', $result->count()));
// Update counters // Update counters
++$page; ++$page;
$processed += count($set); $processed += count($journals);
Log::debug(sprintf('Page is now %d, processed is %d', $page, $processed)); Log::debug(sprintf('Page is now %d, processed is %d', $page, $processed));
// Check for conditions to finish the loop // Check for conditions to finish the loop
$reachedEndOfList = $set->count() < 1; $reachedEndOfList = count($journals) < 1;
$foundEnough = $result->count() >= $this->triggeredLimit; $foundEnough = count($result) >= $this->triggeredLimit;
$searchedEnough = ($processed >= $this->searchLimit); $searchedEnough = ($processed >= $this->searchLimit);
Log::debug(sprintf('reachedEndOfList: %s', var_export($reachedEndOfList, true))); Log::debug(sprintf('reachedEndOfList: %s', var_export($reachedEndOfList, true)));

View File

@ -13,7 +13,7 @@
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:13px;"> <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:13px;">
You can find it in your Firefly III installation:<br /> You can find it in your Firefly III installation:<br />
{% for journal in journals %} {% for journal in journals %}
<a href="{{ route('transactions.show', journal.id) }}">{{ journal.description }}</a> ({{ journal|journalTotalAmount }}) <a href="{{ route('transactions.show', journal.id) }}">{{ journal.description }}</a> (TODO)
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
@ -25,7 +25,7 @@
<ul> <ul>
{% for journal in journals %} {% for journal in journals %}
<li> <li>
<a href="{{ route('transactions.show', journal.id) }}">{{ journal.description }}</a> ({{ journal|journalTotalAmount }}) <a href="{{ route('transactions.show', journal.id) }}">{{ journal.description }}</a> (TODO)
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -10,7 +10,7 @@ Firefly III has created {{ journals.count }} transactions for you.
You can find in in your Firefly III installation: You can find in in your Firefly III installation:
{% for journal in journals %} {% for journal in journals %}
{{ journal.description }}: {{ route('transactions.show', journal.id) }} ({{ journal|journalTotalAmountPlain }}) {{ journal.description }}: {{ route('transactions.show', journal.id) }} (TODO)
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@ -18,7 +18,7 @@ You can find in in your Firefly III installation:
You can find them in your Firefly III installation: You can find them in your Firefly III installation:
{% for journal in journals %} {% for journal in journals %}
- {{ journal.description }}: {{ route('transactions.show', journal.id) }} ({{ journal|journalTotalAmountPlain }}) - {{ journal.description }}: {{ route('transactions.show', journal.id) }} (TODO)
{% endfor %} {% endfor %}
{% endif %} {% endif %}