diff --git a/app/Api/V1/Controllers/AccountController.php b/app/Api/V1/Controllers/AccountController.php index 6dc976cc35..ad85b8e45f 100644 --- a/app/Api/V1/Controllers/AccountController.php +++ b/app/Api/V1/Controllers/AccountController.php @@ -24,15 +24,14 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers; use FireflyIII\Api\V1\Requests\AccountRequest; -use FireflyIII\Helpers\Collector\TransactionCollectorInterface; -use FireflyIII\Helpers\Filter\InternalTransferFilter; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; -use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Http\Api\AccountFilter; use FireflyIII\Support\Http\Api\TransactionFilter; use FireflyIII\Transformers\AccountTransformer; use FireflyIII\Transformers\PiggyBankTransformer; +use FireflyIII\Transformers\TransactionGroupTransformer; use FireflyIII\Transformers\TransactionTransformer; use FireflyIII\User; use Illuminate\Http\JsonResponse; @@ -248,35 +247,43 @@ class AccountController extends Controller /** @var User $admin */ $admin = auth()->user(); - /** @var TransactionCollectorInterface $collector */ - $collector = app(TransactionCollectorInterface::class); - $collector->setUser($admin); - $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); - if ($this->repository->isAsset($account)) { - $collector->setAccounts(new Collection([$account])); - } - if (!$this->repository->isAsset($account)) { - $collector->setOpposingAccounts(new Collection([$account])); - } - if (\in_array(TransactionType::TRANSFER, $types, true)) { - $collector->removeFilter(InternalTransferFilter::class); - } + // use new group collector: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector + ->setUser($admin) + // set the account to filter on to the current one: + ->setAccounts(new Collection([$account])) + // include source + destination account name and type. + ->withAccountInformation() + // include category ID + name (if any) + ->withCategoryInformation() + // include budget ID + name (if any) + ->withBudgetInformation() + // include bill ID + name (if any) + ->withBillInformation() + // set page size: + ->setLimit($pageSize) + // set page to retrieve + ->setPage($this->parameters->get('page')) + // set types of transactions to return. + ->setTypes($types); + // set range if necessary: if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) { $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); } - $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); - $collector->setTypes($types); - $paginator = $collector->getPaginatedTransactions(); - $paginator->setPath(route('api.v1.accounts.transactions', [$account->id]) . $this->buildParams()); - $transactions = $paginator->getCollection(); - /** @var TransactionTransformer $transformer */ - $transformer = app(TransactionTransformer::class); + $paginator = $collector->getPaginatedGroups(); + $paginator->setPath(route('api.v1.accounts.transactions', [$account->id]) . $this->buildParams()); + $groups = $paginator->getCollection(); + + /** @var TransactionGroupTransformer $transformer */ + $transformer = app(TransactionGroupTransformer::class); $transformer->setParameters($this->parameters); - $resource = new FractalCollection($transactions, $transformer, 'transactions'); + $resource = new FractalCollection($groups, $transformer, 'transactions'); $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); diff --git a/app/Helpers/Collector/GroupCollector.php b/app/Helpers/Collector/GroupCollector.php index e81e26c7c6..2f49c40d2e 100644 --- a/app/Helpers/Collector/GroupCollector.php +++ b/app/Helpers/Collector/GroupCollector.php @@ -30,6 +30,7 @@ use FireflyIII\User; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Query\JoinClause; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; /** @@ -41,33 +42,50 @@ class GroupCollector implements GroupCollectorInterface private $accountIds; /** @var array The standard fields to select. */ private $fields; + /** @var bool Will be set to true if query result contains account information. (see function withAccountInformation). */ + private $hasAccountInformation; + /** @var bool Will be true if query result includes bill information. */ + private $hasBillInformation; + /** @var bool Will be true if query result contains budget info. */ + private $hasBudgetInformation; + /** @var bool Will be true if query result contains category info. */ + private $hasCatInformation; /** @var int The maximum number of results. */ private $limit; /** @var int The page to return. */ private $page; /** @var HasMany The query object. */ private $query; + /** @var int Total number of results. */ + private $total; /** @var User The user object. */ private $user; /** * Group collector constructor. */ - public function __construct() { if ('testing' === config('app.env')) { app('log')->warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this))); } + $this->hasAccountInformation = false; + $this->hasCatInformation = false; + $this->hasBudgetInformation = false; + $this->hasBillInformation = false; + + $this->total = 0; $this->limit = 50; $this->page = 0; $this->fields = [ 'transaction_groups.id as transaction_group_id', + 'transaction_groups.created_at as created_at', + 'transaction_groups.updated_at as updated_at', 'transaction_groups.title as transaction_group_title', + 'transaction_journals.id as transaction_journal_id', 'transaction_journals.transaction_type_id', 'transaction_types.type as transaction_type_type', - 'transaction_journals.bill_id', 'transaction_journals.description', 'transaction_journals.date', 'source.id as source_transaction_id', @@ -101,15 +119,28 @@ class GroupCollector implements GroupCollectorInterface $result = $this->query->get($this->fields); // now to parse this into an array. - $array = $this->parseArray($result); + $collection = $this->parseArray($result); + $this->total = $collection->count(); // now filter the array according to the page and the $offset = $this->page * $this->limit; - $limited = $array->slice($offset, $this->limit); + $limited = $collection->slice($offset, $this->limit); return $limited; } + /** + * Same as getGroups but everything is in a paginator. + * + * @return LengthAwarePaginator + */ + public function getPaginatedGroups(): LengthAwarePaginator + { + $set = $this->getGroups(); + + return new LengthAwarePaginator($set, $this->total, $this->limit, $this->page); + } + /** * Define which accounts can be part of the source and destination transactions. * @@ -188,6 +219,20 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * Limit the included transaction types. + * + * @param array $types + * + * @return GroupCollectorInterface + */ + public function setTypes(array $types): GroupCollectorInterface + { + $this->query->whereIn('transaction_types.type', $types); + + return $this; + } + /** * Set the user object and start the query. * @@ -203,10 +248,106 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * Will include the source and destination account names and types. + * + * @return GroupCollectorInterface + */ + public function withAccountInformation(): GroupCollectorInterface + { + if (false === $this->hasAccountInformation) { + // join source account table + $this->query->leftJoin('accounts as source_account', 'source_account.id', '=', 'source.account_id'); + // join source account type table + $this->query->leftJoin('account_types as source_account_type', 'source_account_type.id', '=', 'source_account.account_type_id'); + + // add source account fields: + $this->fields[] = 'source_account.name as source_account_name'; + $this->fields[] = 'source_account.account_type_id as source_account_type_id'; + $this->fields[] = 'source_account_type.type as source_account_type_type'; + + // same for dest + $this->query->leftJoin('accounts as dest_account', 'dest_account.id', '=', 'destination.account_id'); + $this->query->leftJoin('account_types as dest_account_type', 'dest_account_type.id', '=', 'dest_account.account_type_id'); + + // and add fields: + $this->fields[] = 'dest_account.name as destination_account_name'; + $this->fields[] = 'dest_account.account_type_id as destination_account_type_id'; + $this->fields[] = 'dest_account_type.type as destination_account_type_type'; + + + $this->hasAccountInformation = true; + } + + 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; + } + + /** + * Will include budget ID + name, if any. + * + * @return GroupCollectorInterface + */ + public function withBudgetInformation(): GroupCollectorInterface + { + if (false === $this->hasBudgetInformation) { + // join link table + $this->query->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); + // join cat table + $this->query->leftJoin('budgets', 'budget_transaction_journal.budget_id', '=', 'budgets.id'); + // add fields + $this->fields[] = 'budgets.id as budget_id'; + $this->fields[] = 'budgets.name as budget_name'; + $this->hasBudgetInformation = true; + } + + return $this; + } + + /** + * Will include category ID + name, if any. + * + * @return GroupCollectorInterface + */ + public function withCategoryInformation(): GroupCollectorInterface + { + if (false === $this->hasCatInformation) { + // join link table + $this->query->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); + // join cat table + $this->query->leftJoin('categories', 'category_transaction_journal.category_id', '=', 'categories.id'); + // add fields + $this->fields[] = 'categories.id as category_id'; + $this->fields[] = 'categories.name as category_name'; + $this->hasCatInformation = true; + } + + return $this; + } + /** * @param Collection $collection * * @return Collection + * @throws Exception */ private function parseArray(Collection $collection): Collection { @@ -216,21 +357,24 @@ class GroupCollector implements GroupCollectorInterface $groupId = $augumentedGroup->transaction_group_id; if (!isset($groups[$groupId])) { // make new array - $groupArray = [ + $groupArray = [ 'id' => $augumentedGroup->id, 'title' => $augumentedGroup->title, 'count' => 1, 'sum' => $augumentedGroup->amount, 'foreign_sum' => $augumentedGroup->foreign_amount ?? '0', - 'transactions' => [$this->parseAugumentedGroup($augumentedGroup)], + 'transactions' => [], ]; - $groups[$groupId] = $groupArray; + $journalId = (int)$augumentedGroup->transaction_journal_id; + $groupArray['transactions'][$journalId] = $this->parseAugumentedGroup($augumentedGroup); + $groups[$groupId] = $groupArray; continue; } $groups[$groupId]['count']++; - $groups[$groupId]['sum'] = bcadd($augumentedGroup->amount, $groups[$groupId]['sum']); - $groups[$groupId]['foreign_sum'] = bcadd($augumentedGroup->foreign_amount ?? '0', $groups[$groupId]['foreign_sum']); - $groups[$groupId]['transactions'][] = $this->parseAugumentedGroup($augumentedGroup); + $groups[$groupId]['sum'] = bcadd($augumentedGroup->amount, $groups[$groupId]['sum']); + $groups[$groupId]['foreign_sum'] = bcadd($augumentedGroup->foreign_amount ?? '0', $groups[$groupId]['foreign_sum']); + $journalId = (int)$augumentedGroup->transaction_journal_id; + $groups[$groupId]['transactions'][$journalId] = $this->parseAugumentedGroup($augumentedGroup); } return new Collection($groups); @@ -244,8 +388,10 @@ class GroupCollector implements GroupCollectorInterface */ private function parseAugumentedGroup(TransactionGroup $augumentedGroup): array { - $result = $augumentedGroup->toArray(); - $result['date'] = new Carbon($result['date']); + $result = $augumentedGroup->toArray(); + $result['date'] = new Carbon($result['date']); + $result['created_at'] = new Carbon($result['created_at']); + $result['updated_at'] = new Carbon($result['updated_at']); return $result; } @@ -275,8 +421,8 @@ class GroupCollector implements GroupCollectorInterface ) // 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') + ->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') diff --git a/app/Helpers/Collector/GroupCollectorInterface.php b/app/Helpers/Collector/GroupCollectorInterface.php index 589033b4ac..6b1b1a61d8 100644 --- a/app/Helpers/Collector/GroupCollectorInterface.php +++ b/app/Helpers/Collector/GroupCollectorInterface.php @@ -25,6 +25,7 @@ namespace FireflyIII\Helpers\Collector; use Carbon\Carbon; use FireflyIII\User; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; /** @@ -39,6 +40,13 @@ interface GroupCollectorInterface */ public function getGroups(): Collection; + /** + * Same as getGroups but everything is in a paginator. + * + * @return LengthAwarePaginator + */ + public function getPaginatedGroups(): LengthAwarePaginator; + /** * Define which accounts can be part of the source and destination transactions. * @@ -76,6 +84,15 @@ interface GroupCollectorInterface */ public function setRange(Carbon $start, Carbon $end): GroupCollectorInterface; + /** + * Limit the included transaction types. + * + * @param array $types + * + * @return GroupCollectorInterface + */ + public function setTypes(array $types): GroupCollectorInterface; + /** * Set the user object and start the query. * @@ -85,4 +102,32 @@ interface GroupCollectorInterface */ public function setUser(User $user): GroupCollectorInterface; + /** + * Will include the source and destination account names and types. + * + * @return GroupCollectorInterface + */ + public function withAccountInformation(): GroupCollectorInterface; + + /** + * Include bill name + ID. + * + * @return GroupCollectorInterface + */ + public function withBillInformation(): GroupCollectorInterface; + + /** + * Will include budget ID + name, if any. + * + * @return GroupCollectorInterface + */ + public function withBudgetInformation(): GroupCollectorInterface; + + /** + * Will include category ID + name, if any. + * + * @return GroupCollectorInterface + */ + public function withCategoryInformation(): GroupCollectorInterface; + } \ No newline at end of file diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 08deaaaddc..ddb872bb16 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -72,7 +72,7 @@ class LoginController extends Controller public function login(Request $request) { Log::channel('audit')->info(sprintf('User is trying to login using "%s"', $request->get('email'))); - + Log::info(sprintf('User is trying to login.')); if ('ldap' === config('auth.providers.users.driver')) { /** * Temporary bug fix for something that doesn't seem to work in @@ -102,9 +102,14 @@ class LoginController extends Controller // user is logged in. Save in session if the user requested session to be remembered: $request->session()->put('remember_login', $request->filled('remember')); + Log::debug(sprintf('Redirect after login is %s.', $this->redirectPath())); + /** @noinspection PhpInconsistentReturnPointsInspection */ /** @noinspection PhpVoidFunctionResultUsedInspection */ - return $this->sendLoginResponse($request); + $response = $this->sendLoginResponse($request); + Log::debug(sprintf('Response Location header: %s', $response->headers->get('location'))); + + return $response; } // If the login attempt was unsuccessful we will increment the number of attempts diff --git a/app/Http/Middleware/Installer.php b/app/Http/Middleware/Installer.php index cf24d83e54..732289eaa8 100644 --- a/app/Http/Middleware/Installer.php +++ b/app/Http/Middleware/Installer.php @@ -54,6 +54,7 @@ class Installer */ public function handle($request, Closure $next) { + Log::debug(sprintf('Installer middleware for URI %s', $request->url())); // ignore installer in test environment. if ('testing' === config('app.env')) { return $next($request); diff --git a/app/Http/Middleware/StartFireflySession.php b/app/Http/Middleware/StartFireflySession.php index 00b3580734..4b6ece87cc 100644 --- a/app/Http/Middleware/StartFireflySession.php +++ b/app/Http/Middleware/StartFireflySession.php @@ -41,13 +41,17 @@ class StartFireflySession extends StartSession */ protected function storeCurrentUrl(Request $request, $session): void { - $uri = $request->fullUrl(); + $uri = $request->fullUrl(); $isScriptPage = strpos($uri, 'jscript'); - $isDeletePage = strpos($uri, 'delete'); + $isDeletePage = strpos($uri, 'delete'); + $isLoginPage = strpos($uri, '/login'); // also stop remembering "delete" URL's. - if (false === $isScriptPage && false === $isDeletePage && 'GET' === $request->method() && !$request->ajax()) { + if (false === $isScriptPage && false === $isDeletePage + && false === $isLoginPage + && 'GET' === $request->method() + && !$request->ajax()) { $session->setPreviousUrl($uri); Log::debug(sprintf('Will set previous URL to %s', $uri)); diff --git a/app/Models/TransactionGroup.php b/app/Models/TransactionGroup.php index 2dfe932ec6..accad7a5f4 100644 --- a/app/Models/TransactionGroup.php +++ b/app/Models/TransactionGroup.php @@ -59,6 +59,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @property string amount * @property string foreign_amount * @property int transaction_group_id + * @property int transaction_journal_id */ class TransactionGroup extends Model { diff --git a/app/Transformers/TransactionGroupTransformer.php b/app/Transformers/TransactionGroupTransformer.php new file mode 100644 index 0000000000..f5e6e28205 --- /dev/null +++ b/app/Transformers/TransactionGroupTransformer.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Transformers; + +/** + * Class TransactionGroupTransformer + */ +class TransactionGroupTransformer extends AbstractTransformer +{ + /** + * Constructor. + * + * @codeCoverageIgnore + */ + public function __construct() + { + if ('testing' === config('app.env')) { + app('log')->warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this))); + } + } + + /** + * @param array $group + * + * @return array + */ + public function transform(array $group): array + { + + $first =reset($group['transactions']); + $data = [ + 'id' => (int)$first['transaction_group_id'], + 'created_at' => $first['created_at']->toAtomString(), + 'updated_at' => $first['updated_at']->toAtomString(), + 'some_field' => 'some_value', + 'links' => [ + [ + 'rel' => 'self', + 'uri' => '/transactions/' . $first['transaction_group_id'], + ], + ], + ]; + + // do something else. + + return $data; + } +} \ No newline at end of file